本文目錄:
- 一. 注解概念和介紹
- 二. 注解的語法
- 三. 基本注解——五大元注解
- 四. AnnotatedElement 接口
- 五. AbstractProcessor (注解處理器)
- 六. javapoet(第三方庫,生成.java源文件)
- 七. APT技術
- 八. demo實戰(zhàn)
一.注解概念和介紹
- 注解(annotation)也叫元數(shù)據(jù)。JDK1.5版本后引入的特性,它可以聲明在包,類,字段,方法,局部變量,方法參數(shù)等的前面,用來對這些元素進行說明和注釋。
二.注解的語法
- 注解通過@interface關鍵字來定義。
三.基本注解——五大元注解
@Retention, @Documented, @Target, @Inherited, @Repeatable
元注解是指可以注解到注解上的注解,即使用了@Target(ElementType.ANNOTATION_TYPE)。
| 五大元注解 | 介紹 |
|---|---|
| @Retention | 指注解保留的時長 |
| @Documented | 跟文檔相關,其作用是能夠將注解中的元素包含到javadoc中去 |
| @Target | 指定注解可作用的目標 |
| @Inherited | 父類被(Inherited修飾的注解)注解修飾,它的子類如果沒有任何注解修飾,就會繼承父類的這個注解(看下面例子更好理解) |
| @Repeatable | 表明標記的注解可以多次應用于相同的聲明或類型 |
注意:@Repeatable JDK1.8以上才有,Android系統(tǒng)7.0(對應SDK24)以上才有
-
3.1@Retention:中文保留的意思,指注解保留的時長。
三種取值如下:
| @Retention三種取值 | 介紹 | 用途 |
|---|---|---|
| RetentionPolicy.SOURCE | 注解只在源碼階段保留,批注將被編譯器丟棄。 | 主要用于提示開發(fā)者 |
| RetentionPolicy.CLASS | 注解保留到編譯期(注解將由編譯器記錄在類文件中),但在虛擬機VM運行時不需要保留他們。這是默認的。 | 主要用于自動生成代碼 |
| RetentionPolicy.RUNTIME | 注解會保留到程序運行時(注解由編譯器記錄在類文件中,并在運行時由虛擬機VM保留),因此可以通過反射方式讀取它們。 | 主要用于自動注入 |
@Retention 源碼:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
@RetentionPolicy 源碼
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
-
3.2@ Documented: 中文文獻的意思,可看出跟文檔相關,其作用是能夠將注解中的元素包含到javadoc中去。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
-
3.3@Target: 指定注解可作用的目標
10種取值如下:
| @ Target十種取值 | 作用 |
|---|---|
| 1. ElementType.TYPE | 可作用在類、接口、枚舉上 |
| 2. ElementType.FIELD | 可作用在屬性上 |
| 3. ElementType.METHOD | 可作用在方法上 |
| 4. ElementType.PARAMETER | 可作用在方法參數(shù)上 |
| 5. ElementType.CONSTRUCTOR | 可作用在構造方法上 |
| 6. ElementType.LOCAL_VARIABLE | 可作用在局部變量上,例如方法中定義的變量 |
| 7.ElementType.ANNOTATION_TYPE | 可以作用在注解上 |
| 8.ElementType.PACKAGE | 可作用在包上 |
| 9. ElementType.TYPE_PARAMETER | JDK1.8才有 |
| 10. ElementType. TYPE_USE | JDK1.8才有 |
@Target源碼
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
上面看@Target源碼可知其取值為一個數(shù)組,舉兩個例子,一個取值和兩個取值以上:@Target(ElementType.ANNOTATION_TYPE),@Target({ElementType.ANNOTATION_TYPE,ElementType.PACKAGE})
ElementType 源碼
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE
}
-
3.4@Inherited: 繼承的意思,但并不是注解本身可被繼承,而是指一個父類SuperClass被該類注解修飾,那么它的子類SubClass如果沒有任何注解修飾,就會繼承父類的這個注解。
例子:
@Inherited
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface Money{}
@Money
public class Father{}
public class Son extends Father {}
解釋:注解Money被@Inherited修飾,F(xiàn)ather被Money修飾,Son 繼承Father(Son 上又無其他注解),那么Son 就會擁有Money這個注解。
-
3.5@ Repeatable,這個詞是可重復的意思,它是java1.8引入的,算一個新特性。
什么樣的注解可以多次應用來呢,通常是注解可以取多個值
例子:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @Interface Teachers{
Teacher[] value();
}
@Repeatable(Teachers.class)
public @Interface Teacher{
String role() default ""
}
@Teacher(role="Chinese")
@Teacher(role="Math")
@Teacher(role="English")
public class Me {}
public static void main(String[] args) {
if(Me .class.isAnnotationPresent(Teachers.class)) {
Teachers teaches=Man.class.getAnnotation(Teachers.class);
for(Teacher teacher : teachers.value()){
System.out.println(teacher.role());
}
}
例子解析:@Teacher被@Repeatable修飾,所以Teacher可以多次作用在同一個對象Me上,而Repeatable接收一個參數(shù),這個參數(shù)是個容器注解,用來存放多個@Person。
四. AnnotatedElement 接口
- 該接口表示此虛擬機VM中當前正在運行的程序的帶注釋元素。
- 所有實現(xiàn)了這個接口的“元素”都是可以“被注解的元素”。
- 使用這個接口中聲明的方法可以讀取(通過Java的反射機制)“被注解元素”的注解。 即該接口允許以反射方式讀取注解。
- 該接口中方法返回的所有注釋都是不可變的和可序列化的。 調用方可以修改此接口的方法返回的數(shù)組,而不會影響返回給其他調用方的數(shù)組。
1.AnnotatedElement 方法:
| AnnotatedElement 方法 | 介紹 |
|---|---|
| default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定類型的注解出現(xiàn)在當前元素上,則返回true,否則將返回false。這種方法主要是為了方便地訪問一些已知的注解。 |
| <T extends Annotation> T getAnnotation(Class<T> annotationClass) | 如果在當前元素上存在參數(shù)所指定類型(annotationClass)的注解,則返回對應的注解,否則將返回null。 |
| Annotation[] getAnnotations() | 返回在這個元素上的所有注解。如果該元素沒有注釋,則返回值是長度為0的數(shù)組。 |
| default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) | 返回與該元素相關聯(lián)的注解。如果沒有與此元素相關聯(lián)的注解,則返回值是長度為0的數(shù)組。這個方法與getAnnotation(Class)的區(qū)別在于,該方法檢測其參數(shù)是否為可重復的注解類型,如果是,則嘗試通過“l(fā)ooking through”容器注解來查找該類型的一個或多個注解。 |
| default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) | 如果參數(shù)中所指定類型的注解是直接存在于當前元素上的,則返回對應的注解,否則將返回null。這個方法忽略了繼承的注解。(如果沒有直接在此元素上顯示注釋,則返回null。) |
| default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) | 如果參數(shù)中所指定類型的注解是直接存在或間接存在于當前元素上的,則返回對應的注解。這種方法忽略了繼承的注釋。如果沒有直接或間接地存在于此元素上的指定注解,則返回值是長度為0的數(shù)組。這個方法和getDeclaredAnnotation(Class)的區(qū)別在于,這個方法檢測它的參數(shù)是否為可重復的注釋類型(JLS 9.6),如果是,則嘗試通過“l(fā)ooking through”容器注解來查找該類型的一個或多個注解。 |
| Annotation[] getDeclaredAnnotations() | 返回直接出現(xiàn)在這個元素上的注解。這種方法忽略了繼承的注解。如果在此元素上沒有直接存在的注解,則返回值是長度為0的數(shù)組。 |
2.AnnotatedElement 的實現(xiàn)類:
| AnnotatedElement的實現(xiàn)類 | 介紹 | 備注 |
|---|---|---|
| Class | 類,天天打交道的 | \ |
| Package | 包 | \ |
| Parameter | 參數(shù),主要指方法或函數(shù)的參數(shù),其實是這些參數(shù)的類型 | JDK1.8才有 |
| AccessibleObject | 是Field、Method 和 Constructor 對象的基類。所以可訪問對象,如:方法、構造器、屬性等 | \ |
| Field | 屬性,類中屬性的類型 | 繼承于AccessibleObject |
| Executable | 可執(zhí)行的,如構造器和方法 | 繼承于AccessibleObject;JDK1.8才有 |
| Constructor | 構造器 | 繼承于Executable |
| Method | 方法 | 繼承于Executable |
五. AbstractProcessor (注解處理器)
-
5.1AbstractProcessor介紹
注解處理器一共有七個方法,我們一般重寫四個方法
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
5.1.1 init(ProcessingEnvironment env):
每一個注解處理器類都必須有一個空的構造函數(shù)。然而,這里有一個特殊的init()方法,它會被注解處理工具調用,并輸入ProcessingEnviroment參數(shù)。ProcessingEnviroment提供很多有用的工具類Elements, Types和Filer。
5.1.2 process(Set<? extends TypeElement> annotations, RoundEnvironment env):
這相當于每個處理器的主函數(shù)main()。你在這里寫你的掃描、評估和處理注解的代碼,以及生成Java文件。輸入?yún)?shù)RoundEnviroment,可以讓你查詢出包含特定注解的被注解元素。
5.1.3 getSupportedAnnotationTypes():
這里我們必須指定,這個注解處理器是注冊給哪個注解的。它的返回值是一個字符串的集合,包含本處理器想要處理的注解類型的合法全稱。換句話說,我們在這里定義我們的注解處理器注冊到哪些注解上。
5.1.4 getSupportedSourceVersion():
用來指定使用的Java版本。通常這里返回SourceVersion.latestSupported()。
-
5.2 AbstractProcessor使用(Android需新建Java Library)
5.2.1 寫一個注解
5.2.2 寫一個類繼承于AbstractProcessor,然后重寫上述四個方法
5.2.3 添加SPI配置文件(手動或者使用google提供的auto-service庫)
手動配置:在該Java Library下的main目錄下創(chuàng)建resources/META-INF/services目錄,然后在 META-INF/services 目錄文件夾下創(chuàng)建javax.annotation.processing.Processor 文件;在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;包名+自定義處理器文件名)
自定義AbstractProcessor是APT技術的頂梁柱,這里介紹了基本的知識,更多的請看相關的APT知識。
六. javapoet(square公司出品的第三方庫,生成java文件)
-
6.1 javapoet介紹:
JavaPoet是用于生成.java源文件的Java API。由square公司出品的第三方庫。
在執(zhí)行諸如注解處理或與元數(shù)據(jù)文件(例如,數(shù)據(jù)庫模式,協(xié)議格式)交互之類的操作時,源文件的生成非常有用。 通過生成代碼,無需編寫樣板文件,同時還保留了元數(shù)據(jù)的唯一真實來源。
-
6.2 javapoet使用(Android需新建Java Library)
javapoet在AndroidStudio中不能直接使用,需要新建一個Java Library。Java Library中依賴compile 'com.squareup:javapoet:1.12.1'
下面例子MyJavapoet是寫在Java Library的,運行MyJavapoet,就可在目錄下看到生成的名為HelloWorld的Java文件了
public class MyJavapoet {
public static void main(String[] args) {
generateJava();
}
private static void generateJava() {
//1. 生成一個字段
FieldSpec fieldSpec = FieldSpec.builder(String.class, "var", Modifier.PUBLIC).build();
//2. 生成一個方法
MethodSpec main = MethodSpec.methodBuilder("main")//設置方法名稱
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)//設置方法名稱
.returns(void.class)//添加返回值
.addParameter(String[].class, "args")//添加參數(shù)
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加代碼語句 (結束語句的分號不需要, 注意與CodeBlock的區(qū)別)
.build();
//3. 生成類型(enum/class/annotation/interface)
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
//4. 構建Java源文件
JavaFile javaFile = JavaFile.builder("annotation.hsj.pri", helloWorld)
.build();
//5.生成到當前module的源文件目錄下
try {
javaFile.writeTo(System.out);
//輸出到和用例程序相同的源碼目錄下,targetDirectory后面會自動添加以上述包名為文件名的目錄
String targetDirectory = "annotationlib/src/main/java/";
File dir = new File(targetDirectory);
if (!dir.exists()) dir.mkdirs();
javaFile.writeTo(dir); //JavaFile.write(), 參數(shù)為源碼生成目錄(源碼的classpath目錄)
} catch (IOException e) {
e.printStackTrace();
}
}
}
javapoet的詳細信息請見javapoet的GitHub:https://github.com/square/javapoet
七. APT技術
APT技術主要是通過編譯期解析注解,并且生成java代碼的一種技術,熟練掌握了該技術,對于ButterKnife、Dagger、EventBus里面的注解就非常容易理解了。
其三大基礎為:
- 一個注解(@Retention(RetentionPolicy.CLASS));
- 一個自定義的AbstractProcessor(注解解析器);
- 一個生成Java代碼的Javapoet技術。
這里限于篇幅,但又在自定義注解里非常重要的一個綜合知識面,建議大家先看完 八實戰(zhàn)demo 最后再來看APT技術。這里我就不寫了,給大家推薦一篇專門講APT的文章:http://www.itdecent.cn/p/7af58e8e3e18
八. 實戰(zhàn)demo
8.1 按作用域上分(幾種常見的)
8.1.1 作用在屬性上(@Target(ElementType.FIELD)),如成員變量
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MyFieldAnnotation { @IdRes int id() default -1; } --------------------------------------------------------------------------------------------- public class FieldActivity extends AppCompatActivity { @FieldAnnotation(id = R.id.tv) TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_field); field(); } private void field() { Field[] fields = this.getClass().getDeclaredFields(); //通過該方法設置所有的字段都可訪問,否則即使是反射,也不能訪問private修飾的字段 Field.setAccessible(fields, true); for (Field field : fields) { if (field.isAnnotationPresent(FieldAnnotation.class)) { MyFieldAnnotation fieldAnnotation = field.getAnnotation(MyFieldAnnotation.class); int id = fieldAnnotation.id(); if (id == -1) continue; View view = this.findViewById(id); Class aClass = field.getType(); try { field.set(this, aClass.cast(view)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } textView.setText("提莫隊長"); } }例子分析:
- 作用在Field上即@Target(ElementType.FIELD)
- 這里是實現(xiàn)一個跟ButterKnife類似的功能,即自動注入功能,無需手動調用findViewById。
第一步:定義一個注解。我們這里定義了一個運行時注解,其取值id我們作為findVIewById的id值
第二步:定義注解解析工具。例子里是通過field方法代替解析工具的。通過Field[]獲取Activity的field成員變量。然后 AccessibleObject.setAccessible()設置所有字段都可以訪問,這里AccessibleObject也可以改為Field,AccessibleObject是Field的父類。然后循環(huán)遍歷Field[]得到每個Field,判斷指定的注解MyInjectViewAnnotation是否在當前Field元素上,如果在,則取出當前Field元素上的MyInjectViewAnnotation注解,然后得到注解上的id值,然后就可以進行findViewById操作了。
第三步:使用注解。例子里申明一個Field成員變量TextView,將注解作用于 TextView 上。
8.1.2 作用在方法上(@Target(ElementType.METHOD))
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyMethodAnnotation { String name() default ""; String sex() default ""; int age() default 0; } ----------------------------------------------------------------------------------------------------------------- public class MethodActivity extends AppCompatActivity { private TextView textView; private StringBuffer stringBuffer = new StringBuffer(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_method); textView = findViewById(R.id.tv_method); method(); } @MethodAnnotation(name = "德瑪", sex = "男", age = 18) public void method() { Method[] methods = this.getClass().getMethods(); AccessibleObject.setAccessible(methods, true); for (Method method : methods) { if (method.isAnnotationPresent(MethodAnnotation.class)) { MyMethodAnnotation methodAnnotation = method.getAnnotation(MyMethodAnnotation.class); stringBuffer.append("姓名:" + methodAnnotation.name() + methodAnnotation.age() + " 性別:" + " 年齡:" + methodAnnotation.sex()); Log.d("MethodActivity-->", "姓名:" + methodAnnotation.name() + " 年齡:" + methodAnnotation.age() + " 性別:" + methodAnnotation.sex()); } } textView.setText(stringBuffer); } }疑問?這里注解所作用的方法必須為public,即使設置AccessibleObject.setAccessible(methods, true);還是無法訪問private修飾的方法,這里我不太明白,有小伙伴知道的可以教教我哦。
在8.1.3 作用在方法參數(shù)上(@Target(ElementType.PARAMETER))
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @StringDef({ParameterAnnotationStringA.PRIMARY_SCHOOL, ParameterAnnotationStringA.JUNIOR_SCHOOL, >ParameterAnnotationStringA.SENIOR_SCHOOL}) public @interface ParameterAnnotationStringA { String PRIMARY_SCHOOL = "小學"; String JUNIOR_SCHOOL = "初中"; String SENIOR_SCHOOL = "高中"; } --------------------------------------------------------------------------------------------------------------- @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @StringDef({"小學","初中","高中"}) public @interface ParameterAnnotationStringB { } --------------------------------------------------------------------------------------------------------------- @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.SOURCE) @IntRange(from = 5, to = 20) public @interface ParameterAnnotationIntRange { } --------------------------------------------------------------------------------------------------------------- public class ParameterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_parameter); parameterStringA("小學");//報錯:即使PRIMARY_SCHOOL也是小學,但這里必須使用PRIMARY_SCHOOL和下條一樣 parameterStringA(ParameterAnnotationStringA.PRIMARY_SCHOOL); parameterStringB("小學"); parameterStringB("小學生");//報錯:因為ParameterAnnotationStringB沒有小學生 parameterStringB(ParameterAnnotationStringA.PRIMARY_SCHOOL); parameterIntRange(15); parameterIntRange(30);//報錯:因為ParameterAnnotationIntRange的IntRange是從5到20 } private void parameterStringA(@ParameterAnnotationStringA String schoolName) { Log.d("ParameterActivity-->", "parameterStringA():" + "schoolName " + schoolName); } private void parameterStringB(@ParameterAnnotationStringB String schoolName) { Log.d("ParameterActivity-->", "parameterStringB():" + "schoolName " + schoolName); } private void parameterIntRange(@ParameterAnnotationIntRange int age) { Log.d("ParameterActivity-->", "parameterIntRange():" + "age " + age); } }例子解析:
- 打印結果依次為:小學;小學;小學;小學生;小學;15;30 ;也就是這里寫代碼代碼的時候會提示警告信息,但編譯和運行不會出現(xiàn)任何錯誤。
- 作用在方法參數(shù)上即@Target(ElementType.PARAMETER)。案例中有寫@StringDef,@IntRange,這是對方法參數(shù)進行約束。
- 如果注解中有寫了變量并賦值,拿ParameterAnnotationStringA注解舉例,那么就參數(shù)必須寫StringDef里的ParameterAnnotationStringA.PRIMARY_SCHOOL等,而不能寫ParameterAnnotationStringA.PRIMARY_SCHOOL的值“小學”。
- 但是反過來,卻可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL,如例子里的parameterStringA(ParameterAnnotationStringA.PRIMARY_SCHOOL);
- 對第三點和第四點進行總結,即可以使用“小學”的一定可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL,但可以使用ParameterAnnotationStringA.PRIMARY_SCHOOL的不一定可以使用“小學”,即變量名稱大于變量的值。這個跑一下demo更直觀更容易理解。
- 作用在方法參數(shù)上,這個可以常用于對參數(shù)進行約束,比如開發(fā)中會常用到生產(chǎn)環(huán)境地址,測試環(huán)境地址以及其他地址,為了開發(fā)者更統(tǒng)一規(guī)范避免地址錯誤,就可以對這些地址進行注解來進行約束。
8.2 按注解保留時長分(源碼期,編譯期,運行期)
8.2.1 源碼期@Retention(RetentionPolicy.SOURCE)
demo和8.1.3一樣
8.2.2 編譯期@Retention(RetentionPolicy.CLASS)
編譯期很重要的一個是APT技術,具體請詳見上面 七.APT技術
8.2.3 運行期@Retention(RetentionPolicy.RUNTIME)
demo和8.1.1一樣
運行時注解主要通過反射進行解析,代碼運行過程中,通過反射我們可以知道哪些屬性、方法使用了該注解,并且可以獲取注解中的參數(shù),做一些我們想做的事情
參考文章
http://www.itdecent.cn/p/7454a933dcaf
https://www.race604.com/annotation-processing/
https://tool.oschina.net/apidocs/apidoc?api=jdk-zh