浅谈字节码增强技术系列1-字节码增强概览

作者:董子龙

前言

前段时间一直想参照lombok的实现原理写一篇可以生成业务单据修改记录插件的专利,再查阅资料的过程中,偶然了解到了字节码增强工具-byteBuddy。但是由于当时时间紧促,所以没有深入的对该组件进行了解。其实再我们的日常开发中,字节码增强组件的身影无处不在,例如spring-aop和mybatis。本着知其然也要知其所以然的精神,我决定沉下心来,对字节码增强技术做一个深入的学习和总结,本文作为该系列的开篇,主要是对字节码做一下简单的介绍,为我们后面的深入学习打下一个好的基础。

一、字节码简述

字节码是一种中间状态的二进制文件,是由源码编译过来的,可读性没有源码的高。cpu并不能直接读取字节码,在java中,字节码需要经过JVM转译成机械码之后,cpu才能读取并运行。

使用字节码的好处:一处编译,到处运行。java就是典型的使用字节码作为中间语言,在一个地方编译了源码,拿着.class文件就可以在各种计算机运行。

二、字节码增强的使用场景

如果我们不想修改源码,但是又想加入新功能,让程序按照我们的预期去运行,可以通过编译过程和加载过程中去做相应的操作,简单来讲就是:将生成的.class文件修改或者替换称为我们需要的目标.class文件。

由于字节码增强可以在完全不侵入业务代码的情况下植入代码逻辑,所以可以用它来做一些酷酷的事,比如下面的几种常见场景:

1、动态代理

2、热部署

3、调用链跟踪埋点

4、动态插入log(性能监控)

5、测试代码覆盖率跟踪

...

三、字节码增强的实现方式

字节码工具类创建实现接口方法调用类扩展父类方法调用优点缺点常见使用学习成本
java-proxy支持支持支持不支持不支持简单动态代理首选功能有限,不支持扩展spring-aop,MyBatis1星
asm支持支持支持支持支持任意字节码插入,几乎不受限制学习难度大,编写代码多cglib5星
javaassit支持支持支持支持支持java原始语法,字符串形式插入,写入直观不支持jdk1.5以上的语法,如泛型,增强forFastjson,MyBatis2星
cglib支持支持支持支持支持与bytebuddy看起来差不多正在被bytebuddy淘汰EasyMock,jackson-databind3星
bytebuddy支持支持支持支持支持支持任意维度的拦截,可以获取原始类、方法,以及代理类和全部参数不太直观,学习理解有些成本,API非常多SkyWalking,Mockito,Hibernate,powermock3星

四、简单示例

AOP是我们在日常开发中常用的架构设计思想,AOP的主要的实现有cglib,Aspectj,Javassist,java proxy等。接下来,我们就以我们日常开发中会遇到的在方法执行前后打印日志为切入点,手动用字节码来实现一下AOP。

定义目标接口与实现

1public class SayService{ 2 public void say(String str) { 3 System.out.println("hello" + str); 4 } 5 }

定义了类SayService,再执行say方法之前,我们会打印方法开始执行start,方法执行之后,我们会打印方法执行结束end

ASM实现AOP

4.1.1、引入jar包

1<dependency> 2 <groupId>org.ow2.asm</groupId> 3 <artifactId>asm</artifactId> 4 <version>9.1</version> 5</dependency>

4.1.2、AOP具体实现

