一、Java Agent是什么?
通過操作Instrumentation的api就可以實(shí)現(xiàn)不重啟服務(wù)對單個(gè)類進(jìn)行簡單的修改。Instrumentation是一個(gè)interface,它的實(shí)現(xiàn)類InstrumentationImpl只有一個(gè)private的構(gòu)造方法。
怎么拿到這個(gè)對象呢?下面是Instrumentation類的一段注釋說明:
There are two ways to botain an instance of the Instrumentation interface:
- When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation instance is passed to the parent method of the agent class.
- When a JVM provides a mechanism to start agents sometime after the JVM is
launched . In that case an Instrumentation instance is passed yo the agentmain method of the agent code.
These mechainsms are described in the package specification.
Once an agent acquires an Instrumentation instance, the agent may call methods on the instance at any time.
這里對上面的英文原文的注釋做一個(gè)簡單的理解:一共有兩種方式拿到Instrumentation對象:
- 在JVM啟動(dòng)時(shí)指定agent,Instrumentation對象會通過agent的premain方法傳遞過去。
- 在JVM啟動(dòng)后通過JVM提供的機(jī)制加載agent,Instrumentation對象會通過agent的agentmain方法傳遞過去。
本文重點(diǎn)介紹,在主程序運(yùn)行之前啟動(dòng)agent的基本用法。
二、如何使用Java Agent技術(shù)?
2.1 簡單示例
新起一個(gè)簡單的Java工程AgentDemo,新建包路徑com.alibaba.ei.agent,新增一個(gè)類AgentDemo,編寫代碼如下:
public class AgentDemo {
private static Instrumentation instrumentation;
/**
* 該方法在main方法之前運(yùn)行,與main方法運(yùn)行在同一個(gè)JVM中
*
* @param agentArgs 是 premain 函數(shù)得到的程序參數(shù),隨同 “– javaagent”一起傳入。與 main 函數(shù)不同的是,這個(gè)參數(shù)是一個(gè)字符串而不是一個(gè)字符串?dāng)?shù)組,如果程序參數(shù)有多個(gè),程序?qū)⒆孕薪馕鲞@個(gè)字符串。
* @param inst 是一個(gè) java.lang.instrument.Instrumentation 的實(shí)例,由 JVM 自動(dòng)傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個(gè)接口,也是這個(gè)包的核心部分,集中了其中幾乎所有的功能方法,例如類定義的轉(zhuǎn)換和操作等等。
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法執(zhí)行1========");
System.out.println(agentArgs);
instrumentation = inst;
SimpleClassTransformer transformer = new SimpleClassTransformer();
inst.addTransformer(transformer);
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 則會執(zhí)行 premain(String agentArgs)
*
* @param agentArgs
* @author xifeijian
* @create 2018年4月18日
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法執(zhí)行2========");
System.out.println(agentArgs);
}
}
在這個(gè) premain 函數(shù)中,開發(fā)者可以進(jìn)行對類的各種操作。
- agentArgs 是 premain 函數(shù)得到的程序參數(shù),隨同 “– javaagent”一起傳入。與 main 函數(shù)不同的是,這個(gè)參數(shù)是一個(gè)字符串而不是一個(gè)字符串?dāng)?shù)組,如果程序參數(shù)有多個(gè),程序?qū)⒆孕薪馕鲞@個(gè)字符串。
- Inst 是一個(gè) java.lang.instrument.Instrumentation 的實(shí)例,由 JVM 自動(dòng)傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個(gè)接口,它是agent技術(shù)主要使用的API,我們可以使用它來改變和重新定義類的行為。
編寫轉(zhuǎn)換類SimpleClassTransformer:
/**
* @Description
* @Author louxiujun
* @Date 2020/2/4 11:49
**/
public class SimpleClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = null;
try {
clazz = classPool.get("sun.net.www.protocol.http.HttpURLConnection");
CtConstructor[] cs = clazz.getConstructors();
for (CtConstructor constructor : cs) {
// 在構(gòu)造函數(shù)結(jié)束的位置插入如下的內(nèi)容
constructor.insertAfter("System.out.println(this.getURL());");
}
byte[] byteCode = clazz.toBytecode();
// 將類移出
clazz.detach();
return byteCode;
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
}
return null;
}
}
寫完這個(gè)類后,我們還需要做一步配置工作。
1)如果項(xiàng)目是普通Java項(xiàng)目的話,則在 src 目錄下生成 META-INF/MANIFEST.MF 文件。
切換到工程設(shè)置面板,切換到Artifacts面板,點(diǎn)擊?按鈕,新增一個(gè)JAR,選擇From modules with dependencies...選項(xiàng),如下圖所示:

