字节码操作

  1. 1. 字节码操作
    1. 1.1. 实践
      1. 1.1.1. ASM
        1. 1.1.1.1. ASM API
        2. 1.1.1.2. ASM工具
      2. 1.1.2. Javassist
    2. 1.2. 参考

字节码操作

实践

ASM

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为

ASM API

有Core API和Tree API。 Core API可以类比SAX的方式解析XML,Tree API可以类比DOM方式解析XML。一般情况下都使用Core API

它有下面几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。

  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。

  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

    ClassReader和ClassWriter都是继承ClassVisitor

一个Base类

1
2
3
4
5
6
7
8
public class Base {
public Base() {
}

public void process() {
System.out.println("process");
}
}

目标:通过ASM增强后在process前输出start、后输出end。

首先需要定义两个类,一个是继承ClassVisitor的MyClassVisitor类,用于对字节码的visit以及修改;

另一个是Generator类,在这个类中定义ClassReader和ClassWriter,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。

1647058663702.png

Generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package asm;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;


import java.io.File;
import java.io.FileOutputStream;

public class Generator {
public static void main(String[] args) throws Exception {
//读取
ClassReader classReader = new ClassReader("asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//输出
File f = new File("target/classes/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now generator cc success!!!!!");
}
}

MyClassVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package asm;

import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor( ClassVisitor classVisitor) {
super(ASM5, classVisitor);
}

@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
MethodVisitor methodVisitor = cv.visitMethod(i, s, s1, s2, strings);
// TODO: 过滤掉构造函数及sayHello方法
if (s.equals("<init>") || s.equals("sayHello")) {
return methodVisitor;
}
return new MyMethodVisitor(methodVisitor);
}

class MyMethodVisitor extends MethodVisitor implements Opcodes{

public MyMethodVisitor(MethodVisitor methodVisitor) {
super(ASM5, methodVisitor);
}

@Override
public void visitCode() {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.visitCode();
}

@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
}

逻辑理解起来也不难。

Base里有两个方法,构造函数和process。process是我们要增强的方法。

visitMethod方法判断当前读到哪个方法,跳过构造方法 <init> 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。

1647064967263.png

主要的操作就在这一片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}

MethodVisitor的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,所以可以在这里添加前置操作

每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法,判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令。我们写void方法时一般没有return。这里其实涉及到在字节码层面,void方法会执行return指令。

具体实现就是visitXXXXInsn,中间的XXXX就是字节码指令,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

运行Generator.main之后查看编译好的Base.class内容:

1647063200768.png

写一个Test测试

1647063365683.png

ASM工具

ASM ByteCode Outline

算是一个辅助工具,利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了这个工具

在idea里可以直接安装。

工具教程

https://blog.csdn.net/ForwardSailing/article/details/106494116

是个非常非常好用的工具

Javassist

比asm操作简单一点框架。ASM是在指令层次上操作字节码,而Javassist更强调源码层次

利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

  • CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。
  • ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
  • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

跟之前一样,在process前面和后面插入start和end,实现起来更简单直观易懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import javassist.*;

import java.io.IOException;

public class JavassistTest {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("asm.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/Users/fmyyy/Myself/yso/untitled");
Base h = (Base)c.newInstance();
h.process();
}
}

参考

https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html