JDK1.5開(kāi)始引入了Agent機(jī)制(即啟動(dòng)java程序時(shí)添加“-javaagent”參數(shù),Java Agent機(jī)制允許用戶在JVM加載class文件的時(shí)候先加載自己編寫的Agent文件,通過(guò)修改JVM傳入的字節(jié)碼來(lái)實(shí)現(xiàn)注入自定義的代碼。采用這種方式時(shí),必須在容器啟動(dòng)時(shí)添加jvm參數(shù),所以需要重啟Web容器。
JDK1.6新增了attach方式,可以對(duì)運(yùn)行中的java進(jìn)程附加agent,提供了動(dòng)態(tài)修改運(yùn)行中已經(jīng)被加載的類的途徑。一般通過(guò)VirtualMachine的attach(pid)方法獲得VirtualMachine實(shí)例,隨后可調(diào)用loadagent方法將JavaAgent的jar包加載到目標(biāo)JVM中。
什么是java agent?
在JVM中運(yùn)行中,類是通過(guò)classLoader加載.class文件進(jìn)行生成的。在類加載加載.class文件生成對(duì)應(yīng)的類對(duì)象之前時(shí),我們可以通過(guò)修改.class文件內(nèi)容(就是字節(jié)碼修改技術(shù)),達(dá)到修改類的目的。JDK提供了對(duì)字節(jié)碼進(jìn)行操作的一系列api,而使用這些api開(kāi)發(fā)出的程序就可以稱之為java agent。
java agent能做什么?
不修改目標(biāo)應(yīng)用達(dá)到代碼增強(qiáng)的目的,就好像spring的aop一樣,但是java agent是直接修改字節(jié)碼,而不是通過(guò)創(chuàng)建代理類。例如skywalking就是使用java agent技術(shù),為目標(biāo)應(yīng)用代碼植入監(jiān)控代碼,監(jiān)控代碼進(jìn)行數(shù)據(jù)統(tǒng)計(jì)上報(bào)的。這種方式實(shí)現(xiàn)了解耦,通用的功能。
javaagent作用
可以在加載java文件之前進(jìn)行攔截,修改字節(jié)碼。
可以在運(yùn)行期間修改已經(jīng)加載的類的字節(jié)碼。
這種用法有很多的限制。-
javaagent結(jié)合javassist功能更強(qiáng)大:可以創(chuàng)建類、方法、變量等。
這實(shí)際上提供了一種虛擬機(jī)級(jí)別的 AOP 實(shí)現(xiàn)方式。通過(guò)以上方法就能實(shí)現(xiàn)對(duì)一些框架或是技術(shù)的采集點(diǎn)進(jìn)行字節(jié)碼修改,完成這些功能:對(duì)應(yīng)用進(jìn)行監(jiān)控,對(duì)執(zhí)行指定方法或是接口時(shí)額外添加操作(打印日志、打印方法執(zhí)行時(shí)間、采集方法的入?yún)⒑徒Y(jié)果等)。很多APM監(jiān)控系統(tǒng)就是基于此實(shí)現(xiàn)的,例如:Arthas、SkyWalking
javaagent使用方式
- 方式1:在一個(gè)普通 Java 程序(帶有 main 函數(shù)的 Java 類)運(yùn)行時(shí),通過(guò) -javaagent 參數(shù)指定一個(gè)特定的 jar 文件(包含 Instrumentation 代理)來(lái)啟動(dòng) Instrumentation 的代理程序。
-javaagent 這個(gè)參數(shù)的個(gè)數(shù)是不限的,如果指定了多個(gè),則會(huì)按指定的先后執(zhí)行,執(zhí)行完各個(gè) agent 后,才會(huì)執(zhí)行主程序的 main 方法。例如:
java -javaagent:D:\workspace\javaagent.jar=hello1
-javaagent:D:\workspace\javaagent.jar=hello2 -jar D:\workspace\myTest.jar
注: hello1是參數(shù)
- 方式2:在一個(gè)普通 Java 程序(帶有 main 函數(shù)的 Java 類)運(yùn)行時(shí),通過(guò) Java Tool API 中的 attach 方式指定進(jìn)程id和特定jar包地址,啟動(dòng) Instrumentation 的代理程序。
javaagent其他功能
- 獲取所有已經(jīng)被加載過(guò)的類
- 獲取所有已經(jīng)被初始化過(guò)了的類(執(zhí)行過(guò)了clinit方法,是上面的一個(gè)子集)
- 獲取某個(gè)對(duì)象的大小
- 將某個(gè)jar加入到bootstrapclasspath里作為高優(yōu)先級(jí)被bootstrapClassloader加載
- 將某個(gè)jar加入到classpath里供AppClassload去加載
- 設(shè)置某些native方法的前綴,主要在查找native方法的時(shí)候做規(guī)則匹配
靜態(tài)agent與動(dòng)態(tài)agent
Agent分為如下兩種:
- 靜態(tài)Instrument:在main加載之前運(yùn)行的Agent
- 動(dòng)態(tài)Instrument:在main運(yùn)行之后運(yùn)行的Agent(JDK1.6以后提供)。
靜態(tài)Instrument(啟動(dòng)時(shí))加載Instrument過(guò)程
- 創(chuàng)建并初始化 JPLISAgent;
- 監(jiān)聽(tīng)VMInit事件,在JVM初始化完成之后做下面的事情:
- 創(chuàng)建InstrumentationImpl對(duì)象;
- 監(jiān)聽(tīng)ClassFileLoadHook事件;
- 調(diào)用InstrumentationImpl的loadClassAndCallPremain方法,在這個(gè)方法里會(huì)去調(diào)用javaagent中MANIFEST.MF里指定的Premain-Class類的premain方法 ;
- 解析javaagent中MANIFEST.MF文件的參數(shù),并根據(jù)這些參數(shù)來(lái)設(shè)置JPLISAgent里的一些內(nèi)容。
動(dòng)態(tài)Instrument運(yùn)行時(shí)加載Instrument過(guò)程
通過(guò)JVM的attach機(jī)制來(lái)請(qǐng)求目標(biāo)JVM加載對(duì)應(yīng)的agent,過(guò)程大致如下:
- 創(chuàng)建并初始化JPLISAgent;
- 解析 javaagent 里 MANIFEST.MF 里的參數(shù);
- 創(chuàng)建 InstrumentationImpl 對(duì)象;
- 監(jiān)聽(tīng) ClassFileLoadHook 事件;
- 調(diào)用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在這個(gè)方法里會(huì)去調(diào)用javaagent里 MANIFEST.MF 里指定的Agent-Class類的agentmain方法。
示例1: 簡(jiǎn)單例子
agent程序
1.提供premain方法
package com.example.a;
import java.lang.instrument.Instrumentation;
public class DemoAgent {
/**
* 該方法在main方法之前運(yùn)行,與main方法運(yùn)行在同一個(gè)JVM中
*/
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("agent的premain(String arg, Instrumentation instrumentation)方法");
}
/**
* 若不存在 premain(String agentArgs, Instrumentation inst),
* 則會(huì)執(zhí)行 premain(String agentArgs)
*/
public static void premain(String arg) {
System.out.println("agent的premain(String arg)方法");
}
}
2.提供META-INF/MANIFEST.MF
在src/main/java的同級(jí)目錄下新建META-INF文件夾,在里邊新建MANIFEST.MF文件(注意最后一行必須是空行)
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.a.DemoAgent
- Premain-Class :包含 premain 方法的類(類的全路徑名)
- Agent-Class :包含 agentmain 方法的類(類的全路徑名)
- Boot-Class-Path :設(shè)置引導(dǎo)類加載器搜索的路徑列表。查找類的特定于平臺(tái)的機(jī)制失敗后,引導(dǎo)類加載器會(huì)搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個(gè)或多個(gè)空格分開(kāi)。路徑使用分層 URI 的路徑組件語(yǔ)法。如果該路徑以斜杠字符(“/”)開(kāi)頭,則為絕對(duì)路徑,否則為相對(duì)路徑。相對(duì)路徑根據(jù)代理 JAR 文件的絕對(duì)路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動(dòng)之后某一時(shí)刻啟動(dòng)的,則忽略不表示 JAR 文件的路徑。(可選)
- Can-Redefine-Classes :true表示能重定義此代理所需的類,默認(rèn)值為 false(可選)
- Can-Retransform-Classes :true 表示能重轉(zhuǎn)換此代理所需的類,默認(rèn)值為 false (可選)
- Can-Set-Native-Method-Prefix: true表示能設(shè)置此代理所需的本機(jī)方法前綴,默認(rèn)值為 false(可選)
3.將其打包為jar包
步驟1:打包的配置入口
File=> Project Structure=> Project Settings=> Artifacts=> + => JAR=> From modules with dependencies..

