子曰:小胜靠智,大胜靠德,常胜靠身体。
1 什么是javaagent
javaagent是一个JVM“插件”,一种专门精心制作的.jar文件,它能够利用JVM提供的Instrumentation API。
1.1 概要
Java Agent由三部分组成:代理类、代理类元信息和JVM加载.jar和代理的机制,整体内容如下图所示:
Java Agent
1.2 javaagent的基石
java.lang.instrument
为javaagent 通过修改方法字节码的方式操作运行在JVM上的程序提供服务。javaagent以JAR包的形式部署,JAR文件清单中的属性指定要加载的代理类,以启动代理。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
方法一样分别是代理和应用的入口点。JVM初始化完成后首先调用代理的premain
函数,然后调用应用的main
函数,premain
方法必须返回后进程才能启动。
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
方法或抛出了未被捕获的异常,JVM将会退出。
javaagent的启动不要求实现一定提供命令行的方式,如果,实现支持通过命令行启动,实现必须支持在命令行中通过指定-javaagent
参数启动。-javaagent
可以在命令行中使用多次,启动多个代理。premain
函数的调用顺序和命令行中指定的顺序一致,多个代理可以使用相同<jarpath>
.
没有一个严格模型来定义premain
函数的工作范围,任何main
函数可以做的工作,比如创建线程,在premain
函数中都是合法的。
1.3.2 JVM启动后启动
实现可以提供在JVM启动之后再启动代理的机制。代理如何启动的细节特定于实现,通常应用程序已经启动,并且它的main
方法已经被调用。如果实现支持在JVM启动后启动代理,代理必须满足以下条件:
-
清单文件包含
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
函数必须返回。如果,代理不能启动或抛出未捕获的异常,JVM都会退出。
1.3.3 打包为可执行文件
如果代理打包到可执行JAR文件中,可执行JAR文件的清单中必须包含Launcher-Agent-Class
属性,指定一个在应用main函数调用之前代理启动的类。JVM尝试在代理上调用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst)
如果,代理类没有实现上述方法,JVM则调用下面的方法。
public static void agentmain(String agentArgs)
agentArgs
参数的值必须为空字符串。
agentmain
函数必须完成代理启动必须的所有初始化动作并在启动后返回。如果,代理无法启动或抛出未捕获的异常,JVM会退出。
1.3.4 加载代理类以及代理类可用的模块/类
系统类加载器负责加载代理JAR文件中的所有类,并且成为系统类加载器的未命名模块的成员。 系统类加载器通常也定义包含应用程序main方法的类。对代理类可见的所有类都对系统类加载器可见,必须满足下面的最低要求:
-
启动层中的模块导出的包中的类。 启动层是否包含所有平台模块取决于初始模块或应用程序的启动方式。
-
类可被系统类加载器定义。
-
启动类加载器定义的所有代理的类为其未命名模块的成员。
如果代理类需要链接到不在启动层中的平台(或其他)模块中的类,则需要以确保这些模块位于启动层中的方式启动应用程序。 例如,在JDK实现中,--add-modules
命令行选项可用于将模块添加到要在启动时解析的根模块集中。
启动类加载器可以加载代理支持的类(通过appendToBootstrapClassLoaderSearch
或指定Boot-Class-Path
属性)必须仅链接到定义启动类加载器的类。 无法保证启动类加载器可以在所有平台工作。
如果配置了自定义系统类加载器(通过getSystemClassLoader
方法中指定的系统属性java.system.class.loader
),则必须定义appendToSystemClassLoaderSearch
中指定的appendToClassPathForInstrumentation
方法。 换句话说,自定义系统类加载器必须支持将代理JAR文件添加到系统类加载器搜索范围内的机制。
1.4 javaagent清单属性
属性 | 说明 | 是否必选 | 默认值 |
---|---|---|---|
Premain-Class | 包含premain方法的类 | 依赖启动方式 | 无 |
Agent-Class | 包含agentmain方法的类 | 依赖启动方式 | 无 |
Boot-Class-Path | 启动类加载器搜索路径 | 否 | 无 |
Can-Redefine-Classes | 是否可以重定义代理所需的类 | 否 | false |
Can-Retransform-Classes | 是否能够重新转换此代理所需的类 | 否 | false |
Can-Set-Native-Method-Prefix | 是否能够设置此代理所需的本机方法前缀 | 否 | false |
2 写一个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文件即可。除去手动配置外,可以使用maven插件在打包阶段自动生成,maven的插件配置如下:
<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包格式如下:
java-agent.jpg
其中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
网友评论