在项目中遇到一个需求,需要对交易接口返回结果中的指定字段进行脱敏操作,但又不能使用AOP+注解的形式,于是决定使用一种比较笨的方法:
由于返回的结果涉及到嵌套 Map,所以决定采用 YAML 格式的文件存储脱敏规则,那么为了大家统一维护和开发,就需要大家对 YAML 格式进行了解,遵守规范,不易出错,少走弯路。
YAML(YAML Ain’t Markup Language)与传统的 JSON、XML 和 Properties 文件一样,都是用于数据序列化的格式,常用于配置文件和数据传输。
相比于其他格式,YAML 是一种轻量级的数据序列化格式,它的设计初衷是为了简化复杂性,提高人类可读性,并且易于实现和解析。
与 JSON 相比:YAML 在语法上更为灵活,允许使用更简洁的方式来表示数据结构。
与 XML 相比:YAML 的语法更为简洁,没有繁琐的标签和尖括号。
与 Properties 相比:YAML 支持更复杂的数据结构,包括嵌套的键值对和列表。
除此之外,YAML 还支持跨平台、跨语言,可以被多种编程语言解析,这使得YAML非常适合用于不同语言之间的数据传输和交换。
YAML 文件的语法非常简洁明了,以下是它的语法规范:
基本语法:
:
)表示键值对,键值对之间使用换行分隔。-
)表示列表项,列表项之间也使用换行分隔。# 使用缩进表示层级关系
server:
port: 8080
# 使用冒号表示键值对
name: John Smith
age: 30
# 使用破折号表示列表项
hobbies:
- reading
- hiking
- swimming
注释:
#
)表示注释,在 #
后面的内容被视为注释,可以出现在行首或行尾。# 这是一个注释
name: John Smith
age: 30 # 这也是一个注释
字符串:
\n
表示换行)和转义序列(如 \u
表示 Unicode
字符)。# 使用双引号表示字符串
name: "John Smith"
# 使用单引号表示字符串
nickname: 'Johnny'
键值对:
:
)表示,键和值之间使用一个 空格 分隔。# 键和值之间使用一个空格分隔
name: John Smith
# 键可以是字符串或纯量
age: 30
# 值可以是字符串、纯量、列表或嵌套的键值对
address:
city: San Francisco
state: California
zip: 94107
列表:
-
)表示列表项。# 使用破折号表示列表项
hobbies:
- reading
- hiking
- swimming
# 列表项可以是字符串、纯量或嵌套的列表或键值对
people:
- name: John Smith
age: 30
- name: Jane Doe
age: 25
引用:
&
表示引用,使用*
表示引用的内容。# 使用&表示引用
address: &myaddress
city: San Francisco
state: California
zip: 94107
# 使用*表示引用的内容
shippingAddress: *myaddress
多行文本块:
|
保留换行符,保留文本块的精确格式。>
折叠换行符,将文本块折叠成一行,并根据内容自动换行。# 使用|保留换行符
description: |
This is a
multi-line
string.
# 使用>折叠换行符
summary: >
This is a summary
that may contain
line breaks.
数据类型:
!!str
表示字符串类型、!!int
表示整数类型等。# 使用标记表示数据类型
age: !!int 30
weight: !!float 65.5
isMale: !!bool true
created: !!timestamp '2022-01-01 12:00:00'
多文件:
# 第一个YAML文件
name: John Smith
age: 30
---
# 第二个YAML文件
hobbies:
- reading
- hiking
- swimming
对于数据结构简单的接口返回结果,脱敏规则格式定义为【交易号->字段->规则】:
交易号:
字段名:
规则: '/^(1[3-9][0-9])\d{4}(\d{4}$)/'
同时接口返回的结果中可能用有嵌套列表,那么针对这种复杂的结构就定义格式为【交易号->字段(列表)->字段->规则】,即:
交易号:
字段名(列表):
字段名:
规则: '/^(1[3-9][0-9])\d{4}(\d{4}$)/'
使用这种层级结构,我们完全可以通过 Map.get("Key")
的形式获取到指定交易,指定字段的脱敏规则。
首先创建 YAML 文件 sensitive.yml
添加对应交易字段的脱敏规则:
Y3800:
phone:
length: 11
rule: '(\\d{3})\\d{4}(\\d{4})'
Y3801:
idCard:
length: 18
rule: '(?<=\\w{3})\\w(?=\\w{4})'
list:
email:
rule: '(\\w+)\\w{5}@(\\w+)'
定义工具类编写我们的逻辑:
public class YamlUtils {
}
在 YamlUtils
工具类中,我们需要实现在项目启动时,读取 YAML 文件中的内容,并转为我们想要的 Map 键值对数据类型:
// YAML 文件路径
private static final String YAML_FILE_PATH = "/sensitive.yml";
// 存储解析后的 YAML 数据
private static Map<String, Object> map;
static {
// 创建 Yaml 对象
Yaml yaml = new Yaml();
// 通过 getResourceAsStream 获取 YAML 文件的输入流
try (InputStream in = YamlUtils.class.getResourceAsStream(YAML_FILE_PATH)) {
// 解析 YAML 文件为 Map 对象
map = yaml.loadAs(in, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
}
在上述代码中,我们首先定义了一个私有静态常量 YAML_FILE_PATH
,用于存储 YAML 文件的路径;又定义了一个静态变量 map
,用于存储解析后的 YAML 数据。
接着通过 getResourceAsStream
方法根据指定的 YAML 文件的路径从类路径中获取资源文件的输入流。
然后使用 loadAs
方法将输入流中的内容按照 YAML 格式进行解析,并将解析结果转换为指定的 Map.class
类型。
最后使用 try-with-resources 语句来自动关闭输入流。
编写方法通过 Key 获取对应脱敏规则:
public static void main(String[] args) {
// 加载 YAML 文件并获取顶层的 Map 对象
Map<String, Object> yamlMap = loadYaml("/sensitive.yml");
System.out.println(yamlMap);
// 从顶层的 Map 中获取名为 "Y3800" 的嵌套 Map
Map<String, Object> Y3800= (Map<String, Object>) yamlMap.get("Y3800");
System.out.println(Y3800);
// 从 "Y3800" 的嵌套 Map 中获取名为 "phone" 的嵌套 Map
Map<String, Object> phone = (Map<String, Object>) Y3800.get("phone");
System.out.println(phone);
}
输出结果如下:
{Y3800={phone={length=11, rule=(\\d{3})\\d{4}(\\d{4})}}, Y3801={idCard={length=18, rule=(?<=\\w{3})\\w(?=\\w{4})}, list={email={rule=(\\w+)\\w{5}@(\\w+)}}}}
{phone={length=11, rule=(\\d{3})\\d{4}(\\d{4})}}
{length=11, rule=(\\d{3})\\d{4}(\\d{4})}
转为 JSON 格式显示如下:
输出 YAML 文件中的全部数据:
{
"Y3800": {
"phone": {
"length": 11,
"rule": "(\\\\d{3})\\\\d{4}(\\\\d{4})"
}
},
"Y3801": {
"idCard": {
"length": 18,
"rule": "(?<=\\\\w{3})\\\\w(?=\\\\w{4})"
},
"list": {
"email": {
"rule": "(\\\\w+)\\\\w{5}@(\\\\w+)"
}
}
}
}
输出 Y3800
层级下的数据:
{
"phone": {
"length": 11,
"rule": "(\\\\d{3})\\\\d{4}(\\\\d{4})"
}
}
输出 phone
层级下的数据:
{
"length": 11,
"rule": "(\\\\d{3})\\\\d{4}(\\\\d{4})"
}
文章到此需要的功能基本实现,理论上该文到这里就结束了,但是我们仔细思考一下,我们通过 Key 获取指定层级下的数据时,需要我们不断的调用 Map.get("Key")
方法,即结构每嵌套一次,就需要一次 getKey,那么这里是否有优化的方法呢?
答案是:有的,因为有问题就会有答案。
优化思路为:通过递归和判断来遍历嵌套的 Map,直到找到键路径所对应的最里层的嵌套 Map,并返回该 Map 对象。
优化后方法如下:
/**
* 递归获取嵌套 Map 数据
*
* @param map 嵌套数据源的 Map
* @param keys 嵌套键路径
* @return 嵌套数据对应的 Map
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> getNestedMapValues(Map<String, Object> map, String... keys) {
// 如果键路径为空或者第一个键不在 Map 中,则返回 null
if (keys.length == 0 || !map.containsKey(keys[0])) {
return null;
}
// 获取第一个键对应的嵌套对象
Object nestedObject = map.get(keys[0]);
// 如果键路径长度为 1,说明已经到达最里层的嵌套 Map,直接返回该 Map 对象
if (keys.length == 1) {
if (nestedObject instanceof Map) {
return (Map<String, Object>) nestedObject;
} else {
return null;
}
} else {
// 如果嵌套对象是 Map,继续递归查找下一个键的嵌套 Map
if (nestedObject instanceof Map) {
return getNestedMapValues((Map<String, Object>) nestedObject, Arrays.copyOfRange(keys, 1, keys.length));
} else {
// 嵌套对象既不是 Map 也不是 List,返回 null
return null;
}
}
}
调用方法时传入 Key 的嵌套路径即可:
public static void main(String[] args) {
// 加载 YAML 文件并获取顶层的 Map 对象
Map<String, Object> yamlMap = loadYaml("/sensitive.yml");
System.out.println(yamlMap);
// 获取 Y3800 -> phone 下的数据转为 Map
Map<String, Object> y3800PhoneMap = YamlUtils.getNestedMap(yamlMap, "Y3800", "phone");
System.out.println("Y3800 -> phone : " + y3800NameMap);
}
具体来说,主要分为以下几步:
封装成完整的工具类如下:
/**
* @Project demo1
* @ClassName YamlUtils
* @Description 读取YAML配置文件工具类
* @Author 赵士杰
* @Date 2024/1/26 20:15
*/
@SuppressWarnings("unchecked")
public class YamlUtils {
// YAML 文件路径
private static final String YAML_FILE_PATH = "/sensitive.yml";
// 存储解析后的 YAML 数据
private static Map<String, Object> map;
static {
// 创建 Yaml 对象
Yaml yaml = new Yaml();
// 通过 getResourceAsStream 获取 YAML 文件的输入流
try (InputStream in = YamlUtils.class.getResourceAsStream(YAML_FILE_PATH)) {
// 解析 YAML 文件为 Map 对象
map = yaml.loadAs(in, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取嵌套的 Map 数据
*
* @param keys 嵌套键路径
* @return 嵌套数据对应的 Map
*/
public static Map<String, Object> getNestedMap(String... keys) {
return getNestedMapValues(map, keys);
}
/**
* 递归获取嵌套 Map 数据
*
* @param map 嵌套数据源的 Map
* @param keys 嵌套键路径
* @return 嵌套数据对应的 Map
*/
public static Map<String, Object> getNestedMapValues(Map<String, Object> map, String... keys) {
// 如果键路径为空或者第一个键不在 Map 中,则返回 null
if (keys.length == 0 || !map.containsKey(keys[0])) {
return null;
}
// 获取第一个键对应的嵌套对象
Object nestedObject = map.get(keys[0]);
// 如果键路径长度为 1,说明已经到达最里层的嵌套 Map,直接返回该 Map 对象
if (keys.length == 1) {
if (nestedObject instanceof Map) {
return (Map<String, Object>) nestedObject;
} else {
return null;
}
} else {
// 如果嵌套对象是 Map,继续递归查找下一个键的嵌套 Map
if (nestedObject instanceof Map) {
return getNestedMapValues((Map<String, Object>) nestedObject, Arrays.copyOfRange(keys, 1, keys.length));
} else {
// 嵌套对象既不是 Map 也不是 List,返回 null
return null;
}
}
}
}