插件化註解處理(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的編譯也是經過此特性的。css
Pluggable Annotation Processing API的核心是Annotation Processor即註解處理器,通常須要繼承抽象類javax.annotation.processing.AbstractProcessor
。注意,與運行時註解RetentionPolicy.RUNTIME
不一樣,註解處理器只會處理編譯期註解,也就是RetentionPolicy.SOURCE
的註解類型,處理的階段位於Java代碼編譯期間。java
插件化註解處理API的使用步驟大概以下:apache
javax.annotation.processing.AbstractProcessor
,並覆寫process方法。@Retention(RetentionPolicy.SOURCE)
。javax.annotation.processing.SupportedAnnotationTypes
指定在第2步建立的註解類型的名稱(注意須要全類名,"包名.註解類型名稱",不然會不生效)。javax.annotation.processing.SupportedSourceVersion
指定編譯版本。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 { }
定義一個註解處理器:json
@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; } }
編寫一個主類:app
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。框架
<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已經被編譯過,不然編譯的時候就會報錯:eclipse
[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
最後,使用Maven命令mvn compile進行編譯。輸出以下:ide
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。
(本文完)