夯實Java基礎系列15:Java註解簡介和最佳實踐

  • 2019 年 10 月 7 日
  • 筆記

– Java註解

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯繫作者。
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著了解每個Java知識點背後的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

Java註解簡介

Annotation 中文譯過來就是註解、標釋的意思,在 Java 中註解是一個很重要的知識點,但經常還是有點讓新手不容易理解。

我個人認為,比較糟糕的技術文檔主要特徵之一就是:用專業名詞來介紹專業名詞。
比如:

Java 註解用於為 Java 程式碼提供元數據。作為元數據,註解不直接影響你的程式碼執行,但也有一些類型的註解實際上可以用於這一目的。Java 註解是從 Java5 開始添加到 Java 的。
這是大多數網站上對於 Java 註解,解釋確實正確,但是說實在話,我第一次學習的時候,頭腦一片空白。這什麼跟什麼啊?聽了像沒有聽一樣。因為概念太過於抽象,所以初學者實在是比較吃力才能夠理解,然後隨著自己開發過程中不斷地強化練習,才會慢慢對它形成正確的認識。

我在寫這篇文章的時候,我就在思考。如何讓自己或者讓讀者能夠比較直觀地認識註解這個概念?是要去官方文檔上翻譯說明嗎?我馬上否定了這個答案。

後來,我想到了一樣東西————墨水,墨水可以揮發、可以有不同的顏色,用來解釋註解正好。

不過,我繼續發散思維後,想到了一樣東西能夠更好地代替墨水,那就是印章。印章可以沾上不同的墨水或者印泥,可以訂製印章的文字或者圖案,如果願意它也可以被戳到你任何想戳的物體表面。

但是,我再繼續發散思維後,又想到一樣東西能夠更好地代替印章,那就是標籤。標籤是一張便利紙,標籤上的內容可以自由定義。常見的如貨架上的商品價格標籤、圖書館中的書本編碼標籤、實驗室中化學材料的名稱類別標籤等等。

並且,往抽象地說,標籤並不一定是一張紙,它可以是對人和事物的屬性評價。也就是說,標籤具備對於抽象事物的解釋。

所以,基於如此,我完成了自我的知識認知升級,我決定用標籤來解釋註解。

註解如同標籤

之前某新聞客戶端的評論有蓋樓的習慣,於是 「喬布斯重新定義了手機、羅永浩重新定義了傻X」 就經常極為工整地出現在了評論樓層中,並且廣大網友在相當長的一段時間內對於這種行為樂此不疲。這其實就是等同於貼標籤的行為。
在某些網友眼中,羅永浩就成了傻X的代名詞。

廣大網友給羅永浩貼了一個名為「傻x」的標籤,他們並不真正了解羅永浩,不知道他當教師、砸冰箱、辦部落格的壯舉,但是因為「傻x」這樣的標籤存在,這有助於他們直接快速地對羅永浩這個人做出評價,然後基於此,羅永浩就可以成為茶餘飯後的談資,這就是標籤的力量。

而在網路的另一邊,老羅靠他的人格魅力自然收穫一大批忠實的擁泵,他們對於老羅貼的又是另一種標籤。

老羅還是老羅,但是由於人們對於它貼上的標籤不同,所以造成對於他的看法大相徑庭,不喜歡他的人整天在網路上評論抨擊嘲諷,而崇拜欣賞他的人則會願意掙錢購買鎚子手機的發布會門票。

我無意於評價這兩種行為,我再引個例子。

《奇葩說》是近年網路上非常火熱的辯論節目,其中辯手陳銘被另外一個辯手馬薇薇攻擊說是————「站在宇宙中心呼喚愛」,然後貼上了一個大大的標籤————「雞湯男」,自此以後,觀眾再看到陳銘的時候,首先映入腦海中便是「雞湯男」三個大字,其實本身而言陳銘非常優秀,為人師表、作風正派、談吐舉止得體,但是在網路中,因為娛樂至上的環境所致,人們更願意以娛樂的心態來認知一切,於是「雞湯男」就如陳銘自己所說成了一個撕不了的標籤。

我們可以抽象概括一下,標籤是對事物行為的某些角度的評價與解釋。

到這裡,終於可以引出本文的主角註解了。

初學者可以這樣理解註解:想像程式碼具有生命,註解就是對於程式碼中某些鮮活個體的貼上去的一張標籤。簡化來講,註解如同一張標籤。

