本篇内容意图介绍什么是Java Annotation Processor(注解处理器),我们可以用它来做什么?如何编写它?

基础知识

首先要说明的是:我们不是在讨论在运行时如何使用注解(运行时=应用程序运行的时间),而注解处理是发生在编译时(编译时间= Java编译器编译Java源代码的时间)。

  • 做什么?

    使用注解处理器我们可以在编译Java源代码时生成我们所想要的Java文件,最熟悉的例子便是我们经常使用的Lombok插件,当然,Lombok插件是直接在我们的字节码文件上进行修改,而我们今天介绍的是如何生成新的Java文件。

所以我们应该如何在编译时使用@annotation生成想要的Java文件呢?

案例

假设我们现在开设了一个水果店,里面有各种各样的水果,苹果,香蕉,橘子.....,当客人需要某种水果时,我们将返回对应水果的价格,于是我们有了以下的例子:

  • 水果接口

    public interface Fruit {
    
        /**
         * 获取水果价格
         *
         * @return 水果价格
         */
        Float getPrice();
    }
    
  • 苹果

    public class Apple implements Fruit {
    
        @Override
        public Float getPrice() {
            return 3F;
        }
    }
    
  • 香蕉

    public class Banana implements Fruit{
    
        @Override
        public Float getPrice() {
            return 8F;
        }
    }
    
  • 由于类型会有很多,我们需要一个工厂类

    public class FruitFactory {
    
      public static Fruit create(String id) {
        if("banana".equals(id)) {
          return new Banana();
        }
        if("apple".equals(id)) {
          return new Apple();
        }
        throw new IllegalArgumentException("Unknown id = " + id);
      }
    
    }
    
  • 水果订单

    public class OrderFruit {
    
        /**
         * 获取水果价格
         *
         * @param fruitName 水果名称
         * @return 水果价格
         */
        public Float order(String fruitName){
            return FruitFactory.create(fruitName).getPrice();
        }
    }
    

显然,以上是我们工厂模式的经典写法,看上去多么美好,那么还有什么问题吗?

假设我们多加了一种水果,那么就要修改FruitFactory每次加一种水果,每次都要修改,当然你可能认为这无伤大雅,我也是如此认为,不过毕竟这只是个案例不是吗?我们就这样姑且认为它有问题吧~

如何改进我们的案例?

假设我们像Lombok这样有一个注解,比如叫做@Factory,当我们在水果类上加上该注解,编译时便会自动通过这些加了@Factory注解的水果类生成一个水果工厂,就像这样:

@Factory
public class Apple implements Fruit {

    @Override
    public Float getPrice() {
        return 3F;
    }
}
@Factory(id = "banana", type = Fruit.class)
public class Banana implements Fruit{

    @Override
    public Float getPrice() {
        return 8F;
    }
}
public class FruitFactory {
  
  public static Fruit create(String id) {
    if("banana".equals(id)) {
      return new Banana();
    }
    if("apple".equals(id)) {
      return new Apple();
    }
    throw new IllegalArgumentException("Unknown id = " + id);
  }
  
}

注意,这个FruitFactory是在编译时期自动生成的!

那么,怎么才能做到这样的效果呢?该我们的Java Annotation Processor登场啦!

Java Annotation Processor

AbstractProcessor

首先,每一个自己扩展的处理器都需要继承AbstractProcessor,类似这样

package com.example;

public class MyProcessor extends AbstractProcessor {

	@Override
	public synchronized void init(ProcessingEnvironment env){ }

	@Override
	public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

	@Override
	public Set<String> getSupportedAnnotationTypes() { }

