Java奇技淫巧-插件化注解處理API(Pluggable Annotation Processing API)
參考資料
- JDK6的新特性之六:插入式注解處理API(Pluggable Annotation Processing API)
- Java Annotation Processing and Creating a Builder
簡介
插件化注解處理(Pluggable Annotation Processing)APIJSR 269提供一套標准API來處理AnnotationsJSR 175,實際上JSR 269不僅僅用來處理Annotation,我覺得更強大的功能是它建立了Java 語言本身的一個模型,它把method、package、constructor、type、variable、enum、annotation等Java語言元素映射為Types和Elements,從而將Java語言的語義映射成為對象,我們可以在javax.lang.model包下面可以看到這些類。所以我們可以利用JSR 269提供的API來構建一個功能豐富的元編程(metaprogramming)環境。JSR 269用Annotation Processor在編譯期間而不是運行期間處理Annotation, Annotation Processor相當於編譯器的一個插件,所以稱為插入式注解處理.如果Annotation Processor處理Annotation時(執行process方法)產生了新的Java代碼,編譯器會再調用一次Annotation Processor,如果第二次處理還有新代碼產生,就會接着調用Annotation Processor,直到沒有新代碼產生為止。每執行一次process()方法被稱為一個"round",這樣整個Annotation processing過程可以看作是一個round的序列。JSR 269主要被設計成為針對Tools或者容器的API。這個特性雖然在JavaSE 6已經存在,但是很少人知道它的存在。下一篇介紹的Java奇技淫巧-lombok就是使用這個特性實現編譯期的代碼插入的。另外,如果沒有猜錯,像IDEA在編寫代碼時候的標記語法錯誤的紅色下划線也是通過這個特性實現的。KAPT(Annotation Processing for Kotlin),也就是Kotlin的編譯也是通過此特性的。
Pluggable Annotation Processing API的核心是Annotation Processor即注解處理器,一般需要繼承抽象類javax.annotation.processing.AbstractProcessor
。注意,與運行時注解RetentionPolicy.RUNTIME
不同,注解處理器只會處理編譯期注解,也就是RetentionPolicy.SOURCE
的注解類型,處理的階段位於Java代碼編譯期間。
使用步驟
插件化注解處理API的使用步驟大概如下:
- 1、自定義一個Annotation Processor,需要繼承
javax.annotation.processing.AbstractProcessor
,並覆寫process方法。 - 2、自定義一個注解,注解的元注解需要指定
@Retention(RetentionPolicy.SOURCE)
。 - 3、需要在聲明的自定義Annotation Processor中使用
javax.annotation.processing.SupportedAnnotationTypes
指定在第2步創建的注解類型的名稱(注意需要全類名,"包名.注解類型名稱",否則會不生效)。 - 4、需要在聲明的自定義Annotation Processor中使用
javax.annotation.processing.SupportedSourceVersion
指定編譯版本。 - 5、可選操作,可以通在聲明的自定義Annotation Processor中使用
javax.annotation.processing.SupportedOptions
指定編譯參數。
實戰例子
基礎
下面我們模仿一下測試框架Junit里面的@Test注解,在運行時通過Annotation Processor獲取到使用了自定義的@Test注解對應的方法的信息。因為如果想要動態修改一個類或者方法的代碼內容,需要使用到字節碼修改工具例如ASM等,這些操作過於深入,日后再談。先定義一個注解:
package club.throwable.processor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:18
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
}
定義一個注解處理器:
@SupportedAnnotationTypes(value = {"club.throwable.processor.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Log in AnnotationProcessor.process");
for (TypeElement typeElement : annotations) {
System.out.println(typeElement);
}
System.out.println(roundEnv);
return true;
}
}
編寫一個主類:
public class Main {
public static void main(String[] args) throws Exception{
System.out.println("success");
test();
}
@Test(value = "method is test")
public static void test()throws Exception{
}
}
接着需要指定Processor,如果使用IDEA的話,Compiler->Annotation Processors中的Enable annotation processing必須勾選。然后可以通過下面幾種方式指定指定Processor。
- 1、直接使用編譯參數指定,例如:javac -processor club.throwable.processor.AnnotationProcessor Main.java。
- 2、通過服務注冊指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.AnnotationProcessor。
- 3、通過Maven的編譯插件的配置指定如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>
club.throwable.processor.AnnotationProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
值得注意的是,以上三點生效的前提是club.throwable.processor.AnnotationProcessor已經被編譯過,否則編譯的時候就會報錯:
[ERROR] Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider club.throwable.processor.AnnotationProcessor not found
解決方法有兩種,第一種是提前使用命令或者IDEA右鍵club.throwable.processor.AnnotationProcessor對它進行編譯;第二種是把club.throwable.processor.AnnotationProcessor放到一個獨立的Jar包引入。我在這里使用第一種方式解決。
最后,使用Maven命令mvn compile進行編譯。輸出如下:
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[club.throwable.processor.Test,club.throwable.processor.Main, club.throwable.processor.AnnotationProcessor, processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=false]
Log in AnnotationProcessor.process
[errorRaised=false, rootElements=[], processingOver=true]
可見編譯期間AnnotationProcessor生效了。
進階
下面是一個例子直接修改類的代碼,為實體類的Setter方法對應的屬性生成一個Builder類,也就是原來的類如下:
public class Person {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
@Builder
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
@Builder
public void setName(String name) {
this.name = name;
}
}
生成的Builder類如下:
public class PersonBuilder {
private Person object = new Person();
public Person build() {
return object;
}
public PersonBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
public PersonBuilder setAge(int value) {
object.setAge(value);
return this;
}
}
自定義的注解如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
}
自定義的注解處理器如下:
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/5/27 11:21
*/
@SupportedAnnotationTypes(value = {"club.throwable.processor.builder.Builder"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement typeElement : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);
Map<Boolean, List<Element>> annotatedMethods
= annotatedElements.stream().collect(Collectors.partitioningBy(
element -> ((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@Builder must be applied to a setXxx method "
+ "with a single argument", element));
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();
try {
writeBuilderFile(className, setterMap);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.forEach((methodName, argumentType) -> {
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
}
主類如下:
public class Main {
public static void main(String[] args) throws Exception{
//PersonBuilder在編譯之后才會生成,這里需要編譯后才能這樣寫
Person person = new PersonBuilder().setAge(25).setName("doge").build();
}
}
先手動編譯BuilderProcessor,然后在META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.builder.BuilderProcessor
,最后執行Maven命令mvn compile進行編譯。
編譯后控制台輸出:
[errorRaised=false, rootElements=[club.throwable.processor.builder.PersonBuilder], processingOver=false]
編譯成功之后,target/classes包下面的club.throwable.processor.builder子包路徑中會新增了一個類PersonBuilder
:
package club.throwable.processor.builder;
public class PersonBuilder {
private Person object = new Person();
public PersonBuilder() {
}
public Person build() {
return this.object;
}
public PersonBuilder setName(String value) {
this.object.setName(value);
return this;
}
public PersonBuilder setAge(Integer value) {
this.object.setAge(value);
return this;
}
}
這個類就是編譯期新增的。在這個例子中,編譯期新增的類貌似沒有什么作用。但是,如果像lombok那樣對原來的實體類添加新的方法,那樣的話就比較有用了。因為些類或者方法是編譯期添加的,因此在代碼中直接使用會標紅。因此,lombok提供了IDEA或者eclipse的插件,插件的功能的實現估計也是用了插件式注解處理API。
小結
我在了解Pluggable Annotation Processing API的時候,通過搜索引擎搜索到的幾乎都是安卓開發通過插件式注解處理API編譯期動態添加代碼等等的內容,可見此功能的使用還是比較廣泛的。可能在文中的實戰例子並不能體現Pluggable Annotation Processing API功能的強大,因此有時間可以基於此功能編寫一些代碼生成插件,例如下一篇將要介紹的lombok。
(本文完)
技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):
娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力: