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