JavaAgent实战 | 如何通过无侵入式打印方法耗时

2023年 8月 5日 28.2k 0

JavaAgent实战

相关资料

仓库地址:gitee.com/ilovexp/jav…

什么是JavaAgent

JavaAgent是Java中的一种特殊类型的代理技术,它允许在Java应用程序编译期或运行时动态地监控和修改字节码。这使得开发者能够在不修改源代码的情况下,向应用程序添加新功能或执行一些特定的任务。

应用场景

  • 性能监测与优化:JavaAgent可以监控应用程序的性能指标,如方法执行时间、内存使用情况、线程状态等。开发人员可以利用这些信息来识别性能瓶颈,并进行代码优化。
  • AOP(面向切面编程):JavaAgent可以实现AOP编程,通过字节码增强技术,将横切逻辑(如日志记录、安全检查等)从业务逻辑中分离出来,提高代码的可维护性和复用性。
  • 代码注入:JavaAgent可以在运行时动态地向应用程序中注入代码。这可以用于实现一些特定的功能,如方法追踪、事件监听等。
  • 动态修改字节码:JavaAgent可以在类加载时修改类的字节码。通过字节码增强,可以为类添加新的字段或方法,甚至可以修改现有方法的实现。
  • 远程调试与监控:JavaAgent可以充当远程调试和监控的代理,让开发人员能够远程连接到应用程序,并查看和修改运行时的状态。
  • 应用程序安全:JavaAgent可以用于增强应用程序的安全性,例如,拦截敏感信息的传输、检查权限等。
  • 类加载器分析:JavaAgent可以帮助开发人员分析类的加载情况,包括加载哪些类以及类加载器的层次结构。
  • 代码覆盖率测试:JavaAgent可以用于代码覆盖率测试,帮助开发人员评估测试用例的覆盖范围,以及发现未被覆盖的代码块。
  • 核心原理

    Instrumentation API

    Instrumentation API 是 Java 提供的一组用于在 Java 虚拟机 (JVM) 运行时监控和修改类定义的工具。它允许开发人员在类加载时,动态地检测和修改类的字节码,从而实现一系列功能。主要作用如下:

  • 类转换(Class Transformation):Instrumentation API 允许 JavaAgent 在类被加载到 JVM 之前,拦截并修改类的字节码。这使得 JavaAgent 可以在运行时对类进行增强、修改或注入新的代码,从而实现各种功能,如 AOP、性能监测等。
  • 类加载事件监听(Class Loading Events):Instrumentation API 允许 JavaAgent 监听类加载和卸载事件。开发人员可以在类被加载或卸载时,做一些额外的处理,例如记录日志、进行统计等。
  • 获取已加载类信息(Class Information Retrieval):Instrumentation API 提供了一些方法,可以获取已加载类的信息,如类名、类的父类、已加载类的字节码等。这为开发人员提供了一些基础信息,可以用于自定义的类加载过程或类信息的分析。
  • 重新定义类(Redefine Classes):Instrumentation API 允许 JavaAgent 在运行时重新定义类。这个功能允许开发人员对已经加载的类进行修改,但是需要注意,这个功能的使用受到一些限制,比如不能新增或删除字段。
  • 动态注入(Dynamic Code Injection):借助 Instrumentation API,JavaAgent 可以在类加载时将一些额外的代码动态地注入到类中。这可以用于实现动态代理、事件监听等功能。
  • 获取 Instrumentation 实例

    开发人员可以通过调用 Agent.premainAgent.agentmain 方法来获取 Instrumentation 实例,从而使用 Instrumentation API。

    javaAgent的方法.png

    • addTransformer(ClassFileTransformer transformer, boolean canRetransform): 添加一个 ClassFileTransformer,用于在类被加载时转换类的字节码。canRetransform 参数表示是否允许对已加载类进行重新转换。
    • retransformClasses(Class... classes): 对指定的一组类进行重新转换,这些类必须已经被加载。
    • getAllLoadedClasses(): 获取当前已经加载的所有类的数组。
    • getInitiatedClasses(ClassLoader loader): 获取由指定类加载器加载的所有类的数组。

    ClassFileTransformer API

    public interface ClassFileTransformer {
        byte[] transform(ClassLoader    loader,
                    String              className,
                    Class            classBeingRedefined,
                    ProtectionDomain    protectionDomain,
                    byte[]              classfileBuffer)
            throws IllegalClassFormatException;
    }
    

    ClassFileTransformer 接口:这是一个用于类转换的接口,开发人员需要实现它来定义类的转换行为。它只有一个方法 transform,在类被加载时会被调用,开发人员可以在这个方法中对类的字节码进行修改,并返回修改后的字节码。

  • ClassLoader loader: 表示加载当前类的类加载器。该参数指示了加载当前类的类加载器实例,可以用于追溯类的类加载器层次结构。
  • String className: 表示正在被转换的类的类名。这是一个完全限定的类名,例如 "com.example.MyClass"。
  • Class classBeingRedefined: 表示正在被重新定义的类。在进行类的重新定义时才会有值,否则为 null。通常情况下,我们不需要在类加载时关心这个参数。
  • ProtectionDomain protectionDomain: 表示类的保护域。保护域描述了类所属的安全策略和权限信息。它可以用于检查类的访问权限,或者获取与类相关的安全上下文。
  • byte[] classfileBuffer: 表示类的字节码。这是一个字节数组,包含了正在加载的类的原始字节码。在 transform 方法中,可以修改这个字节码,并返回修改后的字节码。
  • throws IllegalClassFormatException: 当 transform 方法无法处理类时,可以抛出这个异常。通常,如果你无法处理正在加载的类,可以直接将原始的 classfileBuffer 返回,不做任何修改。
  • VirtualMachine API

    VirtualMachine 是一个类,它位于 com.sun.tools.attach 包中。它提供了一组用于管理和操作正在运行的Java虚拟机的工具方法。主要作用是允许Java应用程序在运行时连接和控制其他正在运行的Java虚拟机,从而实现一些高级的功能。

    通过使用 VirtualMachine 类,Java应用程序可以在运行时:

  • Attach to VM(连接到虚拟机):Java应用程序可以使用 VirtualMachine.attach() 方法连接到正在运行的Java虚拟机。这允许应用程序获得对目标虚拟机的访问权,并执行后续操作,例如获取虚拟机信息、执行诊断命令等。
  • Load Agent(加载代理):一旦Java应用程序连接到目标虚拟机,它可以使用 VirtualMachine.loadAgent() 方法来加载Java代理(JavaAgent)。JavaAgent是一种特殊类型的代理,它可以在目标虚拟机中运行,并对类加载进行拦截和修改。
  • 执行诊断命令:Java应用程序可以使用 VirtualMachine.startManagementAgent() 方法来启动目标虚拟机的管理代理(management agent)。然后,应用程序可以通过JMX(Java Management Extensions)连接到管理代理,执行一些诊断命令、获取JVM运行时信息等。
  • Detaching(断开连接):Java应用程序还可以使用 VirtualMachine.detach() 方法断开与目标虚拟机的连接,当不再需要连接时,可以通过这种方式释放资源。
  • 实战代码

    整个项目目录

    application项目是实际运行的项目,可以理解为实际执行的业务jar包

    myAgent项目是编写agent代码的项目

    middle项目是中间项目,在agentMain模式下,用于连接application和agentMain的项目;

    ├─application # 实际运行项目
    │  ├─src
    │  │  ├─main
    │  │  │  ├─java
    │  │  │  │  └─cn
    │  │  │  │      └─ixp
    │  │  │  │          └─agent
    │  │  │  └─resources
    │  │  └─test
    │  │      └─java
    ├─middle #中间项目,用于agent和application的连接,仅agentMain模式有用 
    │  ├─src
    │  │  └─main
    │  │      └─java
    │  │          └─cn
    │  │              └─ixp
    │  │                  └─agent
    ├─myAgent # agent项目
    │  ├─src
    │  │  ├─main
    │  │  │  ├─java
    │  │  │  │  └─cn
    │  │  │  │      └─ixp
    │  │  │  │          └─agent
    │  │  │  │              └─transformer
    │  │  │  └─resources
    │  │  └─test
    │  │      └─java
    

    PreMain方式

    premain 方式的 JavaAgent 是在 Java 应用程序启动时,即主函数执行之前,通过指定启动参数 -javaagent 加载的。它的主要效果是在应用程序类加载之前,对类进行拦截并修改类的字节码,从而实现对类的增强、动态注入等操作。

    程序执行顺序:premain() => main()

    已知,我们有一个Man类,应用程序启动时,循环调用Man类的getName()方法;屏幕上不断打印"Dragon like sheep";当启动Agent时,将打印的文字替换为:"This is ordertiny";

    Application项目:

    public class Man {
        public Man() {
            System.out.println("Create Man");
        }
    
        public void getName() {
            try {
                Thread.sleep((long) (Math.random()*10 * 1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Dragon like sheep");
        }
    }
    
    // 启动类
    public class ApplicationMain {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("U are in Application Main");
            while (true) {
                Man man = new Man();
                man.getName();
                Thread.sleep(1000);
            }
        }
    }
    
    编写Agent项目
    引入Jar包
    
    
        
            com.squareup
            javapoet
            1.13.0
        
    
        
            org.javassist
            javassist
            3.27.0-GA
        
    
    
    
    
        
            
            
                org.apache.maven.plugins
                maven-shade-plugin
                3.3.0
                
                    
                        package
                        
                            shade
                        
                        
                            
                                
                                    org.javassist:javassist
                                    
                                
                            
                            
                                
                                    
                                        cn.ixp.agent.InPreMain
                                        true
                                        true
                                    
                                
                            
                        
                    
                
            
        
    
    

    这样可以在打包的时候在MANIFEST.MF中显示声明Premain-Class

    # MANIFEST.MF
    Manifest-Version: 1.0
    Premain-Class: cn.ixp.agent.InPreMain
    Archiver-Version: Plexus Archiver
    Built-By: lenovo
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Created-By: Apache Maven 3.6.0
    Build-Jdk: 1.8.0_181
    
    定义入口方法
    package cn.ixp.agent;
    
    import cn.ixp.agent.transformer.MyClassFileTransformer;
    import java.lang.instrument.Instrumentation;
    
    public class InPreMain {
        /**
         * 固定格式,声明premain函数,然后在其中指定我们自己定义的代理类
         * @param agentArgs 捕获agent参数
         * 例如:java -javaagent:myagent.jar=myarg1,myarg2 -jar myapp.jar
         * 那么agentArgs就是myarg1,myarg2
         * @param inst 自动注入的Instrumentation实例
         */
        public static void premain(String agentArgs, Instrumentation inst) {
            // 打印一下参数
            System.out.println("premain invoke!"+agentArgs);
            // 添加自定义Transformer
            inst.addTransformer(new MyClassFileTransformer(), true);
        }
    }
    
    自定义ClassFileTransformer

    用于将原有cn.ixp.agent.Man.class中的getName()方法体修改为 输出"This is orderting"

    package cn.ixp.agent.transformer;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    
    public class MyClassFileTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                System.out.println("transform invoke! "+className);
                // 修改指定类
                if (className.contains("cn/ixp/agent/Man")){
                    final ClassPool classPool = ClassPool.getDefault();
                    final CtClass clazz = classPool.get("cn.ixp.agent.Man");
                    CtMethod convertToAbbr = clazz.getDeclaredMethod("getName");
                    // 修改指定的方法体
                    String methodBody = "System.out.println("This is ordertiny");";
                    convertToAbbr.setBody(methodBody);
                    // 返回字节码,并且detachCtClass对象
                    byte[] byteCode = clazz.toBytecode();
                    //detach的意思是将内存中曾经被javassist加载过的对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                    clazz.detach();
                    return byteCode;
                }
            }catch (Exception e) {
                System.out.println("error");
            }
            // 表示不修改原有类
            return null;
        }
    }
    

    此时将agent项目打成jar包

    启动application项目后,Man的输出将被agent修改;

    java -javaagent:myAgent-1.0-SNAPSHOT.jar -jar application.jar
    

    AgentMain方式

    实现一个方法的耗时打印

    我们实现一个方法的耗时打印,我们在每个方法进入前和后加入时间戳,用来统计方法耗时;

    修改Pom文件

    指定Agent-Class所在类路径;

    
    
        
            javaAgent
            cn.ixp.agent
            1.0-SNAPSHOT
        
        4.0.0
    
        myAgent
    
        
            8
            8
            UTF-8
        
    
        
            
                com.squareup
                javapoet
                1.13.0
            
    
            
                org.javassist
                javassist
                3.27.0-GA
            
        
    
        
            
                
                
                    org.apache.maven.plugins
                    maven-shade-plugin
                    3.3.0
                    
                        
                            package
                            
                                shade
                            
                            
                                
                                    
                                        org.javassist:javassist
                                        
                                    
                                
                                
                                    
                                        
                                            cn.ixp.agent.InPreMain
                                            cn.ixp.agent.InAgentMain
                                            true
                                            true
                                        
                                    
                                
                            
                        
                    
                
            
        
    
    
    指定AgentMain入口
    package cn.ixp.agent;
    
    import cn.ixp.agent.transformer.InjectMethodCostTimeTransformer;
    import cn.ixp.agent.transformer.MyClassFileTransformer;
    
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    
    public class InAgentMain {
        public static void agentmain(String agentArgs, Instrumentation inst){
            // 添加耗时统计的Transformer
            inst.addTransformer(new InjectMethodCostTimeTransformer(), true);
            try {
                for (Class innerClass : inst.getAllLoadedClasses()) {
                    if (innerClass.getName().contains("cn.ixp")) {
                        // 指定某个包下的类
                        inst.retransformClasses(innerClass);
                    }
                }
            } catch (UnmodifiableClassException e) {
                System.out.println("出错");
                throw new RuntimeException(e);
            }
        }
    }
    
    自定义Transformer
    package cn.ixp.agent.transformer;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    
    public class InjectMethodCostTimeTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("InjectMethodCostTimeTransformer transform invoke! "+className);
            String targetClassName = className.replaceAll("/", ".");
            try {
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get(targetClassName);
                for (CtMethod declaredMethod : clazz.getDeclaredMethods()) {
    
                    declaredMethod.addLocalVariable("_time", CtClass.longType);
                    // 进入时间
                    String param1 = "_time=System.currentTimeMillis();";
                    // 打印语句
                    String param2 = "System.out.println(""+declaredMethod.getName()+" cost:"+(System.currentTimeMillis()-_time));";
                    declaredMethod.insertBefore(param1);
                    declaredMethod.insertAfter(param2);
                }
                // 返回字节码,并且detachCtClass对象
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                clazz.detach();
                return byteCode;
            } catch (Exception e) {
                System.out.println("has error"+targetClassName);
                e.printStackTrace();
            }
            return null;
        }
    }
    
    将Agent项目打成jar包
    编写中间项目

    middle项目只是一个桥接项目,它用于

    package cn.ixp.agent;
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    
    import java.util.List;
    
    public class Main {
    
        // 不会执行
        public static void main(String[] args) throws Exception {
            System.out.println("Here is MyAgent Main");
            List list = VirtualMachine.list();
            for (VirtualMachineDescriptor vmd : list) {
                //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
                //然后加载 agent.jar 发送给该虚拟机
                System.out.println(vmd.displayName());
                if ("cn.ixp.agent.ApplicationMain".equals(vmd.displayName())) {
                    VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                    // 加载agent的jar
             		virtualMachine.loadAgent("myAgent-1.0-SNAPSHOT.jar");
                    // 加载完毕后 断开连接
                    virtualMachine.detach();
                }
            }
        }
    }
    

    首先保证Application正在运行,然后执行middle项目,此时即可将 正在运行的java进程中类,通过agent注入了方法耗时统计功能;

    注意

    我们的agent项目无法单独启动,需要调试时,应该debug应用进程,才能调试agent代码;

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论