日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM插桩之一:JVM字节码增强技术介绍及入门示例

發(fā)布時間:2024/1/23 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM插桩之一:JVM字节码增强技术介绍及入门示例 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

字節(jié)碼增強技術:AOP技術其實就是字節(jié)碼增強技術,JVM提供的動態(tài)代理追根究底也是字節(jié)碼增強技術。

目的:在Java字節(jié)碼生成之后,對其進行修改,增強其功能,這種方式相當于對應用程序的二進制文件進行修改。Java字節(jié)碼增強主要是為了減少冗余代碼,提高性能等。

應用場景:某一天系統(tǒng)出現(xiàn)OOM,通過工具分析,各類的對象占用了很大空間,但是這個對象被許多程序訪問,那么就很難找到,工程的全文匹配也只能找到自己的業(yè)務代碼調(diào)用的地方,深入的反射,三方包調(diào)用無法匹配。這個時候AOP就可以幫助完成。

兩種實現(xiàn)機制:

動態(tài)代理(jdk動態(tài)代理,sprintAOP動態(tài)代理):一種是通過創(chuàng)建原始類的一個子類,也就是動態(tài)創(chuàng)建的這個類繼承原來的類,現(xiàn)在的SprintAOP正是通過這種方式實現(xiàn)。

動態(tài)代理(Cglib,ASM,javassit):另一種是非常暴力的,即直接修改原先的Class字節(jié)碼,在許多類的跟蹤過程中會用到這技術(類加載時修改字節(jié)碼信息,運行時修改)。《Java動態(tài)代理機制詳解(JDK 和CGLIB,Javassist,ASM)》

代理:?Java之代理(jdk靜態(tài)代理,jdk動態(tài)代理,cglib動態(tài)代理,aop,aspectj)

實現(xiàn)字節(jié)碼增強的主要步驟為:

? 1、修改字節(jié)碼

1.在內(nèi)存中獲取到原始的字節(jié)碼,然后通用一些開源提供的API來修改它的byte[]數(shù)組,得到一個新的byte[]數(shù)組.(ASM,javassist,cglib等技術0

? 2、使修改后的字節(jié)碼生效

有兩種方法:

1) 自定義ClassLoader來加載修改后的字節(jié)碼;

2)替換掉原來的字節(jié)碼(將這個新的數(shù)組寫到PermGen區(qū)域,也就是加載它或替換原來的Class字節(jié)碼(也可以在進程外部調(diào)用完成)):在JVM加載用戶的Class時,攔截,返回修改后的字節(jié)碼;或者在運行時,使用Instrumentation.redefineClasses方法來替換掉原來的字節(jié)碼;

使用Java代理和ASM字節(jié)碼技術開發(fā)java探針工具可以修改字節(jié)碼

備注:javassist是一個庫,實現(xiàn)ClassFileTransformer接口中的transform()方法。ClassFileTransformer這個接口的目的就是? 在class被裝載到JVM之前將class字節(jié)碼轉(zhuǎn)換掉,從而達到動態(tài)注入代碼的目的。

備注:ASM是一個java字節(jié)碼操縱框架,它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。

詳細總結

1 使用Java代理和Java字節(jié)碼注入技術

開發(fā)java探針工具來分析復雜的接口方法,無需修改代碼,簡單部署就可以實時抓取代碼的運行軌跡和方法耗時。

2 基于Java代理和Java字節(jié)碼注入技術的java探針工具技術原理

動態(tài)代理功能實現(xiàn)說明

我們利用Java代理和ASM字節(jié)碼技術開發(fā)java探針工具,實現(xiàn)原理如下:

jdk1.5以后引入了Java代理技術,Java代理時運行方法之前的攔截器。我們利用Java代理和ASM字節(jié)碼技術,在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時,同時將方法耗時及內(nèi)部調(diào)用情況放入處理器,處理器利用棧先進后出的特點對方法調(diào)用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡和入?yún)ap輸出到文件中,然后根據(jù)map中相應參數(shù)或耗時方法軌跡中的關鍵代碼區(qū)分出我們要抓取的耗時業(yè)務。最后將相應耗時軌跡文件取下來,轉(zhuǎn)化為xml格式并進行解析,通過瀏覽器將代碼分層結構展示出來,方便耗時分析,如圖所示:

jvm探針工具原理圖

1.在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時;

2.將監(jiān)控的相關方法和耗時及內(nèi)部調(diào)用情況,按照順序放入處理器;

3.處理器利用棧先進后出的特點對方法調(diào)用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡和入?yún)ap輸出到文件中;

