十三、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包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等。

public class TestAgent {
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     * 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        // TODO: 自定义服务操作
        System.out.println("premain start");
        System.out.println(agentArgs);
    }
    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst)
     * 则会执行 premain(String agentArgs)
     */
    public static void premain(String agentArgs) {
        // TODO: 自定义服务操作 
        System.out.println(“premain start");
        System.out.println(agentArgs);
    }
}

        (2)、编写MANIFEST.MF文件

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

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

        (3)、打jar包

            ①、添加maven-assembly-plugin插件

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

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <descriptorRefs>
            <!— 将来在项目中添加的依赖,都会打到jar包中去 -->
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <!-- 自动添加META-INF/MANIFEST.MF文件 -->
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <!-- 指定Premain类,启动时会执行premain方法 -->
                <Premain-Class>TestAgent</Premain-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</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方法

public class TestAgent {
    /**
     * agentmain方法
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        // TODO: 自定义服务操作
        System.out.println("agentmain start");
        System.out.println(agentArgs);
        Class<?>[] classes = inst.getAllLoadedClasses();
        for (Class<?> cls : classes){
            System.out.println(cls.getName());
        }
    }
    /**
     * 如果不存在 agentmain(String agentArgs, Instrumentation inst)
     * 则会执行 agentmain(String agentArgs)
     */
    public static void agentmain(String agentArgs) {
        // TODO: 自定义服务操作
        System.out.println("agentmain start");
        System.out.println(agentArgs);
    }
}

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

        (3)、打jar包

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <descriptorRefs>
            <!— 将来在项目中添加的依赖,都会打到jar包中去 -->
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <!-- 自动添加META-INF/MANIFEST.MF文件 -->
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <!-- 指定agentmain类,启动时会执行agentmain方法 -->
                <Agent-Class>TestAgent</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

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

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

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

    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中的方法。

public class TestClassVisitor extends ClassVisitor {
    /**
     * ClassVisitor中方法访问顺序
     * visit / visitSource / visitOuterClass —> (visitAnnotation | visitAttribute) —> (visitInnerClass / visitField / visitMethod) —> visitEnd
     **/

    /**
     * 当扫描类时第一个调用的方法,主要用于类声明使用
     * visit(类版本, 修饰符, 类名, 泛型信息, 继承的父类, 实现的接口)
     **/
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    }
    /**
     * 当扫描器扫描到类注解声明时进行调用
     * visitAnnotation(注解类型 , 注解是否可以在JVM中可见)
     **/
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return super.visitAnnotation(desc, visible);
    }
    /**
     * 当扫描器扫描到类中字段时进行调用
     * visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
     **/
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return super.visitField(access, name, desc, signature, value);
    }
    /**
     * 当扫描器扫描到类的方法时进行调用
     * visitMethod(修饰符, 方法名, 方法签名, 泛型信息, 抛出的异常)
     **/
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv == null || name.equals("<init>") ||name.equals("<clinit>")) {
            return mv;
        }

        if(className == null){
            return mv;
        }

        //判断无需执行
        if(!exeCmd.execTime(ExecParam.Vf(className.replace("/", "."), annotationDesc, name, ""))){
            return mv;
        }

        final String key = className + name + desc;
        return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
            //方法进入时获取开始时间
            @Override public void onMethodEnter() {
                this.visitLdcInsn(key);
                this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "start", "(Ljava/lang/String;)V", false);
            }

            //方法退出时获取结束时间并计算执行时间
            @Override public void onMethodExit(int opcode) {
                this.visitLdcInsn(key);
                this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "end", "(Ljava/lang/String;)V", false);
                //向栈中压入类名称
                this.visitLdcInsn(className);
                //向栈中压入方法名
                this.visitLdcInsn(name);
                //向栈中压入方法描述
                this.visitLdcInsn(desc);
                this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/attach/demo/exe/ExecTime", "execTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
                        false);
            }
        };
    }
    /**
     * 当扫描器完成类扫描时才会调用,如果想在类中追加某些方法
     **/
    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

            ④、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使用

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

        (2)、Javassist

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

public class JavassistTransformer implements ClassFileTransformer {

