上一篇 主要介紹了什麼是 註解 (Annotation) 以及如何讀取 運行時註解 中的數據, 同時用註解實現了簡單的 ORM
功能. 此次介紹另外一部分: 如何讀取 編譯時註解 ( RetentionPolicy.SOURCE )java
編譯時註解能夠用來動態生成代碼. 使用 SOURCE
類型註解的代碼會在編譯時被解析, 生成新的 java
文件, 而後和原來的 java
文件一塊兒編譯成字節碼. 因爲不使用反射功能, 編譯時註解不會拖累性能, 於是被許多框架使用, 好比 Butter Knife
, Dragger2
等.segmentfault
仍是從簡單的例子開始看. 這裏要作的是生成一個 java 類, 其擁有一個打印註解信息的方法.
先定義一個註解app
package apt; ...... @Retention(RetentionPolicy.SOURCE) // 註解只在源碼中保留 @Target(ElementType.TYPE) // 用於修飾類 public @interface Hello { String name() default ""; }
使用註解的類框架
package apt; @Hello(name = "world") public class Player { }
不使用註解的類, 用於對比jvm
package apt; public class Ignored { }
上一篇說過, 註解沒有行爲, 只有數據, 須要對應的處理器才能發揮做用. javac
提供瞭解析編譯時註解的註解處理器 ( Annotation Processor ). 對於自定義的註解, 須要手動實現它的註解處理器.下面來看一個簡單的註解處理器實現.ide
package apt; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import java.io.IOException; import java.io.Writer; import java.util.Set; /** * Created by away on 2017/6/12. */ @SupportedSourceVersion(SourceVersion.RELEASE_8) // 源碼級別, 這裏的環境是 jdk 1.8 @SupportedAnnotationTypes("apt.Hello") // 處理的註解類型, 這裏須要處理的是 apt 包下的 Hello 註解(這裏也能夠不用註解, 改爲重寫父類中對應的兩個方法) public class HelloProcessor extends AbstractProcessor { // 計數器, 用於計算 process() 方法運行了幾回 private int count = 1; // 用於寫文件 private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); filer = processingEnv.getFiler(); } // 處理編譯時註解的方法 @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { System.out.println("start process, count = " + count++); // 得到全部類 Set<? extends Element> rootElements = roundEnv.getRootElements(); System.out.println("all class:"); for (Element rootElement : rootElements) { System.out.println(" " + rootElement.getSimpleName()); } // 得到有註解的元素, 這裏 Hello 只能修飾類, 因此只有類 Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class); System.out.println("annotated class:"); for (Element element : elementsAnnotatedWith) { String className = element.getSimpleName().toString(); System.out.println(" " + className); String output = element.getAnnotation(Hello.class).name(); // 產生的動態類的名字 String newClassName = className + "_New"; // 寫 java 文件 createFile(newClassName, output); } return true; } private void createFile(String className, String output) { StringBuilder cls = new StringBuilder(); cls.append("package apt;\n\npublic class ") .append(className) .append(" {\n public static void main(String[] args) {\n") .append(" System.out.println(\"") .append(output) .append("\");\n }\n}"); try { JavaFileObject sourceFile = filer.createSourceFile("apt." + className); Writer writer = sourceFile.openWriter(); writer.write(cls.toString()); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } }
代碼的邏輯很簡單:函數
得到全部標有註解的類工具
取出註解中的信息性能
生成新的 java 文件ui
這裏只須要知道, 自定義註解處理器要繼承 AbstractProcessor
類, 並重寫 process
方法.
此時項目目錄以下, 這裏 out
目錄爲手動建立
out
production
apt
src
apt
在命令行中進入項目根目錄, 即 src
文件夾的上一層.
首先編譯註解處理器: javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java
接着執行註解處理器: javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java
獲得以下輸出
start process, count = 1 all class: Hello HelloProcessor Ignored Player annotated class: Player start process, count = 2 all class: Player_New annotated class: start process, count = 3 all class: annotated class:
這時 src/apt
目錄下會出現新的 Player_New.java
文件, 內容以下
package apt; public class Player_New { public static void main(String[] args) { System.out.println("world"); } }
執行 java -cp out\production\elevator apt.Player_New
獲得輸出 world
.
到這裏, 編譯時註解便處理成功了. 咱們定義了一個極其簡單的註解處理器, 讀取了註解信息, 並生成了新的 java
類來打印該信息.
這裏可能會報一個錯誤
編譯器 (1.8.0_131) 中出現異常錯誤。若是在 Bug Database (http://bugs.java.com) 中沒有找到該錯誤, 請經過 Java Bug 報告頁 (http://bugreport.java.com) 創建該 Java 編譯器 Bug。請在報告中附上您的程序和如下診斷信息。謝謝。 java.lang.IllegalStateException: endPosTable already set ... ...
這時把產生的 Player_New.java
文件刪去從新執行註解處理器就行了
這裏稍微解釋一下 javac
命令, IDE
用多了, 寫的時候都忘得差很少了 (:зゝ∠)javac
用於啓動 java 編譯器, 格式爲 javac <options> <source files>
, 其中 <options>
的格式爲 -xx xxxx
, 都是配對出現的, 用於指定一些信息.
這裏 <options>
的位置並無講究, 只要在 javac
後面就好了, 在兩個 xxx.java
之間出現也是能夠的, 好比: javac -d out\production\ src\apt\HelloProcessor.java -encoding UTF-8 src\apt\Hello.java
正常執行.
一些 <option>
-cp <路徑>
和 -classpath <路徑>
同樣, 用於指定查找用戶類文件和註釋處理程序的位置
-d <目錄>
指定放置生成的類文件的位置
-s <目錄>
指定放置生成的源文件的位置
-processorpath <路徑>
指定查找註釋處理程序的位置
不寫的話會使用 -cp
的位置
-processor <class1>[,<class2>,<class3>...]
要運行的註釋處理程序的名稱; 繞過默認的搜索進程
到這裏應該會有一些問題, 好比
AbstractProcessor
, Elememt
分別是什麼
process
爲何執行了 3 次
運行註解處理器的時候會啓動 jvm
嗎
這裏先說一下第三個問題. javac
運行註解處理器的時候, 會開一個完整的 java 虛擬機執行代碼, 因此自定義的註解處理器是可使用各類類庫的.
接下來說一下一些基本概念, 用來回答上面兩個問題.
這是處理器的API,全部的處理器都是基於 AbstractProcessor
, 它實現了接口 Processor
接口
void init(ProcessingEnvironment processingEnv)
;
會被註解處理工具調用, ProcessingEnvironment
提供了一些實用的工具類 Elements
, Types
和 Filer
.
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
;
至關於 main
函數, 是註解處理器的入口. 輸入參數 RoundEnviroment
能夠查詢出包含特定註解的被註解元素
SourceVersion getSupportedSourceVersion()
;
用來指定使用的 java
版本
Set<String> getSupportedAnnotationTypes()
;
指定這個註解處理器是註冊給哪一個註解的, 這裏須要用註解的全稱, 好比上面的 apt.Hello
最後兩個也能夠用註解的形式實現, 例子中的代碼就是這麼作的
程序的元素, 例如包, 類或者方法. 每一個 Element
表明一個靜態的, 語言級別的構件. 能夠參考下面的代碼理解
package com.example; // PackageElement public class Foo { // TypeElement private int a; // VariableElement private Foo other; // VariableElement public Foo () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA // TypeElement ) {} }
因而可知 roundEnv.getElementsAnnotatedWith(xxx.class)
獲得的並不必定是類, 也多是方法, 成員變量等, 只是例子中用的註解只能用於修飾類.
javadoc
中對此的描述以下
Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing.
概況來講, 就是 process() 方法會被調用屢次, 直到沒有新的類產生爲止.
由於新生成的文件中也可能包含 @Hello 註解,它們會繼續被 HelloProcessor
處理.
Round | input | output |
---|---|---|
1 | Hello.java HelloProcessor.java Ignored.java Player.java |
Player_New.java |
2 | Player_New.java | - |
3 | - | - |
下一篇會開始分析 Butter Knife
的源碼.