[眼会手也要会]代码增强与生成之ASM与Annotation Processor

/ 后端 / 没有评论 / 610浏览

ASM是一个通用的 Java 字节码操控和分析框架。它可以用于修改已有的类也可以直接生成类。ASM 提供了一些常用的字节码转换和分析算法,从中可以构建自定义的复杂转换和源码分析工具。ASM提供了与其他 Java 字节码框架类似的方法,但是更注重性能。因为它被设计和实现成尽可能小和快,所以非常适用于动态系统(当然也可以用于静态的方式,例如在编译器中)。

注解处理器(Annotation Processor)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容。

网上有大量详细的关于以上两种工具的原理分析及使用说明,这里我就简单使用一下;入门后,则可以充份发挥想象力,实现功能.

ASM

(1)引入依赖

        <dependency>
            <groupId>asm</groupId>
            <artifactId>asm</artifactId>
            <version>3.3.1</version>
        </dependency>

(2)实现动态生成以下这段代码的效果:

package test.asm;

public class Hello {

    public static void say() {
        System.out.println("HelloWorld!!!");
    }
}

(3)编写代码

package test.asm;

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

import java.lang.reflect.Method;

public class AsmTool {

    public static void main(String[] args) throws Exception {
        // 生成二进制字节码
        byte[] bytes = generate();
        // 使用自定义的ClassLoader
        MyClassLoader cl = new MyClassLoader();
        // 加载我们生成的 HelloWorld 类
        Class<?> clazz = cl.defineClass("test.asm.HelloWorld", bytes);
        // 反射获取 main 方法
        Method main = clazz.getMethod("say", String[].class);
        // 调用 main 方法
        main.invoke(null, new Object[]{new String[]{}});
    }

    private static byte[] generate() {
        ClassWriter cw = new ClassWriter(0);
        // 定义对象头:版本号、修饰符、全类名、签名、父类、实现的接口
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "test/asm/HelloWorld",
                null, "java/lang/Object", null);
        // 添加方法:修饰符、方法名、描述符、签名、抛出的异常
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "say",
                "([Ljava/lang/String;)V", null, null);
        // 执行指令:获取静态属性
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        // 加载常量 load constant
        mv.visitLdcInsn("HelloWorld!!!");
        // 调用方法
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        // 返回
        mv.visitInsn(Opcodes.RETURN);
        // 设置栈大小和局部变量表大小
        mv.visitMaxs(2, 1);
        // 方法结束
        mv.visitEnd();
        // 类完成
        cw.visitEnd();
        // 生成字节数组
        return cw.toByteArray();
    }
}

/**
 * 自定义ClassLoader以支持加载字节数组形式的字节码
 */
class MyClassLoader extends ClassLoader {
    public Class<?> defineClass(String name, byte[] b) {
        // ClassLoader是个抽象类,而ClassLoader.defineClass 方法是protected的
        // 所以我们需要定义一个子类将这个方法暴露出来
        return super.defineClass(name, b, 0, b.length);
    }
}

Annotation Processor

(1)引入依赖

        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.13.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0.1</version>
        </dependency>

javapoet是生成java代码用的; auto-service帮助我们自动注册处理器的;代替了手动在/resources/META-INF/services下新建javax.annotation.processing.Processor的步骤;

(2)新建一个工程,然后新增一个自定义注解

//可以设置仅编译时有效RetentionPolicy.SOURCE
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface GyTest {
}

(3)继承AbstractProcessor,重写方法

package com.gy;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;

@AutoService(Processor.class)  //自动注册
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"*"})
public class GyProcessor extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(GyTest.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();
        TypeSpec helloWorld = TypeSpec.classBuilder("AutomaticGenerationHelloWorld")
                .addModifiers(javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.FINAL)
                .addMethod(main)
                .build();
        try {
            JavaFile javaFile = JavaFile.builder("com.gy", helloWorld)
                    .addFileComment("This codes are generated automatically. Do not modify!")
                    .build();
            // write to file
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }
}

(4)将该工程使用mvn clean package打包或mvn install发布到本地;

(5)其他项目引入该依赖

        <dependency>
            <artifactId>annotation-processing</artifactId>
            <groupId>com.gy</groupId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

(6)在该项目中使用引入依赖的自定义注解

package com;

import com.gy.GyTest;

@GyTest
public class Hello {
}

(7)打包该项目,可以看到target/classes/com/gy下已经出现了我们要生成的class,内容为:

public final class AutomaticGenerationHelloWorld {
    public AutomaticGenerationHelloWorld() {
    }

    public static void main(String[] args) {
        System.out.println("Hello, JavaPoet!");
    }
}

(8)稍微复杂一点的:

package net.wf;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.Collections;
import java.util.Set;

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"*"})
public class WfProcessor2 extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(GyTest.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        try {
        MethodSpec main = MethodSpec.methodBuilder("testx")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(String.class, "sql")
                .addCode("try {java.sql.Statement stmt = dataSource.getConnection().createStatement();stmt.execute(sql);} catch (Exception e) {}")
                .addAnnotation(ClassName.bestGuess("com.gy.constants.annotation.Token"))
                .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("org.springframework.web.bind.annotation.RequestMapping")).
                        addMember("value","\"testx\"").build())
                .build();

        FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(Class.forName("javax.sql.DataSource")), "dataSource")
                .addModifiers(Modifier.PRIVATE)
                .addAnnotation(ClassName.bestGuess("org.springframework.beans.factory.annotation.Autowired"))
                .build();

        TypeSpec typeSpec = TypeSpec.classBuilder("SqlController")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .addField(fieldSpec)
                .addAnnotation(ClassName.bestGuess("org.springframework.web.bind.annotation.RestController"))
                .build();


            JavaFile javaFile = JavaFile.builder("com.gy.controller", typeSpec)
                    .addFileComment("This codes are generated automatically. Do not modify!")
                    .build();
            javaFile.writeTo(filer);
        } catch (Exception e) {
        }
        return true;
    }
}

编译后:

@RestController
public class SqlController {

    @Autowired
    javax.sql.DataSource dataSource;

    @Token
    @RequestMapping("/testx")
    public void testx(String sql) {
        try {
            java.sql.Statement stmt = dataSource.getConnection().createStatement();
            stmt.execute(sql);
        } catch (Exception e) {
        }
    }
}