使用 SpringBoot 读取 YAML 文件并将数据转为 Map,在嵌套 Map 中递归获取指定层级下的所有数据

文章目录

    • 引入
    • 认识 YAML 格式规范
    • 定义脱敏规则格式
    • 主要逻辑实现
    • 优化方法
    • 完整工具类

引入

在项目中遇到一个需求,需要对交易接口返回结果中的指定字段进行脱敏操作,但又不能使用AOP+注解的形式,于是决定使用一种比较笨的方法:

  1. 首先将所有需要脱敏字段及其对应脱敏规则存储到 Map 中。
  2. 在接口返回时,遍历结果中的所有字段,判断字段名在 Map 中是否存在:
    • 如果不存在:说明该字段不需要脱敏,不做处理即可。
    • 如果存在:说明该字段需要脱敏,从 Map 中获取对应的脱敏规则进行脱敏。
  3. 最后返回脱敏之后的结果。

认识 YAML 格式规范

由于返回的结果涉及到嵌套 Map,所以决定采用 YAML 格式的文件存储脱敏规则,那么为了大家统一维护和开发,就需要大家对 YAML 格式进行了解,遵守规范,不易出错,少走弯路。

YAML(YAML Ain’t Markup Language)与传统的 JSON、XML 和 Properties 文件一样,都是用于数据序列化的格式,常用于配置文件和数据传输。

相比于其他格式,YAML 是一种轻量级的数据序列化格式,它的设计初衷是为了简化复杂性,提高人类可读性,并且易于实现和解析。

  • 与 JSON 相比:YAML 在语法上更为灵活,允许使用更简洁的方式来表示数据结构。

  • 与 XML 相比:YAML 的语法更为简洁,没有繁琐的标签和尖括号。

  • 与 Properties 相比:YAML 支持更复杂的数据结构,包括嵌套的键值对和列表。

除此之外,YAML 还支持跨平台、跨语言,可以被多种编程语言解析,这使得YAML非常适合用于不同语言之间的数据传输和交换。

YAML 文件的语法非常简洁明了,以下是它的语法规范:

  1. 基本语法:

    • 使用 缩进表示层级关系,可以使用空格或制表符进行缩进,但不能混用。
    • 使用冒号(:)表示键值对,键值对之间使用换行分隔。
    • 使用破折号(-)表示列表项,列表项之间也使用换行分隔。
    # 使用缩进表示层级关系
    server:
      port: 8080
    
    # 使用冒号表示键值对
    name: John Smith
    age: 30
    
    # 使用破折号表示列表项
    hobbies:
      - reading
      - hiking
      - swimming
    
  2. 注释:

    • 使用井号(#)表示注释,在 # 后面的内容被视为注释,可以出现在行首或行尾。
    # 这是一个注释
    name: John Smith
    age: 30 # 这也是一个注释
    
  3. 字符串:

    • 字符串可以使用单引号或双引号括起来,也可以不使用引号。
    • 使用双引号时,可以使用转义字符(如 \n 表示换行)和转义序列(如 \u 表示 Unicode 字符)。
    # 使用双引号表示字符串
    name: "John Smith"
    
    # 使用单引号表示字符串
    nickname: 'Johnny'
    
  4. 键值对:

    • 键值对使用冒号(:)表示,键和值之间使用一个 空格 分隔。
    • 键可以是字符串或纯量(如整数、布尔值等)。
    • 值可以是字符串、纯量、列表或嵌套的键值对。
    # 键和值之间使用一个空格分隔
    name: John Smith
    
    # 键可以是字符串或纯量
    age: 30
    
    # 值可以是字符串、纯量、列表或嵌套的键值对
    address:
      city: San Francisco
      state: California
      zip: 94107
    
  5. 列表:

    • 使用破折号(-)表示列表项。
    • 列表项可以是字符串、纯量或嵌套的列表或键值对。
    # 使用破折号表示列表项
    hobbies:
      - reading
      - hiking
      - swimming
      
    # 列表项可以是字符串、纯量或嵌套的列表或键值对
    people:
      - name: John Smith
        age: 30
      - name: Jane Doe
        age: 25
    
  6. 引用:

    • 使用&表示引用,使用*表示引用的内容。
    # 使用&表示引用
    address: &myaddress
      city: San Francisco
      state: California
      zip: 94107
      
    # 使用*表示引用的内容
    shippingAddress: *myaddress
    
  7. 多行文本块:

    • 使用|保留换行符,保留文本块的精确格式。
    • 使用>折叠换行符,将文本块折叠成一行,并根据内容自动换行。
    # 使用|保留换行符
    description: |
      This is a
      multi-line
      string.
      
    # 使用>折叠换行符
    summary: >
      This is a summary
      that may contain
      line breaks.
    
  8. 数据类型:

    • YAML支持多种数据类型,包括字符串、整数、浮点数、布尔值、日期和时间等。
    • 可以使用标记来表示一些特殊的数据类型,如 !!str 表示字符串类型、!!int 表示整数类型等。
    # 使用标记表示数据类型
    age: !!int 30
    weight: !!float 65.5
    isMale: !!bool true
    created: !!timestamp '2022-01-01 12:00:00'
    
  9. 多文件:

    • 可以使用—表示多个 YAML 文件之间的分隔符。每个文件可以使用任何 YAML 语法。
    # 第一个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") 的形式获取到指定交易,指定字段的脱敏规则。

主要逻辑实现

  1. 首先创建 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+)'
    
  2. 定义工具类编写我们的逻辑:

    public class YamlUtils {
    }
    
  3. 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 语句来自动关闭输入流。

  4. 编写方法通过 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);
}

具体来说,主要分为以下几步:

  1. 首先判断键路径是否为空或者第一个键是否在 Map 中。如果键路径为空或者第一个键不在 Map 中,则返回 null。
  2. 获取第一个键对应的嵌套对象。通过 get 方法获取第一个键对应的嵌套对象。
  3. 判断是否到达最里层的嵌套 Map。如果键路径长度为 1,说明已经到达最里层的嵌套 Map,直接返回该 Map 对象。
  4. 继续递归查找下一个键的嵌套 Map。如果嵌套对象是 Map,则继续递归查找下一个键的嵌套 Map。
  5. 返回结果。返回递归查找的结果。

完整工具类

封装成完整的工具类如下:

/**
 * @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;
            }
        }
    }

}

你可能感兴趣的:(SpringBoot,spring,boot,java,spring)