一、什么是javaagent
javaagent是一个JVM“插件”,一种专门精心制作的.jar文件,它能够利用JVM提供的Instrumentation API。
1.1、概要
Java Agent由三部分组成:代理类、代理类元信息和JVM加载.jar和代理的机制,整体内容如下图所示:
1.2、javaagent的基石
java.lang.instrument
javaagent的启动方式有以下几种:
- 通过在命令行指定参数启动。
- JVM启动后启动。例如,提供一种工具,该工具可以依附到已运行的应用,并允许在已运行的应用内加载代理。
- 与应用一起打包为可执行文件。
1.3、启动 javaagent
1.3.1、命令行启动
命令行启动参数如下:
-javaagent:<jarpath>[=<options>]
<jarpath>
:javaagent的路径,比如 /opt/var/Agent-1.0.0.jar
。<options>
: javaagent参数,参数的解析由javaagent负责。
javaagent JAR文件清单必须包含 Premain-Class
属性,属性的值为agent class的全路径名(包名+类名)。代理类必须实现 premain
方法,premain
方法和 main
premain
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM首先尝试在代理中调用签名为1的方法,如果代理类没有实现签名为1的方法,JVM尝试调用签名为2的方法:
代理类可以有一个 agentmain
函数,函数会在JVM启动完成之后调用。如果,使用命令行启动代理,agentmain
方式不会被调用。
代理的所有参数被当作一个字符串通过 agentArgs
如果代理因为代理类无法被加载、代理类未实现 premain
javaagent的启动不要求实现一定提供命令行的方式,如果,实现支持通过命令行启动,实现必须支持在命令行中通过指定 -javaagent
参数启动。 -javaagent
可以在命令行中使用多次,启动多个代理。premain
函数的调用顺序和命令行中指定的顺序一致,多个代理可以使用相同 <jarpath>
。
没有一个严格模型来定义 premain
函数的工作范围,任何 main
函数可以做的工作,比如创建线程,在 premain
1.3.2、JVM启动后启动
实现可以提供在JVM启动之后再启动代理的机制。代理如何启动的细节特定于实现,通常应用程序已经启动,并且它的 main
- 清单文件包含
Agent-Class
- 代理类必须实现
public static agentmain
agentmain方法有以下两个函数签名:
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
JVM首先尝试调用具有签名1的方法,如果,代理类没有实现该方法,JVM尝试调用签名为2的方法。
代理类可以同时实现 premain
和 agentmain
两个方法,当代理以命令行方式启动时,JVM调用 premain
函数,当代理在JVM启动之后启动时,JVM调用 agentmain
函数,而且JVM不会调用 premain
agentmain
函数参数的传递也是通过 agentArgs
,所有参数组合为一个字符串,参数的解析由代理负责。
agentmain
函数必须完成启动代理所有必须的初始化动作,当启动完成后,agentmain
1.3.3、打包为可执行文件
如果代理打包到可执行JAR文件中,可执行JAR文件的清单中必须包含 Launcher-Agent-Class
public static void agentmain(String agentArgs, Instrumentation inst)
如果,代理类没有实现上述方法,JVM则调用下面的方法。
public static void agentmain(String agentArgs)
agentArgs
agentmain
1.3.4、加载代理类以及代理类可用的模块/类
系统类加载器负责加载代理JAR文件中的所有类,并且成为系统类加载器的未命名模块的成员。 系统类加载器通常也定义包含应用程序 main
- 启动层中的模块导出的包中的类。 启动层是否包含所有平台模块取决于初始模块或应用程序的启动方式。
- 类可被系统类加载器定义。
- 启动类加载器定义的所有代理的类为其未命名模块的成员。
如果代理类需要链接到不在启动层中的平台(或其他)模块中的类,则需要以确保这些模块位于启动层中的方式启动应用程序。 例如,在JDK实现中,--add-modules
启动类加载器可以加载代理支持的类(通过 appendToBootstrapClassLoaderSearch
如果配置了自定义系统类加载器(通过 getSystemClassLoader
方法中指定的系统属性 java.system.class.loader
),则必须定义 appendToSystemClassLoaderSearch
中指定的 appendToClassPathForInstrumentation
1.4、javaagent清单属性
属性 | 说明 | 是否必选 | 默认值 |
Premain-Class | 包含premain方法的类 | 依赖启动方式 | 无 |
Agent-Class | 包含agentmain方法的类 | 依赖启动方式 | 无 |
Boot-Class-Path | 启动类加载器搜索路径 | 否 | 无 |
Can-Redefine-Classis | 是否可以重定义代理所需的类 | 否 | false |
Can-Retransform-Classis | 是否能够重新转换此代理所需的类 | 否 | false |
Can-Set-Native-Method-Prefix | 是否能够设置此代理所需的本机方法前缀 | 否 | false |
二、写一个Java Agent
基于上面的介绍,我们实现一个下载JVM中所有非系统类的javaagent。
整个开发过程包括以下三步:
- 1)定义代理类,实现类下载功能;
- 2)配置、打包;
- 3)命令行启动测试。
2.1、代理类实现
实现 premain
package io.ct.java.agent;
import java.lang.instrument.Instrumentation;
public class AgentApplication {
public static void premain(String arg, Instrumentation instrumentation) {
System.err.println("agent startup , args is " + arg);
// 注册我们的文件下载函数
instrumentation.addTransformer(new DumpClassesService());
}
}
文件下载类实现 ClassFileTransformer
package io.ct.java.agent;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;
/**
* Copyright (C), 2018-2018, open source
* FileName: DumpClassesService
*
* @author : 大哥
* Date: 2018/12/8 21:01
*/
public class DumpClassesService implements ClassFileTransformer {
private static final List<String> SYSTEM_CLASS_PREFIX = Arrays.asList("java", "sum", "jdk");
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!isSystemClass(className)) {
System.out.println("load class " + className);
FileOutputStream fos = null;
try {
// 将类名统一命名为classNamedump.class格式
fos = new FileOutputStream(className + "dump.class");
fos.write(classfileBuffer);
fos.flush();
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
// 关闭文件输出流
if (null != fos) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return classfileBuffer;
}
/**
* 判断一个类是否为系统类
*
* @param className 类名
* @return System Class then return true,else return false
*/
private boolean isSystemClass(String className) {
// 假设系统类的类名不为NULL而且不为空
if (null == className || className.isEmpty()) {
return false;
}
for (String prefix : SYSTEM_CLASS_PREFIX) {
if (className.startsWith(prefix)) {
return true;
}
}
return false;
}
}
2.2、配置MANIFEST.MF
MANIFEST.MF
文件两种方式生成:手动配置和自动生成,手动配置只需要在 resources
文件下创建 META-INF/MENIFEST.MF
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>io.ct.java.agent.AgentApplication</Premain-Class>
<Agent-Class>io.ct.java.agent.AgentApplication</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
生成的jar包格式如下:
其中MANIFEST.MF的文件内容如下(不同的配置生成的文件内容不完全一致):
Manifest-Version: 1.0
Implementation-Title: agent
Premain-Class: io.ct.java.agent.AgentApplication
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: chentong
Agent-Class: io.ct.java.agent.AgentApplication
Can-Redefine-Classes: true
Implementation-Vendor-Id: io.ct.java
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_171
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/agent
2.3、命令行启动Java Agent
执行下面的命令,运行已经编译好的类Hello,可以在同级目录下生成一个名为Hellodump.class的文件。
java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello