美文网首页
java进阶之agent代理系列(一)——使用premain模式

java进阶之agent代理系列(一)——使用premain模式

作者: moutory | 来源:发表于2023-12-11 17:19 被阅读0次

前言

前段时间接触到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-classretransform-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方法确实被正常执行了

image.png

三、使用redefineClassretransformClass两种方式来进行代理

上一小节中,我们使用了一个简单的案例来演示了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方式来进行代理

RedefineClassesRetransformClass方法其实很类似,区别只是方式一后者是通过手动指定类的全路径来加载被代理的类,而前者是通过 xxx.class的方式来获取要被代理的类。我们还是用上一小节的案例来进行演示。

步骤一、开发客户端代码

这里和上一小节一样,此处不做赘述

步骤二、准备代理文件

这里和上一小节一样,此处不做赘述

步骤三、引入需要代理的对象到代理端项目中

需要把客户端的类复制一份到客户端这里,需要注意的是为了能顺利打包,此处还要和客户端创建一样的包路径。
(PS:也可以选择把相关的dto当做依赖引入进来)

image.png
步骤四、开发代理端代码
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来帮助我们更加高效的完成对方法级别的代理增强,具体方法可以见下一篇文章。

参考文章:
https://zhuanlan.zhihu.com/p/626704405?utm_id=0

相关文章

网友评论

      本文标题:java进阶之agent代理系列(一)——使用premain模式

      本文链接:https://www.haomeiwen.com/subject/lgdugdtx.html