(目录)
Java Agent
背景
因在做到Skywalking Agent的时候,并没有修改程序中任何一行 Java 代码,就可无侵入式的使用组件,便使用到了 Java Agent 技术,接下来对学习学习Java Agent 技术
Java Agent 是什么
Java Agent这个技术对大多数人来说都比较陌生,但是大家都都多多少少接触过一些。
实际上我们平时用过的很多工具都是基于java Agent来实现的。
例如:热部署工具JRebel,springboot的热部署插件,各种线上诊断工具(btrace, greys),阿里开源的arthas等等。
java Agent在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,并且这种方式一个典型的优势就是无代码侵入。

Agent分为两种:
1、在主程序之前运行的Agent,
2、在主程序之后运行的Agent(JDK 1.6以后提供)
Java Agent 使用场景
Java agent 技术结合 Java Intrumentation API 可以实现类修改、热加载等功能。
下面是 Java agent 技术的常见应用场景:

Java Agent 开发
1. premain (主程序之前运行的Agent)
premain:主程序之前运行的Agent

可以看到,我们的代码在转换成机器码之前,需要执行转换,加载和校验。
Java Agent即是在此过程中,对代码进行拦截,进行定制化的操作(有点AOP的意思)。
ByteBuddy提供premain 方法,函数签名如下所示:
public static void premain(String args,Instrumentation inst)
顾名思义,premain 方法在main 方法之前被调用
示例如下所示:


运行HelloWorld程序时,在HelloWorld.class被JVM加载之前,发现有premain方法对其进行拦截。
根据premain中定义的Agent规则,对HelloWorld.class执行转换(Transformed)操作,转换后的helloWorld.class文件被ClassLoader加载,功能增强完成。
2. -javaagent命令
在实际使用过程中,javaagent是java命令的一个参数。
通过java 命令启动我们的应用程序的时候,可通过参数 -javaagent 指定一个 jar 包(也就是我们的代理agent)。
能够实现在我们应用程序的主程序运行之前来执行我们指定jar包中的特定方法,在该方法中我们能够实现动态增强Class等相关功能,并且该 jar包有2个要求:
- 这个 jar 包的 META-INF/MANIFEST.MF 文件必须指定 Premain-Class 项,该选项指定的是一个类的全路径
- Premain-Class 指定的那个类必须实现 premain() 方法。
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.itheima.PreMainAgent
pom文件
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive> <!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itheima.agent.PreMainAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
重点部分关注:
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class :包含 premain 方法的类(类的全路径名)
<manifestEntries>
<!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
<Premain-Class>demo.MethodAgentMain</Premain-Class>
<!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
<Agent-Class>demo.MethodAgentMain</Agent-Class>
<!--Can-Redefine-Classes: 是否可进行类定义。-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--Can-Retransform-Classes: 是否可进行类转换。-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
从字面上理解,Premain-Class 就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
我们可以通过在命令行输入java看到相应的参数,其中就有和java agent相关的

3. 编写Agent demo
1、在agent-demo中添加如下坐标
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive> <!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itheima.agent.PreMainAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
2、编写一个agent程序:PreMainAgent,完成premain方法的签名,先做一个简单的输出
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("我的agent程序跑起来啦!");
System.out.println("收到的agent参数是:"+agentArgs);
}
}
下面先来简单介绍一下 Instrumentation 中的核心 API 方法:
- addTransformer()/removeTransformer() 方法:注册/注销一个
ClassFileTransformer类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义(修改类的字节码) - redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义
- **getAllLoadedClasses()方法:**返回当前 JVM 已加载的所有类
- getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类
- getObjectSize()方法:获取参数指定的对象的大小
3、对agent-demo项目进行打包package,得到 agent-demo-1.0-SNAPSHOT.jar
4、创建agent-test项目,编写一个启动类:Application
public class Application {
public static void main(String[] args) {
System.out.println("main 函数 运行了 ");
}
}
5、启动运行,添加-javaagent参数
-javaagent:/xxx.jar=option1=value1,option2=value2

运行结果为:
我的agent程序跑起来啦!
收到的agent参数是:k1=v1,k2=v2
main 函数 运行了
总结:
agent JVM 会先执行 premain 方法,大部分类加载都会通过该方法。
注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,bytebuddy,javassist,cglib等等来改写实现类。
4. agentmain (主程序之后运行的Agent)
agentmain:主程序之后运行的Agent
上面介绍的是在 JDK 1.5中提供的,开发者只能在main加载之前添加手脚。
在 Java SE 6 中提供了一个新的代理操作方法:agentmain 可以在 main 函数开始运行之后再运行。
跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类,具备以下之一的方法即可
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同样需要在MANIFEST.MF文件里面设置“Agent-Class”来指定包含 agentmain 函数的类的全路径。
1、在agentdemo中创建一个新的类:AgentClass,并编写方法agenmain
public class AgentClass {
public static void agentmain (String agentArgs, Instrumentation inst){
System.out.println("agentmain runing");
}
}
2:在pom.xml中添加配置如下
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive> <!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itheima.agent.PreMainAgent</Premain-Class>
<!--添加这个即可-->
<Agent-Class>com.itheima.agent.AgentClass</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
3:对agent-demo重新打包
4:找到agent-test中的Application,修改如下:
public class Application {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("main 函数 运行了 ");
//获取当前系统中所有 运行中的 虚拟机
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vm : list) {
if (vm.displayName().endsWith("com.itheima.Application")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
virtualMachine.loadAgent("D:/agentdemo.jar");
virtualMachine.detach();
}
}
}
}
vlist()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。
因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。
之所以要这样写是因为:agent要在主程序运行后加载,我们不可能在主程序中编写加载的代码。
怎么另写程序如何与主程序进行通信?
这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行
5. 总结
Java Agent十分强大,它能做到的不仅仅是打印几个监控数值而已,还包括使用Transformer等高级功能进行类替换,方法修改等,要使用Instrumentation的相关API则需要对字节码等技术有较深的认识。