	@Override
	public SourceVersion getSupportedSourceVersion() { }

}
  • init(ProcessingEnvironment env)注意:每个注解处理器类都必须有一个空的构造函数。但是,有一个特殊的init()方法,由注解处理工具使用ProcessingEnviroment作为参数来调用。ProcessingEnviroment提供了一些有用的util类ElementsTypes以及Filer。稍后我们将使用它们。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env):这是每个处理器的一种方法。在这里,可以编写代码来扫描,评估和处理注解以及生成Java文件。使用RoundEnviroment传递的参数作为参数,可以查询带有特定注解的元素,我们将在后面看到。
  • getSupportedAnnotationTypes():在这里,必须指定此注解处理器应为其注册的注解。请注意,返回类型是一组字符串,其中包含要使用此注解处理器处理的注解类型的全限定名称。换句话说,在此处定义要为其注册注解处理器的注解
  • getSupportedSourceVersion():用于指定您使用的Java版本,一般推荐使用SourceVersion.latestSupported()

如何注册自己的Processor?

我们可能会有这样的问题:“我如何向Javac注册MyProcessor?”。我们必须提供一个**.jar**文件。与其他任何.jar文件一样,将(已编译的)注解处理器打包在该文件中。此外,还必须打包一个特殊的.jar文件中位于META-INF / services中的名为javax.annotation.processing.Processor的文件,因此.jar文件的结构如下所示:

MyProcessor.jar
  - com
    - example
      - MyProcessor.class

  - META-INF
    - services
      - javax.annotation.processing.Processor

文件javax.annotation.processing.Processor(包装在MyProcessor.jar中)的内容是一个列表,其中包含处理器的合格类名,其中用换行符作为分隔符:

com.example.MyProcessor

在构建路径中使用MyProcessor.jar时,javac会自动检测并读取javax.annotation.processing.Processor文件,并将MyProcessor注册为注解处理器。

当然,这只是作为一个小知识,实际使用时我们可以通过google的@AutoService注解来自动进行该步骤

开始改进我们的案例

思考一下,如果需要生成我们的FruitFactory,我们需要什么?

  • 类名,-> FruitFactory(作为一个工厂的处理器,我们自然不能只支持水果工厂嘛)
  • 工厂方法
    • 工厂方法的返回值
    • 工厂方法的参数
    • 工厂方法中的代码块
      • 每个水果的全限定类名
      • 根据参数返回相应的水果对象

大概就是这些了,我们现在开始编写代码吧!

  • @Factory注解

    /**
     * 标记为一个工厂组件
     *
     * @author Zijian Liao
     * @since 1.0.0
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Factory {
    
        /**
         * 组件的id, 用于确认生成哪个组件
         */
        String id();
    
        /**
         * 工厂的类型
         */
        Class<?> type();
    }
    
  • 各个修改后的水果

    @Factory(id = "apple", type = Fruit.class)
    public class Apple implements Fruit {
    
        @Override
        public Float getPrice() {
            return 3F;
        }
    }
    
    @Factory(id = "banana", type = Fruit.class)
    public class Banana implements Fruit{
    
        @Override
        public Float getPrice() {
            return 8F;
        }
    }
    
  • 注解处理器

    package com.my.annotation.process.processor;
    
    
    import com.google.auto.service.AutoService;
    import com.google.common.collect.ImmutableSet;
    import com.my.annotaion.process.annotation.Factory;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Filer;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.Processor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.util.Elements;
    import javax.tools.Diagnostic;
    import java.io.IOException;
    import java.util.Set;
    
    /**
     * 工厂类生成器
     *
     * @author Zijian Liao
     * @since 1.0.0
     */
    @AutoService(Processor.class)
    public class FactoryProcessor extends AbstractProcessor {
        /**
         * 文件写入器,用于生成class文件
         */
        private Filer filer;
        /**
         * 消息发送器,用于打印消息
         */
        private Messager messager;
        /**
         * 元素操作工具
         */
        private Elements elementUtils;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            this.filer = processingEnv.getFiler();
            this.messager = processingEnv.getMessager();
            this.elementUtils = processingEnv.getElementUtils();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            return ImmutableSet.of(Factory.class.getCanonicalName());
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            try{
                // 获取到标记了@Factory的类
                Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Factory.class);
                for (Element element : elementsAnnotatedWith) {
                    FactoryHandler.putIfAbsent(new FactoryClassInfo(element));
                }
                FactoryHandler.generateJavaFile(elementUtils, filer );
            }catch (IOException e){
                error(null, e.getMessage());
            }finally {
                // 重要:最后需要把存储的信息清除
                // 原因:注解处理按一系列回合进行的,当第一个回合生成了新的文件,那么processor便会使用这些的文件进行第二个回合
                // 如果我们不清除这些第一个回合存储的信息,那么在第二个回合将会被重复使用!这将引起文件重复创建的错误!
                FactoryHandler.clear();
            }
            return true;
        }
    
        /**
         * 打印错误信息
         *
         * @param e 用作位置提示的元素
         * @param message 错误信息
         */
        private void error(Element e, String message) {
            messager.printMessage(Diagnostic.Kind.ERROR, message, e);
        }
    }
    

