文中代碼示例工程如下,更多參考btrace和arthas:
https://github.com/sayhiai/example-javaagent
5版本以後,jdk有一個包叫做instrument,能夠實現一些非常酷的功能。市面上一些APM工具,就是通過它來進行的增強。
這是基礎架構的必備技能,但對業務開發來說並不是。許多面試會問到這個知識點,並不是因為將來會用到,而是因為你說對jdk比較熟悉,他想殺殺你的威風。
不會用沒問題,但你要說不知道,就過分了點。
javaagent介紹
我們通常的java入口都是一個main方法,而javaagent的入口方法叫做premain,表明是在main運行之前的一些操作。javaagent就是一個jar包,定義了一個標準的premain()方法,並不需要繼承或者實現任何其他的類。
這是一個約定,並木有什麼其他的理由。這個方法,無論是第一次加載,還是每次新的ClassLoader加載,都會執行。
我們可以在這個前置的方法裡,對字節碼進行一些修改,來增加功能或者改變代碼的行為。這種方法沒有侵入性,只需要在啟動命令中加上-javaagent參數就可以。Java6以後,甚至可以通過attach的方式,動態的給運行中的程序設置加載代理類。
有經驗的同學肯定要提出異議了。其實,instrument有兩個main方法,一個是premain,一個是agentmain,在一個JVM中,只會調用一個;前者是main執行之前的修改,後者控制類運行時的行為。它們還是有一些區別的,agentmain因為比較危險,限制會更大一些。
有什麼用
獲取統計信息
許多apm產品,比如Pinpoint、SkyWalking等,就是使用javaagent對代碼進行的增強。通過在方法執行前後動態加入的統計代碼,進行監控信息的收集;通過兼容OpenTracing協議,可以實現分佈式鏈路追蹤的功能。
它的原理類似於aop,最終以字節碼存在,性能損失取決於你的代碼邏輯。
熱部署
通過自定義的ClassLoader,可以實現代碼的熱替換。使用agentmain,實現熱部署功能會更加便捷。通過agentmain獲取到Instrumentation以後,就可以對類進行動態重定義。
診斷
配合JVMTI技術,可以attach到某個進程進行運行時統計和調試,比較流行的btrace和arthas,底層就是這種技術。
如何做
大體分為以下步驟:
- 構建agent jar包,編寫增強代碼
- 在manifest中指定Premain-Class/Agent-Class屬性
- 使用參數加載或者attach方式使用
編寫Agent
javaagent最終的體現方式是一個jar包。使用idea創建一個默認的maven工程即可。
創建一個普通java類,添加premain或者agentmain方法,它們的參數完全一樣。
編寫Transformer
此部分,要藉助額外jar包的功能。
實際的代碼邏輯需要實現ClassFileTransformer接口。假如我們要統計某個方法的執行時間。我們使用javaassist來增強字節碼,則可以通過以下代碼來實現。
- 獲取MainRun類的字節碼實例
- 獲取hello方法的字節碼實例
- 在方法前後,加入時間統計,首先定義變量_begin,然後直接編寫代碼
別忘了加入maven依賴
<dependency>
<groupid>org.javassist/<groupid>
<artifactid>javassist/<artifactid>
<version>3.24.1-GA/<version>
/<dependency>
字節碼增強也可以使用Cglib、asm等其他工具。
MANIFEST.MF文件
那麼我們編寫的代碼是如何讓外界知曉呢?那就是MANIFEST.MF文件。具體路徑在
src/main/resources/META-INF/MANIFEST.MF
Manifest-Version: 1.0
premain-class: com.sayhiai.example.javaagent.AgentApp
一般的,maven打包會覆蓋這個文件,所以我們需要指定需要哪一個。
<build><plugins><plugin>
<groupid>org.apache.maven.plugins/<groupid>
<artifactid>maven-jar-plugin/<artifactid>
<configuration>
<archive>
<manifestfile>src/main/resources/META-INF/MANIFEST.MF/<manifestfile>
/<archive>
/<configuration>/<plugin>/<plugins>/<build>
然後,在命令行,執行mvn install安裝到本地代碼庫,或者使用mvn deploy發佈到私服上。
附,MANIFEST.MF參數清單:
Premain-Class
Agent-Class
Boot-Class-Path
Can-Redefine-Classes
Can-Retransform-Classes
Can-Set-Native-Method-Prefix
使用
使用方式取決於你使用的premain還是agentmain。
premain
直接在啟動命令行中加入參數即可,在jvm啟動時啟用代理。
java -javaagent:agent.jar MainRun
在idea中,可以將參數附著在jvm options裡。
接下來看一下測試代碼。
這是我們的執行類。執行後,直接輸出hello world。通過增強以後,還額外的輸出了執行時間,以及一些debug信息。其中,debug信息在main方法執行之前輸出。
agentmain
一般用在一些診斷工具上。使用jdk/lib/tools.jar中的功能,可以動態的為運行中的程序加入功能。主要有以下步驟:
- 獲取機器上運行的所有jvm的進程id
- 選擇要診斷的jvm
- 將jvm使用attach函數鏈接上
- 使用loadAgent函數加載agent,動態修改字節碼
- 卸載jvm
這些代碼都是比較危險的,這就是為什麼Btrace說了這麼多年,還是隻在小範圍內被小心使用。相對來說,arthas顯的友好而且安全的多。
注意點
一、jar包依賴方式
一般,agent的jar包會以fatjar的方式提供,即將所有的依賴打包到一個大的jar包中。
如果你的功能複雜,依賴多,那麼這個jar包將會特別的大。
使用獨立的bom文件維護這些依賴是另外一種方法。使用方自行管理依賴問題,但這通常會發生一些找不到jar包的錯誤。更糟糕的是,大多數在運行時才發現。
二、類名稱重複
不要使用和jdk以及instrument包中相同的類名(包括包名),有時候你能夠僥倖過關,但也會陷入無法控制的異常中。
三、做有限的功能
可以看到,給系統動態的增加功能是非常酷的,但大多數情況下非常耗費性能。你會發現,一些簡單的診斷工具,佔用你1核的cpu,是稀鬆平常的事情。
四、ClassLoader
如果你用的jvm比較舊,頻繁的生成大量的代理類,會造成perm區的膨脹,容易發生OOM。
ClassLoader有雙親委派機制,如果你想要替換相應的類,一定要搞清楚它的類加載器應該用哪個。否則替換的類,是不生效的哦。
End
將你的增強代碼,加入類似zk的主動通知功能,可以通過管理後臺動態的調整應用的行為。如果再集成一個類似groovy的腳本語言,理論上,你能夠幹任何事情。
所以,使用-javaagent參數引入的jar包,或者使用attach方式提供的一些診斷工具,小姐姐都不敢隨便的用。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。
閱讀更多 小姐姐味道架構設計 的文章