相对修饰Runnable或是线程池的方式,Java Agent方式为什么是应用代码无侵入的?
按框架图,把前面示例代码操作可以分成下面几部分:
- 读取信息设置到
TTL。
这部分在容器中完成,无需应用参与。 - 提交
Runnable到线程池。要有修饰操作Runnable(无论是直接修饰Runnable还是修饰线程池)。
这部分操作一定是在用户应用中触发。 - 读取
TTL,做业务检查。
在SDK中完成,无需应用参与。
只有第2部分的操作和应用代码相关。
如果不通过Java Agent修饰线程池,则修饰操作需要应用代码来完成。
使用Java Agent方式,应用无需修改代码,即做到 相对应用代码 透明地完成跨线程池的上下文传递。
把这些失效情况都解决了是最好的,但复杂化了实现。下面是一些权衡:
- 不推荐使用
Timer类,推荐用ScheduledThreadPoolExecutor。ScheduledThreadPoolExecutor实现更强壮,并且功能更丰富。 如支持配置线程池的大小(Timer只有一个线程);Timer在Runnable中抛出异常会中止定时执行。 - 覆盖了
execute、submit、schedule的问题的权衡是: 业务上没有修改这些方法的需求。并且线程池类提供了beforeExecute方法用于插入扩展的逻辑。
这样可以减少Java命令上Agent的配置。
在自己的ClassFileTransformer中调用TtlTransformer,示例代码如下:
public class TransformerAdaptor implements ClassFileTransformer {
final TtlTransformer ttlTransformer = new TtlTransformer();
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
final byte[] transform = ttlTransformer.transform(
loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
if (transform != null) {
return transform;
}
// Your transform code ...
return null;
}
}注意还是要在bootclasspath上,加上TTL依赖的2个Jar:
-Xbootclasspath/a:/path/to/transmittable-thread-local-2.0.0.jar:/path/to/your/agent/jar/files通过Java命令参数-Xbootclasspath把库的Jar加Bootstrap ClassPath上。Bootstrap ClassPath上的Jar中类会优先于应用ClassPath的Jar被加载,并且不能被覆盖。
TTL在Bootstrap ClassPath上添加了Javassist的依赖,如果应用中如果使用了Javassist,实际上会优先使用Bootstrap ClassPath上的Javassist,即应用不能选择Javassist的版本,应用需要的Javassist和MTC的Javassist有兼容性的风险。
可以通过repackage(重新命名包名)来解决这个问题。
Maven提供了Shade插件,可以完成repackage操作,并把Javassist的类加到TTL的Jar中。
这样就不需要依赖外部的Javassist依赖,也规避了依赖冲突的问题。
- Java Agent规范
- Java SE 6 新特性: Instrumentation 新功能
- Creation, dynamic loading and instrumentation with javaagents
- JavaAgent加载机制分析
Maven的Shade插件
