• 十三、Java Agent


    十三、Java Agent

        1、概述

            (1)、Java Agent出现在JDK1.5之后,我们平时用的很多工具都是基于Java Agent实现的,例如常见的热部署JRebel,各种线上诊断工具(Btrace, Greys),还有阿里开源的Arthas。

            (2)、 Java Agent就是一个Jar包,只是启动方式和普通Jar包有所不同,对于普通的Jar包,通过指定类的main函数进行启动,但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行。可以使用Agent技术构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他JVM上的程序,使用它可以实现虚拟机级别的AOP功能。

            (3)、Agent分为两种

                ①、一种是premain在主程序之前运行的Agent,premain是在jvm启动的时候类加载到虚拟机之前执行;

                ②、一种是agentmain在主程序之后运行的Agent(前者的升级版,1.6以后提供),可以在jvm启动后类已经加载到jvm中再去转换类,这种方式会转换会有一些限制,比如不能增加或移除字段。

            (4)、几乎可以完成所有操作,但是如果出现问题,就容易导致JVM崩溃。

        2、premain方式

            (1)、创建testagent项目,编写premain方法,方法名固定,不可以变。

                ①、如果premain(String agentArgs, Instrumentation inst)存在执行该方法,不存在执行premain(String agentArgs)方法。

                    agentArgs:agentArgs是premain函数得到的程序参数,随同“-javaagent”一起传入

                    inst:Inst是一个java.lang.instrument.Instrumentation的实例,由JVM自动传入。java.lang.instrument.Instrumentation是instrument包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等。

    1. public class TestAgent {
    2. /**
    3. * 该方法在main方法之前运行,与main方法运行在同一个JVM中
    4. * 并被同一个System ClassLoader装载
    5. * 被统一的安全策略(security policy)和上下文(context)管理
    6. */
    7. public static void premain(String agentArgs, Instrumentation inst) {
    8. // TODO: 自定义服务操作
    9. System.out.println("premain start");
    10. System.out.println(agentArgs);
    11. }
    12. /**
    13. * 如果不存在 premain(String agentArgs, Instrumentation inst)
    14. * 则会执行 premain(String agentArgs)
    15. */
    16. public static void premain(String agentArgs) {
    17. // TODO: 自定义服务操作
    18. System.out.println(“premain start");
    19. System.out.println(agentArgs);
    20. }
    21. }

            (2)、编写MANIFEST.MF文件

                在testagent项目中添加META-INF/MANIFEST.MF文件,跟src同级,文件内容如下。MANIFEST.MF文件用于描述Jar包的信息,例如指定入口函数等。

    1. Manifest-Version: 1.0
    2. Premain-Class: com.test.TestAgent
    3. Can-Redefine-Classes: true

            (3)、打jar包

                ①、添加maven-assembly-plugin插件

                    如果是使用Maven来构建的项目,在构建的时候需要加入如下代码,否则Maven会生成自己的MANIFEST.MF覆盖掉自己创建的。

    1. <plugin>
    2. <groupId>org.apache.maven.pluginsgroupId>
    3. <artifactId>maven-assembly-pluginartifactId>
    4. <configuration>
    5. <descriptorRefs>
    6. <descriptorRef>jar-with-dependenciesdescriptorRef>
    7. descriptorRefs>
    8. <archive>
    9. <manifest>
    10. <addClasspath>trueaddClasspath>
    11. manifest>
    12. <manifestEntries>
    13. <Premain-Class>TestAgentPremain-Class>
    14. <Can-Redefine-Classes>trueCan-Redefine-Classes>
    15. <Can-Retransform-Classes>trueCan-Retransform-Classes>
    16. manifestEntries>
    17. archive>
    18. configuration>
    19. plugin>

                ②、打Java Agent的jar包

            (4)、agent jar包使用

                新建一个项目,使用上面封装好的agent,在该项目的JVM配置上加上配置:-javaagent:jar包路径=参数

    -javaagent:user\project\test\testagent-1.0-SNAPSHOT.jar=test

                注:等号后的参数会传入到premain方法的agentArgs参数中。

        3、agentmain方式

            premain的agent模式有一些缺陷,例如需要在主程序运行前就指定javaagent参数,premain方法中代码出现异常会导致主程序启动失败等,为了解决这些问题,JDK1.6以后提供了在程序运行之后改变程序的能力。

            (1)、编写agentmain方法

    1. public class TestAgent {
    2. /**
    3. * agentmain方法
    4. */
    5. public static void agentmain(String agentArgs, Instrumentation inst) {
    6. // TODO: 自定义服务操作
    7. System.out.println("agentmain start");
    8. System.out.println(agentArgs);
    9. Class[] classes = inst.getAllLoadedClasses();
    10. for (Class cls : classes){
    11. System.out.println(cls.getName());
    12. }
    13. }
    14. /**
    15. * 如果不存在 agentmain(String agentArgs, Instrumentation inst)
    16. * 则会执行 agentmain(String agentArgs)
    17. */
    18. public static void agentmain(String agentArgs) {
    19. // TODO: 自定义服务操作
    20. System.out.println("agentmain start");
    21. System.out.println(agentArgs);
    22. }
    23. }

            (2)、编写MANIFEST.MF文件,跟premain相同

            (3)、打jar包

    1. <plugin>
    2. <groupId>org.apache.maven.pluginsgroupId>
    3. <artifactId>maven-assembly-pluginartifactId>
    4. <configuration>
    5. <descriptorRefs>
    6. <descriptorRef>jar-with-dependenciesdescriptorRef>
    7. descriptorRefs>
    8. <archive>
    9. <manifest>
    10. <addClasspath>trueaddClasspath>
    11. manifest>
    12. <manifestEntries>
    13. <Agent-Class>TestAgentAgent-Class>
    14. <Can-Redefine-Classes>trueCan-Redefine-Classes>
    15. <Can-Retransform-Classes>trueCan-Retransform-Classes>
    16. manifestEntries>
    17. archive>
    18. configuration>
    19. plugin>

            (4)、B程序编写测试类

                在程序运行后加载,是不可能在主程序A中编写加载的代码,只能另写程序B,A、B程序之间的通信会用到attach机制,它可以将JVM B连接至JVM A,并发送指令给JVM A执行,JDK自带常用工具如jstack,jps等就是使用该机制来实现的。

    1. public class TestAgent {
    2. /**
    3. * B程序测试方法,启动A程序能看到测试结果
    4. */
    5. public static void main(String[] args){
    6. try {
    7. // 12345为tomcat进程的PID
    8. VirtualMachine vm = VirtualMachine.attach("12345");
    9. // 获取本机上所有的Java进程
    10. List vmList = VirtualMachine.list();
    11. // 第一个参数为Jar包在本机中的路径;第二个参数为agentmain的agentArgs参数,此处为null
    12. vm.loadAgent("user/project/test/testagent-1.0-SNAPSHOT.jar", null);
    13. }catch (Exception e){
    14. e.printStackTrace();
    15. }
    16. }
    17. }

        4、Instrumentation

            “java.lang.instrument”包的具体实现,依赖于 JVMTI,JVMTI(Java Virtual Machine Tool Interface)是一套于JVM 相关的本地编程工具接口集合。

            (1)、方法

                ①、addTransformer:添加字节码转换器

                ②、removeTransformer:移除字节码转换器

                ③、getAllLoadedClasses:返回当前由JVM加载的所有类的数组

                ④、appendToBootstrapClassLoaderSearch:增加BootstrapClassLoader的搜索路径

                ⑤、appendToSystemClassLoaderSearch:增加SystemClassLoader的搜索路径

        5、Instrumentation类

            “java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套于JVM 相关的本地编程工具接口集合。

                ①、addTransformer:添加字节码转换器

                ②、removeTransformer:移除字节码转换器

                ③、getAllLoadedClasses:返回当前由JVM加载的所有类的数组

                ④、appendToBootstrapClassLoaderSearch:增加BootstrapClassLoader的搜索路径

                ⑤、appendToSystemClassLoaderSearch:增加SystemClassLoader的搜索路径

        6、字节码工具

            字节码操作工具,均基于ClassFileTransformer实现

            字节码指令:https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions

            (1)、ASM

                字节码操控框架,能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。

                ①、ClassReader类

                    这个类会将.class文件读入到ClassReader中的字节数组中,它的accept方法接受一个ClassVisitor实现类,并按照顺序调用ClassVisitor中的方法。

                ②、ClassWriter类

                    ClassWriter是ClassVisitor子类,是和ClassReader对应的类,ClassReader是将.class文件读入到一个字节数组中,ClassWriter是将修改后的类的字节码内容以字节数组的形式输出。

                ③、ClassVisitor抽象类

                    这个类会将.class文件读入到ClassReader中的字节数组中,它的accept方法接受一个ClassVisitor实现类,并按照顺序调用ClassVisitor中的方法。

    1. public class TestClassVisitor extends ClassVisitor {
    2. /**
    3. * ClassVisitor中方法访问顺序
    4. * visit / visitSource / visitOuterClass —> (visitAnnotation | visitAttribute) —> (visitInnerClass / visitField / visitMethod) —> visitEnd
    5. **/
    6. /**
    7. * 当扫描类时第一个调用的方法,主要用于类声明使用
    8. * visit(类版本, 修饰符, 类名, 泛型信息, 继承的父类, 实现的接口)
    9. **/
    10. @Override
    11. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    12. super.visit(version, access, name, signature, superName, interfaces);
    13. }
    14. /**
    15. * 当扫描器扫描到类注解声明时进行调用
    16. * visitAnnotation(注解类型 , 注解是否可以在JVM中可见)
    17. **/
    18. @Override
    19. public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    20. return super.visitAnnotation(desc, visible);
    21. }
    22. /**
    23. * 当扫描器扫描到类中字段时进行调用
    24. * visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
    25. **/
    26. @Override
    27. public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    28. return super.visitField(access, name, desc, signature, value);
    29. }
    30. /**
    31. * 当扫描器扫描到类的方法时进行调用
    32. * visitMethod(修饰符, 方法名, 方法签名, 泛型信息, 抛出的异常)
    33. **/
    34. @Override
    35. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    36. MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    37. if (mv == null || name.equals("") ||name.equals("")) {
    38. return mv;
    39. }
    40. if(className == null){
    41. return mv;
    42. }
    43. //判断无需执行
    44. if(!exeCmd.execTime(ExecParam.Vf(className.replace("/", "."), annotationDesc, name, ""))){
    45. return mv;
    46. }
    47. final String key = className + name + desc;
    48. return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
    49. //方法进入时获取开始时间
    50. @Override public void onMethodEnter() {
    51. this.visitLdcInsn(key);
    52. this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "start", "(Ljava/lang/String;)V", false);
    53. }
    54. //方法退出时获取结束时间并计算执行时间
    55. @Override public void onMethodExit(int opcode) {
    56. this.visitLdcInsn(key);
    57. this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "end", "(Ljava/lang/String;)V", false);
    58. //向栈中压入类名称
    59. this.visitLdcInsn(className);
    60. //向栈中压入方法名
    61. this.visitLdcInsn(name);
    62. //向栈中压入方法描述
    63. this.visitLdcInsn(desc);
    64. this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "execTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
    65. false);
    66. }
    67. };
    68. }
    69. /**
    70. * 当扫描器完成类扫描时才会调用,如果想在类中追加某些方法
    71. **/
    72. @Override
    73. public void visitEnd() {
    74. super.visitEnd();
    75. }
    76. }

                ④、FieldVisitor抽象类

                    当ASM的ClassReader读取到Field时就转入FieldVisitor接口处理。

                ⑤、MethodVisitor & AdviceAdapter

                    MethodVisitor是一个抽象类,当ASM的ClassReader读取到Method时就转入MethodVisitor接口处理。

                    AdviceAdapter是MethodVisitor的子类,使用AdviceAdapter可以更方便的修改方法的字节码。

                    AdviceAdapter核心方法如下:

                    a、void visitCode():表示ASM开始扫描这个方法

                    b、void onMethodEnter():进入这个方法

                    c、void onMethodExit():即将从这个方法出去

                    d、void onVisitEnd():表示方法扫码完毕

                ⑥、ASM使用

    1. public class ASMTransformer implements ClassFileTransformer {
    2. /**
    3. * @Description:覆写转换方法
    4. * 参数说明
    5. * loader: 定义要转换的类加载器,如果是引导加载器,则为null
    6. * className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer"
    7. * classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
    8. * protectionDomain:要定义或重定义的类的保护域
    9. * classfileBuffer:类文件格式的输入字节缓冲区(不得修改,一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
    10. */
    11. @Override
    12. public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    13. try {
    14. //第一步:读取类的字节码流
    15. ClassReader reader = new ClassReader(classfileBuffer);
    16. //第二步:创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
    17. ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
    18. //第三步:接受一个ClassVisitor子类进行字节码修改
    19. reader.accept(new TestClassVisitor(writer, className), ClassReader.EXPAND_FRAMES);
    20. //第四步:返回修改后的字节码流
    21. return writer.toByteArray();
    22. } catch (Throwable e) {
    23. System.out.println(e.getMessage());
    24. throw e;
    25. }
    26. }
    27. }

            (2)、Javassist

                提供源级别和字节码级别API,可在运行时操作Java字节码的方法,增加了一层抽象,性能高于反射,低于ASM。

    1. public class JavassistTransformer implements ClassFileTransformer {
    2. private static ClassPool classPool = ClassPool.getDefault();
    3. @Override
    4. public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    5. try {
    6. CtClass ctClass = classPool.get("org.example.domain.Student");
    7. //得到字节码
    8. byte[] bytes = ctClass.toBytecode();
    9. //获取长度
    10. System.out.println(bytes.length);
    11. //获取类名
    12. System.out.println(ctClass.getName());
    13. //获取简要类名
    14. System.out.println(ctClass.getSimpleName());
    15. //获取父类
    16. System.out.println(ctClass.getSuperclass().getName());
    17. //获取接口
    18. System.out.println(Arrays.toString(ctClass.getInterfaces()));
    19. //获取构造方法
    20. for(CtConstructor ctConstructor : ctClass.getConstructors()){
    21. System.out.println("构造方法 "+ctConstructor.getName());
    22. }
    23. //获取方法
    24. for(CtMethod ctMethod : ctClass.getMethods()){
    25. ctMethod.insertBefore("调用前");
    26. ctMethod.insertAfter("调用后");
    27. System.out.println("所有方法:"+ctMethod.getName());
    28. }
    29. for(CtMethod ctMethod : ctClass.getDeclaredMethods()){
    30. System.out.println("定义方法:"+ctMethod.getName());
    31. }
    32. for(CtField ctField : ctClass.getDeclaredFields()){
    33. System.out.println("定义属性:"+ctField.getName());
    34. System.out.println("属性类型:"+ctField.getType());
    35. }
    36. }catch (Exception e){
    37. e.printStackTrace();
    38. throw new IllegalClassFormatException();
    39. }
    40. return new byte[0];
    41. }
    42. }

            (3)、Bytebuddy

                Bytebuddy是一个较高层级的抽象的字节码操作工具,是基于ASM API实现。

            (4)、CGLib

                基于字节码,生成真实对象的子类,运行效率高于JDK代理,不需要实现接口。

        7、Jstack命令

            (1)、命令jstack 24324

            (2)、运行sun.tools.jstack.JStack#main方法,内部执行sun.tools.jstack.JStack#runThreadDump方法利用VirtualMachine类attach到目标进程,然后dump文件。

        8、Attach机制

            (1)、Attach源码解析

                抽象类VirtualMachine是抽象类HotSpotVirtualMachine的父类,抽象类HotSpotVirtualMachine是类LinuxVirtualMachine、WindowsVirtualMachine的父类。

                抽象类AttachProvider是抽象类HotSpotAttachProvider的父类,抽象类HotSpotAttachProvider是类LinuxAttachProvider、WindowsAttachProvider的父类。

                ①、VirtualMachine.attach(pid)

    1. public abstract class VirtualMachine {
    2. /**
    3. * attach到Java虚拟机
    4. **/
    5. public static VirtualMachine attach(String id)
    6. throws AttachNotSupportedException, IOException
    7. {
    8. if (id == null) {
    9. throw new NullPointerException("id cannot be null");
    10. }
    11. // Attach操作通常与Java虚拟机实现、版本甚至操作系统相关联,不同操作系统提供不同的AttachProvider
    12. List providers = AttachProvider.providers();
    13. if (providers.size() == 0) {
    14. throw new AttachNotSupportedException("no providers installed");
    15. }
    16. AttachNotSupportedException lastExc = null;
    17. for (AttachProvider provider: providers) {
    18. try {
    19. // 通过provider执行attach操作
    20. return provider.attachVirtualMachine(id);
    21. } catch (AttachNotSupportedException x) {
    22. lastExc = x;
    23. }
    24. }
    25. throw lastExc;
    26. }
    27. }

                ②、LinuxAttachProvider.attachVirtualMachine(id);

                    AttachProvider:Attach操作通常与Java虚拟机实现、版本甚至操作系统相关联,不同操作系统提供不同的AttachProvider,有LinuxAttachProvider、WindowsAttachProvider等,分别调用不同的LinuxVirtualMachine、WindowsVirtualMachine。

    1. public class LinuxAttachProvider extends HotSpotAttachProvider {
    2. // perf counter for the JVM version
    3. public VirtualMachine attachVirtualMachine(String vmid)
    4. throws AttachNotSupportedException, IOException
    5. {
    6. // 权限校验
    7. checkAttachPermission();
    8. testAttachable(vmid);
    9. return new LinuxVirtualMachine(this, vmid);
    10. }
    11. }

                ③、LinuxVirtualMachine

                    a、attach_pidXXX文件:该文件是给目标JVM一个标记,表示触发SIGQUIT信号的是attach请求。

                    b、java_pidXXX文件:Unix socket通讯是基于该文件的,存在说明目标JVM已经做好连接准备。

    1. public class LinuxVirtualMachine extends HotSpotVirtualMachine {
    2. /**
    3. * Attaches to the target VM
    4. */
    5. LinuxVirtualMachine(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException
    6. {
    7. super(provider, vmid);
    8. // 目标虚拟机pid
    9. int pid;
    10. try {
    11. pid = Integer.parseInt(vmid);
    12. } catch (NumberFormatException x) {
    13. throw new AttachNotSupportedException("Invalid process identifier");
    14. }
    15. // 查找SocketFile,也就是java_pid文件,如果没有找到就发送SIGQUIT信号
    16. path = findSocketFile(pid);
    17. if (path == null) {
    18. // 创建attach_pid文件
    19. File f = createAttachFile(pid);
    20. try {
    21. // 发送QUIT信号,对应C++代码signal_thread_entry函数中的SIGBREAK
    22. if (isLinuxThreads) {
    23. ...
    24. sendQuitToChildrenOf(mpid);
    25. } else {
    26. sendQuitTo(pid);
    27. }
    28. // 再次尝试查找SocketFile,java_pid文件
    29. ...
    30. do {
    31. ...
    32. path = findSocketFile(pid);
    33. } while (i <= retries && path == null);
    34. ...
    35. }
    36. } finally {
    37. f.delete();
    38. }
    39. }
    40. // 权限校验
    41. checkPermissions(path);
    42. // 创建socket
    43. int s = socket();
    44. try {
    45. // 连接
    46. connect(s, path);
    47. } finally {
    48. close(s);
    49. }
    50. }
    51. }

            (2)、JVM启动流程解析

                JVM启动,如果没有配置StartAttachListener为true,就不会执行初始化AttachListener,是通过SingalDispatcher执行的。

                ①、C++代码调用顺序:thread.cpp —> os.cpp —> attachListener.cpp —> attachListener_linux.cpp

            (3)、Attach机制流程图

                Attach机制就是jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。

                ①、attach_pidXXX

                    a、后面的XXX代表pid,例如pid为1234则文件名为.attach_pid1234。

                    b、该文件目的是给目标JVM一个标记,表示触发SIGQUIT信号的是attach请求。

                    c、其默认全路径为/proc/XXX/cwd/.attach_pidXXX,若创建失败则使用/tmp/attach_pidXXX。

                ②、java_pidXXX

                    a、后面的XXX代表pid,例如pid为1234则文件名为.java_pid1234。

                    b、由于Unix domain socket通讯是基于文件的,该文件就是表示external process与target VM进行socket通信所使用的文件,如果存在说明目标JVM已经做好连接准备。

                    c、其默认全路径为/proc/XXX/cwd/.java_pidXXX,若创建失败则使用/tmp/java_pidXXX。

                ③、流程图

                    文章:JVM Attach机制实现 - 你假笨

                ④、流程图

        9、JSR269原理

            现在开发者可继承AbstractProcessor,使用@SupportedSourceVersion指定Java版本,@SupportedAnnotationTypes指定要处理的注解类型名称,并定义process方法来处理注解,而在javac编译时,若使用-processor或-processor -path指定注解处理器来源,或者在类别路径包含的JAR中,META-INF里面,存在如同上述的javax.annotation.processing.Processor设定,在编译器剖析、生成语法树之后,若原始码出现了指定要处理的注解,就会载入注解处理器并执行process方法。

            JSR 269实践_约定291天后的博客-CSDN博客_jsr269

  • 相关阅读:
    vue 中使用 this 更新数据的一次大坑
    实现一个函数,判断aim是 否是str1和str2交错组成。
    CF1004F Sonya and Bitwise OR(线段树平衡复杂度+or 前缀性质)
    java 3至5年常见面试题及答案
    BUG:编写springboot单元测试,自动注入实体类报空指针异常
    【java计算机毕设】高校奖学金管理系统 java springmvc vue mysql 送文档+ppt
    stm32 cubeide 闪退 显示self upgrade failed
    同事:这个页面的逻辑没什么能复用的,不抽组件也没什么影响吧?
    Windows——实现exe自删除
    bp神经网络预测模型优点,bp人工神经网络模型
  • 原文地址:https://blog.csdn.net/L_D_Y_K/article/details/126506936