在介绍javaagent之前,我想有必要向大家介绍一下JVMTI,因为javaagent是基于这个技术实现的
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,JVMTI可以用来开发并监控JVM,可以查看JVM的内部状态,并控制JVM应用程序的执行。
JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库
注:这里提到的agent程序和javaagent不是同一概念
我们通过JVMTI开发好agent程序后,把程序编译成动态链接库,之后可以在jvm启动时指定加载运行该agent。
-agentlib:<agent-lib-name>=<options>
之后JVM启动后该agent程序就会开始工作。
而接下来要提到的Instrumention机制,也是通过实现了一个JVMTI的agent来完成的,这个agent的实现代码在libinstrument.so里(在BSD系统中叫做libinstrument.dylib),由于libinstrument.so是java内置的,所以不需要我们手动通过-agentlib
参数指定就可以使用它
这个动态链接库可以在{JAVA_HOME}/jre/lib
下找到,除此之外,还能看到和调试相关的agent实现——libjdwp.dylib
有了Instrumention,我们就可以通过java语言编写一个javaagent来监控或者操作JVM了,比如对类进行插桩。
Instrumention支持的功能都在java.lang.instrument.Instrumentation
接口中体现,而我们最关注的还是其中涉及到类转换相关的方法,比如addTransformer
以及retransformClasses
public interface Instrumentation {
// 添加一个ClassFileTransformer
// 之后类加载时都会经过这个ClassFileTransformer转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
// 移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
// 将一些已经加载过的类重新拿出来经过注册好的ClassFileTransformer转换
// retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
// 重新定义某个类
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
当我们通过addTransformer添加了一个ClassFileTransformer之后,之后所有的类都会通过ClassFileTransformer.transform()
方法进行转换,而具体怎么转换,我们可以通过重写transform
方法进行自定义,对于已经加载的类,可以通过调用retransformClasses来重新触发这个Transformer的转换,而且Transformer是可以添加多个的,多个transformer会依次执行。
下面,我们来看一下怎么开发一个基于Instrumention的agent吧
开发一个javaagent需要几步呢?
premain()
方法的类然后我们就可以通过命令java -javaagent:agent.jar demo.jar
来使用我们的javaagent了。
接下来,我们开始写代码,首先创建一个包含premain方法的类,其中premain方法需要严格按照下面两种格式的一种:
//agentArgs是一个字符串,会随着jvm启动设置的参数得到
//inst就是我们需要的Instrumention实例了,由JVM传入。我们可以拿到这个实例后进行各种操作
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
javaagent在执行时会首先查找第一个premain方法,如果找到了就不会执行第二个了,如果没有第一个,才回去执行第二个
其实从premain方法的名字上也可以看出来,这个方法会先于main方法执行,实际上,它会在大多数类加载之前运行,这也是为什么它可以对类进行转换
编写一个Agent类:
public class Agent
{
public static void premain(String agentArgs, Instrumentation inst){
inst.addTransformer(new MyClassTransformer(), true);
}
}
其中MyClassTransformer是我自定义的实现了ClassFileTransformer接口的类:
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if("org/example/Person".equals(className)){
try{
ClassPool pool = new ClassPool(true);
//pool.insertClassPath("/Users/momo/IdeaProjects/javaagent-demo/tester/target/classes");
CtClass clazz = pool.get("org.example.Person");
CtMethod method = clazz.getDeclaredMethod("getName");
System.out.println(method.getMethodInfo());
String source = "{System.out.println(\"hello tntaxin, you are good!\");}";
method.setBody(source);
byte[] byteCode = clazz.toBytecode();
clazz.detach();
return byteCode;
}catch(Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
这个类中就实现了一个transform方法,我借助javaassist的
javassist.ClassPool
javassist.CtClass
javassist.CtMethod
这三个类对org.example.Person
类的getName方法的方法体进行了替换,我们看一下Person类原本的实现:
除了javaassist还可以使用asm对字节码进行修改,后者使用难度相对来说更大一点,但是性能更好,asm入门:https://github.com/dengshiwei/asm-module/blob/master/doc/blog/AOP%20%E5%88%A9%E5%99%A8%20ASM%20%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8.md
public class Person
{
public static void main( String[] args )
{
Person p = new Person();
p.getName();
}
public void getName(){
System.out.println("tntaxin");
}
}
可以看到,原本的getName方法会打印tntaxin,而经过agent处理过后的getName应该会打印hello tntaxin, you are good!
接下来我们把javaagent打成jar包验证一下效果,不过,在这之前,不要忘了配置MANIFEST.MF
文件
打包完成后,我们在IDEA中配置一下VM Options使用我们刚刚打包好的agent.jar
然后执行Person.main方法,输出如下:
至此,我们已经掌握了简单的javaagent的实现方法,不过上面这种javaagent需要在jvm启动前设置-javaagent
参数,但是很多时候,我们想要在程序运行的过程中去插入agent,并修改其中的类。而正好,在Java6的新特性中支持通过attach的方式去加载agent
这种agent又要怎么实现呢?
和之前的agent很像,我们需要创建一个实现以下两种方法中的一种的类
public static void agentmain (String agentArgs, Instrumentation inst); [1]
public static void agentmain (String agentArgs);[2]
同样的,第一个agentmain方法优先级更高。之后要在META-INF/MAINIFEST.MF属性当中加入” Agent-Class”来指定拥有agentmain方法的类。
我们在之前的Agent类基础上添加agentmain方法:
public class Agent
{
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("agentArgs: " + agentArgs);
inst.addTransformer(new MyClassTransformer(), true);
}
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException,InterruptedException {
System.out.println("agentArgs: " + agentArgs);
inst.addTransformer(new MyClassTransformer(), true);
// 由于类已经加载完毕,需要执行retransformClasses触发重新加载
Class<?> aClass = Class.forName("org.example.Person");
inst.retransformClasses(aClass);
}
}
然后打包该agent,之后再编写一个Test类去attach目标进程并加载这个agent
public class Test {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
AgentInitializationException, IOException, AttachNotSupportedException, InterruptedException {
//这个pid填写具体要attach的目标进程
VirtualMachine attach = VirtualMachine.attach("99812");
attach.loadAgent("/Users/xxx/IdeaProjects/javaagent-demo/out/artifacts/agent_jar/agent.jar");
attach.detach();
System.out.println("over");
}
}
最后修改一下之前的Person类,确保它一直运行着:
public class Person
{
public static void main( String[] args ) throws InterruptedException {
Person p = new Person();
// 获取当前进程id
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
String name = runtime.getName();
String pid = name.substring(0, name.indexOf('@' ));
System.out.println(pid);
while(true){
Thread.sleep(3000);
p.getName();
}
}
public void getName(){
System.out.println("tntaxin");
}
}
接下来我们看下效果,先运行Person类,然后再运行Test类:
在没运行Test类之前一直输出着tntaxin,运行Test类将agent附加到进程后,输出内容变成了hello tntaxin, you are good!
在写这个demo的过程中遇到了一个错误:
Agent JAR loaded but agent failed to initialize
查资料发现是因为我的agent因为发生异常没有detach,导致我后面再次加载agent时和之前的agent冲突了,因为已经加载过了嘛,解决方案是修改Agent的类以及jar包名,然后重新加载,这样就不会冲突了。