本文由一個(gè)攔截器邏輯的使用場(chǎng)景及演變歷程,引入字節(jié)碼增強(qiáng)技術(shù)。介紹字節(jié)碼的本質(zhì),字節(jié)碼增強(qiáng)的原理及JVM 啟動(dòng)過(guò)程中的 Agent 加載、生效流程,并對(duì)常見(jiàn)字節(jié)碼操作工具進(jìn)行了簡(jiǎn)單應(yīng)用。
注:本文僅討論 javaagent “啟動(dòng)時(shí)加載”。
一、技術(shù)為業(yè)務(wù)需求服務(wù)
技術(shù)是工具,是解決問(wèn)題的途徑。針對(duì)不同的業(yè)務(wù)需求場(chǎng)景,可以使用不同的技術(shù)實(shí)現(xiàn)。
通過(guò)一部攔截器的流浪史來(lái)引入主題:
一個(gè)簡(jiǎn)單的demo
1、基礎(chǔ)版:新建一個(gè)Dog對(duì)象,然后調(diào)用成員方法輸出到控制臺(tái)
被調(diào)用方
調(diào)用方
2、加強(qiáng)版:需要統(tǒng)計(jì)方法執(zhí)行的時(shí)間
常規(guī)開(kāi)發(fā):
被調(diào)用方
調(diào)用方
3、從被調(diào)用方剝離非業(yè)務(wù)邏輯
面向?qū)ο笤O(shè)計(jì)原則,對(duì)象應(yīng)該盡可能專注自己職責(zé)范圍內(nèi)的事情,狗只負(fù)責(zé)叫,不負(fù)責(zé)統(tǒng)計(jì)自己叫了多長(zhǎng)時(shí)間,因此統(tǒng)計(jì)代碼應(yīng)該移出Dog類。
3.1 方法提取
3.2 類提取–(參考SpringMVC-Interceptor)
3.3 類解耦合(使用動(dòng)態(tài)代理方式-CGLib/JDK Proxy,這里Dog類沒(méi)有實(shí)現(xiàn)接口,使用CGLib)
至此,非業(yè)務(wù)邏輯由從被調(diào)用方剝離出來(lái)了,同時(shí)我們也發(fā)現(xiàn)調(diào)用方代碼卻遭到改變,Main class里面需要添加動(dòng)態(tài)代理類的處理邏輯。假如不允許改變調(diào)用方代碼,進(jìn)一步處理。
4、調(diào)用方代碼剝離(切面–AspectJ)
切面
被調(diào)方
調(diào)用方
注意:此時(shí)直接運(yùn)行Main class切面不會(huì)生效,運(yùn)行前先進(jìn)行編譯期織入 java -jar ASPECTJ_RT -sourceroots src/main/java/ -d target/classes ...
至此,調(diào)用方不用顯式地調(diào)用動(dòng)態(tài)代理邏輯,編譯期織入到class中去了(這里已經(jīng)聞到了代碼增強(qiáng)的氣味了)。
切面邏輯雖然與具體的業(yè)務(wù)邏輯解耦合了,獨(dú)立出切面類。但是是否生效仍然由業(yè)務(wù)代碼(切面類)去控制。無(wú)論如何,都需要業(yè)務(wù)方改造,添加切面邏輯代碼。
能不能更進(jìn)一步,連切面都不寫(xiě),也讓切面邏輯生效呢?
5、javaagent 版本–隱式地,無(wú)侵入地添加切面邏輯
新建獨(dú)立的agent工程
添加MANIFEST.MF文件以及Premain-Class,premain屬性
編譯包含目標(biāo)邏輯的源文件生成class文件
注冊(cè)ClassFileTransfer,在transform方法中替換byte[]
MANIFEST.MF指定premain函數(shù)和打開(kāi)類增強(qiáng)開(kāi)關(guān)
編譯輸出jar包
MANIFEST.MF文件。
待替換的新class文件(忽略中文亂碼)。
class轉(zhuǎn)換器,將新的Dog.class替換舊的Dog.class。
maven打包輸出Agent.jar
上面的javaagent實(shí)現(xiàn)細(xì)節(jié)可以先存疑,后面會(huì)深入描述,只需要知道按照這樣的步驟可以實(shí)現(xiàn)我們的需求。
對(duì)于業(yè)務(wù)方而言:代碼完全沒(méi)有變化:
被調(diào)用方
調(diào)用方
想要使切面邏輯生效,只需要在啟動(dòng)命令參數(shù)中加入-javaagent 選項(xiàng),指向 Agent 的 jar 包。
這樣,攔截器邏輯以一種插件的形式抽取出來(lái)了,使用的時(shí)候加載插件就可以了。
小結(jié)一下
不同需求場(chǎng)景下,可以不同的方式實(shí)現(xiàn)切面攔截邏輯;
AspectJ或者SpringAop只是一種對(duì)開(kāi)發(fā)者友好的快捷方式,本質(zhì)上還是修改的業(yè)務(wù)代碼,只不過(guò)隱藏了調(diào)用邏輯,并不能真正“無(wú)侵入“;
javaagent可以無(wú)侵入的修改一個(gè)已發(fā)布的java組件的運(yùn)行邏輯。
二、什么是字節(jié)碼?
byte[]
1、回歸原始:JDK 里面提供了很多有用的工具
在我們剛開(kāi)始學(xué)習(xí) Java 語(yǔ)言時(shí)候的 demo 運(yùn)行:
編寫(xiě)原始 Java 文件:
使用 Javac 編譯字節(jié)碼文件:
Javac生產(chǎn)的 class 文件有什么作用呢?
Java 語(yǔ)言一次編譯,到處運(yùn)行的核心基礎(chǔ)-JVM。
2、class文件到底是個(gè)什么東西?
先用文本編輯器暴力打開(kāi)看看:
看不懂?換個(gè)方式:
想看個(gè)明白?繼續(xù)整:使用010editor打開(kāi)。
各個(gè)數(shù)據(jù)項(xiàng)按順序緊密的從前向后排列, 相鄰的項(xiàng)之間沒(méi)有間隙, 這樣可以使得class文件非常緊湊, 體積輕巧, 可以被JVM快速的加載至內(nèi)存, 并且占據(jù)較少的內(nèi)存空間。
主要包含的信息:
(1)魔數(shù)
(2)版本號(hào)(參考文末例子:JRE版本錯(cuò)誤)
(3)常量池容量
(4)常量池:
文字字符串, 常量值
當(dāng)前類的類名, 字段名, 方法名, 各個(gè)字段和方法的描述符
對(duì)當(dāng)前類的字段和方法的引用信息, 當(dāng)前類中對(duì)其他類的引用信息等等
(5)其他屬性
常量池如何索引:
相互索引:
例如方法索引,獲取classIndex和nameAndTypeIndex,通過(guò)數(shù)組下標(biāo),可以找到該方法所屬的class和方法名稱。
MethodRef
|-----|classIndex
|-----|-----|nameIndex --→ classNmae
|-----|nameAndTypeIndex
|-----|-----|nameIndex --→ methodName
常量池索引和字節(jié)碼指令的執(zhí)行。
使用jre自帶工具javap反編譯class文件如下:
Main.class字節(jié)碼:
Dog.class字節(jié)碼:
可以看到字節(jié)碼具備一定的可讀性,對(duì)照著源碼,可以按照?qǐng)?zhí)行邏輯走一遍字節(jié)碼執(zhí)行流程,相關(guān)指令的含義很容易從網(wǎng)上查詢到。
至此,我們通過(guò)一個(gè)簡(jiǎn)單的demo執(zhí)行流程,大致了解了常量的引用以及一個(gè)簡(jiǎn)單java方法對(duì)應(yīng)的字節(jié)碼指令執(zhí)行過(guò)程。
注:
**stack:**最大操作數(shù)棧,JVM運(yùn)行時(shí)會(huì)根據(jù)這個(gè)值來(lái)分配棧幀(Frame)中的操作棧深度。
**locals:**局部變量所需的存儲(chǔ)空間,單位為Slot。
args_size: 方法參數(shù)的個(gè)數(shù)。
壓棧:字節(jié)碼指令執(zhí)行過(guò)程中涉及到了很多壓棧操作:JVM是一個(gè)基于棧的架構(gòu)。方法執(zhí)行的時(shí)候(包括main方法),在棧上會(huì)分配一個(gè)新的幀,這個(gè)棧幀包含一組局部變量。
這組局部變量包含了方法運(yùn)行過(guò)程中用到的所有變量,包括this引用,所有的方法參數(shù),以及其它局部定義的變量。
小結(jié)一下
class文件即字節(jié)碼是所有屬性,方法邏輯的合集。
通過(guò)字節(jié)碼二進(jìn)制文件將開(kāi)發(fā)者與虛擬機(jī)進(jìn)行了“解耦”。
推理:修改某些字節(jié)或者替換整個(gè)二進(jìn)制流可以修改運(yùn)行時(shí)邏輯 。
三、如何增強(qiáng)字節(jié)碼?
byte[] → byte[]
思路:
如前述方式直接替換為目標(biāo)邏輯編譯后的字節(jié)碼。
手術(shù)刀式精準(zhǔn)操作,修改/添加某些位置的byte。
高級(jí)API。
工具集:/ASM/javaassist/ByteBuddy 等等。
示例:
ASM
指令級(jí)別的字節(jié)碼操作(性能強(qiáng)悍)。
指令→ASM api 對(duì)應(yīng)關(guān)系(這里將原始類做了簡(jiǎn)化,將字符串拼接邏輯去掉,僅僅輸出時(shí)間。因?yàn)橐粋€(gè)簡(jiǎn)單的字符串拼接過(guò)程,轉(zhuǎn)換成字節(jié)碼指令可能需要很多行)。
先看看目標(biāo)源碼與字節(jié)碼指令的一一映射關(guān)系。
再看看增強(qiáng)字節(jié)碼邏輯與目標(biāo)源碼的字節(jié)碼的一一映射關(guān)系。
通過(guò)對(duì)比我們可以發(fā)現(xiàn),ASM的API精確到字節(jié)碼指令級(jí)別,所有的臨時(shí)變量存儲(chǔ),壓棧操作,靜態(tài)/實(shí)例方法的調(diào)用都有對(duì)應(yīng)的API操作。
javassist:(dubbo)
提供字節(jié)碼級(jí)別的API,類似ASM,不再贅述。
提供源碼級(jí)別的API,針對(duì)本文的案例,實(shí)現(xiàn)如下:
ByteBuddy
基于ASM的高級(jí)API,使我們對(duì)字節(jié)碼的操作提升到更抽象層次。開(kāi)發(fā)者只需要知道要實(shí)現(xiàn)什么目標(biāo),如何使用對(duì)應(yīng)的API,不用關(guān)心底層的字節(jié)碼指令排列,甚至可以不用了解字節(jié)碼指令。
關(guān)于相關(guān)框架的API不詳細(xì)說(shuō),有興趣的同學(xué)可以自行查詢相關(guān)資料。
小結(jié)一下
各種級(jí)別的API可以幫助開(kāi)發(fā)者輕松實(shí)現(xiàn)字節(jié)碼增強(qiáng),實(shí)現(xiàn)特定邏輯。
不論什么奇技淫巧,都離不開(kāi)Instrumentation機(jī)制。
四、增強(qiáng)的 byte[] 是如何影響 JVM 的?
Event --> CallBack
由前文總結(jié),引入Instrumentation機(jī)制。
1、鋪墊知識(shí)點(diǎn):
(1)JVMTI
JVMTI 是基于事件驅(qū)動(dòng)的,JVM 每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開(kāi)發(fā)者擴(kuò)展自己的邏輯。
JVMTIAgent 使用JVMTI來(lái)查詢或控制JVM,JVMTIAgent與目標(biāo)JVM運(yùn)行在同一個(gè)進(jìn)程中,通過(guò)JVMTI進(jìn)行通信,最大化控制能力,最小化通信成本。
典型場(chǎng)景下,JVMTI代理會(huì)被實(shí)現(xiàn)的非常緊湊,其他的進(jìn)程會(huì)與JVMTI代理進(jìn)行通信。比如jdwp(IDEA遠(yuǎn)程調(diào)試)。
(2)JVMTIAgent
表現(xiàn)形式:
(1)linux: .so文件
windows: .dll文件
c/c++ 動(dòng)態(tài)鏈接庫(kù)
(2)JPLISAgent: .jar文件
命令行參數(shù)
(1)-agentlib:agent-lib-name=options
(2)-agentpath:path-to-agent=options
(3)-javaagent:/data/../../Agent.jar
- 可加載多個(gè),通過(guò)options區(qū)分
實(shí)現(xiàn)接口
(1)JNIEXPORT jint JNICALL
- Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
(2)JNIEXPORT jint JNICALL
- Agent_OnAttach(JavaVM vm, char options, void* reserved);
(3)JNIEXPORT void JNICALL
- Agent_OnUnload(JavaVM *vm);
JPLISAgent(Java Programming Language Instrumentation Services Agent)-- Instrumentation機(jī)制
(1)JavaSE1.5 啟動(dòng)時(shí)加載(本文重點(diǎn))。
(2)JavaSE1.6 運(yùn)行時(shí)加載。
2、簡(jiǎn)化了的核心流程邏輯
命令參數(shù):-javaagent:/data/../../Agent.jar=optoions。
虛擬機(jī)創(chuàng)建-構(gòu)建并初始化Agent-注冊(cè)VMInit事件。
虛擬機(jī)初始化-觸發(fā)VMInit事件-Agent start方法-注冊(cè)回調(diào)函數(shù)并監(jiān)聽(tīng)ClassFileLoadHook。
類加載-觸發(fā)jvmtiEventClassFileLoadHook事件-替換byt[]-ClassLoader解析。
3、Java 虛擬機(jī)啟動(dòng)過(guò)程中 Agent 相關(guān)的流程:
(1)創(chuàng)建JVM的時(shí)候初始化agent
啟動(dòng)時(shí)讀取jvm命令,-agentlib -agentpath -javaagent,并構(gòu)建了Agent Library鏈表構(gòu)建了Agent Library鏈表。
對(duì)agent鏈表中的每個(gè)agent,加載所指定的動(dòng)態(tài)庫(kù)(如instrument.so), 并調(diào)用里面的Agent_OnLoad方法。
創(chuàng)建并初始化 JPLISAgent,初始化了Premain class和包里的配置文件。
注冊(cè)VMInit事件。
Agent_onLoad
|-----|createNewJPLISAgent
|-----|-----|initializeJPLISAgent
|-----|-----|-----|eventHandlerVMInit ---- > VMInit
(2)虛擬機(jī)初始化
實(shí)際上是調(diào)用 java 類 sun.instrument.InstrumentationImpl 類里的方法loadClassAndCallPremain。
(3)觸發(fā)ClassFileLoadHook事件
|parseClassFile
|-----|post_class_file_load_hook
|-----|-----|post_to_env
|-----|-----|-----|eventHandlerClassFileLoadHook(jvmtiEventClassFileLoadHook回調(diào)函數(shù))
|-----|-----|-----|-----|transformClassFile
|-----|-----|-----|-----|-----|CallObjectMethod
|-----|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.transform()
實(shí)際調(diào)用的java方法 Instrumentationimpl.transform。
debug過(guò)程中通過(guò)ClassFileTransformer的transform函數(shù)的執(zhí)行堆棧印證。
到這里,增強(qiáng)的byte[]如何生效并影響運(yùn)行時(shí)class的過(guò)程基本可以串起來(lái)。
小結(jié)一下
虛擬機(jī)創(chuàng)建階段,初始化agent,解析,加載javaagent jar,注冊(cè)回調(diào)函數(shù)監(jiān)聽(tīng)VMInt事件。
虛擬機(jī)初始化階段,觸發(fā)VMInt回調(diào)函數(shù),注冊(cè)回調(diào)函數(shù)監(jiān)聽(tīng)ClassFileHook事件,同時(shí)執(zhí)行l(wèi)oadClassAndCallPremain函數(shù),注冊(cè)transformer。
ClassLoader加載類的時(shí)候觸發(fā)tranform回調(diào),判斷是否目標(biāo)類,進(jìn)行對(duì)應(yīng)字節(jié)碼替換。
五、應(yīng)用
監(jiān)控
調(diào)試
混淆
AOP增強(qiáng)
日志記錄
非常規(guī)應(yīng)用:IDEA破解。
部分破解教程里面下載插件jar后,會(huì)要求你在IDEA的啟動(dòng)參數(shù)文件idea.vmoptions中添加一行,就是javaagent參數(shù)。
我們可以反編譯這個(gè)插件jar包看看,發(fā)現(xiàn)很多class因?yàn)榧恿嘶煜淳幾g后無(wú)法正常識(shí)別,但是核心入口Agent.class的主要工作就是注冊(cè)Transformer,可以推測(cè)這些Transformer的功能就是在IDEA啟動(dòng)時(shí)之前修改某些鑒定Lisence的邏輯。
六、總結(jié)回顧
通過(guò)介紹字節(jié)碼,字節(jié)碼操作工具以及openJDK關(guān)于Instrumention機(jī)制的部分源碼,探索了字節(jié)碼增強(qiáng)的實(shí)現(xiàn)原理。
簡(jiǎn)單介紹了相關(guān)技術(shù)的應(yīng)用場(chǎng)景。
七、附錄
- SpringMVC-Interceptor
- IDEA 遠(yuǎn)程調(diào)試
- JRE版本錯(cuò)誤
作者: Neo