写在前面

在面向对象编程领域,方法增强有好几种做法:

继承:符合开闭原则,简单地通过继承类的扩展,实现方法增强,但需通过调用子类方法才能增强;

装饰:同样符合开闭原则,继承的优化方案,相比于继承更加灵活。装饰者(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);}@Overridepublic 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);}@Overridepublic 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;}/*** 方法前置*/@Overrideprotected 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*/@Overrideprotected 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 EnhancerPerson 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 实现既有方法的增强?相关推荐

  1. 读取xml文件转成ListT对象的两种方法(附源码)

    读取xml文件转成List<T>对象的两种方法(附源码) 读取xml文件,是项目中经常要用到的,所以就总结一下,最近项目中用到的读取xml文件并且转成List<T>对象的方法, ...

  2. mysql 读取data文件_利用mysql的LOAD DATA INFILE的功能读取客户端文件

    前言:今天在浏览某知论坛时,看到某大佬在渗透过程中使用伪造的MySQL服务端读取客户端文件,此大佬在利用过程中描述得不是很详细,作为小白的我看不懂啊,由此产生了此篇文章. 某大佬文章:https:// ...

  3. python读取pdf文件_深入学习python解析并读取PDF文件内容的方法

    这篇文章主要学习了python解析并读取PDF文件内容的方法,包括对学习库的应用,python2.7和python3.6中python解析PDF文件内容库的更新,包括对pdfminer库的详细解释和应 ...

  4. 多线程读取同一个文件_前端进阶:多线程Web Workers的工作原理及使用场景

    Web Worker 概述 Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行.在主线程运行的同时,Worker ...

  5. c读取txt文件_第93天:文件读写

    文件操作中最基本的当然属于文件的读写操作.当我们利用爬虫抓取到一堆数据时,就需要进行文件写操作,将数据写入到文件当中:当我们需要对抓取到的文件内容进行筛选,获取有效信息时,需要对文件进行读操作.本文将 ...

  6. java qq 传送文件_如何利用Java实现QQ文件传输功能.pdf

    您所在位置:网站首页 > 海量文档 &nbsp>&nbsp计算机&nbsp>&nbspJava 如何利用Java实现QQ文件传输功能.pdf2页 本文 ...

  7. netcore读取json文件_.net core读取json格式的配置文件

    在.Net Framework中,配置文件一般采用的是XML格式的,.NET Framework提供了专门的ConfigurationManager来读取配置文件的内容,.net core中推荐使用j ...

  8. java poi 读取xlsx文件_使用POI读取xlsx文件(SXSSFWorkbook)

    我正在尝试使用POI来读取大型xlsx文件的第一次测试,但要使用小文件进行简单测试,我无法显示单元格的值.使用POI读取xlsx文件(SXSSFWorkbook) 有人可以告诉我什么是我的错误.所有的 ...

  9. python打开并读取csv文件_!python3中使用使用read_csv( )读取csv文件,文件路径中含有中文,无法读取怎么处理?...

    python3如何根据csv文件的列的内容,自动建数据库表 你好,csv格式的和excel格式是差不多的, 下面是读取excel的一些函数,希望帮到你: # -*- coding: cp936 -*- ...

最新文章

  1. 原生JS完成“一对一、一对多”矩形DIV碰撞检测、碰撞检查,通过计算接触面积(重叠覆盖面积)大小来判断接触对象DOM
  2. eAccelerator和ionCube安装不上的解决办法
  3. python使用redis队列_Python的Flask框架应用调用Redis队列数据的方法
  4. 深度技术 GHOST XP SP3 快速专业装机版 v2012.07
  5. MyObjectUtil对象工具类
  6. MVC阻止用户注入JavaScript代码或者Html标记
  7. go的goroutine像水一样自然
  8. ARP***原理及解决方法与CMD命令分类(1)
  9. 编译器为C++ 空类自动生成的成员函数
  10. Python入门--try-except-else
  11. 太强了,神州7号发射flash全程模拟!
  12. 【数学建模】基于matlab GUI彩票仿真系统【含Matlab源码 1501期】
  13. linux嵌入式开发从入门到精通
  14. 2020届华为面试题【Python】
  15. office图标显示异常和新建时图标没有显示等问题解决
  16. 华为交换机根据已知一个IP查他对应的MAC地址和交换机端口命令
  17. Web前端鼠标变小手CSS和JS(Vue)两种实现
  18. win10计算机怎么注销用户,windows10系统如何取消微软账户登陆
  19. 百度云服务器远程密码忘记,2020-11-17 新买了百度云服务器,如何用ssh远程登陆...
  20. 相机下载_坚果pro3拍照不给力,那你可以看看这篇:老虎相机安装教程

热门文章

  1. Linux应用总结(1):自动删除n天前日志
  2. HDU 3709 Balanced Number
  3. 信息安全系统设计基础第十二周学习总结
  4. 【2012.4.22】北京植物园卧佛寺
  5. 因为这两天比较忙,所以没有及时把要发表的东西写来.废话少说:我前面把两个简单的滚动说了下.接下来介绍第三种集合循环滚动....
  6. 图像转置的MATLAB和OpenCV源码
  7. java valueof的用法_Java SignStyle valueOf()用法及代码示例
  8. 模型在gpu上反而速度变慢了_Tensorflow1.13.1+CUDA10.0+CuDNN7.4在GPU上训练模型
  9. sprintf php 数字占位,PHP sprintf()实现格式化输出
  10. asp.net使用mysql教程_在C#程序中使用MYSQL数据库