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

- Java注解

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

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

喜歡的話麻煩點下Star

文章首發于我的個人博客

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源于網絡,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術博客內容,引用其中了一些比較好的博客文章,如有侵權,請聯系作者。

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,并上手進行實戰,接著了解每個Java知識點背后的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

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

<!– more –>

Java注解簡介

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

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

比如:

Java 注解用于為 Java 代碼提供元數據。作為元數據,注解不直接影響你的代碼執行,但也有一些類型的注解實際上可以用于這一目的。Java 注解是從 Java5 開始添加到 Java 的。

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

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

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

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

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

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

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

注解如同標簽

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

在某些網友眼中,羅永浩就成了傻X的代名詞。

廣大網友給羅永浩貼了一個名為“傻x”的標簽,他們并不真正了解羅永浩,不知道他當教師、砸冰箱、辦博客的壯舉,但是因為“傻x”這樣的標簽存在,這有助于他們直接快速地對羅永浩這個人做出評價,然后基于此,羅永浩就可以成為茶余飯后的談資,這就是標簽的力量。

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

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

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

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

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

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

到這里,終于可以引出本文的主角注解了。

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

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

注解,標簽。注解,標簽。

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/201…

https://blog.csdn.net/qq_3793…

https://blog.51cto.com/424764…

https://www.jianshu.com/p/2f2…

https://blog.csdn.net/yuzongt…

微信公眾號

Java技術江湖

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

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

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

個人公眾號:黃小斜

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

程序員3T技術學習資源:一些程序員學習技術的資源大禮包,關注公眾號后,后臺回復關鍵字 “資料” 即可免費無套路獲取。

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

原文 

https://segmentfault.com/a/1190000020605465

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » 夯實Java基礎系列15:Java注解簡介和最佳實踐

贊 (0)
分享到:更多 ()

評論 0

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
手机彩票计划软件超稳