步驟2:打包的配置

步驟3:打包
Build=> Build Artifacts...=> Build
此時(shí)會(huì)生成out目錄,并生成jar包:

也可使用maven配置META-INF/MANIFEST.MF
使用maven,打包方便,而且不用手寫META-INF/MANIFEST.MF,用插件即可:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven編譯的jdk版本。若不指定,maven3默認(rèn)用jdk 1.5 maven2默認(rèn)用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自動(dòng)添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.example.a.DemoAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
maven的項(xiàng)目結(jié)構(gòu)為:

應(yīng)用程序
項(xiàng)目結(jié)構(gòu)

1.提供main程序
package com.example.a;
public class Demo {
public static void main(String[] args) {
System.out.println("應(yīng)用的main方法");
}
}
測(cè)試
java -javaagent:D:\tmp\demo_javaagent.jar -jar demo_java.jar
結(jié)果:

示例2:統(tǒng)計(jì)方法的執(zhí)行時(shí)間
需求:寫一個(gè)agent,統(tǒng)計(jì)應(yīng)用的某個(gè)方法的執(zhí)行時(shí)間。(本處要統(tǒng)計(jì)的方法是:TimeTest#test方法)
agent程序
agent代碼
package com.example.a;
import java.lang.instrument.Instrumentation;
public class DemoAgent {
/**
* 該方法在main方法之前運(yùn)行,與main方法運(yùn)行在同一個(gè)JVM中
*/
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("agent的premain(String arg, Instrumentation instrumentation)方法");
instrumentation.addTransformer(new MyTransformer());
}
/**
* 若不存在 premain(String agentArgs, Instrumentation inst),
* 則會(huì)執(zhí)行 premain(String agentArgs)
*/
public static void premain(String arg) {
System.out.println("agent的premain(String arg)方法");
}
}
Transformer代碼
package com.example.a;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
private final String injectedClass = "com.example.a.TimeTest";
private final String injectedMethod = "test";
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
if (realClassName.equals(injectedClass)) {
CtClass ctClass;
try {
// 使用全稱,取得字節(jié)碼類<使用javassist>
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
// 得到方法實(shí)例
CtMethod ctMethod = ctClass.getDeclaredMethod(injectedMethod);
// 添加變量
ctMethod.addLocalVariable("time", CtClass.longType);
ctMethod.insertBefore("System.out.println(\"------------ Before --------\");");
ctMethod.insertBefore("time = System.currentTimeMillis();");
ctMethod.insertAfter("System.out.println(\"Elapsed Time(ms): \" + (System.currentTimeMillis() - time));");
ctMethod.insertAfter("System.out.println(\"------------- After --------\");");
return ctClass.toBytecode();
} catch (Throwable e) { //這里要用Throwable,不要用Exception
System.out.println(e.getMessage());
e.printStackTrace();
}
}
// 返回原類字節(jié)碼
return classfileBuffer;
}
}
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.example</groupId>
<artifactId>demo_javaagent</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven編譯的jdk版本。若不指定,maven3默認(rèn)用jdk 1.5 maven2默認(rèn)用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自動(dòng)添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.example.a.DemoAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
應(yīng)用程序
main類
package com.example.a;
public class Demo {
public static void main(String[] args) {
System.out.println("應(yīng)用的main方法");
new TimeTest().test();
}
}
測(cè)試類
package com.example.a;
public class TimeTest {
public void test() {
System.out.println("開(kāi)始執(zhí)行TimeTest#test");
System.out.println("sleep開(kāi)始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleep結(jié)束");
}
}
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.example</groupId>
<artifactId>demo_maven</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven編譯的jdk版本。若不指定,maven3默認(rèn)用jdk 1.5 maven2默認(rèn)用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.example.a.Demo</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>assembly</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
測(cè)試
java -javaagent:D:\tmp\demo_javaagent-1.0-SNAPSHOT.jar -jar demo_maven-1.0-SNAPSHOT.jar
結(jié)果:

