大家好筋量,我是tin桨武,這是我的第5篇原創(chuàng)文章
本文講述在考慮對業(yè)務(wù)系統(tǒng)代碼入侵最小的情況下實現(xiàn)日志脫敏的方案原理。文章很長性誉,包括了日志脫敏起由错览、編碼實現(xiàn)倾哺、log4j2.xml文件加載原理、log4j2的插件機制等却邓,最后還抖出注解編譯處理器AbstractProcessor,實現(xiàn)編譯期動態(tài)生成代碼衙耕!有點像撿到寶橙喘,畢竟以前沒關(guān)注過注解編譯處理器饰潜,先上一個目錄:
一彭雾、為什么做日志脫敏
二薯酝、log4j2日志脫敏編碼實現(xiàn)
三、源碼探索log4j2日志脫敏實現(xiàn)原理
1做葵、什么是slf4j酿矢?
2棠涮、log4j2又是什么?
3驳糯、slf4j和log4j2是如何完成綁定的酝枢?
4、log4j.xml配置文件是如何加載的竣付?
5古胆、我們定義log4j2的Plugin插件又是如何加載注冊的逸绎?
6巫糙、AbstractProcessor注解處理器四曲秉、朋友請留步
一、為什么做日志脫敏
日志打印非常常見且重要亥鸠,這毋庸置疑负蚊,但有這樣一種情況:我們打印的日志包含了用戶的隱私信息家妆,比如做登錄支付的打印用戶賬號和密碼,做金融的打印用戶的卡號等哨坪,這些日志先不說放在磁盤上管理不當可能造成用戶隱私泄露,再者就算是合規(guī)檢查忿偷,它也是不過關(guān)的,必須要做處理整治臊泌。
我們打日志是怎么打的鲤桥?先上一個圖(日志中打印我的用戶名和賬號),看看我們自己就是這么用的:
沒做特殊處理缺虐,不出意外芜壁,日志輸出是這樣的:
卡號打印出來了礁凡,隨后這行日志就安詳?shù)靥稍谖覀兎?wù)器磁盤上慧妄。
[圖片上傳失敗...(image-bdaa9d-1611469924727)]
二、log4j2日志脫敏編碼實現(xiàn)
如何借助日志框架實現(xiàn)對賬號打碼脫敏剪芍,而不入侵業(yè)務(wù)代碼塞淹?廢話不多說,先看看我已實現(xiàn)的效果:
本文基于slf4j+log4j2實現(xiàn)罪裹,我們代碼日志輸出處沒有任何改動饱普,打打印出來的日志對卡號做了打碼脫敏。
本文實現(xiàn)日志打碼脫敏的方案涉及開發(fā)的地方有兩個:
一是實現(xiàn)log4j2的RewritePolicy接口状共,重寫logEvent套耕;
二是修改log4j2.xml文件。
看看我寫的RewritePolicy實現(xiàn)類:
log4j2.xml修改峡继,下面是log4j2配置和rewrite配置:
這個文件也非常詳細地把log4j2.xml配置解釋了一遍冯袍,不是很清楚log4j配置的可留圖保存啦。
為了方便復(fù)制碾牌,把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如不設(shè)置,則默認為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如不設(shè)置,則默認為7個-->
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</appenders>
<!--Logger節(jié)點用來單獨指定日志的形式作儿,可以給不同包配置不同的日志打印策略洛二。-->
<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日志脫敏原理
為何上文這么做就能實現(xiàn)日志打碼脫敏?是有什么變法么晾嘶?實現(xiàn)的原理是什么妓雾?背著一大連串疑問,現(xiàn)在我們從slf4j和log4j2原理說起垒迂,來了械姻,搬好凳子了。
1机断、什么是slf4j楷拳?
slf4j全稱simple logging facade for Java。是一個日志接口框架吏奸,配合日志輸出系統(tǒng)實現(xiàn)日志輸出欢揖。slf4j并不是真正輸出日志的系統(tǒng),只是定義了一套日志規(guī)范奋蔚。類似這樣的日志門面還有commons-logging浸颓。
private static final Logger LOGGER = LoggerFactory.getLogger(AccountTest.class);
以上的Logger就是slf4j的類。
2旺拉、log4j2又是什么产上?
log4j2才是一個真正的日志系統(tǒng),它才是我們項目中打印日志的代碼庫實現(xiàn)蛾狗。除了log4j2晋涣,我們常見的日志庫還有l(wèi)og4j、logback沉桌、jdk-logging谢鹊。
slf4j作為連接log和代碼層的中間層,我們只要使用slf4j提供的接口留凭,不用關(guān)心日志的具體實現(xiàn)(想想這樣的好處是我們可以把業(yè)務(wù)系統(tǒng)內(nèi)日志庫比如log4j2換為logback也沒問題)佃扼。說起來有點像jdbc,我們切換不同的數(shù)據(jù)庫蔼夜,jdbc幫我們做好了兼容兼耀。
log4j2的依賴包有3個,slf4j和log4j2的幾個jar包關(guān)系作用如下:
3求冷、slf4j和log4j2是如何完成綁定的瘤运?
從上面圖都看到了,slf4j-api和log4j相關(guān)的包根本不在一起匠题,那么它們之間是通過什么關(guān)聯(lián)的拯坟?
答案是:
slf4j指定路徑進行類加載,log4j必然有橋接實現(xiàn)類韭山。 還是從這行定義和初始化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應(yīng)該在哪里梦裂?
當然是在log4j2包內(nèi)了秃诵!
通過StaticLoggerBinder這個類即完成了slf4j和log4j的綁定,看下圖塞琼。
綁定完之后即通過getLoggerFactory方法獲取到Log4jLoggerFactory:
log4j2和slf4j完成了綁定,那么禁舷,和本文所說的脫敏原理有什么關(guān)系彪杉?
脫敏的實現(xiàn)原理真正在于log4j2,以上只是展開說明日志系統(tǒng)的基本關(guān)聯(lián)原理牵咙,為接下來講述log4j的插件機制打個鋪墊派近。
log4j2 通過使用插件機制加載各種組件,比如appender, logger等洁桌,我們的脫敏方案編碼定義了一個類:
實現(xiàn)了log4j的rewrite策略類渴丸,這其實就是一種插件!
要講清楚Plugin原理得分兩部分講另凌。
一是log4j.xml配置文件是如何加載的谱轨;
二是我們定義的Plugin插件又是如何加載注冊的。
4吠谢、log4j.xml配置文件是如何加載的土童?
我們依然是通過斷點看源碼,畢竟工坊,源碼底下無謊言献汗! 還是從下面這行代碼開始看起:
上文已經(jīng)提到過Log4jLoggerFactory,它繼承了AbstractLoggerAdapter這個抽象類王污,我們直接進入到getContext方法獲取Logger的地方:
anchor中文譯為"錨"罢吃,這里是通過Java反射得到日志類,anchor不為null昭齐,因此進入到后面的語句尿招。
進入getContext,我們的Log4jContextFactory又出現(xiàn)了阱驾,它在LogManager中的靜態(tài)代碼塊中已初始化好泊业。
我們繼續(xù)到Log4jContextFactory內(nèi)看getContext:
已初始化好的selector,內(nèi)部具體如何獲取context有興趣可自行debug啊易,我們進入到ctx.start方法內(nèi):
看到reconfigure()方法吁伺,就知道log4j準備開始加載配置了!W馓浮篮奄!再從reconfigure一直往下看:
687行獲取得到一個XmlConfiguration捆愁,這是因為我們使用的是xml配置文件!?呷础昼丑!正常來說配置文件除了xml,還有properties夸赫,yaml菩帝,json等。
此處既然已獲得配置文件的內(nèi)容茬腿, 那么我們退回去看ConfigurationFactory.getInstance().getConfiguration(this,contextName,configURI,cl)呼奢。
看看XmlConfigurationFactory類
指定了xml后綴,getConfiguration實際返回XmlConfiguration
根據(jù)configSource的log4jx.xml文件切平,進行配置內(nèi)容加載握础。
到這里xml配置就算是加載完成啦。xml里面定義的<DataMaskingRewritePolicy/>標簽也會被加載悴品。
[圖片上傳失敗...(image-28c8f9-1611469924727)]
接下來禀综,自然而然的我們就會問,這個標簽和代碼@Plugin注解定義的插件是如何關(guān)聯(lián)起來的苔严?或者又說Plugin插件是如何加載的定枷?
5、我們定義的Plugin插件又是如何加載注冊的届氢?
log4j中的Plugin注解提供了一種便捷的方法將一個類聲明成 log4j2 的插件依鸥,比如我單測用到的案例:
在log4j2加載上下文的時候會加載Plugin,log4j統(tǒng)一用PluginRegistry注冊中心加載和注冊插件悼沈,并由PluginManager來管理贱迟。
進入到PluginManager:
注釋都寫得很清楚,從指定的指定文件Log4j2Plugin.dat加載插件絮供,繼續(xù)進入loadFromMainClassLoader方法
不同模塊不同jar包都有可能存在Log4j2Plugins.dat文件衣吠,比如log4j-core包存在
我們自己編寫代碼定義的插件則被編譯到target目錄下(因為我的是mac,在控制臺的看得壤靶,win系統(tǒng)也一樣找到編譯產(chǎn)生的target文件夾即可)
我們編譯生成的Log4j2Plugins.dat里面的內(nèi)容又是什么呢缚俏?
文件記錄了插件分類、全限定類名等信息贮乳。
說到這里忧换,產(chǎn)生新的一個疑問,我們自己的Log4j2Plugins.dat 文件究竟是如何被生成到target目錄下的向拆?
6亚茬、AbstractProcessor注解處理器
這不得不說我們的注解編譯處理器咯!注解分為兩種類型浓恳,一種是運行時注解刹缝,另一種是編譯時注解。編譯時注解的核心要依賴APT(Annotation Processing Tools)實現(xiàn)梢夯,基本原理就是在類颂砸、方法噪奄、字段等上添加注解,在編譯時人乓,編譯器通過掃描AbstractProcessor的子類,把對應(yīng)合適的注解傳入process函數(shù),然后我們開發(fā)人員可以在編譯期進行相應(yīng)的邏輯處理了温鸽”M停看看log4j實現(xiàn)的注解編譯處理器:
我們平常編碼很少會用到注解編譯處理器,有興趣可自行寫單元測試試一試姑尺,這種沒玩過的代碼寫起來還挺有趣的蝠猬。不過自行寫的話需要聲明好javax.annotation.processing.Processor文件,再補一張log4j聲明的文件:
四柄粹、朋友請留步
我是tin匆绣,一個在努力讓自己變得更優(yōu)秀的普通攻城獅。閱歷有限堪夭、學(xué)識淺薄拣凹,如你有發(fā)現(xiàn)文章不妥之處嚣镜,非常歡迎加我提出,我一定細心推敲加以修改雕旨。
看到這里請安排個鼓勵再走吧,堅持原創(chuàng)不容易棒搜,你的正反饋是我堅持輸出的最強大動力活箕,謝謝啦。
[圖片上傳失敗...(image-782447-1611469924727)]
總結(jié)克蚂、提升
做一個快樂的攻城獅
構(gòu)筑屬于自己的一方天地