字节码插桩之Java Agent
本篇文章将详细讲解有关Java Agent的知识,揭开它神秘的面纱,帮助开发人员了解它的黑魔法,帮助我们完成更多业务需求
What is Java Agent
Java Agent又称为Java探针,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口;通过使用Java Instrumentation API,它能够侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。
在Java程序运行环境中,instrumentation是一种用于更改现有应用程序并向其添加代码的技术,可以在编译期间或者运行时执行此操作。它的优点就是,我们无需编辑源代码文件就能修改代码、更改它的行为;这非常有效,但也非常危险。
我们可以用它来实现很多功能,例如AOP
Java Agent是Java Instrumentation API的一部分,Java Instrumentation API提供了一种可以动态或者静态地修改字节码的机制,这意味着我们可以在不修改源代码的情况下向Java类中添加代码,这对Java应用程序有着很重要的影响
简单来说,Java Agent是一种特殊的jar文件,但是它包含遵循特殊约定的Java类,并在jar文件中的MANIFEST.MF(该文件通常存放在src/main/resources/META-INF文件夹下)文件中指定遵循特殊约定的Java类;在该特殊的类中有如下两类方法:
-
premain:在JVM启动时加载的Java Agent,例如使用java -jar命令启动jar文件时,添加-javaagent命令添加Java Agent;通俗来说就是在JVM初始化之后&main方法之前运行该agent
其方法签名如下:
public static void premain(String agentArgs, Instrumentation inst)
如果不包含上述方法,可选方法如下:
public static void premain(String agentArgs)
-
agentmain:使用Java Attach API将Java Agent动态加载到JVM中,在JVM初始化之后运行,可以在我们的main方法中通过Attach API调用该agent
其方法签名如下:
public static void agentmain(String agentArgs, Instrumentation inst) public static void agentmain(String agentArgs)
应用程序是如何识别Java Agent的呢,答案就是MANIFEST.MF文件,该文件作为JAR文件的一部分包含jar文件的元数据信息,其中关于Java Agent的属性如下:
- Premain-Class:当JVM启动时,该属性指定代理类,该类为包含premain方法的类,其值为全限定类名;如果在JVM启动时指定Java Agent则必须定义该属性
- Agent-Class:如果实现支持在JVM启动后某个时间启动agent的机制,则此属性指定代理类;该类为包含agentmain方法的类,其值为全限定类名
- Can-Readefine-Classes:定义Java Agent是否能够重定义Java类,其值为true or false,默认false
- Can-Retransform-Classes:定义Java Agent能否重转换Java类,其值为true or false,默认false
- Can-Set-Native-Method-Prefix:定义Java Agent能否设置所需的本机方法前缀,默认false
- Boot-Class-Path:设置启动类加载器搜索的路径列表;查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径;按列出的顺序搜索路径,列表中的路径由一个或多个空格分开;路径使用分层 URI 的路径组件语法;如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径;相对路径根据代理 JAR 文件的绝对路径解析,忽略格式不正确的路径和不存在的路径,如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径
在使用maven构建项目的过程中,我们可以通过pom.xml中的build属性设置maven-jar-plugin插件来自动生成MANIFEST.MF文件,示例如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--true代表自动添加MANIFEST.MF文件-->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<!--设置MANIFEST.MF文件的相关属性,类文件使用全限定类名-->
<manifestEntries>
<Premain-Class>cn.wygandwdn.agent.TestAgent</Premain-Class>
<Agent-Class>cn.wygandwdn.agent.TestAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
Java Instrumentation API解析
官方文档:
java.instrument (Java SE 9 & JDK 9 )
java.lang.instrument (Java Platform SE 8 )
Java Instrumentation API主要提供了两个接口:Instrumentation、ClassFileTransformer:
- Instrumentation:该类提供了向Java中插入代码的服务
- ClassFileTransformer:顾名思义,该类就是类文件转换器接口,Java Agent提供该接口的实现,用于转换类文件
Instrumentation提供了一些添加&移除类转换器(ClassFileTransformer)、获取被JVM加载的类、重定义类、判断类是否支持重转换&重定义的接口,详细介绍如下:
- addTransformer(ClassFileTransformer transformer, boolean canRetransform):注册类转换器。除了注册类转换器依赖的类定义以外,所有的类定义都会经过类转换器;当类加载时、重定义时、重转换时(如果canRetransform为true),会调用类转换器;如果一个类转换器抛出异常,JVM会依次调用其他类转换器;同一个类转换器实例可以被添加多次,但是强烈建议不要这么做,可以通过创建新的实例来避免这种做法
- addTransformer(ClassFileTransformer transformer):注册类转换器,与addTransformer(transformer, false)作用相同
- removeTransformer(ClassFileTransformer transformer):移除已注册的transformer,类定义将不再经过已经移除的transformer;移除策略为移除最近添加的transformer实例;