本文属于Java ASM系列一:Core API当中的一篇。


对于《Java ASM系列一:Core API》有配套的视频讲解,可以点击这里这里进行查看;同时,也可以点击这里查看源码资料。


The CheckClassAdapter class checks that its methods are called in the appropriate order, and with valid arguments, before delegating to the next visitor.

1. 两种使用方式

到目前为止,我们主要介绍了Class Generation和Class Transformation操作。我们可以借助于CheckClassAdapter类来检查生成的字节码内容是否正确,主要有两种使用方式:

  • 在生成类或转换类的过程中进行检查
  • 在生成类或转换类的结束后进行检查

1.1 第一种方式

第一种使用方式,是在生成类(Class Generation)或转换类(Class Transformation)的过程中进行检查:

// 第一步,应用于Class Generation
// 串联ClassVisitor:cv --- cca --- cw
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
CheckClassAdapter cca = new CheckClassAdapter(cw);
ClassVisitor cv = new MyClassVisitor(cca);

// 第二步,应用于Class Transformation
byte[] bytes = ... // 这里是class file bytes
Cla***eader cr = new Cla***eader(bytes);
cr.accept(cv, 0);

示例如下:

import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.CheckClassAdapter;

import static org.objectweb.asm.Opcodes.*;

public class CheckClassAdapterExample01Generate {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]内容
        byte[] bytes = dump();

        // (2) 保存byte[]到文件
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor cv = new CheckClassAdapter(cw);

        // (2) 调用visitXxx()方法
        cv.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            FieldVisitor fv = cv.visitField(ACC_PRIVATE, "intValue", "I", null, null);
            fv.visitEnd();
        }

        {
            MethodVisitor mv1 = cv.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(1, 1);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cv.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            mv2.visitCode();
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("Hello World");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(2, 1);
            mv2.visitEnd();
        }
        cv.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();
    }
}

1.2 第二种方式

第二种使用方式,是在生成类(Class Generation)或转换类(Class Transformation)的结束后进行检查:

byte[] bytes = ... // 这里是class file bytes
PrintWriter printWriter = new PrintWriter(System.out);
CheckClassAdapter.verify(new Cla***eader(bytes), false, printWriter);

示例如下:

import lsieun.utils.FileUtils;
import org.objectweb.asm.Cla***eader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.CheckClassAdapter;

import java.io.PrintWriter;

import static org.objectweb.asm.Opcodes.*;

public class CheckClassAdapterExample02Generate {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]内容
        byte[] bytes = dump();

        // (2) 保存byte[]到文件
        FileUtils.writeBytes(filepath, bytes);

        // (3) 检查
        PrintWriter printWriter = new PrintWriter(System.out);
        CheckClassAdapter.verify(new Cla***eader(bytes), false, printWriter);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            FieldVisitor fv = cw.visitField(ACC_PRIVATE, "intValue", "I", null, null);
            fv.visitEnd();
        }

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(1, 1);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            mv2.visitCode();
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("Hello World");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(2, 1);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();
    }
}

2. 检测案例

2.1 检测:方法调用顺序

如果将mv2.visitLdcInsn()mv2.visitFieldInsn()顺序调换:

mv2.visitLdcInsn("Hello World");
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

会出现如下错误:

Method owner: expected Ljava/io/PrintStream;, but found Ljava/lang/String;

2.2 检测:方法参数不对

如果将方法的描述符((Ljava/lang/String;)V)修改成(I)V

mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitLdcInsn("Hello World");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);

会出现如下错误:

Argument 1: expected I, but found Ljava/lang/String;

2.3 检测:没有return语句

如果注释掉mv2.visitInsn(RETURN);语句,会出现如下错误:

org.objectweb.asm.tree.analysis.AnalyzerException: Execution can fall off the end of the code

2.4 检测:不调用visitMaxs()方法

如果注释掉mv2.visitMaxs(2, 1);语句,会出现如下错误:

org.objectweb.asm.tree.analysis.AnalyzerException: Error at instruction 0: Insufficient maximum stack size.

2.5 检测不出:重复类成员

如果出现重复的字段或者重复的方法,CheckClassAdapter类是检测不出来的:

{
	FieldVisitor fv = cw.visitField(ACC_PRIVATE, "intValue", "I", null, null);
	fv.visitEnd();
}

{
	FieldVisitor fv = cw.visitField(ACC_PRIVATE, "intValue", "I", null, null);
	fv.visitEnd();
}

3. 总结

本文主要是对CheckClassAdapter类进行介绍,内容总结如下:

  • 第一点,作为一个工具类,CheckClassAdapter类的主要作用是检查生成的byte[]内容是否合法,但是它能够实现的检查功能是有限的,有一些问题是无法检测出来的。
  • 第二点,在编写ASM代码的过程中,除了使用CheckClassAdapter类帮助检查,我们自身所具备的“细心认真的态度”和“缜密的思考”是非常重要的、不可替代的因素。