Main Class一欄留空不填,下面的單選按鈕選擇copy to the output directory and link via manifest選項(xiàng),其他的按照默認(rèn)生成的走就可以,完成之后點(diǎn)擊OK按鈕。

完成之后面板顯示如下,點(diǎn)擊OK按鈕完成配置。

然后編輯META-INF/MANIFEST.MF,MANIFEST.MF文件用于描述Jar包的信息,例如指定入口函數(shù)等。我們需要在該文件中加入如下配置,指定我們編寫的含有premain方法類的全路徑,然后將agent類打成Jar包。
1 Manifest-Version: 1.0
2 Premain-Class: com.alibaba.ei.agent.AgentDemo
3
4
要特別注意的是:最后一行是空行,還有就是Premain-Class冒號后面有個(gè)空格。
接下來選擇菜單欄中的Build下拉中的Build Artifacts..選項(xiàng),

然后我們在彈出的快捷菜單中選擇Action為Build,從而將整個(gè)工程打包代碼為 javaagent.jar

此時(shí),會在工程目錄的out文件夾成生成一個(gè)jar文件,復(fù)制下這個(gè)jar文件的絕對路徑備用。本文的jar文件所在目錄為/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar。當(dāng)前整個(gè)工程的結(jié)構(gòu)圖如下圖所示:

2)如果你是使用Maven來構(gòu)建的項(xiàng)目,則在pom.xml文件中添加如下的內(nèi)容,Maven幫助生成MANIFEST.MF文件。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--方式1:在主程序運(yùn)行之前的代理程序-->
<Premain-Class>com.alibaba.ei.agent.AgentDemo</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
再添加一下pom依賴:
<javassist.version>3.20.0-GA</javassist.version>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version>
</dependency>
執(zhí)行mvn clean install指令,則會在當(dāng)前工程目錄下生成一個(gè)target文件夾,里面有一個(gè)agentDemo-1.0-SNAPSHOT.jar的jar包,拷貝起文件路徑備用即可,感興趣的可以解壓看一下里面是否有一個(gè)MANIFEST.MF文件。
接著我們再創(chuàng)建一個(gè)新的工程agentTest,新建包路徑com.alibaba.ei.agent,新建文件AgentTest.java。
public class AgentTest {
public static void main(String[] args) {
System.out.println("===========執(zhí)行main方法=============");
HttpUtil.fetch("http://www.baidu.com");
HttpUtil.fetch("http://www.163.com");
}
}
這里的程序就是我們要代理的程序,我們在主程序的VM options添加上啟動(dòng)參數(shù)
-javaagent: 你的路徑/test-1.0-SNAPSHOT.jar=hello
其中hello為上文中傳入permain方法的agentArgs參數(shù)。運(yùn)行我們的主程序
編輯應(yīng)用的JVM啟動(dòng)參數(shù)如下

點(diǎn)擊運(yùn)行按鈕后輸出如下:
=========premain方法執(zhí)行1========
Hello
=========premain方法執(zhí)行2========
World
===========執(zhí)行main方法=============
http://www.baidu.com
Content size:2283
http://www.163.com
Content size:488569
我們也可以將項(xiàng)目打包成jar包,再以命令行的方式啟動(dòng):
java -javaagent:/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar=Hello -javaagent:/Users/XXX/Documents/code/javaagent/agentDemo/out/artifacts/agentDemo_jar/agentDemo.jar=World -jar agentTest.jar
objc[18080]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/bin/java (0x106aea4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x106b964e0). One of the two will be used. Which one is undefined.
=========premain方法執(zhí)行1========
Hello
=========premain方法執(zhí)行2========
World
=========Main主方法執(zhí)行=========
http://www.baidu.com
Content size:2283
http://www.163.com
Content size:488569
特別提醒:如果你把 -javaagent相關(guān)的參數(shù)放在-jar相關(guān)參數(shù)的后面,則不會生效。也就是說,放在主程序后面的 agent 是無效的。