一、介绍
Agent
中文含义代理,但是在java
中我跟喜欢称它为探针而非代理,尽管他也属于代理技术,但是代理本身并不能体现agent
的作用。
agent
技术是在JDK1.5
引入的,通过agent
技术,我们可以构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他JVM
上的程序。使用它可以实现虚拟机级别的AOP
功能。
Agent
分为两种,一种是在主程序之前运行的Agent
,一种是在主程序之后运行的Agent
(前者的升级版,1.6
以后提供)
agent 的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略 (security policy) 和上下文 (context) 所管理。
Java Agent 这个技术,对于大多数同学来说都比较陌生,但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现的,例如常见的热部署 JRebel,各种线上诊断工具(btrace, greys),还有阿里开源的线上诊断工具 arthas,分布式链路追踪 Skywalking。
其实Java Agent一点都不神秘,也是一个Jar包,只是启动方式和普通Jar包有所不同,对于普通的Jar包,通过指定类的main函数进行启动,但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行,有点像寄生虫的感觉。
Agent能干什么
首先它最大的作用就是解耦,比如skywalking
中的应用,我们不需要对我们的程序做任何修改,只需要通过agent
技术引入skywalking
的代理包即可;其次最常应用的场景就是jvm
级的AOP
,比如jvm
的监测;另一种就是类似热部署这样的字节码动态操作。
二、自定义JavaAgent
1、Agent启动类
public class AppAgent {
/**
* 以vm参数的方式载入,在Java程序的main方法执行之前执行
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("AppAgent.premain start");
System.out.println("agentArgs: " + agentArgs);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println("premainAgent LoadedClass: " + allLoadedClass.getName());
}
}
/**
* 以Attach的方式载入,在Java程序启动后执行
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("AppAgent.agentmain start");
System.out.println("agentArgs: " + agentArgs);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println("agentmain LoadedClass: " + allLoadedClass.getName());
}
}
}
两种加载方式区别
1、premain
需要目标程序中载入,是在主线程上运行
2、agentmain
需要在启动一个程序(或线程),即 attach 去监听主线程并对其进行扩展
2、加载Agent启动类
(1)方法一
因为Java Agent的特殊性,需要一些特殊的配置,在 META-INF 目录下创建MANIFEST.MF 文件,并在 MANIFEST.MF 文件中指定Agent的启动类;
Manifest-Version: 1.0
Premain-Class: com.rewind.AppAgent
Archiver-Version: Plexus Archiver
Built-By: rewind
Agent-Class: com.rewind.AppAgent
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_301
Can-Redefine-Classes: true
Can-Retransform-Classes: true
pom.xml 需设置打包为 jar包
<packaging>jar</packaging>
(2)方法二
pom 文件加入以下配置
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.rewind.AppAgent</Premain-Class>
<Agent-Class>com.rewind.AppAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3、打成 jar 包
执行 maven package
4、加载 premain
(1)目标项目启动加载Agent
运行SpringBoot项目
java -javaagent:D:\code_project\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar -jar springboot.jar
如果是 idea,将下面这个加到 VM选项即可
-javaagent:D:\code_project\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar
(2)执行
AppAgent.premain start
agentArgs: null
main 方法执行
5、加载agentmain
(1)目标主程序
无侵入
public static void main(String[] args) throws InterruptedException {
// get name representing the running Java virtual machine.
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(name);
// get pid
String pid = name.split("@")[0];
System.out.println("Pid:" + pid);
while (true) {
Thread.sleep(2000);
System.out.println("Hello World");
}
}
(2)attach程序
依赖
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
main方法
public class AgentAttach {
public static void main(String[] args) throws IOException, AttachNotSupportedException, InterruptedException {
// 85355 表示目标进程的PID
VirtualMachine virtualMachine = VirtualMachine.attach("25068");
// 指定Java Agent的jar包路径
try {
while (true) {
virtualMachine.loadAgent("D:\\code_project\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "Agent");
Thread.sleep(3000);
}
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}finally {
virtualMachine.detach();
}
}
}
改进,类似 arthas 的切入方式
//获取当前系统所有的虚拟机,类似 jps 命令。
List<VirtualMachineDescriptor> machineDescriptorList = VirtualMachine.list();
HashMap<String, VirtualMachineDescriptor> map = new HashMap<>();
int i = 0;
StringBuilder menu = new StringBuilder("请输入需要连接的虚拟机对应的id\n");
menu.append("id\tpid\tname\n");
for (VirtualMachineDescriptor virtualMachineDescriptor : machineDescriptorList) {
map.put(Integer.toString(i), virtualMachineDescriptor);
menu.append(Integer.toString(i)).append("\t")
.append(virtualMachineDescriptor.id()).append("\t")
.append(virtualMachineDescriptor.displayName()).append("\n");
i++;
}
System.out.println(menu);
Scanner scanner = new Scanner(System.in);
String next = scanner.next();
VirtualMachineDescriptor virtualMachineDescriptor = map.get(next);
String id = virtualMachineDescriptor.id();
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(id);
// 将当前JVM 链接上目标JVM,并加载 loadAgent jar 包且传递参数。
virtualMachine.loadAgent("D:\\code_project\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "Agent");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (virtualMachine != null){
// 卸载虚拟机
virtualMachine.detach();
}
} catch (IOException e) {
e.printStackTrace();
}
}
(3)测试
目标程序输出
前面未启动 attach 时,不会执行 agentmain
方法
attach 程序执行后,将会执行 agentmain
方法
Hello World
Hello World
Hello World
Hello World
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
AppAgent.agentmain start
agentArgs: Agent
三、Instrumentation API
public interface Instrumentation {
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,
//如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
//对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
//是否允许对class retransform
boolean isRetransformClassesSupported();
//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
//是否允许对class重新定义
boolean isRedefineClassesSupported();
//此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
//在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
//该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
//获取已经被JVM加载的class,有className可能重复(可能存在多个classloader)
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
}