本篇内容意图介绍什么是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类Elements
,Types
以及Filer
。稍后我们将使用它们。process(Set 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
(前面提到过)