• Java 调式、热部署、JVM 背后的支持者 Java Agent


    我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着。但其实我们一直在用它,而且接触的机会非常多。下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了。

    -各个 Java IDE 的调试功能,例如 eclipse、IntelliJ ;

    -热部署功能,例如 JRebel、XRebel、 spring-loaded;

    -各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;

    -各种性能分析工具,例如 Visual VM、JConsole 等;

    Java Agent 直译过来叫做 Java 代理,还有另一种称呼叫做 Java 探针。首先说 Java Agent 是一个 jar 包,只不过这个 jar 包不能独立运行,它需要依附到我们的目标 JVM 进程中。我们来理解一下这两种叫法。

    代理:比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理;

    探针:这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。

    拿上面的几个我们平时会用到的技术举例子。拿 IDEA 调试器来说吧,当开启调试功能后,在 debugger 面板中可以看到当前上下文变量的结构和内容,还可以在 watches 面板中运行一些简单的代码,比如取值赋值等操作。还有 Btrace、Arthas 这些线上排查问题的工具,比方说有接口没有按预期的返回结果,但日志又没有错误,这时,我们只要清楚方法的所在包名、类名、方法名等,不用修改部署服务,就能查到调用的参数、返回值、异常等信息。

    上面只是说到了探测的功能,而热部署功能那就不仅仅是探测这么简单了。热部署的意思就是说再不重启服务的情况下,保证最新的代码逻辑在服务生效。当我们修改某个类后,通过 Java Agent 的 instrument 机制,把之前的字节码替换为新代码所对应的字节码。

    Java Agent 结构


    Java Agent 最终以 jar 包的形式存在。主要包含两个部分,一部分是实现代码,一部分是配置文件。

    配置文件放在 META-INF 目录下,文件名为 MANIFEST.MF 。包括以下配置项:

    Manifest-Version: 版本号
    Created-By: 创作者
    Agent-Class: agentmain 方法所在类
    Can-Redefine-Classes: 是否可以实现类的重定义
    Can-Retransform-Classes: 是否可以实现字节码替换
    Premain-Class: premain 方法所在类

    入口类实现 agentmainpremain 两个方法即可,方法要实现什么功能就由你的需求决定了。

    Java Agent 实现和使用

    接下来就来实现一个简单的 Java Agent,基于 Java 1.8,主要实现两点简单的功能:

    1、打印当前加载的所有类的名称;

    2、监控一个特定的方法,在方法中动态插入简单的代码并获取方法返回值;

    在方法中插入代码主要是用到了字节码修改技术,字节码修改技术主要有 javassist、ASM,已经 ASM 的高级封装可扩展 cglib,这个例子中用的是 javassist。所以需要引入相关的 maven 包。

    <dependency>
        <groupId>javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.12.1.GA</version>
    </dependency>
    

    实现入口类和功能逻辑

    入口类上面也说了,要实现 agentmainpremain 两个方法。这两个方法的运行时机不一样。这要从 Java Agent 的使用方式来说了,Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain方法,并且是在主进程的 main方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain方法。

    加载方式 执行方法
    -javaagent:xxx.jar 参数形式 premain
    动态 attach agentmain

    代码实现如下:

    package kite.lab.custom.agent;
    
    import java.lang.instrument.Instrumentation;
    
    public class MyCustomAgent {
        /**
         * jvm 参数形式启动,运行此方法
         * @param agentArgs
         * @param inst
         */
        public static void premain(String agentArgs, Instrumentation inst){
            System.out.println("premain");
            customLogic(inst);
        }
    
        /**
         * 动态 attach 方式启动,运行此方法
         * @param agentArgs
         * @param inst
         */
        public static void agentmain(String agentArgs, Instrumentation inst){
            System.out.println("agentmain");
            customLogic(inst);
        }
    
        /**
         * 打印所有已加载的类名称
         * 修改字节码
         * @param inst
         */
        private static void customLogic(Instrumentation inst){
            inst.addTransformer(new MyTransformer(), true);
            Class[] classes = inst.getAllLoadedClasses();
            for(Class cls :classes){
                System.out.println(cls.getName());
            }
        }
    }
    

    我们看到这两个方法都有参数 agentArgs 和 inst,其中 agentArgs 是我们启动 Java Agent 时带进来的参数,比如-javaagent:xxx.jar agentArgs。Instrumentation Java 开放出来的专门用于字节码修改和程序监控的实现。我们要实现的打印已加载类和修改字节码也就是基于它来实现的。其中 inst.getAllLoadedClasses()一个方法就实现了获取所以已加载类的功能。

    inst.addTransformer方法则是实现字节码修改的关键,后面的参数就是实现字节码修改的实现类,代码如下:

    public class MyTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("正在加载类:"+ className);
            if (!"kite/attachapi/Person".equals(className)){
                return classfileBuffer;
            }
    
            CtClass cl = null;
            try {
                ClassPool classPool = ClassPool.getDefault();
                cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                CtMethod ctMethod = cl.getDeclaredMethod("test");
                System.out.println("获取方法名称:"+ ctMethod.getName());
    
                ctMethod.insertBefore("System.out.println(" 动态插入的打印语句 ");");
                ctMethod.insertAfter("System.out.println($_);");
    
                byte[] transformed = cl.toBytecode();
                return transformed;
            }catch (Exception e){
                e.printStackTrace();
    
            }
            return classfileBuffer;
        }
    }
    

    以上代码的逻辑就是当碰到加载的类是 kite.attachapi.Person的时候,在其中的 test 方法开始时插入一条打印语句,打印内容是"动态插入的打印语句",在test方法结尾处,打印返回值,其中$_就是返回值,这是 javassist 里特定的标示符。

    MANIFEST.MF 配置文件

    在目录 resources/META-INF/ 下创建文件名为 MANIFEST.MF 的文件,在其中加入如下的配置内容:

    Manifest-Version: 1.0
    Created-By: fengzheng
    Agent-Class: kite.lab.custom.agent.MyCustomAgent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Premain-Class: kite.lab.custom.agent.MyCustomAgent
    

    配置打包所需的 pom 设置

    最后 Java Agent 是以 jar 包的形式存在,所以最后一步就是将上面的内容打到一个 jar 包里。

    在 pom 文件中加入以下配置

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
        </plugins>
    </build>
    

    用的是 maven 的 maven-assembly-plugin 插件,注意其中要用 manifestFile 指定 MANIFEST.MF 所在路径,然后指定 jar-with-dependencies ,将依赖包打进去。

    上面这是一种打包方式,需要单独的 MANIFEST.MF 配合,还有一种方式,不需要在项目中单独的添加 MANIFEST.MF 配置文件,完全在 pom 文件中配置上即可。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>kite.agent.vmargsmethod.MyAgent</Premain-Class>
                                    <Agent-Class>kite.agent.vmargsmethod.MyAgent</Agent-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    

    这种方式是将 MANIFEST.MF 的内容全部写作 pom 配置中,打包的时候就会自动将配置信息生成 MANIFEST.MF 配置文件打进包里。

    运行打包命令

    接下来就简单了,执行一条 maven 命令即可。

    mvn assembly:assembly
    

    最后打出来的 jar 包默认是以「项目名称-版本号-jar-with-dependencies.jar」这样的格式生成到 target 目录下。

    运行打包好的 Java Agent

    首先写一个简单的测试项目,用来作为目标 JVM,稍后会以两种方式将 Java Agent 挂到这个测试项目上。

    package kite.attachapi;
    
    import java.util.Scanner;
    
    public class RunJvm {
    
        public static void main(String[] args){
            System.out.println("按数字键 1 调用测试方法");
            while (true) {
                Scanner reader = new Scanner(System.in);
                int number = reader.nextInt();
                if(number==1){
                    Person person = new Person();
                    person.test();
                }
            }
        }
    }
    

    以上只有一个简单的 main 方法,用 while 的方式保证线程不退出,并且在输入数字 1 的时候,调用 person.test()方法。

    以下是 Person 类

    package kite.attachapi;
    
    public class Person {
    
        public String test(){
            System.out.println("执行测试方法");
            return "I'm ok";
        }
    }
    

    以命令行的方式运行

    因为项目是在 IDEA 里创建的,为了省事儿,我就直接在 IDEA 的 「Run/Debug Configurations」里加参数了。

    -javaagent:/java-agent路径/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
    

    然后直接运行就可以看到效果了,会看到加载的类名称。然后输入数字键 "1",会看到字节码修改后的内容。

    以动态 attach 的方式运行

    测试之前先要把这个测试项目跑起来,并把之前的参数去掉。运行后,找到这个它的进程id,一般利用jps -l即可。

    动态 attach 的方式是需要代码实现的,实现代码如下:

    public class AttachAgent {
    
        public static void main(String[] args) throws Exception{
            VirtualMachine vm = VirtualMachine.attach("pid(进程号)");
            vm.loadAgent("java-agent路径/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
        }
    }
    

    运行上面的 main 方法 并在测试程序中输入“1”,会得到上图同样的结果。

    发现了没,我们到这里实现的简单的功能是不是和 BTrace 和 Arthas 有点像呢。我们拦截了指定的一个方法,并在这个方法里插入了代码而且拿到了返回结果。如果把方法名称变成可配置项,并且把返回结果保存到一个公共位置,例如一个内存数据库,是不是我们就可以像 Arthas 那样轻松的检测线上问题了呢。当然了,Arthas 要复杂的多,但原理是一样的。

    sun.management.Agent 的实现

    不知道你平时有没有用过 visualVM 或者 JConsole 之类的工具,其实,它们就是用了 management-agent.jar 这个Java Agent 来实现的。如果我们希望 Java 服务允许远程查看 JVM 信息,往往会配置上一下这些参数:

    -Dcom.sun.management.jmxremote
    -Djava.rmi.server.hostname=192.168.1.1
    -Dcom.sun.management.jmxremote.port=9999
    -Dcom.sun.management.jmxremote.rmi.port=9999
    -Dcom.sun.management.jmxremote.authenticate=false
    -Dcom.sun.management.jmxremote.ssl=false
    

    这些参数都是 management-agent.jar 定义的。

    我们进到 management-agent.jar 包下,看到只有一个 MANIFEST.MF 配置文件,配置内容为:

    Manifest-Version: 1.0
    Created-By: 1.7.0_07 (Oracle Corporation)
    Agent-Class: sun.management.Agent
    Premain-Class: sun.management.Agent
    

    可以看到入口 class 为 sun.management.Agent,进到这个类里面可以找到 agentmain 和 premain,并可以看到它们的逻辑。在这个类的开始,能看到我们前面对服务开启远程 JVM 监控需要开启的那些参数定义。

    不要吝惜你的「推荐」呦

    欢迎关注,不定期更新本系列和其他文章
    古时的风筝 ,进入公众号可以加入交流群

  • 相关阅读:
    CodeForces 681D Gifts by the List (树上DFS)
    UVa 12342 Tax Calculator (水题,纳税)
    CodeForces 681C Heap Operations (模拟题,优先队列)
    CodeForces 682C Alyona and the Tree (树上DFS)
    CodeForces 682B Alyona and Mex (题意水题)
    CodeForces 682A Alyona and Numbers (水题,数学)
    Virtualizing memory type
    页面跳转
    PHP Misc. 函数
    PHP 5 Math 函数
  • 原文地址:https://www.cnblogs.com/fengzheng/p/11502963.html
Copyright © 2020-2023  润新知