javaagent的使用

在Java中,java agent和java反射调用一样,都是非常重要的特性,利用这种语言特性,可以做一些有意思的工作。比如说线程间传递threadlocal,又或者运行时AOP。除了功能以外,另一点也很重要:java agent是使用java语言开发。对于java程序员来讲,门槛并不高。

java agent的原理网上也有很多介绍,主要是利用了JVMTI的一个agent库:libinstrument。在使用上,一般有2种方式:

  • 启动时进行加载:-javaagent:myagent.jar
  • 或者动态attach

如果是动态attach的话,一般还需要依赖jdk目录下的/lib/tools.jar,做法是找到java home变量,然后去加载tools.jar。

针对2种不同场景,java agent的触发点略有不同。

1
2
3
4
// 启动时
public static void premain(String args, Instrumentation instrumentation) {};
// 动态attach
public static void agentmain(String args, Instrumentation instrumentation) {};

我们要做的工作就是利用这个Instrumentation对象上提供的方法。

比如使用addTransformer,我们可以添加一个修改字节码的对象上去

  • 当加载一个class文件时,会进行拦截,对字节码做修改。
  • 还可以在运行期对已加载类的字节码做变更

因为Java里面的类、对象通常和classloader有关系,不同的classloader可以加载同一个类,所以会对ClassFileTransformer的归属有疑问?实际上呢,java agent总是由AppClassLoader进行加载的,premainagentmain也都是在AppClassLoader里触发的,但是它修改类的效果可以超出当前的classloader。我的理解是,这个ClassFileTransformer只是输出一堆字节码、二进制文件,并不涉及到类加载的问题,所以2者不冲突。不过要注意的是,如果调用retransformClasses(Class<?>... classes)修改已经加载过的类,这个class对象不要用Class.forname("A"),因为Class对象是在当前AppClassLoader里创建的,所以ClassFileTransformer只会影响AppClassLoader里的A,对于其他ClassLoader加载的A,并不起作用。这个时候需要利用Instrumentation对象,简单点可以先getAllLoadedClasses()获得所有类集合,再通过比较class name的方式进行过滤,然后再retransform。又或者你将Instrumentation对象保存起来,将来需要用到的时候,再拿出来使用等等。花样很多,可以自由组合!

还有一个需要注意的是JEP 159: Enhanced Class Redefinition。修改已经加载过的类是有限制条件的,虽然有jep在追踪这个问题,但感觉openjdk对这个需求热情不高,短时间内怕是不会有进展(它不在任何已经规划的发行版feature中)

另外有一个注意点是关于retransform(还有一个redefineClasses,这个用的倒是不多)。关于它的细节可以自行google,infoq上也有一篇解释仅供参考。

我写了一个简单的Repo,希望能帮助理解。

比如现在有1个ClassFileTransformer A,它会给一个类增加新的方法,那么

  • 如果A是在premain里加入到list里去的,则可以canRetransform=true
  • 如果是在agentmain里,不行!

可以理解为第一次加载类C的时候,已经进行过A的transform变为C‘;进行A的retransform的时候,出去的还是C‘;这2个C’对比发现并没有破坏jep159里的内容,所以ok;但是agentmain里就不一样了,第一次加载的就是C本身,做完retransform变成C‘,C’对比C是有破坏性的改变的。

第2个例子,是有1个ClassFileTransformer A,在类C已经被加载过之后,先通过A进行一次retransform,增加一行打印

  1. 如果再进行一次,打印只会有一行,不会有2行
  2. 如果删掉A之后再进行一次retransform,则打印会消失

这个例子说的其实是,每次retransform进来的代码都是会回到原始的字节码,在原始字节码上进行修改。

针对上面这个例子扩展一下,如果有2个ClassFileTransformer AB,对类C进行retransform,则A和B的改动都会保留!说明一个transformer拿到的是上一个transformer修改之后的代码。所以前一个例子也能说通了,因为只有一个transformer。

除了这个字节码这个功能经常被提及之外,Instrumentation还有其他一些我觉得不错的功能

  • 获取所有已经加载过的类
  • 获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
  • 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
  • 将某个jar加入到classpath里供AppClassloader去加载

apangin/jattach可以不依赖jdk完成load agent功能