JavaAgent是什么
Java agent 是 JDK1.5 中引入的一个强大工具,它在分析活动中非常有用,开发人员和应用程序用户可以在这些活动中监视在服务器和应用程序中执行的任务。Agent 基于 JVMTI 接口规范,提供了一种在加载字节码时,对字节码进行修改的方式,他共有两种方式执行,
- 一种是在 main 方法执行之前,通过 premain 来实现,
- 另一种是在程序运行中,通过 attach api 来实现。
这门技术对大多数人来说都比较陌生,就像一个黑盒子,但是日常工作中,又或多或少接触过基于 JavaAgent 技术实现的工具。
如上图,这上面的一些工具都是基于 JavaAgent 来实现的,还有大名鼎鼎的 Skywalking 和 ja-netfilter,它们基本上对你的业务没有入侵,但功能都很强悍,用起来非常爽。
知其然也要知其所以然,用的舒服的同时,我们也要搞清楚它们是怎么工作的,只有把工作原理和流程弄清楚了,在使用的过程中才不会迷糊。今天就带大家来入个门,了解一下 JavaAgent 到底是怎么玩的,磨刀不误砍柴工,我们先将 JavaAgent 中的一些基本概念搞清楚,请大家耐心看完,这样在后面的讲解中才不会一脸懵逼。
基础概念
premain
这个 premain 方法跟我们以往使用的一些 API 不太一样,它并没有一些父类和接口预先定义好,然后我们自己去复写实现,而是固定写死的(不能写错),在目标程序启动的时候,它会先于目标程序加载。
import java.lang.instrument.Instrumentation;
/**
* 预先处理,程序启动时优先加载
*/
public class HelloPremain {
/**
* premain()有两种写法,Instrumentation参数可以不传递,带有Instrumentation参数的方法优先级更高
*
* @param agentArgs 字符串参数,可以在启动的时候手动传入
* @param inst 此参数由jvm传入,Instrumentation中包含了对class文件操作的一些api,可以让我们基于此来进行class文件的编辑
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Yes, I am a real Agent for premain Class.");
}
/**
* 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法
*
* @param agentArgs 字符串参数,可以在启动的时候手动传入
*/
public static void premain(String agentArgs) {
System.out.println("Yes, I am a real Agent Class.");
}
}
agentmain
如果说 premain 是预先处理,那么与之相对应的 agentmain 就可以理解为后置处理,无需在程序启动的时候就指定它,而是可以在程序启动之后,再利用 attach api 来加载它。
import java.lang.instrument.Instrumentation;
/**
* 后置处理,启动时无需加载,可在程序启动之后进行加载
*/
public class HelloAgentmain {
/**
* agentmain()有两种写法,Instrumentation参数可以不传递,带有Instrumentation参数的方法优先级更高
*
* @param agentArgs 字符串参数,可以在启动的时候手动传入
* @param inst 此参数由jvm传入,Instrumentation中包含了对class文件操作的一些api,可以让我们基于此来进行class文件的编辑
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Yes, I am a real Agent for agentmain Class.");
}
/**
* 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法
*
* @param agentArgs 字符串参数,可以在启动的时候手动传入
*/
public static void agentmain(String agentArgs) {
System.out.println("Yes, I am a real Agent Class.");
}
}
Instrumentation
大家重点看 premain 和 agentmain 的第二个参数,如果我们想要在后续对 java 字节码进行修改,那么就必须通过 Instrumentation 来实现,它由 JVM 传入,是 JDK1.5 提供的 API,用于拦截类加载事件,并对字节码进行修改。
Instrumentation 的功能非常强悍,提供了大概有十几个 API,若我们只想要修改字节码的话,就关心其中的三个主要方法即可。
public interface Instrumentation {
//注册一个转换器,类加载事件会被注册的转换器所拦截
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//重新触发类加载
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
//直接替换类的定义
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
}
ClassFileTransformer
Instrumentation 中又引入了一个新的 ClassFileTransformer 类,名字取的非常语义化,一眼就能看出来是用来转换 class 文件的。
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
* Transformer.class
*/
public class HelloTransformer implements ClassFileTransformer {
/**
* 转换提供的类文件并返回一个新的替换类文件
*
* @param loader 要转换的类的定义加载器,如果引导加载器可能为null
* @param className Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。例如, "java/util/List" 。
* @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则该类被重新定义或重新转换;如果这是一个类加载, null
* @param protectionDomain 被定义或重新定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区 - 不得修改
* @return 转换后的字节码数组
* @throws IllegalClassFormatException class文件格式异常
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 实现ClassFileTransformer接口之后,这里默认是return了一个[]空数组
// 千万注意:
// 如果不需要改变,那就return null,如果return了一个[],那么你的class就会被置空,程序就会报错
// return new byte[0];
return null;
}
}
理论太枯燥,所以每一个解释下面我都放了实战代码来给大家做演示,这样会更好理解一点儿,这几个基础概念,大家先简单看一下,有一个大概的印象之后,后续会就每个概念进行实战,明确如何使用。
JavaAgent使用方式
正常我们运行一个 java 程序,都是需要找到一个 main 方法入口,如果是 jar 包的话,一般都是直接 java -jar <jar全路径>。
如果我们是用的是 premain 方式,那我们直接通过追加 -javaagent 参数来引入 agent。
例如:
java -javaagent:/agent.jar -jar /test.jar
如果我们是用的是 agent 方式,那么就需要借助于 JDK 的 tools.jar 中的 API 了。
// 连接jvm,并利用相关的api找到HelloTest工程运行时的进程id,也就是PID
VirtualMachine vm = VirtualMachine.attach("12345");
// 加载agent,大家注意使用自己的路径
vm.loadAgent("/Users/wolffy/agent.jar");
// 脱离jvm
vm.detach();
后面会专门写一篇文章进行实战说明。
评论区