本文由一個攔截器邏輯的使用場景及演變歷程,引入字節碼加強技術。介紹字節碼的本質,字節碼加強的原理及JVM 啓動過程當中的 Agent 加載、生效流程,並對常見字節碼操做工具進行了簡單應用。java
注:本文僅討論 javaagent 「啓動時加載」。linux
技術是工具,是解決問題的途徑。針對不一樣的業務需求場景,可使用不一樣的技術實現。c++
經過一部攔截器的流浪史來引入主題:segmentfault
被調用方windows
調用方api
常規開發:數組
被調用方架構
調用方框架
面向對象設計原則,對象應該儘量專一本身職責範圍內的事情,狗只負責叫,不負責統計本身叫了多長時間,所以統計代碼應該移出Dog類。jvm
至此,非業務邏輯由從被調用方剝離出來了,同時咱們也發現調用方代碼卻遭到改變,Main class裏面須要添加動態代理類的處理邏輯。假如不容許改變調用方代碼,進一步處理。
切面
被調方
調用方
注意:此時直接運行Main class切面不會生效,運行前先進行編譯期織入 java -jar $ASPECTJ\_TOOLS -cp $ASPECTJ_RT -sourceroots src/main/java/ -d target/classes ...
至此,調用方不用顯式地調用動態代理邏輯,編譯期織入到class中去了(這裏已經聞到了代碼加強的氣味了)。
切面邏輯雖然與具體的業務邏輯解耦合了,獨立出切面類。可是是否生效仍然由業務代碼(切面類)去控制。不管如何,都須要業務方改造,添加切面邏輯代碼。
能不能更進一步,連切面都不寫,也讓切面邏輯生效呢?
MANIFEST.MF文件。
待替換的新class文件(忽略中文亂碼)。
class轉換器,將新的Dog.class替換舊的Dog.class。
maven打包輸出Agent.jar
上面的javaagent實現細節能夠先存疑,後面會深刻描述,只須要知道按照這樣的步驟能夠實現咱們的需求。
對於業務方而言:代碼徹底沒有變化:
被調用方
調用方
想要使切面邏輯生效,只須要在啓動命令參數中加入-javaagent 選項,指向 Agent 的 jar 包。
這樣,攔截器邏輯以一種插件的形式抽取出來了,使用的時候加載插件就能夠了。
byte[]
在咱們剛開始學習 Java 語言時候的 demo 運行:
編寫原始 Java 文件:
使用 Javac 編譯字節碼文件:
Javac生產的 class 文件有什麼做用呢?
Java 語言一次編譯,處處運行的核心基礎-JVM。
先用文本編輯器暴力打開看看:
看不懂?換個方式:
想看個明白?繼續整:使用010editor打開。
各個數據項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙, 這樣可使得class文件很是緊湊, 體積輕巧, 能夠被JVM快速的加載至內存, 而且佔據較少的內存空間。
主要包含的信息:
(1)魔數
(2)版本號(參考文末例子:JRE版本錯誤)
(3)常量池容量
(4)常量池:
(5)其餘屬性
常量池如何索引:
相互索引:
例如方法索引,獲取classIndex和nameAndTypeIndex,經過數組下標,能夠找到該方法所屬的class和方法名稱。
MethodRef
|-----|classIndex |-----|-----|nameIndex --→ classNmae |-----|nameAndTypeIndex |-----|-----|nameIndex --→ methodName
常量池索引和字節碼指令的執行。
使用jre自帶工具javap反編譯class文件以下:
Main.class字節碼:
Dog.class字節碼:
能夠看到字節碼具有必定的可讀性,對照着源碼,能夠按照執行邏輯走一遍字節碼執行流程,相關指令的含義很容易從網上查詢到。
至此,咱們經過一個簡單的demo執行流程,大體瞭解了常量的引用以及一個簡單java方法對應的字節碼指令執行過程。
注:
**stack:**最大操做數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操做棧深度。
**locals:**局部變量所需的存儲空間,單位爲Slot。
args_size: 方法參數的個數。
壓棧:字節碼指令執行過程當中涉及到了不少壓棧操做:JVM是一個基於棧的架構。方法執行的時候(包括main方法),在棧上會分配一個新的幀,這個棧幀包含一組局部變量。
這組局部變量包含了方法運行過程當中用到的全部變量,包括this引用,全部的方法參數,以及其它局部定義的變量。
byte[] → byte[]
思路:
工具集:/ASM/javaassist/ByteBuddy 等等。
示例:
指令級別的字節碼操做(性能強悍)。
指令→ASM api 對應關係(這裏將原始類作了簡化,將字符串拼接邏輯去掉,僅僅輸出時間。由於一個簡單的字符串拼接過程,轉換成字節碼指令可能須要不少行)。
先看看目標源碼與字節碼指令的一一映射關係。
再看看加強字節碼邏輯與目標源碼的字節碼的一一映射關係。
經過對比咱們能夠發現,ASM的API精確到字節碼指令級別,全部的臨時變量存儲,壓棧操做,靜態/實例方法的調用都有對應的API操做。
提供字節碼級別的API,相似ASM,再也不贅述。
提供源碼級別的API,針對本文的案例,實現以下:
基於ASM的高級API,使咱們對字節碼的操做提高到更抽象層次。開發者只須要知道要實現什麼目標,如何使用對應的API,不用關心底層的字節碼指令排列,甚至能夠不用瞭解字節碼指令。
關於相關框架的API不詳細說,有興趣的同窗能夠自行查詢相關資料。
Event --> CallBack
由前文總結,引入Instrumentation機制。
JVMTI 是基於事件驅動的,JVM 每執行到必定的邏輯就會調用一些事件的回調接口(若是有的話),這些接口能夠供開發者擴展本身的邏輯。
JVMTIAgent 使用JVMTI來查詢或控制JVM,JVMTIAgent與目標JVM運行在同一個進程中,經過JVMTI進行通訊,最大化控制能力,最小化通訊成本。
典型場景下,JVMTI代理會被實現的很是緊湊,其餘的進程會與JVMTI代理進行通訊。好比jdwp(IDEA遠程調試)。
表現形式:
(1)linux: .so文件
(2)JPLISAgent: .jar文件
命令行參數
(1)-agentlib:agent-lib-name=options
(2)-agentpath:path-to-agent=options
(3)-javaagent:/data/../../Agent.jar
實現接口
(1)JNIEXPORT jint JNICALL
(2)JNIEXPORT jint JNICALL
(3)JNIEXPORT void JNICALL
JPLISAgent(Java Programming Language Instrumentation Services Agent)-- Instrumentation機制
(1)JavaSE1.5 啓動時加載(本文重點)。
(2)JavaSE1.6 運行時加載。
命令參數:-javaagent:/data/../../Agent.jar=optoions。
虛擬機建立-構建並初始化Agent-註冊VMInit事件。
虛擬機初始化-觸發VMInit事件-Agent start方法-註冊回調函數並監聽ClassFileLoadHook。
類加載-觸發jvmtiEventClassFileLoadHook事件-替換byt[]-ClassLoader解析。
Agent_onLoad |-----|createNewJPLISAgent |-----|-----|initializeJPLISAgent |-----|-----|-----|eventHandlerVMInit ---- > VMInit
其實是調用 java 類 sun.instrument.InstrumentationImpl 類裏的方法loadClassAndCallPremain。
|parseClassFile |-----|post_class_file_load_hook |-----|-----|post_to_env |-----|-----|-----|eventHandlerClassFileLoadHook(jvmtiEventClassFileLoadHook回調函數) |-----|-----|-----|-----|transformClassFile |-----|-----|-----|-----|-----|CallObjectMethod |-----|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.transform()
實際調用的java方法 Instrumentationimpl.transform。
debug過程當中經過ClassFileTransformer的transform函數的執行堆棧印證。
到這裏,加強的byte[]如何生效並影響運行時class的過程基本能夠串起來。
很是規應用:IDEA破解。
部分破解教程裏面下載插件jar後,會要求你在IDEA的啓動參數文件idea.vmoptions中添加一行,就是javaagent參數。
咱們能夠反編譯這個插件jar包看看,發現不少class由於加了混淆,反編譯後沒法正常識別,可是核心入口Agent.class的主要工做就是註冊Transformer,能夠推測這些Transformer的功能就是在IDEA啓動時以前修改某些鑑定Lisence的邏輯。
經過介紹字節碼,字節碼操做工具以及openJDK關於Instrumention機制的部分源碼,探索了字節碼加強的實現原理。
簡單介紹了相關技術的應用場景。
做者: Neo