1.背景
Java Agent出现在JDK1.5版本以后,它允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序,先从下面这张图直观的看一下它都被应用在哪些场景:
2.Premain模式
Premain模式允许在主程序执行前执行一个agent代理,实现起来非常简单,下面我们分别实现两个组成部分。
2.1 agent
先写一个简单的功能,在主程序执行前打印一句话,并打印传递给代理的参数:
public class MyPreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Starting to invoke premain");
System.out.println("args:"+agentArgs);
}
}
这里我们直接使用maven插件打包的方式,在打包前进行一些配置。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>MyPreMainAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
- Premain-Class:包含premain方法的类,需要配置为类的全路径
- Can-Redefine-Classes:为true时表示能够重新定义class
- Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换
- Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀
其中Premain-Class为必须配置,其余几项是非必须选项,默认情况下都为false,执行下面Maven命令完成打包,打包结果是javaagent_test-1.jar
mvn clean package
2.2 主程序
创立一个独立的项目名字是MainProject, 在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。
public class AgentTest {
public static void main(String[] args) {
System.out.println("main project start");
}
}
在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下:
java -javaagent:javaagent_test-1.jar -jar MainProject.jar
并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:
java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar
如果是用eclipse运行的,直接在vm的参数里加就可以,注意路径
执行结果如下:
根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。
3. Agentmain模式
agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。
3.1 agent
agent部分和上面一样,实现简单的打印功能:
public class MyAgentMain {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("MyAgentMain start..");
instrumentation.addTransformer(new DefineTransformer(), true);
}
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插件配置,指定Agent-Class:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>MyAgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
3.2 主程序
这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。
public class AgentmainTest {
public static void main(String[] args) throws IOException {
System.in.read();
}
}
attach机制
和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}\lib\tools.jar</systemPath>
</dependency>
写下面类把agent加载到主程序上面
public class TestAgentMain {
public static void main(String[] args) throws Exception {
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("com.AgentmainTest")) {
System.out.println("helloworld");
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/javaagent_test-1.jar");
virtualMachine.detach();
}
}
}
}
list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()
看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。
先执行AgentmainTest类,然后执行TestAgentMain类把agent加载的主程序中,执行后结果如下:
attach实现动态注入的原理如下:
通过VirtualMachine类的attach(pid)
方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)
来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。