Agentmain(attach)
在 Java SE 6 的 Instrumentation 當(dāng)中,提供了一個(gè)新的代理操作方法:agentmain,可以在 main 函數(shù)開(kāi)始運(yùn)行之后再運(yùn)行。
跟premain函數(shù)一樣, 開(kāi)發(fā)者可以編寫一個(gè)含有agentmain函數(shù)的 Java 類:
//采用attach機(jī)制,被代理的目標(biāo)程序VM有可能很早之前已經(jīng)啟動(dòng),當(dāng)然其所有類已經(jīng)被加載完成,
//這個(gè)時(shí)候需要借助Instrumentation#retransformClasses(Class<?>... classes)
//讓對(duì)應(yīng)的類可以重新轉(zhuǎn)換,從而激活重新轉(zhuǎn)換的類執(zhí)行ClassFileTransformer列表中的回調(diào)
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
agentMain 主要用于對(duì)java程序的監(jiān)控,調(diào)用java進(jìn)程,將自己編寫的agentMain 注入目標(biāo)完成對(duì)程序的監(jiān)控,修改。
創(chuàng)建agentmain
public class TestMainAgent {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("loadagent after main run.args=" + agentArgs);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
for (Class<?> cls : classes)
{
System.out.println(cls.getName());
}
System.out.println("agent run completely.");
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
添加maven插件打包
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動(dòng)添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>com.tttiger.TestMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
測(cè)試agentMain插樁到其他類
另外啟用了一個(gè)jvm進(jìn)程,找到需要attach的jvm進(jìn)程,讓它加載agentMain,那么agentMain就會(huì)被加載到對(duì)方j(luò)vm執(zhí)行。arthas就是使用這種方式attach進(jìn)jvm進(jìn)程,開(kāi)啟一個(gè)socket然后進(jìn)行目標(biāo)jvm的監(jiān)控。
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, InterruptedException {
//獲取當(dāng)前系統(tǒng)中所有 運(yùn)行中的 虛擬機(jī)
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虛擬機(jī)的名稱為 xxx 則 該虛擬機(jī)為目標(biāo)虛擬機(jī),獲取該虛擬機(jī)的 pid
//然后加載 agent.jar 發(fā)送給該虛擬機(jī)
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("com.tttiger.TestJVM")) {
System.out.println(vmd.id());
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("e:/test-agentMain-1.0-SNAPSHOT.jar");
virtualMachine.detach();
System.out.println("attach");
}
}
Thread.sleep(10000L);
}
VirtualMachine 字面意義表示一個(gè)Java 虛擬機(jī),也就是程序需要監(jiān)控的目標(biāo)虛擬機(jī),提供了獲取系統(tǒng)信息(比如獲取內(nèi)存dump、線程dump,類信息統(tǒng)計(jì)(比如已加載的類以及實(shí)例個(gè)數(shù)等), loadAgent,Attach 和 Detach (Attach 動(dòng)作的相反行為,從 JVM 上面解除一個(gè)代理)等方法,可以實(shí)現(xiàn)的功能可以說(shuō)非常之強(qiáng)大 。該類允許我們通過(guò)給attach方法傳入一個(gè)jvm的pid(進(jìn)程id),遠(yuǎn)程連接到j(luò)vm上 。
代理類注入操作只是它眾多功能中的一個(gè),通過(guò)loadAgent方法向jvm注冊(cè)一個(gè)代理程序agent,在該agent的代理程序中會(huì)得到一個(gè)Instrumentation實(shí)例,該實(shí)例可以 在class加載前改變class的字節(jié)碼,也可以在class加載后重新加載。在調(diào)用Instrumentation實(shí)例的方法時(shí),這些方法會(huì)使用ClassFileTransformer接口中提供的方法進(jìn)行處理。
VirtualMachineDescriptor 則是一個(gè)描述虛擬機(jī)的容器類,配合 VirtualMachine 類完成各種功能
通過(guò)VirtualMachine類的attach(pid)方法,便可以attach到一個(gè)運(yùn)行中的java進(jìn)程上,之后便可以通過(guò)loadAgent(agentJarPath)來(lái)將agent的jar包注入到對(duì)應(yīng)的進(jìn)程,然后對(duì)應(yīng)的進(jìn)程會(huì)調(diào)用agentmain方法。

Instrumentation的局限性
大多數(shù)情況下,我們使用Instrumentation都是使用其字節(jié)碼插樁的功能,或者籠統(tǒng)說(shuō)就是類重定義(Class Redefine)的功能,但是有以下的局限性:
- premain和agentmain兩種方式修改字節(jié)碼的時(shí)機(jī)都是類文件加載之后,也就是說(shuō)必須要帶有Class類型的參數(shù),不能通過(guò)字節(jié)碼文件和自定義的類名重新定義一個(gè)本來(lái)不存在的類。
- 類的字節(jié)碼修改稱為類轉(zhuǎn)換(Class Transform),類轉(zhuǎn)換其實(shí)最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
- 2.1 新類和老類的父類必須相同;
- 2.2 新類和老類實(shí)現(xiàn)的接口數(shù)也要相同,并且是相同的接口;
- 2.3 新類和老類訪問(wèn)符必須一致。 新類和老類字段數(shù)和字段名要一致;
- 2.4 新類和老類新增或刪除的方法必須是private static/final修飾的;
- 2.5 可以修改方法體。
除了上面的方式,如果想要重新定義一個(gè)類,可以考慮基于類加載器隔離的方式:創(chuàng)建一個(gè)新的自定義類加載器去通過(guò)新的字節(jié)碼去定義一個(gè)全新的類,不過(guò)也存在只能通過(guò)反射調(diào)用該全新類的局限性。