前言
前段时间接触到
Java Agent
代理的工程,功能是实现接口的mock自测效果,发现这部分知识点之前很少接触,所以查阅了网上一些资料结合自己实践,写一个agent系列的文章。希望对各位读者有所帮助
本系列更多相关文章:
java进阶之agent代理系列(一)——使用premain模式进行代理
java进阶之agent代理系列(二)——使用agentmain模式进行代理
java进阶之agent代理系列(三)——使用javassist实现接口耗时统计功能
先简单聊聊java agent
说起来Java Agent
的技术由来已久,从JDK1.5版本开始出现,Java Agent
允许程序员构建一个独立于应用程序的代理程序,可以协助监测、运行、甚至替换其他JVM上的程序。简单理解的话,Agent
技术其实就是一门代理技术,思想和AOP很相似,都是实现对程序非入侵性的动态增强。
但这两种代理的生效的方式和作用级别不同,
①在作用级别上:Java Agent
是在JVM级别生效的,而AOP
则是在方法层面上生效的,
②在生效方式上:AOP
的配置或者代码还是需要写在我们的代码工程里面的,而Java Agent
往往是独立一个工程来开发的。
那么聊到这里,大家对Java Agent
应该也有了一个大概的印象,简而言之Java Agent
是一门JVM级别的代理技术
一、Jave Agent
的两种模式
(一)了解代理的两种模式
Jave Agent
提供了两种模式来供我们实现代理
- preMain模式: jdk1.5版本之后提供,代理程序在主程序运行前执行
- agentMain模式:jdk1.6版本之后提供,在主程序运行后执行
两种模式其实本质上没有太大区别,主要差异是在代理的时机。由于premain模式是在主程序运行前执行的,若agent在运行过程中出现异常,那么也会导致主程序的启动失败。而agentMain模式是在主程序运行之后执行的,即使执行失败也不会影响主程序的运行。
(二)写一个简单的agent代理demo
步骤一:新创建一个maven工程(不需要加其他第三方依赖),在src目录中创建一个代理类
类中需要有premain
方法
public class AgentDemo {
public static void premain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
System.out.println("premain start");
System.out.println("args:"+agentArgs);
}
}
步骤二:在pom文件中引入maven打包插件
PS:这一步也可以改为手动在resources目录下创建META-INF/MANIFEST.MF
文件来指定代理入口类
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!-- 包含premain方法的类,需要配置为类的全路径 -->
<Premain-Class>com.qiqv.demo.AgentDemo</Premain-Class>
<!-- 为true时表示能够重新定义class -->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!-- 为true时表示能够重新转换class,实现字节码替换 -->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!-- 为true时表示能够设置native方法的前缀 -->
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
需要注意的主要是下面四个配置:
- Premain-Class:包含premain方法的类,需要配置为类的全路径
- Can-Redefine-Classes:为true时表示能够重新定义class
- Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换
- Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀
redefine-class
和retransform-classes
这两个配置的具体使用场景我们在下文会再具体介绍
步骤三:对项目进行打包,得到代理的jar文件
mvn clean package
步骤四:在被代理应用的启动参数上,加入代理作为启动参数
-javaagent:步骤三得到的jar文件全路径
eg: -javaagent:D:/MyCode/java-agent/java-agent-dev/java-agent-mock/target/java-agent-mock.jar
在idea中配置agent路径
步骤五:执行main方法
我们可以看到,在main方法执行之前,代理类中的premain
方法确实被正常执行了
三、使用redefineClass
和 retransformClass
两种方式来进行代理
上一小节中,我们使用了一个简单的案例来演示了premain方法的执行,但代理的能力远不止于此。我们更加常用的是利用premain
方法中的Instrumentation入参,通过它来定义处理代理逻辑的具体方式。
(一)使用retransformClass方式
步骤一、开发客户端代码
在客户端代码中,我们定义了一个Apple对象,在 getName
方法中固定返回apple
字符串。在MainApplication
类中,我们调用了apple对象的getName
方法。
public class Apple {
public String getName(){
return "banana";
}
}
public class MainApplication {
public static void main(String[] args) {
Apple apple = new Apple();
System.out.println("apple.getName() = " + apple.getName());
}
}
步骤二、准备代理文件
我们把Apple.class
文件复制一份出来,单独放在一个文件夹中,再把原来的Apple
类对应的getName方法返回值改为apple
这里我们把这个文件放在了C:\Users\98093\Desktop\class
目录下面
步骤三、开发代理端代码
这里我们通过addTransformer
方法来引入了一个自定义的转化器,java agent允许我们通过自定义的转换器来修改类的字节码对象
public class JavaAgentTest {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new AppleTransformer());
}
}
下面是AppleTransformer类的具体代码,自定义的转换类需要实现ClassFileTransformer
接口,核心的逻辑在于transform
方法中,Agent代理会在主程序加载之前,找到类路径在"com/qiqv/agentDemo/Apple"的类对象,将其替换为我们指定的文件的字节码文件。
这里的字节码文件,我们选择从步骤二中准备的代理文件。
public class AppleTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(!className.equals("com/qiqv/agentDemo/Apple")){
return classfileBuffer;
}
System.out.println("find Apple class, start to replace");
byte[] newClassByte = getNewClassBytes();
return newClassByte;
}
private byte[] getNewClassBytes() {
File file = new File("C:\\Users\\98093\\Desktop\\class\\Apple.class");
try(InputStream is = new FileInputStream(file);
ByteArrayOutputStream bs = new ByteArrayOutputStream()){
long length = file.length();
byte[] bytes = new byte[(int) length];
int n;
while ((n = is.read(bytes)) != -1) {
bs.write(bytes, 0, n);
}
return bytes;
}catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
这里配置pom文件和打包和上一章节一致,就不重复赘述了。
步骤四、打包代理端代码,在客户端进行测试
mvn clean package
打包完成后,和之前的步骤一样,我们在main方法中配置启动好代理的启动参数,并执行main方法
PS:只有配置了Can-Retransform-Classes
值为true,这里的代理方法才能执行成功。
可以看到,由于Apple类的字节码被代理替换了,所以最终输出的结果是banana。
(二)使用RedefineClasses
方式来进行代理
RedefineClasses
和RetransformClass
方法其实很类似,区别只是方式一后者是通过手动指定类的全路径来加载被代理的类,而前者是通过 xxx.class的方式来获取要被代理的类。我们还是用上一小节的案例来进行演示。
步骤一、开发客户端代码
这里和上一小节一样,此处不做赘述
步骤二、准备代理文件
这里和上一小节一样,此处不做赘述
步骤三、引入需要代理的对象到代理端项目中
需要把客户端的类复制一份到客户端这里,需要注意的是为了能顺利打包,此处还要和客户端创建一样的包路径。
(PS:也可以选择把相关的dto当做依赖引入进来)
步骤四、开发代理端代码
public class JavaAgentTest {
public static void premain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
System.out.println("premain start");
System.out.println("args:"+agentArgs);
String fileName="C:\\Users\\98093\\Desktop\\class\\Apple.class";
ClassDefinition def=new ClassDefinition(Apple.class,getNewClassBytes(fileName));
inst.redefineClasses(new ClassDefinition[]{def});
}
private static byte[] getNewClassBytes(String fileName) {
File file = new File(fileName);
try(InputStream is = new FileInputStream(file);
ByteArrayOutputStream bs = new ByteArrayOutputStream()){
long length = file.length();
byte[] bytes = new byte[(int) length];
int n;
while ((n = is.read(bytes)) != -1) {
bs.write(bytes, 0, n);
}
return bytes;
}catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
步骤五、打包代理端代码,在客户端进行测试
我们可以看到,apple类同样成功被代理对象替换了。
客户端测试结果
说在最后
我们可以发现,虽然原生的ClassFileTransformer
等接口可以支持我们在字节码层面做一些事情,但是颗粒度还是不高,如果我们希望对某些方法进行增强,实现的复杂度可能会比较大,所以我们可以借助Javaassist
来帮助我们更加高效的完成对方法级别的代理增强,具体方法可以见下一篇文章。
网友评论