大家好,我是 tin,这是我的第 5 篇原创文章
本文讲述在思考对业务零碎代码入侵最小的状况下实现日志脱敏的计划原理。文章很长,包含了日志脱敏起由、编码实现、log4j2.xml 文件加载原理、log4j2 的插件机制等,最初还抖出注解编译处理器 AbstractProcessor,实现编译期动静生成代码!有点像捡到宝,毕竟以前没关注过注解编译处理器,先上一个目录:
- 一、为什么做日志脱敏
- 二、log4j2 日志脱敏编码实现
- 三、源码摸索 log4j2 日志脱敏实现原理
1、什么是 slf4j?
2、log4j2 又是什么?
3、slf4j 和 log4j2 是如何实现绑定的?
4、log4j.xml 配置文件是如何加载的?
5、咱们定义 log4j2 的 Plugin 插件又是如何加载注册的?
6、AbstractProcessor 注解处理器 - 四、敌人请留步
一、为什么做日志脱敏
日志打印十分常见且重要,这毋庸置疑,但有这样一种状况:咱们打印的日志蕴含了用户的隐衷信息,比方做登录领取的打印用户账号和明码,做金融的打印用户的卡号等,这些日志先不说放在磁盘上治理不当可能造成用户隐衷泄露,再者就算是合规查看,它也是不过关的,必须要做解决整治。
咱们打日志是怎么打的?先上一个图 (日志中打印我的用户名和账号),看看咱们本人就是这么用的:
没做非凡解决,不出意外,日志输入是这样的:
卡号打印进去了,随后这行日志就安详地躺在咱们服务器磁盘上。
二、log4j2 日志脱敏编码实现
如何借助日志框架实现对账号打码脱敏,而不入侵业务代码?废话不多说,先看看我已实现的成果:
本文基于 slf4j+log4j2 实现,咱们代码日志输入处没有任何改变,打打印进去的日志对卡号做了打码脱敏。
本文实现日志打码脱敏的计划波及开发的中央有两个:
一是实现 log4j2 的 RewritePolicy 接口,重写 logEvent;
二是批改 log4j2.xml 文件。
看看我写的 RewritePolicy 实现类:
log4j2.xml 批改,上面是 log4j2 配置和 rewrite 配置:
这个文件也十分具体地把 log4j2.xml 配置解释了一遍,不是很分明 log4j 配置的可留图保留啦。
为了不便复制,把 log4j2.xml 配置的源码粘贴一份进去:
<?xml version="1.0" encoding="UTF-8"?>
<configuration monitorInterval="5">
<!-- 变量配置 -->
<Properties>
<!-- 格式化输入:%date 示意日期,HH:mm:ss.SSS 示意日期格局,%thread 示意线程名,%-5level 示意级别从左显示 5 个字符宽度,%C{1.} 示意类全限定名输出精度,%-4L 输入日志所在行行号,%msg 代表日志音讯,%n 是换行符 -->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %C{1.} %-4L : %msg%n"/>
<!-- 定义日志存储的门路.${web:rootDir} 示意以后工程目录,-->
<property name="FILE_PATH" value="../log/tin-example"/>
<property name="FILE_NAME" value="tin-example"/>
</Properties>
<appenders>
<!-- 控制台输入 -->
<console name="Console" target="SYSTEM_OUT">
<!-- 输入日志的格局 -->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!-- 示意输入 level=debug 级别及以上日志(onMatch),debug 级别以下不输入(onMismatch)-->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<Rewrite name="rewrite">
<DataMaskingRewritePolicy/>
<AppenderRef ref="Console"/>
</Rewrite>
<!-- 打印出所有级别的日志信息,并主动滚动存档 -->
<RollingFile name="AllLevelRollingFile" fileName="${FILE_PATH}/${FILE_NAME}.log"
filePattern="${FILE_PATH}/${FILE_NAME}-ALL-%d{yyyy-MM-dd}_%i.log.gz">
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="ACCEPT"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval 属性用来指定多久滚动一次,interval= 1 示意 1 小时滚动一次 -->
<TimeBasedTriggeringPolicy interval="1"/>
<!--size=20 示意文件大于 20M 滚动一次 -->
<SizeBasedTriggeringPolicy size="20MB"/>
</Policies>
<!-- max=15 示意同文件夹下最多 10 个文件,再多则会笼罩,DefaultRolloverStrategy 如不设置,则默认为 7 个 -->
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<!-- 打印出所有 error 及以上级别的信息,并主动滚动存档 -->
<RollingFile name="ErrorRollingFile" fileName="${FILE_PATH}/error.log"
filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<!-- 输入 level 及以上级别的信息(onMatch),level 以下间接回绝(onMismatch)-->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval 属性用来指定多久滚动一次,interval= 1 示意 1 小时滚动一次 -->
<TimeBasedTriggeringPolicy interval="1"/>
<!--size=20 示意文件大于 20M 滚动一次 -->
<SizeBasedTriggeringPolicy size="20MB"/>
</Policies>
<!-- max=15 示意同文件夹下最多 10 个文件,再多则会笼罩,DefaultRolloverStrategy 如不设置,则默认为 7 个 -->
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</appenders>
<!--Logger 节点用来独自指定日志的模式,能够给不同包配置不同的日志打印策略。-->
<loggers>
<logger name="com.tin.example.spring.log4j2" level="info" additivity="false">
<AppenderRef ref="rewrite"/>
</logger>
<root level="debug">
<appender-ref ref="Console"/>
<appender-ref ref="AllLevelRollingFile"/>
<appender-ref ref="ErrorRollingFile"/>
</root>
</loggers>
</configuration>
三、源码摸索 log4j2 日志脱敏原理
为何上文这么做就能实现日志打码脱敏?是有什么变法么?实现的原理是什么?背着一大连串疑难,当初咱们从 slf4j 和 log4j2 原理说起,来了,搬好凳子了。
1、什么是 slf4j?
slf4j 全称 simple logging facade for Java。是一个日志接口框架,配合日志输入零碎实现日志输入。slf4j 并不是真正输入日志的零碎,只是定义了一套日志标准。相似这样的日志门面还有 commons-logging。
private static final Logger LOGGER = LoggerFactory.getLogger(AccountTest.class);
以上的 Logger 就是 slf4j 的类。
2、log4j2 又是什么?
log4j2 才是一个真正的日志零碎,它才是咱们我的项目中打印日志的代码库实现。除了 log4j2,咱们常见的日志库还有 log4j、logback、jdk-logging。
slf4j 作为连贯 log 和代码层的中间层,咱们只有应用 slf4j 提供的接口,不必关怀日志的具体实现(想想这样的益处是咱们能够把业务零碎内日志库比方 log4j2 换为 logback 也没问题)。说起来有点像 jdbc,咱们切换不同的数据库,jdbc 帮咱们做好了兼容。
log4j2 的依赖包有 3 个,slf4j 和 log4j2 的几个 jar 包关系作用如下:
3、slf4j 和 log4j2 是如何实现绑定的?
从下面图都看到了,slf4j-api 和 log4j 相干的包基本不在一起,那么它们之间是通过什么关联的?
答案是:
slf4j 指定门路进行类加载,log4j 必然有桥接实现类。还是从这行定义和初始化 Logger 的代码开始看起:
private static final Logger LOGGER = LoggerFactory.getLogger(AccountTest.class);
从 LoggerFactory.getLogger 始终进入到 LoggerFactory 类的 bind 办法,找到 staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(),这里即是 slf4j 实现绑定 log4j2 的中央:
findPossibleStaticLoggerBinderPathSet() 通过指定门路加载一个 StaticLoggerBinder 类:
指定查找 org/slf4j/impl/StaticLoggerBinder.class 进行加载。
那么 StaticLoggerBinder 应该在哪里?
当然是在 log4j2 包内了!
通过 StaticLoggerBinder 这个类即实现了 slf4j 和 log4j 的绑定,看下图。
绑定完之后即通过 getLoggerFactory 办法获取到 Log4jLoggerFactory:
log4j2 和 slf4j 实现了绑定,那么,和本文所说的脱敏原理有什么关系?
脱敏的实现原理真正在于 log4j2,以上只是开展阐明日志零碎的根本关联原理,为接下来讲述 log4j 的插件机制打个铺垫。
log4j2 通过应用插件机制加载各种组件,比方 appender, logger 等,咱们的脱敏计划编码定义了一个类:
实现了 log4j 的 rewrite 策略类,这其实就是一种插件!
要讲清楚 Plugin 原理得分两局部讲。
一是 log4j.xml 配置文件是如何加载的;
二是咱们定义的 Plugin 插件又是如何加载注册的。
4、log4j.xml 配置文件是如何加载的?
咱们仍然是通过断点看源码,毕竟,源码底下无谎话!还是从上面这行代码开始看起:
上文曾经提到过 Log4jLoggerFactory,它继承了 AbstractLoggerAdapter 这个抽象类,咱们间接进入到 getContext 办法获取 Logger 的中央:
anchor 中文译为 ” 锚 ”,这里是通过 Java 反射失去日志类,anchor 不为 null,因而进入到前面的语句。
进入 getContext,咱们的 Log4jContextFactory 又呈现了,它在 LogManager 中的动态代码块中已初始化好。
咱们持续到 Log4jContextFactory 内看 getContext:
已初始化好的 selector,外部具体如何获取 context 有趣味可自行 debug,咱们进入到 ctx.start 办法内:
看到 reconfigure() 办法,就晓得 log4j 筹备开始加载配置了!!!再从 reconfigure 始终往下看:
687 行获取失去一个 XmlConfiguration,这是因为咱们应用的是 xml 配置文件!!!失常来说配置文件除了 xml,还有 properties,yaml,json 等。
此处既然已取得配置文件的内容,那么咱们退回去看 ConfigurationFactory.getInstance().getConfiguration(this,contextName,configURI,cl)。
看看 XmlConfigurationFactory 类
指定了 xml 后缀,getConfiguration 理论返回 XmlConfiguration
依据 configSource 的 log4jx.xml 文件,进行配置内容加载。
到这里 xml 配置就算是加载实现啦。xml 外面定义的 <DataMaskingRewritePolicy/> 标签也会被加载。
接下来,自然而然的咱们就会问,这个标签和代码 @Plugin 注解定义的插件是如何关联起来的?或者又说 Plugin 插件是如何加载的?
5、咱们定义的 Plugin 插件又是如何加载注册的?
log4j 中的 Plugin 注解提供了一种便捷的办法将一个类申明成 log4j2 的插件,比方我单测用到的案例:
在 log4j2 加载上下文的时候会加载 Plugin,log4j 对立用 PluginRegistry 注册核心加载和注册插件,并由 PluginManager 来治理。
进入到 PluginManager:
正文都写得很分明,从指定的指定文件 Log4j2Plugin.dat 加载插件,持续进入 loadFromMainClassLoader 办法
不同模块不同 jar 包都有可能存在 Log4j2Plugins.dat 文件,比方 log4j-core 包存在
咱们本人编写代码定义的插件则被编译到 target 目录下 (因为我的是 mac,在控制台的看得,win 零碎也一样找到编译产生的 target 文件夹即可)
咱们编译生成的 Log4j2Plugins.dat 外面的内容又是什么呢?
文件记录了插件分类、全限定类名等信息。
说到这里,产生新的一个疑难,咱们本人的 Log4j2Plugins.dat 文件到底是如何被生成到 target 目录下的?
6、AbstractProcessor 注解处理器
这不得不说咱们的注解编译处理器咯!注解分为两种类型,一种是运行时注解,另一种是编译时注解。编译时注解的外围要依赖 APT(Annotation Processing Tools) 实现,基本原理就是在类、办法、字段等上增加注解,在编译时,编译器通过扫描 AbstractProcessor 的子类,把对应适合的注解传入 process 函数,而后咱们开发人员能够在编译期进行相应的逻辑解决了。看看 log4j 实现的注解编译处理器:
咱们平时编码很少会用到注解编译处理器,有趣味可自行写单元测试试一试,这种没玩过的代码写起来还挺乏味的。不过自行写的话须要申明好 javax.annotation.processing.Processor 文件,再补一张 log4j 申明的文件:
四、敌人请留步
我是 tin,一个在致力让本人变得更优良的一般攻城狮。经历无限、学识肤浅,如你有发现文章不妥之处,十分欢送加我提出,我肯定仔细斟酌加以批改。
看到这里请安顿个激励再走吧,保持原创不容易,你的正反馈是我保持输入的最弱小能源,谢谢啦。
总结、晋升
做一个高兴的攻城狮
构筑属于本人的一方天地