1. 程式人生 > >java註解(Annotation)總結

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 {
}
  1. 取一個你喜歡的名字,並使用@interface 宣告

  2. 使用@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}) 
    
  3. 使用@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;
}   

注意:

  1. 使用註解的時候,註解的每個屬性都要賦值,有預設值的屬性可以不賦。
  2. 自定義註解後,需要編寫對應的解析類。
  3. 解析執行時註解和編譯時註解的方法是不一樣的。
  4. 解析執行時(RUNTIME)註解,必須通過Java的反射技術來獲取 Annotation物件。
  5. 解析編譯時(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) 
    

=============================================================================

參考文獻