1. 字节码插桩
字节码插桩是对现有字节码进行修改或者动态生成全新字节码文件的技术。常用的字节码框架有ASM、Javassist、Byte-Buddy,这里只介绍ASM。
2. ASM
ASM是一个多用途的Java字节码操作和分析框架。它可以用来修改现有的类或动态地生成类,直接用二进制形式。ASM提供了一些常见的字节码转换和分析算法,可以从中建立自定义的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但它的重点是性能。因为它的设计和实现是为了尽可能的小和快,所以它很适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。参考官方文档https://asm.ow2.io/ 应用
在OpenJDK中,生成lambda调用站点,也在Nashorn编译器中。
Groovy编译器和Kotlin编译器。
Cobertura和Jacoco,用来检测类,以测量代码覆盖率。
Byte Buddy,动态生成类,本身也用于其他项目,如Mockito(生成模拟类)。
Gradle,用于在运行时生成一些类。
2.1 Core API
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。核心包中包含几个关键类如下 ClassReader:用于读取class文件。 ClassWriter:用于修改class文件,如修改类名、属性以及方法;或生成新的class文件。 ClassVisitor:抽象类,可以对注解、变量、方法解析,对应的具体子类分别是 AnnotationVisitor、FieldVisitor、MethodVisitor(会解析方法上的注解、参数、代码等)
2.2 Tree API
ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。
3. 基本用法
3.1 准备工作
3.1.1 安装插件
在Idea插件中搜索ASM Bytecode Viewer(ASM Bytecode Outline在2015年后不在更新,新版Idea不能使用),可以用来查看字节码,和对应的ASM编写的代码
3.1.2 jar包导入
< dependency>
< groupId> org.ow2.asm</ groupId>
< artifactId> asm-commons</ artifactId>
< version> 9.3</ version>
</ dependency>
3.2 生成新的类
3.2.1 待生成类
新建一个maven工程,创建一个待生成的类,里面包含常见的属性和方法
package org. example. asm8 ;
import java. util. ArrayList ;
import java. util. List ;
public class Human {
private String name;
protected int age;
public static long no;
public static final String GENDER = "male" ;
public Human ( ) {
}
public static void hello ( ) {
System . out. println ( "Hello world!" ) ;
}
public int sum ( int a, int b) {
long nanoTime = System . nanoTime ( ) ;
StringBuilder stringBuilder = new StringBuilder ( ) ;
stringBuilder. append ( "Hello StringBuilder!" ) ;
System . out. println ( stringBuilder) ;
List < String > list = new ArrayList < String > ( ) ;
return a + b;
}
}
在class文件中右击,点击ASM Bytecode Viewer,会弹出一个对话框
3.2.2 代码实现
这里重新创建一个工程用来编写生成这个class文件的代码,同时需要参考这个插件里提供的ASM代码 这个工程需要引入上面提供的jar包
package org. example. asm8. create ;
import java. io. FileOutputStream ;
import java. lang. reflect. Method ;
import org. objectweb. asm. ClassWriter ;
import org. objectweb. asm. FieldVisitor ;
import org. objectweb. asm. MethodVisitor ;
import org. objectweb. asm. Opcodes ;
public class CreateHuman implements Opcodes {
public static void main ( String [ ] args) throws Exception {
byte [ ] bytes = generateBytes ( ) ;
String path = CreateHuman . class . getResource ( "/" ) . getPath ( ) + "org/example/asm8/Human.class" ;
System . out. println ( "输出路径:" + path) ;
try ( FileOutputStream fos = new FileOutputStream ( path) ) {
fos. write ( bytes) ;
}
Class < ? > clazz = Class . forName ( "org.example.asm8.Human" ) ;
Method hello = clazz. getMethod ( "hello" ) ;
Object helloResult = hello. invoke ( clazz. getDeclaredConstructor ( ) . newInstance ( ) ) ;
System . out. println ( helloResult) ;
Method sum = clazz. getMethod ( "sum" , int . class , int . class ) ;
Object sumResult = sum. invoke ( clazz. getDeclaredConstructor ( ) . newInstance ( ) , 1 , 2 ) ;
System . out. println ( sumResult) ;
}
public static byte [ ] generateBytes ( ) {
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
ClassWriter classWriter = new ClassWriter ( ClassWriter . COMPUTE_FRAMES ) ;
classWriter. visit ( V1_8 , ACC_PUBLIC | ACC_SUPER , "org/example/asm8/Human" , null , "java/lang/Object" , null ) ;
fieldVisitor = classWriter. visitField ( ACC_PRIVATE , "name" , "Ljava/lang/String;" , null , null ) ;
fieldVisitor. visitEnd ( ) ;
fieldVisitor = classWriter. visitField ( ACC_PROTECTED , "age" , "I" , null , null ) ;
fieldVisitor. visitEnd ( ) ;
fieldVisitor = classWriter. visitField ( ACC_PUBLIC | ACC_STATIC , "no" , "J" , null , null ) ;
fieldVisitor. visitEnd ( ) ;
fieldVisitor = classWriter. visitField ( ACC_PUBLIC | ACC_FINAL | ACC_STATIC , "GENDER" , "Ljava/lang/String;" , null , "male" ) ;
fieldVisitor. visitEnd ( ) ;
methodVisitor = classWriter. visitMethod ( ACC_PUBLIC , "<init>" , "()V" , null , null ) ;
methodVisitor. visitCode ( ) ;
methodVisitor. visitVarInsn ( ALOAD , 0 ) ;
methodVisitor. visitMethodInsn ( INVOKESPECIAL , "java/lang/Object" , "<init>" , "()V" , false ) ;
methodVisitor. visitInsn ( RETURN ) ;
methodVisitor. visitMaxs ( 0 , 0 ) ;
methodVisitor. visitEnd ( ) ;
methodVisitor = classWriter. visitMethod ( ACC_PUBLIC | ACC_STATIC , "hello" , "()V" , null , null ) ;
methodVisitor. visitCode ( ) ;
methodVisitor. visitFieldInsn ( GETSTATIC , "java/lang/System" , "out" , "Ljava/io/PrintStream;" ) ;
methodVisitor. visitLdcInsn ( "Hello world!" ) ;
methodVisitor. visitMethodInsn ( INVOKEVIRTUAL , "java/io/PrintStream" , "println" , "(Ljava/lang/String;)V" , false ) ;
methodVisitor. visitInsn ( RETURN ) ;
methodVisitor. visitMaxs ( 0 , 0 ) ;
methodVisitor. visitEnd ( ) ;
methodVisitor = classWriter. visitMethod ( ACC_PUBLIC , "sum" , "(II)I" , null , null ) ;
methodVisitor. visitCode ( ) ;
methodVisitor. visitMethodInsn ( INVOKESTATIC , "java/lang/System" , "nanoTime" , "()J" , false ) ;
methodVisitor. visitVarInsn ( LSTORE , 3 ) ;
methodVisitor. visitTypeInsn ( NEW , "java/lang/StringBuilder" ) ;
methodVisitor. visitInsn ( DUP ) ;
methodVisitor. visitMethodInsn ( INVOKESPECIAL , "java/lang/StringBuilder" , "<init>" , "()V" , false ) ;
methodVisitor. visitVarInsn ( ASTORE , 5 ) ;
methodVisitor. visitVarInsn ( ALOAD , 5 ) ;
methodVisitor. visitLdcInsn ( "Hello StringBuilder!" ) ;
methodVisitor. visitMethodInsn ( INVOKEVIRTUAL , "java/lang/StringBuilder" , "append" ,
"(Ljava/lang/String;)Ljava/lang/StringBuilder;" , false ) ;
methodVisitor. visitInsn ( POP ) ;
methodVisitor. visitFieldInsn ( GETSTATIC , "java/lang/System" , "out" , "Ljava/io/PrintStream;" ) ;
methodVisitor. visitVarInsn ( ALOAD , 5 ) ;
methodVisitor. visitMethodInsn ( INVOKEVIRTUAL , "java/io/PrintStream" , "println" , "(Ljava/lang/Object;)V" , false ) ;
methodVisitor. visitTypeInsn ( NEW , "java/util/ArrayList" ) ;
methodVisitor. visitInsn ( DUP ) ;
methodVisitor. visitMethodInsn ( INVOKESPECIAL , "java/util/ArrayList" , "<init>" , "()V" , false ) ;
methodVisitor. visitVarInsn ( ASTORE , 6 ) ;
methodVisitor. visitVarInsn ( ILOAD , 1 ) ;
methodVisitor. visitVarInsn ( ILOAD , 2 ) ;
methodVisitor. visitInsn ( IADD ) ;
methodVisitor. visitInsn ( IRETURN ) ;
methodVisitor. visitMaxs ( 0 , 0 ) ;
methodVisitor. visitEnd ( ) ;
classWriter. visitEnd ( ) ;
return classWriter. toByteArray ( ) ;
}
}
运行代码,成功生成class文件,被调用方法的结果打印正确
3.2.3 注意点
ClassWriter(ClassWriter.COMPUTE_FRAMES),这个ClassWriter的flag参数,建议设置COMPUTE_FRAMES,按照官网所说,性能虽然差点,但是可以自动更新操作数栈和方法调用帧计算,这时候methodVisitor.visitMaxs(0, 0)里面的参数就可以随便写了,但这个方法必须要调用,不然即使生成了class,在执行这个class中的方法时会报错。具体说明可以看jar中的注释。
类型描述符
Java 类型 类型描述符 boolean Z char C byte B short S int I float F long J double D Object Ljava/lang/Object; int[] [I Object[][] [[Ljava/lang/Object;
方法描述符
源文件中的方法声明 方法描述符 void m(int i, float f) (IF)V int m(Object o) (Ljava/lang/Object;)I int[] m(int i, String s) (ILjava/lang/String;)[I Object m(int[] i) ([I)Ljava/lang/Object;
表示行号的代码,可以删除。
Label label0 = new Label ( ) ;
methodVisitor. visitLabel ( label0) ;
methodVisitor. visitLineNumber ( 16 , label0) ;
methodVisitor. visitLocalVariable ( "this" , "Lorg/example/asm8/Human;" , null , label0, label2, 0 ) ;
具体的字节码指令这里不做介绍,可以自行查阅相关资料
3.3 修改已存在的类
3.3.1 待修改的类
package org. example. asm8. modify ;
import javax. annotation. PostConstruct ;
public class OriginalClass {
private int age;
public OriginalClass ( ) {
}
private void methodA ( ) {
System . out. println ( "I am methodA!" ) ;
}
@PostConstruct
public void methodB ( ) {
System . out. println ( "I am methodB!" ) ;
}
public int methodC ( ) {
return age;
}
}
3.3.2 修改后的类
import javax. annotation. PostConstruct ;
public class UpdatedClass {
private int age;
public String name;
@PostConstruct
public void methodB ( ) {
long start = System . currentTimeMillis ( ) ;
System . out. println ( "I am methodB!" ) ;
long end = System . currentTimeMillis ( ) ;
System . out. println ( "spend time: " + ( end - start) + " ms" ) ;
}
protected int methodC ( ) {
return this . age;
}
public String getName ( ) {
return name;
}
}
3.3.3 代码实现
package org. example. asm8. modify ;
import java. io. FileOutputStream ;
import java. util. Arrays ;
import org. example. asm8. create. CreateHuman ;
import org. objectweb. asm. AnnotationVisitor ;
import org. objectweb. asm. ClassReader ;
import org. objectweb. asm. ClassVisitor ;
import org. objectweb. asm. ClassWriter ;
import org. objectweb. asm. FieldVisitor ;
import org. objectweb. asm. MethodVisitor ;
import org. objectweb. asm. Opcodes ;
import org. objectweb. asm. commons. AdviceAdapter ;
public class ModifyOriginalClass implements Opcodes {
public static void main ( String [ ] args) throws Exception {
ClassReader cr = new ClassReader ( OriginalClass . class . getName ( ) ) ;
ClassWriter cw = new ClassWriter ( cr, ClassWriter . COMPUTE_FRAMES ) ;
cr. accept ( new MyClassVisitor ( ASM9 , cw) , 0 ) ;
byte [ ] bytes = cw. toByteArray ( ) ;
String path = CreateHuman . class . getResource ( "/" ) . getPath ( ) + "UpdatedClass.class" ;
System . out. println ( "输出路径:" + path) ;
try ( FileOutputStream fos = new FileOutputStream ( path) ) {
fos. write ( bytes) ;
}
}
static class MyClassVisitor extends ClassVisitor {
protected MyClassVisitor ( int api, ClassVisitor classVisitor) {
super ( api, classVisitor) ;
}
@Override
public MethodVisitor visitMethod ( int access, String name, String descriptor, String signature,
String [ ] exceptions) {
System . out. println ( "visit method: " + access + " " + name + " " + signature + " "
+ Arrays . toString ( exceptions) + " ----> " + descriptor) ;
if ( "methodA" . equals ( name) ) {
return null ;
}
if ( "methodC" . equals ( name) ) {
access = ACC_PROTECTED ;
}
MethodVisitor methodVisitor = cv. visitMethod ( access, name, descriptor, signature, exceptions) ;
return new MyMethodVisitor ( api, methodVisitor, access, name, descriptor) ;
}
@Override
public void visitEnd ( ) {
FieldVisitor fieldVisitor = cv. visitField ( ACC_PUBLIC , "name" , "Ljava/lang/String;" , null , null ) ;
fieldVisitor. visitEnd ( ) ;
MethodVisitor methodVisitor = cv. visitMethod ( ACC_PUBLIC , "getName" , "()Ljava/lang/String;" , null , null ) ;
methodVisitor. visitCode ( ) ;
methodVisitor. visitVarInsn ( ALOAD , 0 ) ;
methodVisitor. visitFieldInsn ( GETFIELD , "org/example/asm8/UpdatedClass" , "name" , "Ljava/lang/String;" ) ;
methodVisitor. visitInsn ( ARETURN ) ;
methodVisitor. visitMaxs ( 0 , 0 ) ;
methodVisitor. visitEnd ( ) ;
super . visitEnd ( ) ;
}
}
static class MyMethodVisitor extends AdviceAdapter {
boolean flag = false ;
protected MyMethodVisitor ( int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super ( api, methodVisitor, access, name, descriptor) ;
if ( "methodB" . equals ( name) ) {
System . out. println ( "这是methodB方法" ) ;
}
}
@Override
public AnnotationVisitor visitAnnotation ( String descriptor, boolean visible) {
if ( "Ljavax/annotation/PostConstruct;" . equals ( descriptor) ) {
flag = true ;
}
return super . visitAnnotation ( descriptor, visible) ;
}
@Override
protected void onMethodEnter ( ) {
if ( ! flag) {
return ;
}
System . out. println ( "即将对有PostConstruct注解的方法进行增强====》" ) ;
mv. visitMethodInsn ( INVOKESTATIC , "java/lang/System" , "currentTimeMillis" , "()J" , false ) ;
mv. visitVarInsn ( LSTORE , 1 ) ;
}
@Override
protected void onMethodExit ( int opcode) {
if ( ! flag) {
return ;
}
mv. visitMethodInsn ( INVOKESTATIC , "java/lang/System" , "currentTimeMillis" , "()J" , false ) ;
mv. visitVarInsn ( LSTORE , 3 ) ;
mv. visitFieldInsn ( GETSTATIC , "java/lang/System" , "out" , "Ljava/io/PrintStream;" ) ;
mv. visitTypeInsn ( NEW , "java/lang/StringBuilder" ) ;
mv. visitInsn ( DUP ) ;
mv. visitMethodInsn ( INVOKESPECIAL , "java/lang/StringBuilder" , "<init>" , "()V" , false ) ;
mv. visitLdcInsn ( "spend time: " ) ;
mv. visitMethodInsn ( INVOKEVIRTUAL , "java/lang/StringBuilder" , "append" , "(Ljava/lang/String;)Ljava/lang/StringBuilder;" , false ) ;
mv. visitVarInsn ( LLOAD , 3 ) ;
mv. visitVarInsn ( LLOAD , 1 ) ;
mv. visitInsn ( LSUB ) ;
mv. visitMethodInsn ( INVOKEVIRTUAL , "java/lang/StringBuilder" , "append" , "(J)Ljava/lang/StringBuilder;" , false ) ;
mv. visitLdcInsn ( " ms" ) ;
mv. visitMethodInsn ( INVOKEVIRTUAL , "java/lang/StringBuilder" , "append" , "(Ljava/lang/String;)Ljava/lang/StringBuilder;" , false ) ;
mv. visitMethodInsn ( INVOKEVIRTUAL , "java/lang/StringBuilder" , "toString" , "()Ljava/lang/String;" , false ) ;
mv. visitMethodInsn ( INVOKEVIRTUAL , "java/io/PrintStream" , "println" , "(Ljava/lang/String;)V" , false ) ;
}
}
}
3.3.4 修改结果