1. 为什么我们需要把YAML配置“拍平”?
如果你用过Spring Boot,肯定对application.yml这个文件不陌生。它用起来确实方便,结构清晰,层级分明。但不知道你有没有遇到过这样的烦恼:当你想在代码里直接获取某个配置项的值时,比如想拿到spring.jackson.time-zone,你会发现Spring Boot的@Value注解或者Environment对象用起来很顺手,可一旦你想自己写个工具类去解析整个yml文件,事情就变得有点棘手了。
我刚开始也这么干过,直接用snakeyaml库把yml文件加载成一个Map<String, Object>。满心欢喜地以为拿到了一个“字典”,结果打印出来一看,傻眼了。它根本不是我想象中的那种扁平化的键值对。比如下面这个简单的配置:
server:
servlet:
context-path: /myapp
port: 8080
spring:
jackson:
time-zone: GMT+8
用Yaml().loadAs(input, Map.class)加载后,得到的Map结构是这样的:最外层Map的key是server和spring,但它们的value不是字符串,而是新的Map对象!你得像剥洋葱一样一层层往里找:map.get("server")得到一个Map,再对这个Map做get("servlet"),又得到一个Map,最后才能get("context-path")拿到真正的值/myapp。
这种嵌套结构在程序内部处理时问题不大,但如果你想实现一个功能,比如根据用户动态传入的一个配置键("server.servlet.context-path")来快速获取值,你就得写一个循环或者递归去遍历这个多层Map。每次获取都要遍历,这在配置项很多、访问很频繁的场景下,就是个性能隐患。我就在一个需要实时读取大量动态配置的服务里踩过这个坑,当时没在意,上线后偶尔会有响应延迟,一查日志,时间都花在层层遍历配置Map上了。
所以,“拍平”YAML配置,就是把这种树形的、嵌套的Map结构,转换成一个扁平的、一维的Map。转换后,上面的配置会变成这样两个直接的键值对:
server.servlet.context-path=/myappspring.jackson.time-zone=GMT+8
这样一来,你想获取任何配置,都只需要一次map.get(key)操作,时间复杂度是O(1),瞬间完成。这对于编写配置中心客户端、动态配置刷新、或者单纯想做一个轻量级配置管理工具来说,都是非常实用的底层优化。
2. 核心思路:用递归“遍历”这棵树
要把一个嵌套的Map拍平,核心算法其实就是深度优先遍历。你可以把原始的嵌套Map想象成一棵文件夹树。根目录下有两个文件夹server和spring。server文件夹里又有子文件夹servlet和文件port。我们的目标,是生成一份包含所有文件完整路径的清单。
递归是解决这类“树形结构”问题的天然利器。思路非常直接:
- 从根
Map开始,遍历它的每一个键值对。 - 如果当前键对应的
值是一个Map,那么说明我们进入了一个新的“文件夹”。我们就把当前的“路径”(也就是键名)记录下来,然后递归地处理这个子Map。在递归时,需要把当前的路径作为前缀传递给下一层。 - 如果当前键对应的
值不是Map(比如是字符串、数字、布尔值等),那么恭喜,我们找到了一个“文件”。此时,当前的“路径”加上这个键,就构成了完整的扁平化键(如server.servlet.context-path),这个值就是我们要存储的对象。
这个过程听起来有点抽象,我画个简单的步骤图帮你理解(我们用伪代码描述逻辑):
假设我们有一个入口函数
flattenMap(原始Map, 当前路径前缀)。
- 遍历原始Map。
- 遇到
{key: server, value: {servlet: {context-path: /myapp}, port: 8080}}。
- 值是个Map,递归调用
flattenMap({servlet:..., port:...}, "server")。- 在递归中,遍历
{servlet: {context-path: /myapp}, port: 8080}。
- 遇到
{key: servlet, value: {context-path: /myapp}}。
- 值还是个Map,继续递归
flattenMap({context-path: /myapp}, "server.servlet")。- 这次遇到
{key: context-path, value: /myapp},值不是Map,存入结果:"server.servlet.context-path" -> "/myapp"。- 遇到
{key: port, value: 8080},值不是Map,存入结果:"server.port" -> 8080。- 回溯,继续处理其他顶层键。
这个递归过程会遍历到每一个叶子节点(即非Map的值),并把从根到叶子的路径用点号.连接起来,作为新Map的键。这里有个细节需要注意,就是对于List类型的值(对应yml里以短横线-开头的列表),通常我们不对其进行进一步的扁平化,而是将整个List作为一个值对象存入。因为列表内的结构可能非常复杂,强行扁平化会失去其语义,键名也会变得难以定义。
3. 手把手实现:基础工具类版本
光说不练假把式,咱们直接上代码。我先给你展示一个最直接、最易懂的工具类实现。这个版本把加载YAML和转换Map的逻辑分成了几个清晰的方法,适合在一次性任务或者简单工具中使用。
首先,确保你的pom.xml里引入了snakeyaml依赖:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</versio


320

被折叠的 条评论
为什么被折叠?



