十三、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.pluginsgroupId>
- <artifactId>maven-assembly-pluginartifactId>
- <configuration>
- <descriptorRefs>
-
- <descriptorRef>jar-with-dependenciesdescriptorRef>
- descriptorRefs>
- <archive>
- <manifest>
-
- <addClasspath>trueaddClasspath>
- manifest>
- <manifestEntries>
-
- <Premain-Class>TestAgentPremain-Class>
- <Can-Redefine-Classes>trueCan-Redefine-Classes>
- <Can-Retransform-Classes>trueCan-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.pluginsgroupId>
- <artifactId>maven-assembly-pluginartifactId>
- <configuration>
- <descriptorRefs>
-
- <descriptorRef>jar-with-dependenciesdescriptorRef>
- descriptorRefs>
- <archive>
- <manifest>
-
- <addClasspath>trueaddClasspath>
- manifest>
- <manifestEntries>
-
- <Agent-Class>TestAgentAgent-Class>
- <Can-Redefine-Classes>trueCan-Redefine-Classes>
- <Can-Retransform-Classes>trueCan-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
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("
" ) ||name.equals("" )) { - 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
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。
③、流程图
④、流程图
9、JSR269原理
现在开发者可继承AbstractProcessor,使用@SupportedSourceVersion指定Java版本,@SupportedAnnotationTypes指定要处理的注解类型名称,并定义process方法来处理注解,而在javac编译时,若使用-processor或-processor -path指定注解处理器来源,或者在类别路径包含的JAR中,META-INF里面,存在如同上述的javax.annotation.processing.Processor设定,在编译器剖析、生成语法树之后,若原始码出现了指定要处理的注解,就会载入注解处理器并执行process方法。