本篇内容意图介绍什么是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<? 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(); } }
测试我们的案例
-
增加一个橘子的水果类
@Factory(id = "orange", type = Fruit.class) public class Orange implements Fruit { @Override public Float getPrice() { return 3.5F; } }
-
进行编译
-
查看编译后的水果工厂
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调试呢?
-
打开Idea的配置
-
添加一个maven配置
-
编写配置
主要编辑
Working directory
和Command line
两项配置Working directory
: 填写使用Processor
的模块Command line
: maven命令,-X
表示debug模式 -
开始调试
断点到我们的
FactoryProcessor
类中,点击IDEA的debug启动按钮注意:调试前一定要先把
processor
模块打包
案例地址
https://github.com/lzj960515/annotation-processor-demo/tree/main