    private static ClassPool classPool = ClassPool.getDefault();

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            CtClass ctClass = classPool.get("org.example.domain.Student");
            //得到字节码
            byte[] bytes = ctClass.toBytecode();
            //获取长度
            System.out.println(bytes.length);
            //获取类名
            System.out.println(ctClass.getName());
            //获取简要类名
            System.out.println(ctClass.getSimpleName());
            //获取父类
            System.out.println(ctClass.getSuperclass().getName());
            //获取接口
            System.out.println(Arrays.toString(ctClass.getInterfaces()));
            //获取构造方法
            for(CtConstructor ctConstructor : ctClass.getConstructors()){
                System.out.println("构造方法 "+ctConstructor.getName());
            }
            //获取方法
            for(CtMethod ctMethod : ctClass.getMethods()){
                ctMethod.insertBefore("调用前");
                ctMethod.insertAfter("调用后");
                System.out.println("所有方法:"+ctMethod.getName());
            }

            for(CtMethod ctMethod : ctClass.getDeclaredMethods()){
                System.out.println("定义方法:"+ctMethod.getName());
            }

            for(CtField ctField : ctClass.getDeclaredFields()){
                System.out.println("定义属性:"+ctField.getName());
                System.out.println("属性类型:"+ctField.getType());
            }
        }catch (Exception e){
            e.printStackTrace();
            throw new IllegalClassFormatException();
        }
        return new byte[0];
    }
}

        (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)

public abstract class VirtualMachine {
    /**
     * attach到Java虚拟机
     **/
    public static VirtualMachine attach(String id)
            throws AttachNotSupportedException, IOException
    {
        if (id == null) {
            throw new NullPointerException("id cannot be null");
        }
        // Attach操作通常与Java虚拟机实现、版本甚至操作系统相关联,不同操作系统提供不同的AttachProvider
        List<AttachProvider> providers = AttachProvider.providers();
        if (providers.size() == 0) {
            throw new AttachNotSupportedException("no providers installed");
        }
        AttachNotSupportedException lastExc = null;
        for (AttachProvider provider: providers) {
            try {
                // 通过provider执行attach操作
                return provider.attachVirtualMachine(id);
            } catch (AttachNotSupportedException x) {
                lastExc = x;
            }
        }
        throw lastExc;
    }
}

            ②、LinuxAttachProvider.attachVirtualMachine(id);

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

public class LinuxAttachProvider extends HotSpotAttachProvider {
    // perf counter for the JVM version
    public VirtualMachine attachVirtualMachine(String vmid)
        throws AttachNotSupportedException, IOException
    {
        // 权限校验
        checkAttachPermission();
        testAttachable(vmid);

        return new LinuxVirtualMachine(this, vmid);
    }
}

            ③、LinuxVirtualMachine

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

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

public class LinuxVirtualMachine extends HotSpotVirtualMachine {
    /**
     * Attaches to the target VM
     */
    LinuxVirtualMachine(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException
    {
        super(provider, vmid);
        // 目标虚拟机pid
        int pid;
        try {
            pid = Integer.parseInt(vmid);
        } catch (NumberFormatException x) {
            throw new AttachNotSupportedException("Invalid process identifier");
        }
        // 查找SocketFile,也就是java_pid文件,如果没有找到就发送SIGQUIT信号
        path = findSocketFile(pid);
        if (path == null) {
            // 创建attach_pid文件
            File f = createAttachFile(pid);
            try {
                // 发送QUIT信号,对应C++代码signal_thread_entry函数中的SIGBREAK
                if (isLinuxThreads) {
                    ...
                    sendQuitToChildrenOf(mpid);
                } else {
                    sendQuitTo(pid);
                }
                // 再次尝试查找SocketFile,java_pid文件
                ...
                do {
                    ...
                    path = findSocketFile(pid);
                } while (i <= retries && path == null);
                    ...
                }
            } finally {
                f.delete();
            }
        }
        // 权限校验
        checkPermissions(path);

        // 创建socket
        int s = socket();
        try {
            // 连接
            connect(s, path);
        } finally {
            close(s);
        }
    }
}

        (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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值