字节码改写方式的多样性主要源于不同的使用场景、需求和设计哲学。
以下两个方面是比较重要的原因:
- 学习和使用成本
- 精细化控制能力与性能需求
首先说一下jdk代理 ,它是在2000年5月发布的jdk 1.3中引入的,这里对jdk代理就不做过于详细的介绍了,它的出现主要是为了提供一种灵活的方法来实现面向切面编程(AOP) 、远程方法调用(RMI) 、事务管理和其他需要在运行时动态增强对象 功能的应用场景,说到AOP大家应该耳熟能详了吧,接下来是一个经典的八股问题------为什么有了jdk代理还要有cglib代理?
公布下面试标准答案: 因为JDK动态代理不能直接代理类,而只能代理接口,这限制了其在一些情况下的使用灵活性,尤其是当需要代理没有接口的第三方类时。
但你想过为什么jdk动态代理为什么必须要基于接口吗?感兴趣的小伙伴可以移步我这篇回答
有点跑题,我们接着聊一下cglib代理 , 它的出现就是为了解决jdk代理的一些问题比如无法代理没有接口/final 的类, 这使得它变得无比强大,对要代理的类没有任何限制这一条是它成功的主要因素,而这都是依赖于asm ,asm自2002年(asm.ow2.io/versions.ht...
在jvm世界中大约有200条左右的指令。这些指令包括各种操作,用于信息加载、存储、算术计算、类型转换、对象创建、调用方法、控制流管理和异常处理等,是整个java世界的基石,但理解它们需要对JVM工作机制以及java语言特性有深入了解,这对于绝大多数人来说太困难了。
asm库提供了一套API,使得开发者可以以更高效和结构化的方式构建、修改和分析java字节码,避免开发者直接处理字节码的复杂性。从某种角度来看可以说asm是对jvm指令的一种抽象,在字节码的世界里asm几乎无往不利,但强大的代价就是它还是太难了 ------它的学习和使用成本还是太过高昂,使用者还是要去了解jvm的指令集学习自己需要的指令。这对于入门人员甚至普通开发者来说简直就是一场噩梦,而javasist(github.com/jboss-javas...)无疑是在降低使用门槛这条路上走的最远的,它使开发者能够以接近java源码的方式来操作字节码。它本是为jboss的aop功能而开源的,但逐渐成为了很多对字节码指令没那么熟悉的人做字节码改写时的首选,除了mybatis ,还有阿里著名的transmittable-thread-local 、dubbo都是使用此种方式,如果你想做简单的字节码改写它会是一个不错的选择。
以下为transmittable-thread-local节选代码
java
private boolean updateBeforeAndAfterExecuteMethodOfExecutorSubclass(@NonNull final CtClass clazz) throws NotFoundException, CannotCompileException {
final CtClass runnableClass = clazz.getClassPool().get(RUNNABLE_CLASS_NAME);
final CtClass threadClass = clazz.getClassPool().get("java.lang.Thread");
final CtClass throwableClass = clazz.getClassPool().get("java.lang.Throwable");
boolean modified = false;
try {
final CtMethod beforeExecute = clazz.getDeclaredMethod("beforeExecute", new CtClass[]{threadClass, runnableClass});
// unwrap runnable if IsAutoWrapper
String code = "$2 = com.alibaba.ttl.threadpool.agent.internal.transformlet.impl.Utils.doUnwrapIfIsAutoWrapper($2);";
logger.info("insert code before method " + signatureOfMethod(beforeExecute) + " of class " +
beforeExecute.getDeclaringClass().getName() + ": " + code);
beforeExecute.insertBefore(code);
modified = true;
} catch (NotFoundException e) {
// clazz does not override beforeExecute method, do nothing.
}
try {
final CtMethod afterExecute = clazz.getDeclaredMethod("afterExecute", new CtClass[]{runnableClass, throwableClass});
// unwrap runnable if IsAutoWrapper
String code = "$1 = com.alibaba.ttl.threadpool.agent.internal.transformlet.impl.Utils.doUnwrapIfIsAutoWrapper($1);";
logger.info("insert code before method " + signatureOfMethod(afterExecute) + " of class " +
afterExecute.getDeclaringClass().getName() + ": " + code);
afterExecute.insertBefore(code);
modified = true;
} catch (NotFoundException e) {
// clazz does not override afterExecute method, do nothing.
}
return modified;
}
但软件行业有个定律:抽象在软件开发中的确涉及信息选择和隐藏的过程,而这种信息的隐藏可能会被认为是对底层细节描述能力的某种"丧失"。
同样的javasist作为一个更高级的抽象层,这意味着会有一些性能开销,这在需要进行大量字节码操作的场合可能成为瓶颈,同时对于一些非常细致的字节码操作或者需要很细粒度控制的场景来说,它不够灵活,因此它更适合于简单或常见的字节码操作。
而除了上述提到的一些,还有很多非常优秀的字节码改写框架:
- bytebuddy (bytebuddy.net/#/),链路追踪系统中...
- jvm-sandbox (github.com/alibaba/jvm...) ,arthas的 前身greys)的作者基于greys沉淀出来的作品,底层是基于asm的(大神对于asm的理解和使用功力非常深),它出现是为了做jvm上的spring aop,强烈建议如果你要做一个aop类型(不需要对行间代码进行修改仅在方法进入退出时执行某些操作)的agent可以使用此框架
- bytekit (github.com/alibaba/byt...%25EF%25BC%258C%25E6%2598%25AFarthas%25E7%259A%2584%25E5%25BC%2580%25E5%258F%2591%25E4%25BA%25BA%25E5%2591%2598%25E5%259F%25BA%25E4%25BA%258Earthas%25E6%258A%25BD%25E8%25B1%25A1%25E8%2580%258C%25E6%259D%25A5%25EF%25BC%258C%25E5%25BA%2595%25E5%25B1%2582%25E4%25B9%259F%25E6%2598%25AFasm "https://github.com/alibaba/bytekit)%EF%BC%8C%E6%98%AFarthas%E7%9A%84%E5%BC%80%E5%8F%91%E4%BA%BA%E5%91%98%E5%9F%BA%E4%BA%8Earthas%E6%8A%BD%E8%B1%A1%E8%80%8C%E6%9D%A5%EF%BC%8C%E5%BA%95%E5%B1%82%E4%B9%9F%E6%98%AFasm")
字节码改写的方式有很多,不同的场景也有不同的选择,javasist开箱即用,asm可以锻炼你的java内功,赶进度时你所熟悉的方式则是最合适的。
ps:在java世界里,字节码改写 + 反射可以让你变成"上帝",你可以完成任何你想做的事情!