原文:
Java探針-Java Agent技術
利用JAVA探針分析復雜代碼運維實踐
總結:
使用java代理來實現java字節(jié)碼注入
使用JavaSsist可以對字節(jié)碼進行修改
使用ASM可以修改字節(jié)碼
使用Java代理和ASM字節(jié)碼技術開發(fā)java探針工具可以修改字節(jié)碼
備注:javassist是一個庫,實現ClassFileTransformer接口中的transform()方法。ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉換掉,從而達到動態(tài)注入代碼的目的。
備注:ASM是一個java字節(jié)碼操縱框架,它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
詳細總結
1 使用Java代理和Java字節(jié)碼注入技術
開發(fā)java探針工具來分析復雜的接口方法,無需修改代碼,簡單部署就可以實時抓取代碼的運行軌跡和方法耗時。
2 基于Java代理和Java字節(jié)碼注入技術的java探針工具技術原理

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

1:在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時;
2:將監(jiān)控的相關方法 和 耗時及內部調用情況,按照順序放入處理器;
3:處理器利用棧先進后出的特點對方法調用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡和入參map輸出到文件中;
4:然后區(qū)分出耗時的業(yè)務,轉化為xml格式進行解析和分析。
Java探針工具功能點:
1、支持方法執(zhí)行耗時范圍抓取設置,根據耗時范圍抓取系統(tǒng)運行時出現在設置耗時范圍的代碼運行軌跡。
2、支持抓取特定的代碼配置,方便對配置的特定方法進行抓取,過濾出關系的代碼執(zhí)行耗時情況。
3、支持APP層入口方法過濾,配置入口運行前的方法進行監(jiān)控,相當于監(jiān)控特有的方法耗時,進行方法專題分析。
4、支持入口方法參數輸出功能,方便跟蹤耗時高的時候對應的入參數。
5、提供WEB頁面展示接口耗時展示、代碼調用關系圖展示、方法耗時百分比展示、可疑方法凸顯功能。
2.1 Java代理小例子
JavaAgent 是運行在 main方法之前的攔截器,它內定的方法名叫 premain ,也就是說先執(zhí)行 premain 方法然后再執(zhí)行 main 方法。
那么如何實現一個 Java代理呢?很簡單,只需要增加 premain 方法即可。
源碼:JavaAgent

Test1.pre_MyProgram.java
package agent;
import java.lang.instrument.Instrumentation;
public class pre_MyProgram
{
/**
* 該方法在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);
}
public static void main(String[] args)
{
}
}
在 src 目錄下添加 META-INF/MANIFEST.MF 文件
Manifest-Version: 1.0
Premain-Class: agent.pre_MyProgram
Can-Redefine-Classes: true
打包代碼為 pre_MyProgram.jar;注意打包的時候選擇我們自己定義的 MANIFEST.MF
Test2.MyProgram.java
package alibaba;
public class MyProgram
{
public static void main(String[] args)
{
System.out.println("====MyProgram====");
}
}
Manifest-Version: 1.0
Main-Class: alibaba.MyProgram
導出main的jar包命名為:MyProgram.jar

命令中的Hello1為我們傳遞給 premain 方法的字符串參數,這就是一個簡單的Java代理
2.2 基于 JavaAgent 的應用實例
JDK5中只能通過命令行參數在啟動JVM時指定javaagent參數來設置代理類,而JDK6中已經不僅限于在啟動JVM時通過配置參數來設置代理類,JDK6中通過 Java Tool API 中的 attach 方式,我們也可以很方便地在運行過程中動態(tài)地設置加載代理類,以達到 instrumentation 的目的。
Instrumentation 的最大作用,就是類定義動態(tài)改變和操作。
最簡單的一個例子,計算某個方法執(zhí)行需要的時間,不修改源代碼的方式,使用Instrumentation 代理來實現這個功能,給力的說,這種方式相當于在JVM級別做了AOP支持,這樣我們可以在不修改應用程序的基礎上就做到了AOP,是不是顯得略吊。
創(chuàng)建一個 ClassFileTransformer 接口的實現類 MyTransformer實現 ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉換掉,從而達到動態(tài)注入代碼的目的。那么首先要了解MonitorTransformer 這個類的目的,就是對想要修改的類做一次轉換,這個用到了javassist對字節(jié)碼進行修改,可以暫時不用關心jaavssist的原理,用ASM同樣可以修改字節(jié)碼,只不過比較麻煩些。

源碼:JavaAgent
MyAgent.MyAgent.java
package agent;
import java.lang.instrument.Instrumentation;
public class MyAgent
{
/**
* 該方法在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);
// 添加Transformer
inst.addTransformer(new MyTransformer());
}
/**
* 如果不存在 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)
{
}
}
MyAgent.MyTransformer
package agent;
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 MyTransformer 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 MyTransformer()
{
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);
}
// 重寫此方法
@Override
public 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$old
ctmethod.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");// 調用原有代碼,類似于method();($$)表示所有的參數
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;
}
}
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.12.1-GA.jar
package alibaba;
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();
}
}
}
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: alibaba.TimeTest
結果:

3 使用 spring-loaded 實現 jar 包熱部署
在項目開發(fā)中我們可以把一些重要但又可能會變更的邏輯封裝到某個 logic.jar 中,當我們需要隨時更新實現邏輯的時候,可以在不重啟服務的情況下讓修改后的 logic.jar 被重新加載生效。
spring-loaded是一個開源項目,項目地址:https://github.com/spring-projects/spring-loaded
使用方法:
在啟動主程序之前指定參數
-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的情況下,實現應用的熱部署。
通過使用 -noverify 參數,關閉 Java 字節(jié)碼的校驗功能。
使用參數 -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,官網已經有人提出:https://github.com/spring-projects/spring-loaded/issues/145
- 對于一些第三方框架的注解的修改,不能自動加載,比如:spring mvc的@RequestMapping
- log4j的配置文件的修改不能即時生效。