概述
写在前面
在面向对象编程领域,方法增强有好几种做法:
继承:符合开闭原则,简单地通过继承类的扩展,实现方法增强,但需通过调用子类方法才能增强;
装饰:同样符合开闭原则,继承的优化方案,相比于继承更加灵活。装饰者(Decorator)本身就是某一块功能增强的组件,可以通过一层一层的装饰实现渐进式功能增强,既能无限增强也能直接一层到位,这些都是灵活的,开发者可以根据现有的装饰者组合成自己想要最终对象。典型实现就是 http://java.io 中的 stream;
代理:代理分静态代理和动态代理。静态代理比较麻烦了,需要开发者显式创建代理类,把真实类实例赋给代理类,此时代理类持有了对真实类实例的引用,做到对真实类的增强,而一旦真实类方法签名发生变化,静态代理类一样需要跟着变化,这便是静态代理的痛点。
而我们今天的重点就是在动态代理(Dynamic Proxy)。
动态代理
动态代理好处不用说,无代码侵入且更加灵活,其实现方式很多,JDK 的默认实现便是通过实现java.lang.reflect.InvocationHandler#invoke接口方法,进而实现对具体类的方法增强。而功能强大的 CGlib 便是通过 ASM 工具修改字节码的方式实现的。
本文便基于 ASM 操作,完成一次入门级的演示。
“偷梁换柱”
图中我们可以看到,正常的类加载过程中,byte code 被加载进内存,走完类验证以及初始化流程以后,理论上就可以直接执行了。
然而这次,我们需要在 JVM 进程完全启动之前,监听类加载事件,替换字节码。此所谓 “偷梁换柱”。
具体怎么做?
借助 java.lang.instrument.ClassFileTransformer。
javaagent 的 premain 方法是在 main方法执行前执行的,那么我们只需要在 premain 方法里通过 instrumentation 指定transformer 实现对类加载事件的监听,代码奉上:
io.libriraries.asm.agent.Agent
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(
(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
// 只对 io/libriraries/asm/agent/Person 类做方法增强
if ("io/libriraries/asm/agent/Person".equals(className)) {
System.out.println("io/libriraries/asm/agent/Person transforming...");
ClassReader reader = new ClassReader(classfileBuffer);
// 要指定 COMPUTE_MAXS 新生成字节码需要自动计算操作数栈的最大值,否则会报错
ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS);
ClassVisitor cv = new EnhancerAdapter(writer);
reader.accept(cv, 0);
System.out.println("io/libriraries/asm/agent/Person transformed");
// debug 输出文件到磁盘,方便核查
try (FileOutputStream fos = new FileOutputStream("F:Person.class")) {
fos.write(writer.toByteArray());
} catch (IOException e) {
e.printStackTrace();
}
return writer.toByteArray();
}
return classfileBuffer;
}
);
}
}
lambda 表达式部分,实际上就是 ClassFileTransformer 的匿名类,实现其 transform 方法便可以做到对类加载事件的监听。 在这里我们做了过滤,只对 io/libriraries/asm/agent/Person 这个类做方法增强。
transform 方法的返回值便是 ASM 修改以后的类的二进制流,这部分的二进制流会替代之前原始的二进制流,进入到类加载的流程中,验证并初始化。
io.libriraries.asm.agent.EnhancerAdapter
/**
* 增强适配器
*/
class EnhancerAdapter extends ClassVisitor {
private final TraceClassVisitor tracer;
public EnhancerAdapter(ClassVisitor cv) {
super(ASM6, cv);
PrintWriter pw = new PrintWriter(System.out);
tracer = new TraceClassVisitor(cv, pw);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
final MethodVisitor mv = tracer.visitMethod(access, name, desc, signature, exceptions);
if (isIgnore(mv, access, name)) {
return mv;
}
return new EnhancerMethodAdapter(mv, access, name, desc);
}
@Override
public void visitEnd() {
System.out.println(tracer.p.getText());
super.visitEnd();
}
/**
* 忽略构造方法、类加载初始化方法,final方法和 abstract 方法
*
* @param mv
* @param access
* @param methodName
* @return
*/
private boolean isIgnore(MethodVisitor mv, int access, String methodName) {
return null == mv
|| isAbstract(access)
|| isFinalMethod(access)
|| "<clinit>".equals(methodName)
|| "<init>".equals(methodName);
}
private boolean isAbstract(int access) {
return (ACC_ABSTRACT & access) == ACC_ABSTRACT;
}
private boolean isFinalMethod(int methodAccess) {
return (ACC_FINAL & methodAccess) == ACC_FINAL;
}
}
io.libriraries.asm.agent.EnhancerMethodAdapter
/**
* 方法级适配器
*/
class EnhancerMethodAdapter extends AdviceAdapter {
private final String name;
protected EnhancerMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
super(ASM6, mv, access, name, desc);
this.name = name;
}
/**
* 方法前置
*/
@Override
protected void onMethodEnter() {
// 前置逻辑 => System.out.println("method : " + name + " invoke start...");
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("method : " + name + " invoke start...");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
/**
* 方法后置
*
* @param opcode
*/
@Override
protected void onMethodExit(int opcode) {
// 后置逻辑 => System.out.println("method : " + name + " invoke end...");
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("method : " + name + " invoke end...");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.onMethodExit(opcode);
}
}
ASM 语法很有趣,是基于Visit Design Pattern的。所以你能看到ClassVisitor,MethodVisitor,FieldVisitor甚至AnnotationVisitor这些接口的存在。作为开发者,你只需要根据需求实现它们的方法即可。
实现它们有什么用呢?
答:实现基于事件的回调。
在通过 ClassReader 读取类二进制流的过程中,ClassReader会根据一定的顺序读取类结构元素,当读取(visit)到某个元素的时候,会触发你实现的回调逻辑(也就是上述好几个visitor的实现方法,由你自己实现),典型例子参考上面的 EnhancerAdapter 。所以通常说 ASM 也是事件驱动的 Event-Based API。
ClassReader 的 visit 顺序 是这样的:
visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod)* visitEnd
当然 ClassReader 不能修改、删除或新增类元素。这里我们要借助 ClassWriter 实现。visit 这个词也很有意思,在 ASM 的设计上是模棱两可的,对于 ClassReader 来说,可以理解为访问、读取;对于 ClassWriter来说, 就得理解成写入了。当然这里的写入并非写入到class文件,也不是写入到类加载读取的 Class 二进制流里,这里仅仅是写入到 ClassWriter 维护的内存副本。这个副本到最后可以通过 toByteArray() 方法拿到修改后的类字节码二进制流。
无论 ClassWriter 还是 ClassReader ,他们都是 ClassVisitor的实现类。因此不难理解 visit 本身的多义性。
TraceClassVisitor 在这里是可有可无,不影响逻辑,只是为了方便观察修改后的字节码是怎样的。可以理解 tracer 是 ClassWriter 的代理。
onMethodEnter 和 onMethodExit 分别对应方法的进入和退出,这就和我们之前的动态代理对应上了。ASM 方法增强本质就是在这两个回调方法里注入逻辑(当然是以字节码的形式注入啦!)
嗯,这里就涉及到 JVM 字节码指令问题,回头我会在一篇文章里整理字节码的速查表(Cheat Sheet),以备随时翻查。除此之外还有异常处理的逻辑,不在此篇阐述。
再回头看上面代码,是不是容易理解很多呢?
上面的代码已经完成了必要的逻辑,而我们在使用 premain 时千万不要忘记在 MANIFEST中填写这个所属类的全限定名。
具体我借助了 Maven 插件,指定 io.libriraries.asm.agent.Agent 为Premain Class。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
io.libriraries.asm.agent.Agent
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
以上,我们的 Agent 端就大功告成了!我们将其打成 jar 包。
这时候我们需要写个简单的调用来验证下这里的方法增强是否成功。
public class Main {
public static void main(String[] args) {
// p => ASM Enhancer
Person p = new Person();
p.doSth();
}
}
执行这个 main 方法的时候要带上一个 JVM 参数
-javaagent:target/asm-enhance-agent-1.0-SNAPSHOT.jar
asm-enhance-agent-1.0-SNAPSHOT.jar 就是刚才打出来的 jar 包。
执行结果
io/libriraries/asm/agent/Person transforming...
io/libriraries/asm/agent/Person transformed
method : doSth invoke start...
this guy is doing sth
method : doSth invoke end...
done.
小结
ASM 是个很庞大的工具,除了本篇涉及到的仍然有许多 API 需要探索,这里仅仅是九牛一毛。希望本篇有助于初学者打破 ASM 入门的壁垒。
本篇代码完整奉上:
https://gist.github.com/leonlibraries/7c56db347939866f9513b961088e64d6
参考资料:
《ASM 使用指南》
http://web.cs.ucla.edu/~msb/cs239-tutorial/
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
最后
以上就是傲娇鞋子为你收集整理的tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?的全部内容,希望文章能够帮你解决tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复