美文网首页
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