asm不是一个新的东西,javaee领域的开源框架都有asm的用武之地。准确来说 asm是用来操作字节码的,源代码是java编写。

asm官网 https://asm.ow2.io/index.html

asm的使用稍微复杂,需要了解字节码。我强烈建议从事java开发的同学必须会asm的基本操作,这会让你非常容易接近jvm的编译指令,类加载等原理上的东西,便于更好的理解jvm与java特性。

一、创建一个字节码的例子

源代码 https://github.com/liuchengts/asm-demo

下面是核心代码:

/**
 * 创建class并且加载到内存
 *
 * @param name              要创建的class名称 相当于 class.getName()
 * @param interfaceClasss   要实现的的接口class,可以是null
 * @param extendsClasssImpl 要继承的类class
 * @param isInterface       是一个接口true
 * @return 返回创建好的class对象
 */
public static Class createClass(String name, Set<Class> interfaceClasss, Class extendsClasssImpl, boolean isInterface) {
    String classPath = name.replace(".", File.separator);
    //定义class书写器
    ClassWriter cw = new ClassWriter(0);
    if (extendsClasssImpl == null) {
        //默认继承顶级 Object 类
        extendsClasssImpl = Object.class;
    }
    if (interfaceClasss == null || interfaceClasss.isEmpty()) {
        //第一个参数 V1_1 是生成的class的版本号, 对应class文件中的主版本号和次版本号, 即minor_version和major_version
        //第二个参数 ACC_PUBLIC 表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags
        //第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.zhang.Example, 那么这里传入的参数必须是com/jg/zhang/Example  。对应class文件中的this_class
        //第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute)
        //第五个参数是当前类的父类的全限定名。 没有的话默认应该继承Object。 这个参数对应class文件中的super_class
        //第六个参数是String[]类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces
        cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, classPath, null, Type.getInternalName(extendsClasssImpl), null);
    } else {
        Set<String> interfaceSet = new HashSet<>();
        for (Class las : interfaceClasss) {
            interfaceSet.add(Type.getInternalName(las));
        }
        cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, classPath, null, Type.getInternalName(extendsClasssImpl), interfaceSet.toArray(new String[interfaceSet.size()]));
    }

    //生成默认的构造方法
    //第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应method_info 中的access_flags 。 
    //第二个参数是方法的方法名。 对于构造方法来说, 方法名为<init> 。 这个参数对应method_info 中的name_index , name_index引用常量池中的方法名字符串。 
    //第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V  。 这个参数对应method_info 中的descriptor_index 。 
    //第四个参数是和泛型相关的, 这里传入null表示该方法不是泛型方法。这个参数对应method_info 中的Signature属性。
    //第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入null 。 这个参数对应method_info 中的Exceptions属性
    MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC,
            "<init>",
            "()V",
            null,
            null);
    //生成构造方法的字节码指令
    //1 调用visitVarInsn方法,生成aload指令, 将第0个本地变量(也就是this)压入操作数栈。
    //2 调用visitMethodInsn方法, 生成invokespecial指令, 调用父类(也就是Object)的构造方法。
    //3 调用visitInsn方法,生成return指令, 方法返回。 
    //4 调用visitMaxs方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应Code属性中的max_stack和max_locals 。 
    //5 最后调用visitEnd方法, 表示当前要生成的构造方法已经创建完成。
    mw.visitVarInsn(Opcodes.ALOAD, 0);
    mw.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(extendsClasssImpl), "<init>", "()V", isInterface);
    mw.visitInsn(Opcodes.RETURN);
    mw.visitMaxs(1, 1);
    mw.visitEnd();

    //生成mian方法
    mw = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
            "main",
            "([Ljava/lang/String;)V",
            null,
            null);

    //生成main方法中的字节码指令
    mw.visitFieldInsn(Opcodes.GETSTATIC,
            "java/lang/System",
            "out",
            "Ljava/io/PrintStream;");
    mw.visitLdcInsn("Hello world!");
    mw.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream",
            "println",
            "(Ljava/lang/String;)V", isInterface);


    // 获取生成的class文件对应的二进制流
    byte[] code = cw.toByteArray();
    // 写文件到本地
    String classFile = writerFile(code, classPath);
    Map map = new HashMap();
    map.put(name, classFile);
    mapThreadLocal.set(map);
    //直接将二进制流加载到内存中
    return ASMUtils.getInstance().defineClass(name, code, 0, code.length);
}

调用:

public static void main(String[] args) {
    String name = "com.lc.study.Test";//这是一个不存在的包和类
    //将这个类继承 TestServiceImpl 并且标记这不是一个接口
    Class test = ASMUtils.createClass(name, null, TestServiceImpl.class, false);
    try {
        String classFile = ASMUtils.getClassFilePath(name);
        System.out.println("class文件位置:" + classFile);
        System.out.println("************* 获取到的当前类(包括父类的)所有方法");
        for (Method method : test.getMethods()) {
            System.out.println("> 方法:" + method.getName());
            Class[] parameterTypes = method.getParameterTypes();
            System.out.println(">>> 入参数量:" + parameterTypes.length);
            for (Class c : parameterTypes) {
                System.out.println(">>>>>> 参数:" + c.getSimpleName());
            }
            System.out.println(">>> 返回类型:" + method.getReturnType().getSimpleName());
            Annotation[] annotations = method.getDeclaredAnnotations();
            System.out.println(">>> 方法注解数量:" + annotations.length);
            for (Annotation a : annotations) {
                System.out.println(">>>>>> 注解:" + a.toString());
            }
        }
        System.out.println("************* 打印完成,开始方法调用测试");
        //调用当前类的main方法
        test.getDeclaredMethod("main", new String[]{}.getClass()).invoke(null, new Object[]{null});

        //调用父类中的的方法
        //先实例化,创建一个对象
        Object object = test.newInstance();
        //调用父类的 getTest 方法
        Method getTest = test.getMethod("getTest");
        getTest.invoke(object);
        //调用父类的 updateTest 方法
        Method updateTest = test.getMethod("updateTest", Integer.class);
        Object obj = updateTest.invoke(object, new Integer[]{1});
        System.out.println("updateTest 返回参数:" + obj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这里会生成一个class文件,当然可以选择不生成。

java ASM 技术 asm代码_父类

 

假设 classFile打印输出的是 /a/b/c/Test.class

打开这个Test.class 可以看到原始代码

执行./shell.sh  /a/b/c/Test.class   反编译回来看看字节码

asm还可以创建字段 注解等,考虑到asm偏于底层,已经有很多上层扩展包封装创建好了 所以这里不过多演示。