ClassLoader与量子化的配置文件

同样是困难 ,同样是问题 ,同样是不幸 ,对于有些人来说是苦难 ,而对于有些人来说却是磨难
苦难磨难区别在于,是否是你主观意志选择的。 苦难是无差别的打击。 磨难是你努力时候的必经之路。
尼采说:那些杀不死你的,终将使你变得更强大

好了,如果你看到这,说明你也是一个时不时爱装小叉的人。咱们这不搞这么高大上的格调啊,咱就是纯纯地想装个×了。

好了,请系好安全带,下面就开始我的表演了。

老规矩,直接上菜

1.问题是个啥问题?

如下,是研发人员与脱敏组件维护者之间的一段对话。【关于脱敏组件的背景内容,可以参考Guava在日志脱敏场景下的奇遇

一般,当别人使用了我们的组件的时候出现了问题,一般先怀疑对方是不是使用姿势不对 。如果姿势正常,且用户也没啥特别的骚操作 ,那么就只能遇事不决,量子力学了【实际上,一般没啥怀疑对象的时候,就只能央求用户直接开gitlab权限了】。

我们知道,一般我们查问题,如果能复现问题,那么离解决问题也就不远了。 但如果复现不了问题,并且线上出现问题还不是必现的问题,那么你就知道了,这TM有点不妙啊

不过,从上面的对话中我们大概可以总结出以下几点

  • 1.用户使用方式大概率没啥问题(不然就会都脱敏失败)
  • 2.本地无法复现,线上固定一台机器脱敏正常,另外一台机器脱敏异常(还好不是随机正常,随机异常)

2.简单聊一下背景

为了防止敏感信息打印到日志中,我们用Java语言开发了一个脱敏组件,这样用户在打印日志的时候就可以实现敏感信息的脱敏了。

对于用户来讲,大致分为两步来使用这个脱敏组件

1.配置脱敏规则

配置脱敏规则大概分为两种:

  • 使用注解(在DTO Field上标注)
  • 使用配置文件(配置字段名称对应的字段脱敏规则)

这里我们重点介绍一下配置文件 的脱敏规则配置方式,因为本次问题用户是使用了配置文件的方式来实现脱敏的

如下,大致是在用户项目的classpath下配置一个data_mask.properties文件来配置脱敏规则

具体的脱敏规则如下:

拿第一行简单举例来说明脱敏规则:

字段名称为email,或者是email_addr的都是Email类型的字段,都需要按照Email的脱敏规则来脱敏

具体效果,我们接着看

2.调用脱敏工具类来脱敏

假如有这样一个DTO

Java 复制代码
public class Person {
   // id是非敏感字段,不需要脱敏
   private Long id;
   
   private String nickName;
   
   private String mobile;
   
   private String idCardAccount;
   // setter and gettr ...
}

我们调用脱敏组件来脱敏日志

Java 复制代码
log.info("个人信息:{}", DataMask.toJSONString(person));

打印出来的效果如下:

json 复制代码
个人信息:{"id":1,"nickName":"**忌","mobile":"177******90","idCardAccount":"146*********49"}

3.深度分析问题

了解好了大概背后,我们继续分析问题。现在的情况是:一台机器脱敏都正常 ,一台机器脱敏都异常

这说明跟用户的DTO是什么类型无关,大概率是因为一台机器加载到了脱敏规则,另一台机器没加载到脱敏规则。

还好我们在加载脱敏规则的时候打印了日志,那实际上就比较好办了,直接让用户去搜索应用启动时候加载规则的logger对象对应的所有日志就可以了

如下:从日志中,确实证实了我们的猜想:一台机器加载到了脱敏规则,一台机器没有加载到脱敏规则

问:那为什么一台机器加载到了脱敏规则,一台机器没有加载到脱敏规则呢?
答:会不会是因为一台机器打包的时候丢失了classpath下的data_mask.properties资源文件,另外一台没有丢失呢?
再问:那如果一台丢失了classpath下的data_mask.properties资源文件,那其他资源文件丢失了吗?
再答:显然classptah下的其他资源文件(如application.yml)没有丢失,不然应用启动应该就会有问题
再问:会不会都丢失了,application.yml实际上在SpringBoot启动的时候用的是外置的配置文件呢?

思考到这,你大概能明确一点:大概率可能不是因为缺失classpath下的data_mask.properties资源文件(因为打包后只有一个jar包文件,然后生成了同一个docker镜像,启动了不同的容器。当然也可以拉取到镜像,然后查看资源文件来验证)

到这,发现,data_mask.properties资源文件缺失这个猜想应该是不可能了

那还能是什么原因呢?猜是猜不到的,还得回到代码本身。

还真是,不看不知道,一看就是惊喜啊:

加载资源文件的ClassLoader用的是线程下上文ClassLoader

如果用的是线程下上文ClassLoader 来加载classpath下的资源文件的话,那么确实是有可能加载不到的,因为 线程下上文ClassLoader可以由用户自己指定的。

那这个就比较好验证了,全局搜索用户的代码仓库中调用java.lang.Thread#setContextClassLoader的地方就可以了

结果如下:

到这,应该有掌声。

请给我3秒钟装个×,不,不够给我5秒钟吧,算了,还是3秒钟吧。

1
2
3

