(目录)
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则需要对字节码等技术有较深的认识。