其前身是来自 java 6 的 APT (abstract processor tool),自 java 8 之后被插件式注解 API (pluggable annotation processing api) 取代。其本质上是提供用户在编译器访问注解元数据,处理和自定义编译输出,并能够创建新的源文件等等。
目的
- 用于生成模板代码,减少工作量,并保证了实际源码的简洁(如:lombok);
- 取代大量通过反射处理注解的方式,提高代码的运行性能(如:对象的序列化);
应用
- MapStruct:java bean 映射注解处理器;
代码实现
插件式注解是通过继承虚注解处理器(AbstractProcessor)来实现的,通过在其核心方法 (process)中匹配相应注解,可以实现在编译时向项目中输出 java 源文件。
01.注解
创建注解 “@Hello”。
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Hello { }
|
02.处理器
创建名为 “HelloProcessor”的处理器,并指定其支持的源码为 java 8,处理上述注解( “@Hello”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("com.example.annotation.Hello") public class HelloProcessor extends AbstractProcessor {
private static final String HELLO_TEMPLATE = "package %1$s;\n\npublic class %2$sHello {\n public static void sayHello() {\n System.out.println(\"Hello %3$s\");\n }\n}\n";
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { roundEnvironment.getElementsAnnotatedWith(Hello.class).forEach(element -> { TypeElement typeElem = (TypeElement) element; String typeName = typeElem.getQualifiedName().toString(); Filer filer = processingEnv.getFiler(); try (Writer sw = filer.createSourceFile(typeName + "Hello").openWriter()) { log("生成 " + typeName + "Hello 源码"); int lastIndex = typeName.lastIndexOf('.'); sw.write(String.format(HELLO_TEMPLATE, typeName.substring(0, lastIndex), typeName.substring(lastIndex + 1), typeName)); } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); } }); return true; }
private void log(String msg) { if (processingEnv.getOptions().containsKey("debug")) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg); } } }
|
03.作用
凡是被 “@Hello” 标记的类,都会生成以其类名 + Hello 形式的类,该类仅有一个类方法(sayHello)。在 Person 类上标记注解,如下:
1 2
| @Hello public class Person
|
04.效果
最终在 【模块名】\target\generated-sources\annotations 下生成如下类:
1 2 3 4 5
| public class PersonHello { public static void sayHello() { System.out.println("Hello com.example.bean.Person"); } }
|
配置
01.Maven 编译插件
通过在 maven-compiler-plugin 插件中指定注解处理器。在多模块中,可以仅仅在需要提供注解处理器支持的模块中添加配置,亦可在父模块中添加(为所有子模块支持)。但实际上,注解处理器的代码在编译完成(生成源文件)之后,就已经完成其使命,因此在其他模块中,使用 provided 引用,在编译时会排除该依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <dependency> <groupId>com.example</groupId> <artifactId>processor</artifactId> <scope>provided</scope> </dependency>
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding>
<annotationProcessors> <annotationProcessor> com.example.processor.HelloProcessor </annotationProcessor> </annotationProcessors> </configuration> </plugin>
|
问题
无法支持 lombok 插件。与其一起使用,会出现如下报错:
02.Java 服务注册
通过 spi 服务注册的方式。
当然需要添加(注解处理器的模块)依赖。
在注解处理器所在模块(或项目中)的 resources,创建 javax.annotation.processing.Processor 文件,目录结构如下:
1 2 3 4 5
| resources └── META-INF └── services ├── java.lang.Process └── javax.annotation.processing.Processor
|
在此文件中,填写自定义的注解处理器(如上面的):com.example.processor.HelloProcessor。
(实测)该方法兼容 lombok 插件。
额外的配置
直接注册会产生注解处理器无法找到的错误:
1 2
| Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project processor: Compilation failure 服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider com.example.processor.HelloProcessor not found时抛出异常错误
|
此时需要在该模块下配置 maven-compiler-plugin 插件:
1 2 3 4 5 6 7 8 9 10 11
| <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <!-- 不加这一句编译会报找不到processor的异常--> <compilerArgument>-proc:none</compilerArgument> </configuration> </plugin>
|
问题
在其他博客中提到,可以使用 google 的 AutoService 注解,为注解处理器自动生成服务注册文件。但是在实际测试时,发现其所生成的文件名为:java.lang.process,与上述不符,且并不能为注解提供注解处理器的支持。
注意
注解处理器在编译器生成代码,因此在使用(生成的代码)时需要先编译,使用 maven 插件 compiler(或 javac 命令)进行编译,最终可以得到此效果。
其他
下面是用于生成 builder 类的注解及处理器:
1 2 3 4
| @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Builder { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| @SupportedAnnotationTypes("com.example.Builder") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class BuilderProcessor extends AbstractProcessor {
private Filer filer;
@Override public synchronized void init(ProcessingEnvironment processingEnv) { filer = processingEnv.getFiler(); }
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { System.out.println("Processing " + annotations + roundEnv); Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Builder.class); elementsAnnotatedWith.forEach(element -> { if (element.getKind() != ElementKind.CLASS) { System.out.println("not class"); return; } TypeMirror elementTypeMirror = element.asType(); String builderClassName = element.getSimpleName() + "Builder"; TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(builderClassName) .addModifiers(Modifier.FINAL, Modifier.PUBLIC);
MethodSpec.Builder builderMethod = MethodSpec.methodBuilder("build") .returns(TypeName.get(element.asType())) .addModifiers(Modifier.PUBLIC) .addStatement("$T instance = new $T()", TypeName.get(elementTypeMirror), TypeName.get(elementTypeMirror));
element.getEnclosedElements().forEach(field -> { if (field.getKind() == ElementKind.FIELD) { boolean isStatic = field.getModifiers().contains(STATIC); if (isStatic) { System.out.println(field.getSimpleName() + " is static"); return; } String fieldName = field.getSimpleName().toString(); System.out.println(fieldName); String transformedName = upperFirstChar(fieldName);
MethodSpec methodSpec = MethodSpec.methodBuilder("build" + transformedName) .returns(TypeName.get(element.asType())) .returns(ClassName.get("com.example", builderClassName)) .addModifiers(Modifier.PUBLIC) .addParameter(TypeName.get(field.asType()), field.getSimpleName().toString()) .addStatement("this.$L = $L;return this", field.getSimpleName().toString() , field.getSimpleName().toString()) .build();
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(field.asType()), fieldName, Modifier.PRIVATE).build();
String fieldSetName = upperFirstChar(fieldName); builderMethod.addStatement("instance.set$L(this.$L)", fieldSetName, fieldName); typeBuilder.addField(fieldSpec); typeBuilder.addMethod(methodSpec); } });
builderMethod.addStatement("return instance");
typeBuilder.addMethod(builderMethod.build());
TypeSpec typeSpec = typeBuilder.build();
try { JavaFile.builder("com.example", typeSpec).build().writeTo(System.out); JavaFile.builder("com.example", typeSpec).build().writeTo(filer); } catch (IOException e) { e.printStackTrace(); } }); return true; }
private static String upperFirstChar(String name) { if (name.length() < 1) { return name; } String firstChar = name.substring(0, 1).toUpperCase(); if (name.length() > 1) { return firstChar + name.substring(1); } return firstChar; } }
|
问题
一些在测试中遇到但还没有解决的问题:
apt jdk 8 注解 代码生成