java類載入及動態代理之位元組碼插莊技術
本文介紹一下,當下比較基礎但是使用場景卻很多的一種技術,稍微偏底層點,就是位元組碼插莊技術了...,如果之前大家熟悉了asm,cglib以及javassit等技術,那麼下面說的就很簡單了...,因為下面要說的功能就是基於javassit實現的,接下來先從javaagent的原理說起,最後會結合一個完整的例項演示實際中如何使用..
javaagent的主要功能有哪些?
& 可以在載入java檔案之前做攔截把位元組碼做修改
& 獲取所有已經被載入過的類
& 獲取所有已經被初始化過了的類(執行過了clinit方法,是上面的一個子集)
& 獲取某個物件的大小
& 將某個jar加入到bootstrapclasspath裡作為高優先順序被bootstrapClassloader載入
& 將某個jar加入到classpath裡供AppClassloard去載入
& 設定某些native方法的字首,主要在查詢native方法的時候做規則匹配
JVMTI
JVM Tool Interface,是jvm暴露出來的一些供使用者擴充套件的介面集合,JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者去擴充套件自己的邏輯。
比如說我們最常見的想在某個類的位元組碼檔案讀取之後類定義之前能修改相關的位元組碼,從而使建立的class物件是我們修改之後的位元組碼內容,那我們就可以實現一個回撥函式賦給JvmtiEnv(JVMTI的執行時,通常一個JVMTIAgent對應一個jvmtiEnv,但是也可以對應多個)的回撥方法集合裡的ClassFileLoadHook,這樣在接下來的類檔案載入過程中都會呼叫到這個函式裡來了,大致實現如下:
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent
JVMTIAgent其實就是一個動態庫,利用JVMTI暴露出來的一些介面來幹一些我們想做但是正常情況下又做不到的事情,不過為了和普通的動態庫進行區分,它一般會實現如下的一個或者多個函式:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
JAVAGENT
說到javaagent必須要講的是一個叫做instrument的JVMTIAgent(linux下對應的動態庫是libinstrument.so),因為就是它來實現javaagent的功能的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),從這名字裡也完全體現了其最本質的功能:就是專門為java語言編寫的插樁服務提供支援的。
INSTRUMENT AGENT
instrument agent實現了Agent_OnLoad
和Agent_OnAttach
兩方法,也就是說我們在用它的時候既支援啟動的時候來載入agent,也支援在執行期來動態來載入這個agent,其中啟動時載入agent還可以通過類似-javaagent:myagent.jar
的方式來間接載入instrument agent,執行期動態載入agent依賴的是jvm的attach機制JVM Attach機制實現,通過傳送load命令來載入agent。
這裡解釋下幾個重要項:
- mNormalEnvironment:主要提供正常的類transform及redefine功能的。
- mRetransformEnvironment:主要提供類retransform功能的。
- mInstrumentationImpl:這個物件非常重要,也是我們java agent和JVM進行互動的入口,或許寫過javaagent的人在寫
premain
以及agentmain
方法的時候注意到了有個Instrumentation的引數,這個引數其實就是這裡的物件。 - mPremainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallPremain
方法,如果agent是在啟動的時候載入的,那該方法會被呼叫。 - mAgentmainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain
方法,該方法在通過attach的方式動態載入agent的時候呼叫。 - mTransform:指向
sun.instrument.InstrumentationImpl.transform
方法。 - mAgentClassName:在我們javaagent的MANIFEST.MF裡指定的
Agent-Class
。 - mOptionsString:傳給agent的一些引數。
- mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF裡設定
Can-Redefine-Classes:true
。 - mNativeMethodPrefixAvailable:是否支援native方法字首設定,通樣在javaagent的MANIFEST.MF裡設定
Can-Set-Native-Method-Prefix:true
。 - mIsRetransformer:如果在javaagent的MANIFEST.MF檔案裡定義了
Can-Retransform-Classes:true
,那將會設定mRetransformEnvironment的mIsRetransformer為true。
紅色標註的是我們最常用的,下面的列子也是會用到的...,接下來看一個具體的例子,如果熟悉分散式呼叫鏈系統的人肯定知道,呼叫鏈中最基礎的一個功能就是統計一個服務裡面的某個方法執行了多長時間...,其實這個就目前來說大多數系統底層都是基於位元組碼插樁技術實現的,接下來就演示一個完整的例子....,定義一個業務類,類裡面定義幾個方法,然後在執行這個方法的時候,會動態實現方法的耗時統計..
看業務類定義:
package com.suning.dynamic_proxy.javaagent.service; import java.util.LinkedList; import java.util.List; /** * @Author 18011618 * @Description 模擬資料插入服務 * @Date 10:45 2018/6/21 * @Modify By */ public class InsertService { /** * 模擬資料插入 * @param num */ public void insert2(int num){ List<Integer>list = new LinkedList<>(); for (int i =0;i<num;i++){ list.add(i); } } public void insert1(int num){ List<Integer>list = new LinkedList<>(); for (int i =0;i<num;i++){ list.add(i); } } public void insert3(int num){ List<Integer>list = new LinkedList<>(); for (int i =0;i<num;i++){ list.add(i); } } }
刪除服務:
package com.suning.dynamic_proxy.javaagent.service; import java.util.List; /** * @Author 18011618 * @Description 模擬資料刪除的方法 * @Date 10:45 2018/6/21 * @Modify By */ public class DeleteService { public void delete(List<Integer>list){ for (int i=0;i<list.size();i++){ list.remove(i); } } }
ok,接下來就是要編寫javaagent的相關實現:
定義agent的入口
package com.suning.dynamic_proxy.javaagent; import java.lang.instrument.Instrumentation; /** * agent的入口類 */ public class TimeMonitorAgent {
//peremain 這個方法名稱是固定寫法 不能寫錯或修改public static void premain(String agentArgs, Instrumentation inst) { System.out.println("execute insert method interceptor...."); System.out.println(agentArgs); //新增自定義類轉換器 inst.addTransformer(new TimeMonitorTransformer(agentArgs)); } }
接下來看最重要的Transformer的實現:
package com.suning.dynamic_proxy.javaagent; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.Objects; /** * 類方法的位元組碼替換 * @author Allen Lee * @date 2017-03-11 */ public class TimeMonitorTransformer implements ClassFileTransformer { private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n"; private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n"; private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result"; private static final String EMPTY = ""; private String classNameKeyword; public TimeMonitorTransformer(String classNameKeyword){ this.classNameKeyword = classNameKeyword; } /** * * @param classLoader 預設類載入器 * @param className 類名的關鍵字 因為還會進行模糊匹配* @param classBeingRedefined * @param protectionDomain * @param classfileBuffer * @return * @throws IllegalClassFormatException */ public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/", "."); CtClass ctClass = null; try { //使用全稱,用於取得位元組碼類ctClass = ClassPool.getDefault().get(className); //匹配類的機制是基於類的關鍵字 這個是客戶端傳過來的引數 滿足就會獲取所有的方法 不滿足跳過 if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){ //所有方法CtMethod[] ctMethods = ctClass.getDeclaredMethods(); //遍歷每一個方法 for(CtMethod ctMethod:ctMethods){ //修改方法的位元組碼transformMethod(ctMethod, ctClass); } } //重新返回修改後的類return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 為每一個攔截到的方法 執行一個方法的耗時操作 * @param ctMethod * @param ctClass * @throws Exception */ private void transformMethod(CtMethod ctMethod,CtClass ctClass) throws Exception{ //抽象的方法是不能修改的 或者方法前面加了final關鍵字if((ctMethod.getModifiers()&Modifier.ABSTRACT)>0){ return; } //獲取原始方法名稱String methodName = ctMethod.getName(); String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" +(endTime - startTime) +\"ms.\");"; //例項化新的方法名稱String newMethodName = methodName + "$impl"; //設定新的方法名稱ctMethod.setName(newMethodName); //建立新的方法,複製原來的方法 ,名字為原來的名字CtMethod newMethod = CtNewMethod.copy(ctMethod,methodName, ctClass, null); StringBuilder bodyStr = new StringBuilder(); //拼接新的方法內容bodyStr.append("{"); //返回型別 CtClass returnType = ctMethod.getReturnType(); //是否需要返回 boolean hasReturnValue = (CtClass.voidType != returnType); if (hasReturnValue) { String returnClass = returnType.getName(); bodyStr.append("\n").append(returnClass + " " + METHOD_RUTURN_VALUE_VAR + ";"); } bodyStr.append(START_TIME); if (hasReturnValue) { bodyStr.append("\n").append(METHOD_RUTURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);"); } else { bodyStr.append("\n").append(newMethodName + "($$);"); } bodyStr.append(END_TIME); bodyStr.append(monitorStr); if (hasReturnValue) { bodyStr.append("\n").append("return " + METHOD_RUTURN_VALUE_VAR+" ;"); } bodyStr.append("}"); //替換新方法newMethod.setBody(bodyStr.toString()); //增加新方法 ctClass.addMethod(newMethod); } }
其實也很簡單就兩個類就實現了要實現的功能,那麼如何使用呢?需要把上面的程式碼打成jar包才能執行,建議大家使用maven打包,下面是pom.xml的配置檔案
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.sn.dynamic</groupId> <artifactId>dynamic</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/asm/asm --> <!-- <dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3</version> </dependency>--> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.1.GA</version> </dependency> <!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.5</version> </dependency> <!-- https://mvnrepository.com/artifact/oro/oro --> <dependency> <groupId>oro</groupId> <artifactId>oro</artifactId> <version>2.0.8</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>com.suning.dynamic_proxy.javaagent.TimeMonitorAgent</Premain-Class> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
強調一下,紅色標準的非常關鍵,因為如果要想jar能夠執行,必須要把執行清單打包到jar中,且一定要讓jar的主類是Permain-Class,否則無法執行,執行清單的目錄是這樣的,
如果打包正確的話,裡面的內容應該如下所示:
Manifest-Version: 1.0
Premain-Class: com.suning.dynamic_proxy.javaagent.TimeMonitorAgent
Archiver-Version: Plexus Archiver
Built-By: jack
Created-By: Apache Maven
Build-Jdk: 1.8.0_144
OK至此整體程式碼和打包就完成了,那麼接下來再講解如何使用
部署方式:
1 基於IDE開發環境執行
首先,編寫一個service的測試類如下:
/** * @Author 18011618 * @Description * @Date 10:51 2018/6/21 * @Modify By */ public class ServiceTest { public static void main(String[] args) {
//插入服務 InsertService insertService = new InsertService(); //刪除服務
DeleteService deleteService = new DeleteService(); System.out.println("....begnin insert...."); insertService.insert1(1003440); insertService.insert2(2000000); insertService.insert3(30003203); System.out.println(".....end insert....."); List<Integer> list = new LinkedList<>(); for (int i =0;i<29988440;i++){ list.add(i); } System.out.println(".....begin delete......"); deleteService.delete(list); System.out.println("......end delete........"); } }
選擇編輯配置:如下截圖所示
進行如下修改:
javassit-5.0.jar這個就是剛剛編寫那個javaagent類的程式碼打成的jar包,ok 讓我們看一下最終的效果如何:
實際應用場景中,可以把這些結果寫入到log然後傳送到es中,就可以做視覺化資料分析了...還是蠻強大的,接下來對上面的業務進行擴充套件,因為上面預設是攔截類裡面的所有方法,如果業務需求是攔截類的特定的方法該怎麼實現呢?其實很簡單就是通過正則匹配,下面給出核心程式碼:
定義入口agent:
package com.suning.dynamic_proxy.javaagent.patter; import com.suning.dynamic_proxy.javaagent.TimeMonitorTransformer; import java.lang.instrument.Instrumentation; /** * Created by jack on 2018/6/19. */ public class TimeMonitorPatterAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new PatternTransformer()); } }
定義transformer:
package com.suning.dynamic_proxy.javaagent.patter; import javassist.CtClass; import org.apache.oro.text.regex.PatternCompiler; import org.apache.oro.text.regex.PatternMatcher; import org.apache.oro.text.regex.Perl5Compiler; import org.apache.oro.text.regex.Perl5Matcher; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; /** * Created by jack on 2018/6/19. * */ public class PatternTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { PatternMatcher matcher = new Perl5Matcher(); PatternCompiler compiler = new Perl5Compiler(); //指定的業務類String interceptorClass ="com.service.InsertService"; //指定的方法String interceptorMethod = "insert1"; try { if (matcher.matches(className,compiler.compile(interceptorClass))){ ByteCode byteCode = new ByteCode(); CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod); return ctClass.toBytecode(); } } catch (Exception e){ e.printStackTrace(); } return null; } }
修改位元組碼的實現:
package com.suning.dynamic_proxy.javaagent.patter; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; /** * Created by jack on 2018/6/19. * 修改攔截類的方法的位元組碼 */ public class ByteCode { public CtClass modifyByteCode(String className,String method)throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(className); CtMethod oldMethod = ctClass.getDeclaredMethod(method); String oldMethodName = oldMethod.getName(); String newName = oldMethodName + "$impl"; oldMethod.setName(newName); CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null); StringBuffer sb = new StringBuffer(); sb.append("{"); sb.append("\nSystem.out.println(\"start to modify bytecode\");\n"); sb.append("long start = System.currentTimeMillis();\n"); sb.append(newName+"($$);\n"); sb.append("System.out.println(\"call method"+oldMethodName+"took\"+(System.currentTimeMillis()-start))"); sb.append("}"); newMethod.setBody(sb.toString()); ctClass.addMethod(newMethod); return ctClass; } }
OK,這個時候再重新打包,然後修改上面的執行配置之後再看效果,只能攔截到insert1方法
最後 再說一下如何使用jar執行,其實很簡單如下:把各個專案都打成jar,比如把上面的service打成service.jar,然後使用java命令執行:
java -javaagent:d://javassit-5.0.jar=Service -jar service.jar,效果是一樣的!