1public class ResourceClassVisitor extends ClassVisitor implements Opcodes { 2 3 public ResourceClassVisitor(ClassVisitor cv) { 4 super(Opcodes.ASM4, cv); 5 } 6 7 public ResourceClassVisitor(int i, ClassVisitor classVisitor) { 8 super(i, classVisitor); 9 } 10 11 /**访问类基本信息*/ 12 @Override 13 public void visit(int version, int access, String name, 14 String signature, String superName, String[] interfaces) { 15 this.cv.visit(version, access, name, signature, superName, interfaces); 16 } 17 18 /**访问方法基本信息*/ 19 @Override 20 public MethodVisitor visitMethod(int access, String name, 21 String desc, String signature, String[] exceptions) { 22 MethodVisitor mv = this.cv.visitMethod(access, name, desc, 23 signature, exceptions); 24 //假如不是构造方法,我们构建方法的访问对象(MethodVisitor) 25 if (!name.equals("<init>") && mv != null) { 26 mv = new ResourceClassVisitor.MyMethodVisitor((MethodVisitor)mv); 27 } 28 29 return (MethodVisitor)mv; 30 } 31 32 /**自定义方法访问对象*/ 33 class MyMethodVisitor extends MethodVisitor implements Opcodes { 34 35 public MyMethodVisitor(MethodVisitor mv) { 36 super(Opcodes.ASM4, mv); 37 } 38 /**此方法会在方法执行之前执行*/ 39 @Override 40 public void visitCode() { 41 super.visitCode(); 42 this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", 43 "Ljava/io/PrintStream;"); 44 this.mv.visitLdcInsn("方法开始执行start"); 45 this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", 46 "println", "(Ljava/lang/String;)V", false); 47 } 48 /**对应方法体本身*/ 49 @Override 50 public void visitInsn(int opcode) { 51 //在方法return或异常之前,添加一个end输出 52 if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { 53 this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", 54 "Ljava/io/PrintStream;"); 55 this.mv.visitLdcInsn("方法执行结束end"); 56 this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", 57 "println", "(Ljava/lang/String;)V", false); 58 } 59 this.mv.visitInsn(opcode); 60 } 61 } 62} 63public class AopTest { 64 65 public static void main(String[] args) throws IOException { 66 //第一步:构建ClassReader对象,读取指定位置的class文件(默认是类路径-classpath) 67 ClassReader classReader = new ClassReader("com/aop/SayService"); 68 //第二步:构建ClassWriter对象,基于此对象创建新的class文件 69 //ClassWriter.COMPUTE_FRAMES 表示ASM会自动计算max stacks、max locals和stack map frame的具体内容。 70 //ClassWriter.COMPUTE_MAXS 表示ASM会自动计算max stacks和max locals,但不会自动计算stack map frames。 71 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//推荐使用COMPUTE_FRAMES 72 //第三步:构建ClassVisitor对象,此对象用于接收ClassReader对象的数据,并将数据处理后传给ClassWriter对象 73 ClassVisitor classVisitor = new ResourceClassVisitor(classWriter); 74 //第四步:基于ClassReader读取class信息,并将数据传递给ClassVisitor对象 75 //这里的参数ClassReader.SKIP_DEBUG表示跳过一些调试信息等,ASM代码看上去就会更简洁 76 //这里的参数ClassReader.SKIP_FRAMES表示跳过一些方法中的部分栈帧信息,栈帧手动计算非常复杂,所以交给系统去做吧 77 //推荐用这两个参数 78 classReader.accept(classVisitor, ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES); 79 //第五步:从ClassWriter拿到数据,并将数据写出到一个class文件中 80 byte[] data = classWriter.toByteArray(); 81 //将字节码写入到磁盘的class文件 82 File f = new File("target/classes/com/aop/SayService.class"); 83 FileOutputStream fout = new FileOutputStream(f); 84 fout.write(data); 85 fout.close(); 86 SayService rs = new SayService(); 87 rs.say("asm");//start,handle(),end 88 } 89}

4.1.3、测试类输出结果

Javassist实现AOP

4.2.1、引入jar包

1<dependency> 2 <groupId>org.javassist</groupId> 3 <artifactId>javassist</artifactId> 4 <version>3.28.0-GA</version> 5</dependency>

4.2.2、AOP具体实现

1public class AopTest { 2 3 public static void main(String[] args) throws Exception { 4 ClassPool pool = ClassPool.getDefault(); 5 CtClass cc = pool.get("com.aop.SayService"); 6 CtMethod personFly = cc.getDeclaredMethod("say"); 7 personFly.insertBefore("System.out.println("方法开始执行start");"); 8 personFly.insertAfter("System.out.println("方法执行结束end");"); 9 cc.toClass(); 10 SayService sayService = new SayService(); 11 sayService.say("assist"); 12 } 13}

4.2.3、测试类输出结果

五、总结

作为字节码增强系列文章的开篇,只是简单的介绍了一下字节码的定义、字节码的实现方式,最后通过具体示例向大家展示了如何对字节码进行增强。再后续的文章中,会对相关框架的原理及具体应用做一个细化的总结,欢迎各位大佬的批评与指正。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

UIWebView长按保存图片和识别图片二维码的实现方案(使用缓存)

0x00需求:长按识别UIWebView中的二维码,如下图长按识别二维码0x01方案1:给UIWebView增加一个长按手势,激活长按手势时获取当前UIWebView的截图,分析是否包含二维码。核心代码:略优点:流程简单,可以快速实现。不足:无法实现保存UIWebView中图片,如果当前We

Python Challenge Level 18

初学Python,挑战一下流行的PythonChallenge,很不幸,卡在了18关~~被字符字节码之间的转换搞得焦头烂额,不过终于搞定了还是很happy的~~~主要的问题就是16进制形式的字符如何转成字节码(注意:不是encoding)如:\'89','50','4e','47','0d','0a','1a','0a','00

FLV文件格式

1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  

浅谈字节码增强技术系列1-字节码增强概览 - HelloWorld