標簽: java annotation
上一篇博客討論了關於注解的基礎知識,以及運行時(Runtime)通過反射機制來處理注解,但既然是Runtime,那么總會有效率上的損耗,如果我們能夠在編譯期(Compile time)就能處理注解,那自然更好,而很多框架其實都是在編譯期處理注解,比如大名鼎鼎的bufferknife,這個過程並不復雜,只需要我們自定義注解處理器(Annotation Processor)就可以了。(Annotation Processor下文有些地方直接簡稱處理器,不要理解成cpu那個處理器)。
在Compile time注解就能起作用,這才是真正體現注解價值的地方,不過自定義Compile time的注解處理器也沒什么神秘的。注解處理器是編譯器(javac)的一個工具,它用來在編譯時掃描和處理注解。我們可以自定義一個注解,並編寫和注冊對應的處理器。在寫法上它其實就是我們自定義一個類,該類 extends javax.annotation.processing.AbstractProcessor
, AbstractProcessor
是一個abstract的基類。它以我們寫好的java源碼或者編譯好的代碼做為輸入,然后就可以通過處理器代碼來實現我們所希望的輸出了,比如輸出一份新的java代碼,此時注解管理器就以遞歸的形式進行多趟處理,直到把代碼(包括你手寫的代碼,以及注解處理器生成的代碼)中所有的注解都被處理完畢。
我們已經寫好的代碼固然是不能修改了,但是這並不影響通過注解處理器來生成新的代碼。還以bufferknife為例,寫findViewById實在太無聊了,所以我們就使用了bufferknife的注解方式省略這個過程。
public class TestMainActivity extends BaseActivity {
@BindView(R.id.mainSwitchGoneBtn)
Button goneBtn;
.......
}
但是實際上呢,是bufferknife通過其注解處理器器來生成了相應的代碼,它生成的文件是這樣的:
public class TestMainActivity_ViewBinding<T extends TestMainActivity> implements Unbinder {
protected T target;
@UiThread
public TestMainActivity_ViewBinding(T target, View source) {
this.target = target;
target.goneBtn = Utils.findRequiredViewAsType(source, R.id.mainSwitchGoneBtn, "field 'goneBtn'", Button.class);
}
}
所以bufferknife就是通過這種方式來麻煩了自己,方便了我們。
注解處理器是運行在它自己的虛擬機jvm當中的,也就是說,javac啟動了一個完整的java虛擬機來運行注解處理器,這點非常重要,因為這說明你編寫的注解處理器代碼,和你寫的其他java代碼是沒什么區別的。不管是你使用的API,還是設計時的思想,編碼習慣,甚至你想使用的其他第三方類庫,框架等,都是一樣的。
認識處理器
前面就說過,我們自定義的過程,就是extends AbstractProcessor
,先來看看這個抽象處理器類。
package com.yaoxiaowen.testprocessor;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
public class TestProcessor extends AbstractProcessor{
/**
* 每個注解處理器都必須有一個空的構造方法(父類已經實現了),這個init方法會被構造器調用,
* 並傳入一個 ProcessingEnvironment 參數,該參數提供了很多工具類,
* 比如 Elements, Filer, Messager, Types
* @author www.yaoxiaowen.com
*/
@Override
public synchronized void init(ProcessingEnvironment env) {
// TODO Auto-generated method stub
super.init(env);
}
/**
* 這個方法在父類中是abstract的,所以子類必須實現。
* 這個方法就是相當於 注解處理器的 入口 main()方法,我們說在編譯時,對注解進行的處理,
* 比如對注解的掃描,評估和處理,以及后續的我們要做的其他操作。(比如生成其他java代碼文件),
* 都是在這里發生的。
*
* 參數RoundEnvironment可以讓我們查出包含特定注解的被注解元素。
* @author www.yaoxiaowen.com
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TODO Auto-generated method stub
return false;
}
/**
* 這個方法雖然在父類當中不是 abstract的,但是我們也必須實現。
* 因為該方法的作用是指定我們要處理哪些注解的,
* 比如你想處理注解MyAnnotation,可是該處理器怎么知道你想處理MyAnnotation,而不是OtherAnnotation呢。
* 所以你要在這里指明,你需要處理的注解的全稱。
*
* 返回值是一個字符串的集合,包含着本處理器想要處理的注解類型的合法全稱。
* @author www.yaoxiaowen.com
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
// TODO Auto-generated method stub
return super.getSupportedAnnotationTypes();
}
/**
* 本方法用來指明你支持的java版本,
* 不過一般使用 SourceVersion.latestSupported() 就可以了。
*/
@Override
public SourceVersion getSupportedSourceVersion() {
// TODO Auto-generated method stub
return super.getSupportedSourceVersion();
}
}
這幾個主要方法,在代碼片段的注釋已經寫的很清楚了。
我們使用TestProcessor.java這個處理器的目的就是分析處理java代碼,而代碼是遵循一定的結構規范的,代碼文件被讀取后,各個字符串會被分解成token進行處理,而javac的編譯器首先將java代碼分解為抽象語法樹(AST)。而這個結構,在處理器內部,其實是被表示成這樣的:
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
){}
}
處理器在處理代碼時,其實就是對抽象語法樹進行遍歷操作,分解出每一個的類,方法,屬性等,然后再將這些元素的內容進行處理。
而實際上,這些PackageElement,VariableElement等元素模型都是在一個專門的類包中javax.lang.model
。javax.lang.model
用來為 Java 編程語言建立模型的包的類和層次結構。 此包及其子包的成員適用於語言建模、語言處理任務和 API(包括但並不僅限於注釋處理框架)。
繼承 AbstractProcessor實現自定義處理器
我們現在通過繼承AbstractProcessor來實現一個小demo。
流程和功能如下:我們定義了一個注解SQLString
,然后實現注解處理器 DbProcessor
。該注解處理器功能很簡單,就是生成一個文件,將實現了SQLString
的屬性元素的相關內容寫入到這個文件(比如所在類的名字,屬性名,所設置的注解的值)。
我們先自定義一個注解
package com.yaoxiaowen.comp.proce.db;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.lang.model.element.Element;
/**
* 該注解的 使用范圍是 屬性(域) 上
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface SQLString {
int value() default 0;
String name() default "";
}
然后再來定義注解處理器
package com.yaoxiaowen.comp.proce.db;
import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
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.element.VariableElement;
import javax.tools.Diagnostic;
/**
* @author www.yaoxiaowen.com
*/
public class DbProcessor extends AbstractProcessor{
private Messager messager;
private int count = 0;
private int forCount = 0;
private StringBuilder generateStr = new StringBuilder();
@Override
public synchronized void init(ProcessingEnvironment env) {
// TODO Auto-generated method stub
super.init(env);
messager = env.getMessager();
String logStr = "enter init(), 進入 init()";
printMsg(logStr);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
// TODO Auto-generated method stub
String logStr = "enter process(), 進入process";
//用來 存儲 (className, 輸出語句) 這種結構
Map<String, String> maps = new HashMap<>();
//得到 使用了 SQLString注解的元素
Set<? extends Element> eleStrSet = env.getElementsAnnotatedWith(SQLString.class);
count++;
for (Element eleStr : eleStrSet){
//因為我們知道SQLString元素的使用范圍是在域上,所以這里我們進行了強制類型轉換
VariableElement eleStrVari = (VariableElement)eleStr;
forCount++;
// 得到該元素的封裝類型,也就是 包裹它的父類型
TypeElement enclosingEle = (TypeElement)eleStrVari.getEnclosingElement();
String className = enclosingEle.getQualifiedName().toString();
generateStr.append("className = " + className);
generateStr.append("\t fieldName = " + eleStrVari.getSimpleName().toString());
//得到在元素上,使用了注解的相關情況
SQLString sqlString = eleStrVari.getAnnotation(SQLString.class);
generateStr.append("\t annotationName = " + sqlString.name());
generateStr.append("\t annotationValue = " + sqlString.value());
generateStr.append("\t forCount=" + forCount);
generateStr.append("\n");
}
generateStr.append("test File yaowen");
generateStr.append("\t count=" + count);
generateFile(generateStr.toString());
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// TODO Auto-generated method stub
Set<String> strings = new TreeSet<>();
strings.add("com.yaoxiaowen.comp.proce.db.SQLString");
return strings;
}
@Override
public SourceVersion getSupportedSourceVersion() {
// TODO Auto-generated method stub
return SourceVersion.latestSupported();
}
//將內容輸出到文件
private void generateFile(String str){
try {
//這是mac環境下的路徑
File file = new File( "/Users/yw/code/dbCustomProcFile");
FileWriter fw = new FileWriter(file);
fw.append(str);
fw.flush();
fw.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
printMsg(e.toString());
}
}
private void printMsg(String msg){
messager.printMessage(Diagnostic.Kind.ERROR, msg);
}
}
結合着注釋,我們知道,這個處理器的功能就是將一些信息輸出到 /Users/yw/code/dbCustomProcFile
這個文件中。
我在代碼中使用了 javax.annotation.processing.Messager
來輸出一些log信息,因為這個過程是在編譯時輸出的,所以System.out.println()
就沒用了,這個輸出信息是給使用了該處理器的第三方程序員看的,不是給該處理器的作者看的。
比如demo當中的log代碼,在最后成功的打包成jar,在另一個項目中使用時(Android Studio環境下,Eclipse我愣是沒找到哪個窗口輸出編譯信息),編譯時期輸出信息如下:
.......
:app:compileSc_360DebugJavaWithJavac
注: enter init, 進入init
注: enter process, 進入process
注: Creating DefaultRealmModule
注: enter process, 進入process
注: enter process, 進入process
注: 某些輸入文件使用了未經檢查或不安全的操作。
注: 有關詳細信息, 請使用 -Xlint:unchecked 重新編譯。
:app:generateJsonModelSc_360Debug UP-TO-DATE
:app:externalNativeBuildSc_360Debug
......
添加注冊信息
處理器的代碼雖然寫完了,但是這還沒完呢,剩下還有非常重要的步驟,那就是添加注冊信息。因為注解處理器是屬於javac的一個平台級的功能,所以我們的使用方式是將代碼打包成jar的形式,這樣就可以在其他第三方項目當中使用了。而在打包jar之前,則要在項目中添加注冊信息。
先看一下這個目錄的結構:
(eclipse)注冊的步驟如下:
1,選中工程,鼠標右鍵,New -> Source Folder,創建 resources文件夾,然后依次通過New -> Folder 創建兩個文件夾 : META-INF,services
2,在services文件夾下,New -> File, 創建一個文件,javax.annotation.processing.Processor。在文件中,輸入自定義的處理器的全名:
com.yaoxiaowen.comp.proce.db.DbProcessor
輸入之后記得鍵入回車一下。
其實這個手動注冊的過程,也是可以不用我們麻煩的。google開發了一個注解工具AutoService,我們可以直接在處理器代碼上使用。類似下面這樣:
/**
* @author www.yaoxiaowen.com
*/
@AutoService(Processor.class)
public class DbProcessor extends AbstractProcessor{
.......
這個注解工具自動生成META-INF/services/javax.annotation.processing.Processor文件,文件里還包含了處理器的全名:
com.yaoxiaowen.comp.proce.db.DbProcessor
看到這里,你也許會比較震驚,我們在注解處理器的代碼中也可以使用注解。
那么此時請再看看本文開頭的那句話
注解處理器是運行在它自己的虛擬機jvm當中的,也就是說,javac啟動了一個完整的java虛擬機來運行注解處理器.....
做完這些,我們的項目就已經完成了,下面要做的就是打包成jar了。
打包和使用jar(eclipse為例)
1: 打包jar
前面說過,編譯期的注解處理器是平台級的功能,是要注冊給javac的, 所以需要打包成jar, 我們的項目打包的名字是
AnnoCustomProce.jar
關於具體的打包過程,參見gif圖(這是從鴻洋大神的博客上學習到的)。
2: 建立新項目
eclipse下的java項目,新建立一個lib文件夾,然后將AnnoCustomProce.jar手動拷貝到這個目錄下。
3: 引用包,並啟用annotation processor。
具體操作見gif圖。
要注意一下兩個gif圖中的各種選項和配置。
使用注解
現在呢,已經大功告成了。下面就是使用了。
新建一個類,使用我們的注解。
public class AnnoCreateFile {
@SQLString(name="yw")
String filed;
@SQLString(name="yaow", value=1)
String name;
/**
* @author www.yaoxiaowen.com
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("hello world");
}
}
當編譯完這個項目時(eclipse默認就是Build Automatically),我們就能在 /Users/yw/code/
目錄下找到 dbCustomProcFile 文件了,打開這個文件,內容如下:
className = com.yaoxiaowen.testjar.AnnoCreateFile fieldName = filed annotationName = yw annotationValue = 0 forCount=1
className = com.yaoxiaowen.testjar.AnnoCreateFile fieldName = name annotationName = yaow annotationValue = 1 forCount=2
test File yaowen count=1test File yaowen count=2
大功告成,我們成功的實現了一個能夠在編譯時期起作用的自定義注解處理器。
當然,這個demo沒有什么實際作用,它的功能也非常簡單,但是了解了這個過程,我們在實際需求當中,就可以通過類似的方式來實現想要的功能了。
很多時候,我們都是希望注解處理器是來輸出java代碼的,既然是代碼,那么總有格式的,這就不像簡單的文件那樣進行輸出了,輸出java代碼,一般使用一個類庫:javapoet。而我們如果在注解處理器中引入了第三方的類庫,那么將其打包成jar的過程,就和我們演示的有所不同。這點需要自行google。另外,如果想對java源碼進行游刃有余的處理,那么需要對於javax.lang.model
包下的各種Elements,工具之類的比較熟悉。具體的api,需要參考oracle的文檔.
總結和反思
我在學習自定義注解處理器的過程中,參考了網上的很多博客,敲代碼進行實測,但是實際上依舊碰到了很多的問題,也折騰了好久,在這里我將自己所碰到的問題,都羅列出來。(雖然有些是可笑的低級錯誤),希望對大家有所幫助。
- resource/META-INF/services com.yaoxiaowen.annotation.createjson.BeanProcessor 文件中,寫的是 處理器的類
DbProcessor
,而不是你的注解類:SQLString
。 - resource 這個文件夾 是 New Source Folder,后面兩個文件,才是 New Folder, 關於兩者之間的區別: 后者就是一個普通的文件夾而已,但是前者,是屬於項目的一部分,eclipse會編譯這個文件夾。所以有些文章說建立的 是res(而不是resource)文件夾,這個其實無所謂,只要它是 Souce Folder.
- 在導入包的時候,注意不要導入錯誤的包(比如
import java.awt.Window.Type
)。因為往往同一個類名,它在不同的包里都有實現。像常用的List
,經常導錯包。java.awt.List
和java.util.List
。List
比較常用,我們很容易找到錯誤,但是Type
之類的不常用,所以不是那么容易發現。 - 在處理器的生成文件的代碼中,有這樣一句:
那如果我將代碼改成這樣:File file = new File( "/Users/yw/code/dbCustomProcFile");
那么請問此時,這個dbCustomProcFile文件到底在那里呢?File file = new File("./dbCustomProcFile");
在我的電腦上,該文件路徑分別如下
D:\software\eclipse\dbCustomProcFile
(window)或/Users/yw//Downloads/Eclipse.app/Contents/MacOS/dbCustomProcFile
(mac)。
一般我們在工程中使用./
我們都認為是工程當前的目錄,但是在注解處理器中,這個路徑實際上是eclipse 安裝路徑下 javac的路徑。的確應該如此,因為注解處理器畢竟是javac的一個工具。 - 某一次測試時,當我向新工程導入生成的jar包時,剛剛導入eclipse就報錯。
后來終於發現了問題所在。
在編碼過程中,函數的行參原來是processingEnv
,后來我嫌長,就修改為了env
,但是下面一句super.init()
卻忘記修改了。所以導致找不到這個參數,而問題是在window和mac下,這句代碼IDE都沒有任何提示,我又在下面故意寫了一句錯代碼String = 2
,此時IDE提示錯誤。 那為什么第一句話IDE就是沒報錯呢。測試了普通java文件的同種類型的錯誤,IDE就會報錯,所以這真的讓我不解。
以上就是本篇文章的全部內容,關於注解處理器深處的很多東西其實也沒搞懂。也歡迎大家留言指點交流。
github: https://github.com/yaowen369
歡迎對於本人的博客內容批評指點,如果問題,可評論或郵件(yaowen369@gmail.com)聯系
<p >
歡迎轉載,轉載請注明出處.謝謝
</p>
<script type="text/javascript">
function Curgo()
{
window.open(window.location.href);
}
</script>