什么是 ASM ?

ASM 是一款直接操作字节码(即 class 文件)的框架,可以都已生成好的字节码进行改动或者生成。类似框架有 javassist 相对 ASM 简单许多。

谁在用 ASM ?

大名鼎鼎的 FastJSON & CGLIB

说在前面

官网 API 文档 网上关于 ASM 都是一些零散的资料,框架本事并不是很难。但是要彻底学习需要一段时间。建议理解就好。日常工作基本上不会使用。

字节码操作

众所周知 .java 文件经过,javac 的编译后会生成一个后缀为 .class 的字节码文件。而这个文件才是 JVM 要真正读取运行的文件。而这个 .class 文件是一个可编辑的文件。你使用任何一个文本编辑都可以看到以下内容

asm java 分析类关系 java asm教程_asm java 分析类关系

你可以通过开头的 cafe babe 认定他是一个 .class 文件。关于字节码的解读网上资料较多你可以百度一下

手动修改字节码

上方字节码源码如下:

public class Hello{
	public static void main(String[] args) {
		int i =  8888;
		System.out.print(i);
	}
}

我们都知道如果使用 java Hello 运行上方编译过后的 .class 文件会输出 8888

asm java 分析类关系 java asm教程_字节码_02

我们回过头再是观察上方字节码会发现 .class 文件是由 16进制 组成的。我们通过 进制转换工具 将 8888 转换成16进制就是 22b8

asm java 分析类关系 java asm教程_asm java 分析类关系_03


然后在编译后的文件找到 22b8

asm java 分析类关系 java asm教程_asm java 分析类关系_04


替换成 270f(转换为10进制为 9999 ) 再使用 java Hello 运行(不要再使用 javac 编译)得出结果:

asm java 分析类关系 java asm教程_字节码_05


手动修改字节码码就成功了。

通过 ASM 修改字节码。
  1. ASM 有两种修改字节码的方式

流 和 树。
流:一边读取 class 文件一边修改。
树:全部读取出来,选择任意节点修改。

  1. ASM 中的部分修改并不是真正的修改。例如:

你打算通过 ASM 修改一个方法的返回值。ASM 会重建这个方法。把之前的删除掉。

  1. ASM 有三个重要的类

ClassReader:读取 class 文件转换成的 byte 数组
ClassVisitor & ClassNode:分别对应 流 & 树 中字节码操作类
ClassWriter:将处理好的 class 文件转换成 byte 数组

演示,因为代码有点多。完整的代码前往 github

ASM 中创建一个方法

我们先要获取这个类的 class 文件,然后转成 byte 数组

package com.annie.util;
import com.annie.App;
import java.io.*;
public class FileUtil {
    // 将 class 文件转成 byte 数组(就是下方 MyMain 实体类编译后的 class 文件位置)
    public static byte[] File2Byte() throws IOException {
        // 获取 class 文件所在的位置
        String path = App.getMyMainPath();
        File file = new File(path);
        byte[] bytes = new byte[(int) file.length()];
        try (FileInputStream fileInputStream = new FileInputStream(file);) {
            fileInputStream.read(bytes);
        }
        return bytes;
    }
    // 将 byte 数组写进 class 文件
    public static boolean Byte2File(byte[] bytes) throws IOException {
        String path = App.getMyMainPath();
        File file = new File(path);
        try (FileOutputStream fileOutputStream = new FileOutputStream(file);) {
            fileOutputStream.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

新建一个实体类

// 类中有 两个参数 五个方法
class MyMain {
    int i = 1;
    int j = 2;

    public void t01() {

    }
    public void t02(String str) {

    }

    public String t03() {

        return "hello";
    }

    public String t04(String str) {


        return str;
    }

    public int t05(int i) {

        return i;
    }
}

编写添加属性的方法

public static void createField() throws IOException {
        byte[] bytes = FileUtil.File2Byte();

        // 读取 byte 数组
        ClassReader cr = new ClassReader(bytes);

        // 创建一个树节点
        ClassNode cn = new ClassNode();

        // 将 cr 转换好的字节码放进 cn 树中
        // SKIP_DEBUG 跳过类文件中的调试信息,比如行号信息(LineNumberTable)等
        // SKIP_CODE:跳过方法体中的 Code 属性(方法字节码、异常表等)
        cr.accept(cn, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);

        // 新建一个属性
        // 参数作用域,参数名称,参数类型,参数签名,参数初始值
        FieldNode fn = new FieldNode(Opcodes.ACC_PUBLIC, "code", "Ljava/lang/String;", null, null);

        // 将属性添加到 cn 参数节点中
        cn.fields.add(fn);

        // 再 JVM 存在,数栈和局部变量表
        // new ClassWriter(0):表示我们手动设置。(这里我们用不到)
        ClassWriter cw = new ClassWriter(0);

        // 将新生成的 'class' 放进 cw 中
        cn.accept(cw);

        // cw 将新的 'class' 处理成byte覆盖之前原始的class文件
        FileUtil.Byte2File(cw.toByteArray());
    }

然后通过观察项目中的 MyMain.class 文件会发现。

asm java 分析类关系 java asm教程_字节码_06


你会发现多了一个 code 属性。我这是使用的是 idea 进行的反编译。只将 .class 文件拖进 idea 中就行。使用 javap -l MyMain.class 一样可以达到效果。

说在后面的话,

再博客中可能演示的不是那么仔细。如果有兴趣可以去github上下载一分代码看一下。关键的还是要多看官网 API 文档说白了。ASM 也就是一个类而已,和我们平时用的 HTTP IO 也差不了多少。只是领域不同。

ASM 能做什么?
Java 序列的软件破解。JavaAgent 技术等。