4.然后區(qū)分出耗時的業(yè)務,轉(zhuǎn)化為xml格式進行解析和分析。

Java探針工具功能點:

1.支持方法執(zhí)行耗時范圍抓取設置,根據(jù)耗時范圍抓取系統(tǒng)運行時出現(xiàn)在設置耗時范圍的代碼運行軌跡。

2.支持抓取特定的代碼配置,方便對配置的特定方法進行抓取,過濾出關系的代碼執(zhí)行耗時情況。

3.支持APP層入口方法過濾,配置入口運行前的方法進行監(jiān)控,相當于監(jiān)控特有的方法耗時,進行方法專題分析。

4.支持入口方法參數(shù)輸出功能,方便跟蹤耗時高的時候?qū)娜雲(yún)?shù)。

5.提供WEB頁面展示接口耗時展示、代碼調(diào)用關系圖展示、方法耗時百分比展示、可疑方法凸顯功能。

2.1、入門示例

JavaAgent是運行在main方法之前的攔截器,它內(nèi)定的方法名叫premain,也就是說先執(zhí)行premain方法然后再執(zhí)行main方法。

那么如何實現(xiàn)一個Java代理呢?很簡單,只需要增加premain方法即可。

package com.dxz.chama.javaagent;import java.lang.instrument.Instrumentation;/*** agent的入口類*/ public class SampleAgent {/*** 該方法在main方法之前運行,與main方法運行在同一個JVM中,并被同一個System ClassLoader裝載,被統(tǒng)一的安全策略(security policy)和上下文(context)管理*/public static void premain(String agentOps, Instrumentation inst) {System.out.println("=======premain方法執(zhí)行");System.out.println(agentOps);}/*** 如果不存在 premain(String agentOps, Instrumentation inst) 則會執(zhí)行 premain(String agentOps)*/public static void premain(String agentOps) {System.out.println("====premain方法執(zhí)行2====");System.out.println(agentOps);} }

修改pom文件(完整的pom文件見:《java類加載及動態(tài)代理之字節(jié)碼插莊技術》)

指定prmain的路徑。

javaagent打包,mvn clean package,生成jar:chama-0.0.1-SNAPSHOT.jar

業(yè)務類

package com.dxz.chama.service;public class SampleService {public static void main(String[] args) {System.out.println("SampleService main()==========");} }

運行配置

運行結果:

2.2 基于 JavaAgent 的應用實例

JDK5中只能通過命令行參數(shù)在啟動JVM時指定javaagent參數(shù)來設置代理類,而JDK6中已經(jīng)不僅限于在啟動JVM時通過配置參數(shù)來設置代理類,JDK6中還可以通過 Java Tool API 中的 attach 方式,我們也可以很方便地在運行過程中動態(tài)地設置加載代理類,以達到 instrumentation 的目的。

Instrumentation 的最大作用,就是類定義動態(tài)改變和操作。

基于JavaAgent的Instrumentation代理示例

最簡單的一個例子,計算某個方法執(zhí)行需要的時間,不修改源代碼的方式,使用Instrumentation 代理來實現(xiàn)這個功能,給力的說,這種方式相當于在JVM級別做了AOP支持,這樣我們可以在不修改應用程序的基礎上就做到了AOP,是不是顯得略吊。

創(chuàng)建一個 ClassFileTransformer 接口的實現(xiàn)類 MyTransformer實現(xiàn) ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉(zhuǎn)換掉,從而達到動態(tài)注入代碼的目的。那么首先要了解MonitorTransformer 這個類的目的,就是對想要修改的類做一次轉(zhuǎn)換,這個用到了javassist對字節(jié)碼進行修改,可以暫時不用關心jaavssist的原理,用ASM同樣可以修改字節(jié)碼,只不過比較麻煩些。

項目結構

MyAgent.MyAgent.java