看到这,看来真的是铁粉了。给你也鼓个掌吧

好的,我们继续装×,不,不,不,我们继续分析问题

是因为用户把 SystemClassLoader 作为 线程下上文ClassLoader ,然后脱敏组件使用SystemClassLoader去加载classpath下的data_mask.properties资源文件的,所以没有加载到data_mask.properties资源文件。
至于为什么SystemClassLoader 加载不到SpringBoot项目下classptah下的资源文件,大家有兴趣可以去了解一下SpringBoot的启动过程以及对应的SpringBoot的类加载器LaunchedURLClassLoader

4.用原因去解释现象

问题出现时,我们发现两个奇怪的现象:

  • 1.本地没有复现脱敏不成功的场景
  • 2.线上一台机器脱敏都正常,另一台机器脱敏都异常

当我们找到了问题的根因 后,需要使用根因来解释现象来反向验证根因是否是真的根因。

1.本地没有复现脱敏不成功的场景

本地是直接调用脱敏组件脱敏的,如下:

java 复制代码
DataMask.toJSONString(person)

这个时候的classloader实际上还是SystemClassLoader,但是却把本地项目的路径作为classpath传到了启动命令中: 大概如下:

shell 复制代码
-classpath :/xxx/xxx/your-project/xxx-service/target/classes:

这样本地就可以加载到本地的资源文件了

2.线上一台机器脱敏都正常,另一台机器脱敏都异常

  • 正常的那台机器使用的是SpringBootClassLoader(请求先到达的是线程不是CustomizeForkJoinWorkerThreadFactory new 出来的线程)
  • 异常的那台机器使用的是SystemClassLoader(请求先到达的是线程就是CustomizeForkJoinWorkerThreadFactory new 出来的线程)

这个其实可以通过日志中的thread来验证

5.如何解决

既然是因为请求先后顺讯导致使用的ClassLoader不一样,那么其实只需要在请求到达之前,确保使用一下脱敏组件 ,这样就能保证脱敏组件 加载资源的ClassLoader就是SpringBoot的ClassLoader 就可以了。

大概如下

java 复制代码
@SpringBootApplication  
public class Application {  
    public static void main(String[] args) {  
        DataMask.toJSONString(new Person())  
        SpringApplication.run(Application.class, args);  
    }  
}

当然,在脱敏组件这一侧,也是可以不要使用线程上下文ClassLoader来加载资源文件的,就固定使用加载DataMask类的类加载器来加载就可以了

6.番外篇

上面的问题解决后一段时间,又来了个差不多的问题,大致对话内容如下:

本来以为问题非常简单,结果却啪啪打脸。以为一样,却有优点不一样。

然后就开始排查问题,发现现象几乎一模一样:昨天正常的那台机器,加载到了脱敏规则资源文件data_mask.properties,今天异常的两台机器都没有加载到data_mask.properties资源文件。

然后我让研发把加载不到脱敏规则日志的那个线程对象看了下,如下:使用的是ForkJoinPool线程池

然后又看了ForkJoinPool源代码(如果看代码,觉得累,直接看最后一张图片就可以了)

然后搜索了一下用户代码,发现用了并行流(默认会使用ForkJoinPool线程池)

还以为有啥新奇的,原来还是老问题啊

到这里原因就比较明确了

由于用户使用了并行流,背后默认会使用ForkJoinPool线程池,由于默认情况下,ForkJoinPool线程池默认会把SystemClassLoader作为线程池上下文ClassLoader。所以一旦用户先请求的是使用并行流的接口,那么就会先使用SystemClassLoader来加载data_mask.properties资源文件,进而加载不到。如果新请求的不是使用并行流的接口,那么就是用SpringBoot的ClassLoader加载data_mask.properties资源文件,就可以加载到脱敏规则。

7.一点点闲聊

  • 不要轻易使用线程上下文ClassLoader 去加载classpath下的资源文件
  • 生产级别的配置文件最好外置化 ,不要使用ClassLoader去加载,因为不同ClassLoader会加载出来的结果不一样
  • 没有思路的时候,审视代码就是唯一的思路
  • 作为组件负责人,当一时半会定位不出问题的时候,不要浪费研发人员的宝贵时间来辅助你定位问题,直接使用对方代码调试即可
  • 所有难不倒你的问题,最终都会使你变得更加强大
相关推荐
懒洋洋大魔王7 分钟前
7.Java高级编程 多线程
java·开发语言·jvm
茶馆大橘11 分钟前
【黑马点评】已解决java.lang.NullPointerException异常
java·开发语言
星辰@Sea15 分钟前
服务注册中心对比及使用场景分析
java·云原生
马剑威(威哥爱编程)19 分钟前
除了递归算法,要如何优化实现文件搜索功能
java·开发语言·算法·递归算法·威哥爱编程·memoization
bug菌¹21 分钟前
滚雪球学SpringCloud[4.1讲]: Spring Cloud Gateway详解
java·spring cloud·微服务
MuseLss40 分钟前
HashMap高频面试知识点
java·开发语言·哈希算法
tyler-泰勒42 分钟前
初始c++:入门基础(完结)
java·开发语言·c++
重生之我要进大厂1 小时前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
_祝你今天愉快1 小时前
技术成神之路:设计模式(十四)享元模式
java·设计模式
小筱在线2 小时前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务