第一次见到javaagent
时,是偶然了解到Spring的AOP中使用了一个Instrumentation技术,对自己来说是一个新的知识点,所以很好奇,因此查阅相关文档和资料进行学习,在此记录,如有不妥之处,请指正。
运行环境:
- 操作系统:Windows10
- jdk版本:openjdk version 11.0.7
javaagent
顾名思义就是一个java代理,我们知道任何一项java应用的启动都需要有一个入口函数,加载从入口函数开始一直扩散到整个应用。类在jvm中的加载顺序是:加载——>验证——>准备——>解析——>初始化。
在加载阶段,jvm需要读入类的二进制信息,如果我们有一项技术,可以在验证阶段开始前对类的二进制信息进行操作,岂不是美滋滋?此时就需要用到java代理,从外部视角可以将它看做一个java程序的代理,可以通过它在类加载过程中对类信息进行合法操作。
当我们在启动应用程序时,可以使用-javaagent
选项指定对应的javaagent:
-javaagent:<jar 路径>[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument
对于javaagent类,它的主函数前置启动方法(即main函数开始前的方法)
的方法签名有特定的要求,必须要满足以下两种格式:
public static void premain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
注意:文章的
主函数前置启动方法
和主函数后置启方法
都是我自己为了便于称呼起的。
jvm会优先加载第一种,如果第一种没有则加载第二种方法,反之则忽略:
Instrumentation
接口的方法如下:
public interface Instrumentation { /** * 注册一个Class文件转换器,转换器可以观测到除转换器所用到的Class外的所有Class的定义。 * 转换器的顺序由ClassFileTransformer传入顺序决定,当转换器出现异常时,也会按照顺序 * 调用下一个转换器。 * 参数 canRetransform 设置是否允许重新转换。 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); /** * Registers the supplied transformer. *
* Same as
addTransformer(transformer, false)
. * * @param transformer the transformer to register * @throws java.lang.NullPointerException if passed anull
transformer * @see #addTransformer(ClassFileTransformer,boolean) */ void addTransformer(ClassFileTransformer transformer); /** * 删除一个Class转换器 */ boolean removeTransformer(ClassFileTransformer transformer); /** * 判断JVM配置是否支持转换这个类 */ boolean isRetransformClassesSupported(); /** * 在类加载之后,重新定义该Class */ void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; /** * 判断JVM配置是否支持重新定义这个类 */ boolean isRedefineClassesSupported(); /** * 使用提供的类文件重新定义提供的类集 */ void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; /** * 判断该Class是否被retransformation或redefinition修饰 */ boolean isModifiableClass(Class<?> theClass); /** * 返回已经被JVM加载的Class */ @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); /** * 返回被初始化的所有类的集合 */ @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); /** * 返回指定对象的大小 */ long getObjectSize(Object objectToSize); /** * 添加一个包含由引导类加载器定义的instrumentation类的JAR文件。 */ void appendToBootstrapClassLoaderSearch(JarFile jarfile); /** * 添加一个包含由系统类加载器定义的instrumentation类的JAR文件。 */ void appendToSystemClassLoaderSearch(JarFile jarfile); /** * 判断当前JVM配置是否支持设置本地方法前缀 */ boolean isNativeMethodPrefixSupported(); /** * 设置本地方法浅醉 */ void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); /** * 重定义Module */ void redefineModule(Module module, Set<Module> extraReads, Map<String, Set<Module>> extraExports, Map<String, Set<Module>> extraOpens, Set<Class<?>> extraUses, Map<Class<?>, List<Class<?>>> extraProvides); /** * 判断Module是否重定义过 */ boolean isModifiableModule(Module module); }
对于javaagent的主函数后置启动方法(main函数执行后)
的方法签名有以下两种:
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同主函数前置启动方法一样,不带Instrumentation参数的方法优先级较高。对于主函数后置启动方法,它在加载时通过java的Attach API实现:
我们要注意一下VirtualMachine
和VirtualMachineDescriptor
类:
VirtualMachine
映射的是一个java虚拟机,它提供了java虚拟机的相关信息访问和操作。它里面有个attache(String id)
方法,用于通过pid来连接对应的目标虚拟机。它的loadAgent(String agent)
方法可以给虚拟机注册一个agent,在这个agent中会得到一个Instrumentation实例,该实例可以在class加载前改变class字节码,也可以在 class加载后改变其字节码。VirtualMachineDescriptor
是一个描述虚拟机的容器类,其中的displayName
方法和id
方法用于返回虚拟机的名字和pid。VirtualMachineImpl
是VirtualMachine
的进一步实现,它继承了HotSpotVirtualMachine
类(不同的虚拟机的类不同,我的是HotSpot),而HotSpotVirtualMachine
类又继承了VirtualMachine
类。注意:对于主函数后置启动方法的绑定,需要在
MASIFEST.MF
中添加如下信息:# Agent-Class的值是实现了agentmain方法的全限定类名 Agent-Class: bigkai.javaagent.AgentTest
测试的目录结构为:
|--src
|--main
|--java
|--AgentTest.java
|--MainTest.java
|--resources
|--META-INF
|--MANIFEST.MF
注意:当你使用maven进行打包时,默认情况下maven会自动生成
MANIFEST.MF
,从而将你的文件覆盖,所以你需要对pom.xml
文件进行一些配置:<build> <plugins> <plugin> <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-jar-pluginartifactId> <version>2.4version> <configuration> <archive> <manifest> <addClasspath>trueaddClasspath> manifest> <manifestEntries> <Premain-Class>bigkai.javaagent.AgentTestPremain-Class> <Agent-Class>bigkai.javaagent.AgentTestAgent-Class> <Can-Redefine-Classes>trueCan-Redefine-Classes> <Can-Retransform-Classes>trueCan-Retransform-Classes> manifestEntries> archive> configuration> plugin> plugins> build>
public class AgentTest {
/**
* 将在主程序的main方法之前运行
* @param agentArgs 传递过来的参数
* @param inst agent技术主要使用的API可以使用它来改变和重新定义类的行为
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain start");
System.out.println(agentArgs);
}
}
public class MainTest {
public static void main(String[] args) throws Exception {
System.out.println("main starting");
}
}
修改MANIFEST.MF
文件:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: AgentTest
当创建了上面两个类后,便对文件进行打包,然后根据上面所说,对JVM参数进行设置:
-javaagent:E:\test_demo-1.0-SNAPSHOT.jar
接下来你可以启动你的MainTest
程序,结果打印出来后是这样的:
public class AgentTest {
/**
* 将在主程序的main方法之后运行
* @param agentArgs 传递过来的参数
* @param inst agent技术主要使用的API可以使用它来改变和重新定义类的行为
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agentmain start");
System.out.println(agentArgs);
}
}
public class MainTest {
public static void main(String[] args) throws Exception {
System.out.println("main starting");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list){
if (vmd.displayName().equals("MainTest")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("E:\\test_demo-1.0-SNAPSHOT.jar");
virtualMachine.detach();
}
}
}
}
修改MANIFEST.MF
文件:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: AgentTest
Agent-Class: AgentTest
当创建了上面两个类后,便对文件进行打包,然后根据上面所说,对JVM参数进行设置:
-javaagent:E:\test_demo-1.0-SNAPSHOT.jar -Djdk.attach.allowAttachSelf=true