在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 | // 启动时 |
我们要做的工作就是利用这个Instrumentation
对象上提供的方法。
比如使用addTransformer
,我们可以添加一个修改字节码的对象上去
- 当加载一个class文件时,会进行拦截,对字节码做修改。
- 还可以在运行期对已加载类的字节码做变更
因为Java里面的类、对象通常和classloader有关系,不同的classloader可以加载同一个类,所以会对ClassFileTransformer
的归属有疑问?实际上呢,java agent总是由AppClassLoader进行加载的,premain
和agentmain
也都是在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,增加一行打印
- 如果再进行一次,打印只会有一行,不会有2行
- 如果删掉A之后再进行一次retransform,则打印会消失
这个例子说的其实是,每次retransform进来的代码都是会回到原始的字节码,在原始字节码上进行修改。
针对上面这个例子扩展一下,如果有2个ClassFileTransformer A
和B
,对类C进行retransform,则A和B的改动都会保留!说明一个transformer拿到的是上一个transformer修改之后的代码。所以前一个例子也能说通了,因为只有一个transformer。
除了这个字节码这个功能经常被提及之外,Instrumentation
还有其他一些我觉得不错的功能
- 获取所有已经加载过的类
- 获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
- 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
- 将某个jar加入到classpath里供AppClassloader去加载
apangin/jattach可以不依赖jdk完成load agent功能