其中@AutoService注解处理器由Google开发,并生成META-INF/services/javax.annotation.processing.Processor文件。这是因为我们需要这个文件来注册自己的Processor(前面提到过)

  • 为了让代码更加优雅,我们将类元信息分装到一个类中

    FactoryClassInfo

    package com.my.annotation.process.processor;
    
    import com.my.annotaion.process.annotation.Factory;
    
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.type.DeclaredType;
    import javax.lang.model.type.MirroredTypeException;
    
    /**
     * 工厂类信息
     *
     * @author Zijian Liao
     * @since 1.0.0
     */
    public class FactoryClassInfo {
        /**
         * 子类的类元信息
         */
        private final TypeElement typeElement;
        /**
         * 组件的id, 用于确认生成哪个组件
         */
        private final String id;
        /**
         * 注解上的值的类名信息
         */
        private String simpleName;
        /**
         * 注解上的值的全限定类名信息
         */
        private String qualifiedName;
    
        public FactoryClassInfo(Element element){
            this.typeElement = (TypeElement) element;
            // 获取注解信息
            Factory annotation = typeElement.getAnnotation(Factory.class);
            // apple
            this.id = annotation.id();
            try{
                // 获取注解的值 若该类未被编译,此处将抛出异常
                Class<?> type = annotation.type();
                // 获取注解上的全限定类名信息
                this.qualifiedName = type.getCanonicalName();
                // 获取注解上的类名信息
                this.simpleName = type.getSimpleName();
            }catch (MirroredTypeException e){
                // 所幸的是,该异常里也具备了我们需要的信息
                DeclaredType classTypeMirror = (DeclaredType) e.getTypeMirror();
                TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
                // com.my.annotation.process.example.Fruit
                this.qualifiedName = classTypeElement.getQualifiedName().toString();
                // Fruit
                this.simpleName = classTypeElement.getSimpleName().toString();
            }
        }
    
        public TypeElement getTypeElement() {
            return typeElement;
        }
    
        public String getId() {
            return id;
        }
    
        public String getSimpleName() {
            return simpleName;
        }
    
        public String getQualifiedName() {
            return qualifiedName;
        }
    
    }
    
  • 最后,通过这些类元信息生成我们的工厂类

    FactoryHandler

    package com.my.annotation.process.processor;
    
    import com.squareup.javapoet.JavaFile;
    import com.squareup.javapoet.MethodSpec;
    import com.squareup.javapoet.TypeName;
    import com.squareup.javapoet.TypeSpec;
    
    import javax.annotation.processing.Filer;
    import javax.lang.model.element.Modifier;
    import javax.lang.model.element.PackageElement;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.util.Elements;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 处理器,用于生成Java File
     *
     * @author Zijian Liao
     * @since 1.0.0
     */
    public class FactoryHandler {
    
        /**
         * 创建出的类名后缀
         */
        private static final String SUFFIX = "Factory";
        /**
         * 存放类信息
         */
        private static final Map<String, List<FactoryClassInfo>> FACTORY_CLASS_INFO_MAP = new ConcurrentHashMap<>(32);
    
        /**
         * 存放类信息
         * @param factoryClassInfo 类信息
         */
        public static void putIfAbsent(FactoryClassInfo factoryClassInfo){
            String qualifiedName = factoryClassInfo.getQualifiedName();
            if(!FACTORY_CLASS_INFO_MAP.containsKey(qualifiedName)){
                FACTORY_CLASS_INFO_MAP.put(qualifiedName, new ArrayList<>(4));
            }
            List<FactoryClassInfo> factoryClassInfos = FACTORY_CLASS_INFO_MAP.get(qualifiedName);
            factoryClassInfos.add(factoryClassInfo);
        }
    
        /**
         * 生成Java文件
         * @param elementUtils 元素工具
         * @param filer 文件工具
         * @throws IOException 异常
         */
        public static void generateJavaFile(Elements elementUtils, Filer filer) throws IOException {
            if(FACTORY_CLASS_INFO_MAP.isEmpty()){
                return;
            }
            for (Map.Entry<String, List<FactoryClassInfo>> entry : FACTORY_CLASS_INFO_MAP.entrySet()) {
                String qualifiedName = entry.getKey();
                List<FactoryClassInfo> factoryClassInfos = entry.getValue();
                // 1.得到接口(抽象类)的类名信息
                TypeElement superClassElement = elementUtils.getTypeElement(qualifiedName);
                // 2.创建出工厂方法
                MethodSpec.Builder method = MethodSpec.methodBuilder("create") // 方法名
                        .addParameter(String.class, "id") //方法参数 (类型|名称)
                        .addModifiers(Modifier.PUBLIC) // 修饰符
                        .addModifiers(Modifier.STATIC)
                        .returns(TypeName.get(superClassElement.asType())); // 返回类型
                // 3.遍历子类创建出代码信息
                factoryClassInfos.forEach(factoryClassInfo -> {
                    // $S 表示String类型占位符
                    // $L 表示名称占位符 给啥就是啥
                    method.beginControlFlow("if($S.equals(id))", factoryClassInfo.getId()) // 开启一个控制语句
                            .addStatement("return new $L()", factoryClassInfo.getTypeElement().getQualifiedName().toString()) // 添加一条语句
                            .endControlFlow(); // 结束控制语句
                });
                // 由于工厂方法需要返回值,若最后都不匹配则抛出异常
                // 添加一条抛异常语句
                method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");
                // 4. 创建工厂类
                // 接口(抽象类)的类名
                String superSimpleName = superClassElement.getSimpleName().toString();
                // 类名则为 superSimpleName + Factory
                String className = superSimpleName + SUFFIX;
                TypeSpec typeSpec = TypeSpec.classBuilder(className)
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(method.build())
                        .build();
                // 5.写入java文件
                // 获取到包名信息
                PackageElement packageElement = elementUtils.getPackageOf(superClassElement);
                String packageName = packageElement.getQualifiedName().toString();
                JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
            }
        }
    
        public static void clear(){
            FACTORY_CLASS_INFO_MAP.clear();
        }
    }
    