在未開始學習任何註解具體語法而言,你可以把註解看成一張標籤。這有助於你快速地理解它的大致作用。如果初學者在學習過程有大腦放空的時候,請不要慌張,對自己說:

註解,標籤。註解,標籤。

Java 註解概述

什麼是註解?

對於很多初次接觸的開發者來說應該都有這個疑問?Annontation是Java5開始引入的新特徵,中文名稱叫註解。它提供了一種安全的類似注釋的機制,用來將任何的資訊或元數據(metadata)與程式元素(類、方法、成員變數等)進行關聯。為程式的元素(類、方法、成員變數)加上更直觀更明了的說明,這些說明資訊是與程式的業務邏輯無關,並且供指定的工具或框架使用。Annontation像一種修飾符一樣,應用於包、類型、構造方法、方法、成員變數、參數及本地變數的聲明語句中。

  Java註解是附加在程式碼中的一些元資訊,用於一些工具在編譯、運行時進行解析和使用,起到說明、配置的功能。註解不會也不能影響程式碼的實際邏輯,僅僅起到輔助性的作用。包含在 java.lang.annotation 包中。

註解的用處

1、生成文檔。這是最常見的,也是java 最早提供的註解。常用的有@param @return 等
2、跟蹤程式碼依賴性,實現替代配置文件功能。比如Dagger 2依賴注入,未來java開發,將大量註解配置,具有很大用處;
3、在編譯時進行格式檢查。如@override 放在方法前,如果你這個方法並不是覆蓋了超類方法,則編譯時就能檢查出。

註解的原理

  註解本質是一個繼承了Annotation的特殊介面,其具體實現類是Java運行時生成的動態代理類。而我們通過反射獲取註解時,返回的是Java運行時生成的動態代理對象$Proxy1。通過代理對象調用自定義註解(介面)的方法,會最終調用AnnotationInvocationHandler的invoke方法。該方法會從memberValues這個Map中索引出對應的值。而memberValues的來源是Java常量池。

元註解

java.lang.annotation提供了四種元註解,專門註解其他的註解(在自定義註解的時候,需要使用到元註解):
@Documented –註解是否將包含在JavaDoc中
@Retention –什麼時候使用該註解
@Target –註解用於什麼地方
@Inherited – 是否允許子類繼承該註解

1.)@Retention– 定義該註解的生命周期

  ●   RetentionPolicy.SOURCE : 在編譯階段丟棄。這些註解在編譯結束之後就不再有任何意義,所以它們不會寫入位元組碼。@Override, @SuppressWarnings都屬於這類註解。      ●   RetentionPolicy.CLASS : 在類載入的時候丟棄。在位元組碼文件的處理中有用。註解默認使用這種方式      ●   RetentionPolicy.RUNTIME : 始終不會丟棄,運行期也保留該註解,因此可以使用反射機制讀取該註解的資訊。我們自定義的註解通常使用這種方式。

2.)Target – 表示該註解用於什麼地方。默認值為任何元素,表示該註解用於什麼地方。可用的ElementType參數包括

  ● ElementType.CONSTRUCTOR:用於描述構造器    ● ElementType.FIELD:成員變數、對象、屬性(包括enum實例)    ● ElementType.LOCAL_VARIABLE:用於描述局部變數    ● ElementType.METHOD:用於描述方法    ● ElementType.PACKAGE:用於描述包    ● ElementType.PARAMETER:用於描述參數    ● ElementType.TYPE:用於描述類、介面(包括註解類型) 或enum聲明

3.)@Documented–一個簡單的Annotations標記註解,表示是否將註解資訊添加在java文檔中。

4.)@Inherited – 定義該注釋和子類的關係
@Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。

JDK里的註解

JDK 內置註解
先來看幾個 Java 內置的註解,讓大家熱熱身。

@Override 演示

class Parent {      public void run() {      }  }    class Son extends Parent {      /**       * 這個註解是為了檢查此方法是否真的是重寫父類的方法       * 這時候就不用我們用肉眼去觀察到底是不是重寫了       */      @Override      public void run() {      }  }

