java註解(Annotation)總結
java註解(Annotation)總結
吐糟時間
想寫部落格也不是一天兩天了,苦於各種原因,從來都沒有寫過,雖然平常印象筆記裡記了一堆東西,但始終沒有一個很好的總結,歸根到底還是一個字,懶吧 ! 哎,真是怠惰啊~
作為不接觸後臺的android程式猿,前幾天用了SpringBoot和各種元件後,發現這怎麼這麼多註解啊,喵喵喵???(不過話說回來,SpringBoot用起來真是爽啊,比ssh好用多了)
所以就回頭看看註解的東西,順便寫上這篇部落格~
註解是什麼
@Override 這就是最常用的註解了~
@Override
public String toString() {
}
註解有什麼用
- 標記,可以為編譯器提供一些資訊,以便於檢測錯誤,抑制警告等(@Override、@SuppressWarnings)
- 編譯時動態處理,如動態生成程式碼(android中的Retrofit、DataBinding、Dagger2、ButterKnife、EventBus3)
- 執行時動態處理,如得到註解資訊(SpringBoot中的@AutoWired)
這裡的三個作用實際對應著後面自定義 Annotation 時說的 @Retention 三種值分別表示的 Annotation
註解的分類
- 標準註解:Override, Deprecated, SuppressWarnings
指 Java 自帶的幾個 Annotation,上面三個分別表示重寫函式,不鼓勵使用(有更好方式、使用有風險或已不在維護),忽略某項 Warning - 元註解:@Retention, @Target, @Inherited, @Documented
元註解是定義註解的註解,在自定義註解的時候會用到 - 自定義註解
根據自己的需要,使用上面的元註解自定義註解
怎麼自定義註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
-
取一個你喜歡的名字,並使用@interface 宣告
-
使用@Target 註解宣告它會被使用的地方(類、屬性、方法等)
取值(ElementType)有 1.CONSTRUCTOR:構造器 2.FIELD:成員變數 3.LOCAL_VARIABLE:區域性變數 4.METHOD:方法 5.PACKAGE:包 6.PARAMETER:引數 7.TYPE:類、介面(包括註解型別) 或enum宣告 注意,當註解未指定Target值時,則此註解可以用於任何元素之上,多個值使用{}包含並用逗號隔開,如下: @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
-
使用@Retention 註解宣告生命週期,即被描述的註解在什麼範圍內有效
1.SOURCE:在原始檔中有效(即原始檔保留,註解僅存在程式碼中,註解會被編譯器丟棄) 2.CLASS:在class檔案中有效(對應編譯時註解,註解會在class檔案中保留,但會被VM丟棄,在執行時期,這類註解是沒有的) 3.RUNTIME:在執行時有效(對應執行時註解,VM執行期間也會保留該註解,因此可以通過反射來獲得該註解) 注意,當註解未定義Retention值時,預設值是CLASS,如Java內建註解,@Override、@Deprecated、@SuppressWarnning等
除了這兩種元註解外,還有 @Inherited 和 @Documented。
- @Inherited 可以讓註解被繼承,但這並不是真的繼承,只是通過使用@Inherited,可以讓子類Class物件使用getAnnotations()獲取父類被@Inherited修飾的註解。
- @Documented 被修飾的註解會生成到javadoc中。
注意: 所有的Annotation會自動繼承java.lang.Annotation這一介面,並且不能再去繼承別的類或是介面.
新增屬性
自定義註解還會定義一些屬性,在解析註解的時候使用到
@Target(ElementType.FIELD)//指定該註解使用的範圍是成員變數
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {
String name();
String hero() default "";
int damage() default 0;
Constraints connstranints() default @Constraints;
}
註解的屬性可支援資料型別:
-
所有基本資料型別(int,float,boolean,byte,double,char,long,short)
-
String型別
-
Class型別
-
enum型別
-
Annotation型別
-
以上所有型別的陣列
注意: 1. 屬性只能用public或預設(default)這兩個訪問權修飾。例如,String value();這裡把方法設為defaul預設型別。 2. 如果一個註解的屬性只有一個,且屬性名是value(),那麼使用註解的時候不需要寫屬性名字,直接賦值。如下面的 @Team註解。
註解的使用
@Target(ElementType.TYPE)//指定該註解使用的範圍是類或者介面
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
String value() default "";
}
@Team("IG")
public class IG {
@Player(name = "the Shy", hero = "劍魔", damage = 28000, connstranints = @Constraints(isMvp = true))
private String top;
}
注意:
- 使用註解的時候,註解的每個屬性都要賦值,有預設值的屬性可以不賦。
- 自定義註解後,需要編寫對應的解析類。
- 解析執行時註解和編譯時註解的方法是不一樣的。
- 解析執行時(RUNTIME)註解,必須通過Java的反射技術來獲取 Annotation物件。
- 解析編譯時(CLASS)註解,需要自定義一個Processor繼承註解處理器 ( Annotation Processor )。
解析執行時(RUNTIME)註解
上面說了,我們寫的註解都是自動繼承Annotation介面的,要獲取類方法,欄位的註解資訊,必須通過反射。在java.lang.reflect 反射包下有一個AnnotatedElement介面,通過這個介面提供的方法可以利用反射獲取註解資訊,反射包中的Constructor類、Field類、Method類、Package類和Class類都實現了AnnotatedElement介面。
下面是AnnotatedElement中相關的API方法,以上5個類都實現以下的方法
返回值 | 方法名稱 | 說明 |
---|---|---|
<A extends Annotation> | getAnnotation(Class<A> annotationClass) | 該元素如果存在指定型別的註解,則返回這些註解,否則返回 null。 |
Annotation[] | getAnnotations() | 返回此元素上存在的所有註解,包括從父類繼承的 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定型別的註解存在於此元素上,則返回 true,否則返回 false。 |
Annotation[] | getDeclaredAnnotations() | 返回直接存在於此元素上的所有註解,注意,不包括父類的註解,呼叫者可以隨意修改返回的陣列;這不會對其他呼叫者返回的陣列產生任何影響,沒有則返回長度為0的陣列 |
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {
String name() default "";
Constraints connstranints() default @Constraints;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean isMvp() default false;
boolean isDirector() default false;
}
@Team("IG")
public class IG {
@Player(name = "the Shy")
private String top;
@Player(name = "ning", connstranints = @Constraints(isMvp = true))
private String jungle;
@Player(name = "rookie")
private String mid;
@Player(name = "baolan")
private String support;
@Player(name = "jackeylove", connstranints = @Constraints(isDirector = true))
private String botLane;
@Player(name = "duke")
private String substitute;
}
public class Parse {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cl = Class.forName("com.test.lol.IG");
String team = cl.getAnnotation(Team.class).value();
StringBuilder teamPlayer = new StringBuilder();
for (int i = 0; i < cl.getDeclaredFields().length; i++) {
Field[] fields = cl.getDeclaredFields();
String fieldName = fields[i].getName();
Annotation[] anns = fields[i].getDeclaredAnnotations();
if (anns[0] instanceof Player) {
Player player = (Player) anns[0];
String name = player.name();
Constraints connstranints = player.connstranints();
boolean director = connstranints.isDirector();
boolean mvp = connstranints.isMvp();
teamPlayer.append(fieldName).append(":").append(name);
if (mvp)
teamPlayer.append("(MVP)");
if (director)
teamPlayer.append("(指揮)");
if (i != cl.getDeclaredFields().length - 1)
teamPlayer.append(",").append("\n");
}
}
String content = "2018英雄聯盟S8總冠軍:" + team + "\n"
+ "冠軍成員:" + "\n"
+ teamPlayer;
System.out.println(content);
}
======================= 輸出 =======================
2018英雄聯盟S8總冠軍:IG
冠軍成員:
top:the Shy,
jungle:ning(MVP),
mid:rookie,
support:baolan,
botLane:jackeylove(指揮),
substitute:duke
解析編譯時(CLASS)註解
自定義一個繼承AbstractProcessor 的 Processor
public class Processor 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() { }
}
-
init()
在這裡可以初始化一些工具類。
ProcessingEnviroment提供很多有用的工具類Elements, Types和Filer。
Elements:一個用來處理Element的工具類;
Types:一個用來處理TypeMirror的工具類;
Filer:正如這個名字所示,使用Filer你可以建立檔案。 -
getSupportedAnnotationTypes()
這裡你必須指定,這個註解處理器是註冊給哪個註解的,並且是帶包名+類名的全稱。 -
getSupportedSourceVersion()
用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported()。 -
processor()
輸入引數annotations 請求處理的註解型別集合。
輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素,相當於“有關全域性原始碼的上下文環境”。
@return 如果返回 true,則這些註解已宣告並且不要求後續 Processor 處理它們;如果返回 false,則這些註解未宣告並且可能要求後續 Processor 處理它們
在Java 7中,你也可以使用註解來代替getSupportedAnnotationTypes()和getSupportedSourceVersion()
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.example.lib_annotation.BindView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyButterknifeProcessor extends AbstractProcessor {}
自定義Processor執行過程
上述的四個方法,前三個只會被呼叫一次,processor()方法可能會被呼叫多次,當沒有輸出檔案也沒有輸入檔案,處理結束。
用一個例子看一下 processor()呼叫的次數:
在看例子之前,先介紹幾個會出現的演員,比較猴急的可以直接跳過
Element介面
Element在邏輯上代表語言元素,比如包,類,方法等
其實Element是定義的一個介面,定義了外部呼叫暴露出的介面
方法 | 解釋 |
---|---|
TypeMirror asType() | 返回此元素定義的型別,實際的java型別(eg:String型別) |
ElementKind getKind() | 返回此元素的種類:包、類、介面、方法、欄位…,如下列舉值,方法返回一個列舉值TypeKind |
Set getModifiers() | 返回此元素的修飾符,如下列舉值 |
Name getSimpleName() | 返回此元素的簡單名稱,比如activity名,變數就是變數名 |
Element getEnclosingElement() | 返回封裝此元素的最裡層元素,即最近的外層元素,如果此元素的宣告在詞法上直接封裝在另一個元素的宣告中,則返回那個封裝元素; 如果此元素是頂層型別,則返回它的包如果此元素是一個包,則返回 null; 如果此元素是一個泛型引數,則返回 null. |
List getEnclosedElements() | 獲取所有的內層元素 |
< A extends Annotation> A getAnnotation(Class< A> var1) | 返回此元素針對指定型別的註解(如果存在這樣的註解),否則返回 null。註解可以是繼承的,也可以是直接存在於此元素上的 |
Element子類
Element在邏輯上代表語言元素,比如包,類,方法等,因此也會有五個直接子介面,它們分別代表一種特定型別的元素
子類 | 解釋 |
---|---|
PackageElement | 一個包程式元素 |
TypeElement | 一個類或介面程式元素 |
ExecutableElement | 某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註解型別元素 |
TypeParameterElement | 一般類、介面、方法或構造方法元素的泛型引數 |
VariableElement | 一個欄位、enum 常量、方法或構造方法引數、區域性變數或異常引數 |
TypeElement詳解
TypeElement定義的一個類或介面程式元素,相當於當前註解所在的class物件
方法 | 解釋 |
---|---|
NestingKind getNestingKind(); | 返回此型別元素的嵌套種類 |
Name getQualifiedName(); | 返回此型別元素的完全限定名稱。更準確地說,返回規範 名稱。對於沒有規範名稱的區域性類和匿名類,返回一個空名稱.譬如 Activity就是包名+類名 |
TypeMirror getSuperclass(); | 返回此型別元素的直接超類。如果此型別元素表示一個介面或者類 java.lang.Object,則返回一個種類為 NONE 的 NoType |
List getInterfaces(); | 返回直接由此類實現或直接由此介面擴充套件的介面型別 |
List getTypeParameters(); | 按照宣告順序返回此型別元素的形式型別引數 |
VariableElement詳解
VariableElement標示一個欄位、enum 常量、方法或構造方法引數、區域性變數或異常引數
方法 | 解釋 |
---|---|
getConstantValue | 變數初始化的值 |
getEnclosingElement | 獲取相關類資訊 |
package apt;
//註解
@Retention(RetentionPolicy.SOURCE) // 註解只在原始碼中保留
@Target(ElementType.TYPE) // 用於修飾類
public @interface Hello {
String name() default "";
}
//使用註解
package apt;
@Hello(name = "world")
public class Player {
}
//不使用註解
package apt;
public class Ignored {
}
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;
@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();
}
}
}
在專案根目錄新建一個out/production 目錄
程式碼邏輯:
1、獲得所有標有註解的類
2、取出註解中的資訊
3、生成新的 java 檔案
在idea的terminal裡編譯註解處理器
W:\workspace3\testAnnotation>javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java
接著執行註解處理器
W:\workspace3\testAnnotation>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:
可以看到processor()的執行的次數為3次,當沒有輸入和輸出後,不會再次執行。
過程 | 輸入 | 輸出 |
---|---|---|
第一次 | Hello.java , HelloProcessor.java ,Ignored.java, Player.java | Player_New.java |
第二次 | Player_New.java | - |
第三次 | - | - |
注意: 執行註解處理器的時候, 會開一個完整的 java 虛擬機器執行程式碼, 所以自定義的註解處理器是可以使用各種類庫的。
簡單實現一個ButterKnife
好了,亂78遭的說了一堆,不管你懂沒懂,DuangDuangDuang的看了一遍,然後自己動手來一遍就能大致理解了。
這個up的例子寫的很好,也比較簡單易懂,這裡我就不再那啥了,畢竟貼圖,貼程式碼還是很麻煩的…
從0到1:實現 Android 編譯時註解 連結的文章結尾也有github的程式碼可以參考。
這裡我還要提的是,關於除錯 abstractProcessor的一點體會:
- debug 自定義的remote – apt的時候,有可能會碰上連線5005埠失敗的錯誤,這個時候可以先執行assembleDebug,然後在debug apt,就可以連上5005埠了,接著執行assembleDebug,你會發現在abstractProcessor中打的斷點就能除錯了
- 用javaPoet成功生成檔案以後,想再次進入abstractProcessor除錯, 需要刪除生成的MainActivity$$ViewInjector
- 除錯的時候你會發現,processor()方法也一共被執行了3次,通過檢視這兩個引數的值,再結合上面也執行了3次的那個例子,想必聰明的你一定能懂。
-
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
=============================================================================