测试我们的案例

  1. 增加一个橘子的水果类

    @Factory(id = "orange", type = Fruit.class)
    public class Orange implements Fruit {
    
        @Override
        public Float getPrice() {
            return 3.5F;
        }
    }
    
  2. 进行编译

  3. 查看编译后的水果工厂

    public class FruitFactory {
        public FruitFactory() {
        }
    
        public static Fruit create(String id) {
            if ("orange".equals(id)) {
                return new Orange();
            } else if ("banana".equals(id)) {
                return new Banana();
            } else if ("apple".equals(id)) {
                return new Apple();
            } else {
                throw new IllegalArgumentException("Unknown id = " + id);
            }
        }
    }
    

    成功了~

如何调试Processor

因为我们的Processor是在编译时生效的,那么我们怎么进行debug调试呢?

  1. 打开Idea的配置

    edit_config.png

  2. 添加一个maven配置

    add_maven.png

  3. 编写配置

    maven_config.png

    主要编辑Working directoryCommand line两项配置

    Working directory: 填写使用Processor的模块

    Command line: maven命令,-X表示debug模式

  4. 开始调试

    断点到我们的FactoryProcessor类中,点击IDEA的debug启动按钮

    注意:调试前一定要先把processor模块打包

案例地址

https://github.com/lzj960515/annotation-processor-demo/tree/main