package com.dxz.chama.javaagent; import java.lang.instrument.Instrumentation;public class StatAgent{/*** 該方法在main方法之前運行,與main方法運行在同一個JVM中 并被同一個System ClassLoader裝載* 被統(tǒng)一的安全策略(security policy)和上下文(context)管理*/public static void premain(String agentOps, Instrumentation inst){System.out.println("=========premain方法執(zhí)行========");System.out.println(agentOps);// 添加Transformerinst.addTransformer(new StatTransformer());}/*** 如果不存在 premain(String agentOps, Instrumentation inst) 則會執(zhí)行 premain(String* agentOps)*/public static void premain(String agentOps){System.out.println("====premain方法執(zhí)行2====");System.out.println(agentOps);}public static void main(String[] args){}}

StatTransformer.java

package com.dxz.chama.javaagent;import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod;/*** 檢測方法的執(zhí)行時間*/ public class StatTransformer implements ClassFileTransformer {final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";// 被處理的方法列表final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();public StatTransformer() {add("com.shanhy.demo.TimeTest.sayHello");add("com.shanhy.demo.TimeTest.sayHello2");}private void add(String methodString) {String className = methodString.substring(0, methodString.lastIndexOf("."));String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);List<String> list = methodMap.get(className);if (list == null) {list = new ArrayList<String>();methodMap.put(className, list);}list.add(methodName);}// 重寫此方法@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {className = className.replace("/", ".");if (methodMap.containsKey(className)) {// 判斷加載的class的包路徑是不是需要監(jiān)控的類CtClass ctclass = null;try {ctclass = ClassPool.getDefault().get(className);// 使用全稱,用于取得字節(jié)碼類<使用javassist>for (String methodName : methodMap.get(className)) {String outputStr = "\nSystem.out.println(\"this method " + methodName+ " cost:\" +(endTime - startTime) +\"ms.\");";CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到這方法實例String newMethodName = methodName + "$old";// 新定義一個方法叫做比如sayHello$oldctmethod.setName(newMethodName);// 將原來的方法名字修改// 創(chuàng)建新的方法,復制原來的方法,名字為原來的名字CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);// 構建新的方法體StringBuilder bodyStr = new StringBuilder();bodyStr.append("{");bodyStr.append(prefix);bodyStr.append(newMethodName + "($$);\n");// 調(diào)用原有代碼,類似于method();($$)表示所有的參數(shù)bodyStr.append(postfix);bodyStr.append(outputStr);bodyStr.append("}");newMethod.setBody(bodyStr.toString());// 替換新方法ctclass.addMethod(newMethod);// 增加新方法System.err.println(outputStr);}return ctclass.toBytecode();} catch (Exception e) {System.out.println(e.getMessage());e.printStackTrace();}}return null;} }

修改pom文件(完整的pom文件見:《java類加載及動態(tài)代理之字節(jié)碼插莊技術》)

javaagent打包

mvn clean package

META-INF/MANIFEST.MF

業(yè)務測試類:

package com.dxz.chama.service; public class TimeTest {public static void main(String[] args){System.err.println("======TimeTest執(zhí)行========");sayHello();sayHello2("hello world222222222");}public static void sayHello(){try{Thread.sleep(2000);System.out.println("hello world!!");}catch (InterruptedException e){e.printStackTrace();}}public static void sayHello2(String hello){try{Thread.sleep(1000);System.out.println(hello);}catch (InterruptedException e){e.printStackTrace();}} }

運行配置:

結果:

3 使用 spring-loaded 實現(xiàn) jar 包熱部署

在項目開發(fā)中我們可以把一些重要但又可能會變更的邏輯封裝到某個 logic.jar 中,當我們需要隨時更新實現(xiàn)邏輯的時候,可以在不重啟服務的情況下讓修改后的 logic.jar 被重新加載生效。

spring-loaded是一個開源項目,項目地址:https://github.com/spring-projects/spring-loaded

使用方法:

在啟動主程序之前指定參數(shù) -javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify 123

如果你想讓 Tomat 下面的應用自動熱部署,只需要在 catalina.sh 中添加:

set JAVA_OPTS=-javaagent:springloaded-1.2.5.RELEASE.jar -noverify1

這樣就完成了 spring-loaded 的安裝,它能夠自動檢測Tomcat 下部署的webapps ,在不重啟Tomcat的情況下,實現(xiàn)應用的熱部署。

通過使用 -noverify 參數(shù),關閉 Java 字節(jié)碼的校驗功能。
使用參數(shù) -Dspringloaded=verbose;explain;watchJars=tools.jar 指定監(jiān)視的jar

(verbose;explain; 非必須),多個jar用“冒號”分隔,如 watchJars=tools.jar:utils.jar:commons.jar

當然,它也有一些小缺限:

  • 目前官方提供的1.2.4 版本在linux上可以很好的運行,但在windows還存在bug,官網(wǎng)已經(jīng)有人提出:https://github.com/spring-projects/spring-loaded/issues/145
  • 對于一些第三方框架的注解的修改,不能自動加載,比如:spring mvc的@RequestMapping
  • log4j的配置文件的修改不能即時生效。
  • 總結

    以上是生活随笔為你收集整理的JVM插桩之一:JVM字节码增强技术介绍及入门示例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。