@Deprecated 演示
class Parent {

/**   * 此註解代表過時了,但是如果可以調用到,當然也可以正常使用   * 但是,此方法有可能在以後的版本升級中會被慢慢的淘汰   * 可以放在類,變數,方法上面都起作用   */  @Deprecated  public void run() {  }  }    public class JDKAnnotationDemo {      public static void main(String[] args) {          Parent parent = new Parent();          parent.run(); // 在編譯器中此方法會顯示過時標誌      }  }

@SuppressWarnings 演示
class Parent {

// 因為定義的 name 沒有使用,那麼編譯器就會有警告,這時候使用此註解可以屏蔽掉警告  // 即任意不想看到的編譯時期的警告都可以用此註解屏蔽掉,但是不推薦,有警告的程式碼最好還是處理一下  @SuppressWarnings("all")  private String name;  }

@FunctionalInterface 演示
/**

  • 此註解是 Java8 提出的函數式介面,介面中只允許有一個抽象方法
  • 加上這個註解之後,類中多一個抽象方法或者少一個抽象方法都會報錯
    */
    @FunctionalInterface
    interface Func {
    void run();
    }

註解處理器實戰

註解處理器
註解處理器才是使用註解整個流程中最重要的一步了。所有在程式碼中出現的註解,它到底起了什麼作用,都是在註解處理器中定義好的。
概念:註解本身並不會對程式的編譯方式產生影響,而是註解處理器起的作用;註解處理器能夠通過在運行時使用反射獲取在程式程式碼中的使用的註解資訊,從而實現一些額外功能。前提是我們自定義的註解使用的是 RetentionPolicy.RUNTIME 修飾的。這也是我們在開發中使用頻率很高的一種方式。

我們先來了解下如何通過在運行時使用反射獲取在程式中的使用的註解資訊。如下類註解和方法註解。

類註解
Class aClass = ApiController.class;
Annotation[] annotations = aClass.getAnnotations();

for(Annotation annotation : annotations) {      if(annotation instanceof ApiAuthAnnotation) {          ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;          System.out.println("name: " + apiAuthAnnotation.name());          System.out.println("age: " + apiAuthAnnotation.age());      }  }  方法註解  Method method = ... //通過反射獲取方法對象  Annotation[] annotations = method.getDeclaredAnnotations();    for(Annotation annotation : annotations) {      if(annotation instanceof ApiAuthAnnotation) {          ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;          System.out.println("name: " + apiAuthAnnotation.name());          System.out.println("age: " + apiAuthAnnotation.age());      }  }   

此部分內容可參考: 通過反射獲取註解資訊

註解處理器實戰
接下來我通過在公司中的一個實戰改編來演示一下注解處理器的真實使用場景。
需求: 網站後台介面只能是年齡大於 18 歲的才能訪問,否則不能訪問
前置準備: 定義註解(這裡使用上文的完整註解),使用註解(這裡使用上文中使用註解的例子)
接下來要做的事情: 寫一個切面,攔截瀏覽器訪問帶註解的介面,取出註解資訊,判斷年齡來確定是否可以繼續訪問。

在 dispatcher-servlet.xml 文件中定義 aop 切面

<aop:config>      <!--定義切點,切的是我們自定義的註解-->      <aop:pointcut id="apiAuthAnnotation" expression="@annotation(cn.caijiajia.devops.aspect.ApiAuthAnnotation)"/>      <!--定義切面,切點是 apiAuthAnnotation,切面類即註解處理器是 apiAuthAspect,主處理邏輯在方法名為 auth 的方法中-->      <aop:aspect ref="apiAuthAspect">          <aop:around method="auth" pointcut-ref="apiAuthAnnotation"/>      </aop:aspect>  </aop:config>

切面類處理邏輯即註解處理器程式碼如

@Component("apiAuthAspect")  public class ApiAuthAspect {        public Object auth(ProceedingJoinPoint pjp) throws Throwable {          Method method = ((MethodSignature) pjp.getSignature()).getMethod();          ApiAuthAnnotation apiAuthAnnotation = method.getAnnotation(ApiAuthAnnotation.class);          Integer age = apiAuthAnnotation.age();          if (age > 18) {              return pjp.proceed();          } else {              throw new RuntimeException("你未滿18歲,禁止訪問");          }      }  }

不同類型的註解

類註解

你可以在運行期訪問類,方法或者變數的註解資訊,下是一個訪問類註解的例子:

 Class aClass = TheClass.class;  Annotation[] annotations = aClass.getAnnotations();    for(Annotation annotation : annotations){      if(annotation instanceof MyAnnotation){          MyAnnotation myAnnotation = (MyAnnotation) annotation;          System.out.println("name: " + myAnnotation.name());          System.out.println("value: " + myAnnotation.value());      }  }  

你還可以像下面這樣指定訪問一個類的註解:

Class aClass = TheClass.class;  Annotation annotation = aClass.getAnnotation(MyAnnotation.class);    if(annotation instanceof MyAnnotation){      MyAnnotation myAnnotation = (MyAnnotation) annotation;      System.out.println("name: " + myAnnotation.name());      System.out.println("value: " + myAnnotation.value());  }  

方法註解

下面是一個方法註解的例子:

public class TheClass {    @MyAnnotation(name="someName",  value = "Hello World")    public void doSomething(){}  }  

你可以像這樣訪問方法註解:

Method method = ... //獲取方法對象  Annotation[] annotations = method.getDeclaredAnnotations();    for(Annotation annotation : annotations){      if(annotation instanceof MyAnnotation){          MyAnnotation myAnnotation = (MyAnnotation) annotation;          System.out.println("name: " + myAnnotation.name());          System.out.println("value: " + myAnnotation.value());      }  }  

你可以像這樣訪問指定的方法註解:

Method method = ... // 獲取方法對象  Annotation annotation = method.getAnnotation(MyAnnotation.class);    if(annotation instanceof MyAnnotation){      MyAnnotation myAnnotation = (MyAnnotation) annotation;      System.out.println("name: " + myAnnotation.name());      System.out.println("value: " + myAnnotation.value());  }  

參數註解

方法參數也可以添加註解,就像下面這樣:

public class TheClass {    public static void doSomethingElse(          @MyAnnotation(name="aName", value="aValue") String parameter){    }  }  

你可以通過 Method對象來訪問方法參數註解:

Method method = ... //獲取方法對象  Annotation[][] parameterAnnotations = method.getParameterAnnotations();  Class[] parameterTypes = method.getParameterTypes();    int i=0;  for(Annotation[] annotations : parameterAnnotations){    Class parameterType = parameterTypes[i++];      for(Annotation annotation : annotations){      if(annotation instanceof MyAnnotation){          MyAnnotation myAnnotation = (MyAnnotation) annotation;          System.out.println("param: " + parameterType.getName());          System.out.println("name : " + myAnnotation.name());          System.out.println("value: " + myAnnotation.value());      }    }  }  

需要注意的是 Method.getParameterAnnotations()方法返回一個註解類型的二維數組,每一個方法的參數包含一個註解數組。

變數註解

下面是一個變數註解的例子:

public class TheClass {      @MyAnnotation(name="someName",  value = "Hello World")    public String myField = null;  }  

你可以像這樣來訪問變數的註解:

Field field = ... //獲取方法對象</pre>  <pre>Annotation[] annotations = field.getDeclaredAnnotations();    for(Annotation annotation : annotations){   if(annotation instanceof MyAnnotation){   MyAnnotation myAnnotation = (MyAnnotation) annotation;   System.out.println("name: " + myAnnotation.name());   System.out.println("value: " + myAnnotation.value());   }  }  

你可以像這樣訪問指定的變數註解:

Field field = ...//獲取方法對象</pre>  <pre>  Annotation annotation = field.getAnnotation(MyAnnotation.class);    if(annotation instanceof MyAnnotation){   MyAnnotation myAnnotation = (MyAnnotation) annotation;   System.out.println("name: " + myAnnotation.name());   System.out.println("value: " + myAnnotation.value());  }  

Java註解相關面試題

什麼是註解?他們的典型用例是什麼?

註解是綁定到程式源程式碼元素的元數據,對運行程式碼的操作沒有影響。

他們的典型用例是:

  • 編譯器的資訊 – 使用註解,編譯器可以檢測錯誤或抑制警告
  • 編譯時和部署時處理 – 軟體工具可以處理註解並生成程式碼,配置文件等。
  • 運行時處理 – 可以在運行時檢查註解以自定義程式的行為

描述標準庫中一些有用的註解。

java.lang和java.lang.annotation包中有幾個註解,更常見的包括但不限於此:

  • @Override -標記方法是否覆蓋超類中聲明的元素。如果它無法正確覆蓋該方法,編譯器將發出錯誤
  • @Deprecated – 表示該元素已棄用且不應使用。如果程式使用標有此批註的方法,類或欄位,編譯器將發出警告
  • @SuppressWarnings – 告訴編譯器禁止特定警告。在與泛型出現之前編寫的遺留程式碼介面時最常用的
  • @FunctionalInterface – 在Java 8中引入,表明類型聲明是一個功能介面,可以使用Lambda Expression提供其實現

可以從註解方法聲明返回哪些對象類型?

返回類型必須是基本類型,String,Class,Enum或數組類型之一。否則,編譯器將拋出錯誤。

這是一個成功遵循此原則的示例程式碼:

enum Complexity {      LOW, HIGH  }    public @interface ComplexAnnotation {      Class<? extends Object> value();        int[] types();        Complexity complexity();  }    

下一個示例將無法編譯,因為Object不是有效的返回類型:

public @interface FailingAnnotation {      Object complexity();  }    

哪些程式元素可以註解?

註解可以應用於整個源程式碼的多個位置。它們可以應用於類,構造函數和欄位的聲明:

@SimpleAnnotation  public class Apply {      @SimpleAnnotation      private String aField;        @SimpleAnnotation      public Apply() {          // ...      }  }    

方法及其參數:

@SimpleAnnotation  public void aMethod(@SimpleAnnotation String param) {      // ...  }    

局部變數,包括循環和資源變數:

@SimpleAnnotation  int i = 10;    for (@SimpleAnnotation int j = 0; j < i; j++) {      // ...  }    try (@SimpleAnnotation FileWriter writer = getWriter()) {      // ...  } catch (Exception ex) {      // ...  }    

其他註解類型:

@SimpleAnnotation  public @interface ComplexAnnotation {      // ...  }    

甚至包,通過package-info.java文件:

@PackageAnnotation  package com.baeldung.interview.annotations;    

從Java 8開始,它們也可以應用於類型的使用。為此,註解必須指定值為ElementType.USE的@Target註解:

@Target(ElementType.TYPE_USE)  public @interface SimpleAnnotation {      // ...  }    

現在,註解可以應用於類實例創建:

new @SimpleAnnotation Apply();    

類型轉換:

aString = (@SimpleAnnotation String) something;    

介面中:

public class SimpleList<T>    implements @SimpleAnnotation List<@SimpleAnnotation T> {      // ...  }    

拋出異常上:

void aMethod() throws @SimpleAnnotation Exception {      // ...  }    

有沒有辦法限制可以應用註解的元素?

有,@ Target註解可用於此目的。如果我們嘗試在不適用的上下文中使用註解,編譯器將發出錯誤。

以下是僅將@SimpleAnnotation批註的用法限制為欄位聲明的示例:

@Target(ElementType.FIELD)  public @interface SimpleAnnotation {      // ...  }    

如果我們想讓它適用於更多的上下文,我們可以傳遞多個常量:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })    

我們甚至可以製作一個註解,因此它不能用於註解任何東西。當聲明的類型僅用作複雜註解中的成員類型時,這可能會派上用場:

@Target({})  public @interface NoTargetAnnotation {      // ...  }    

什麼是元註解?

元註解適用於其他註解的註解。

所有未使用@Target標記或使用它標記但包含ANNOTATION_TYPE常量的註解也是元註解:

@Target(ElementType.ANNOTATION_TYPE)  public @interface SimpleAnnotation {      // ...  }    

下面的程式碼會編譯嗎?

@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.FIELD })  public @interface TestAnnotation {      int[] value() default {};  }    

不能。如果在@Target註解中多次出現相同的枚舉常量,那麼這是一個編譯時錯誤。

刪除重複常量將使程式碼成功編譯:

@Target({ ElementType.FIELD, ElementType.TYPE})    

參考文章

https://blog.fundodoo.com/2018/04/19/130.html
https://blog.csdn.net/qq_37939251/article/details/83215703
https://blog.51cto.com/4247649/2109129
https://www.jianshu.com/p/2f2460e6f8e7
https://blog.csdn.net/yuzongtao/article/details/83306182

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專註 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中間件、集群、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後台回復關鍵字 「Java」 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專註於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中間件、微服務,同時也懂點投資理財,偶爾講點演算法和電腦理論基礎,堅持學習和寫作,相信終身學習的力量!

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後台回復關鍵字 「資料」 即可免費無套路獲取。