Java 核心知識體系:註解機制詳解

1 Java 註解基礎

註解是 JDK1.5 版本開始引入的一個特性,用於對程序代碼的說明,可以對包、類、接口、字段、方法參數、局部變量等進行註解。
它主要的作用有以下四方面:

註解的常見分類有三種:

接下來我們通過這三種分類來逐一理解註解。

1.1 Java 內置註解

我們先從 Java 內置註解開始說起,先看下下面的代碼:

class Parent {    public void rewriteMethod() {
        
    }
}class Child extends Parent {    /**
        * 重載父類的 rewriteMethod() 方法
        */
    @Override
    public void rewriteMethod() {
    }    /**
        * 被棄用的過時方法
        */
    @Deprecated
    public void oldMethod() {
    }    /**
        * 忽略告警
        * 
        * @return
        */
    @SuppressWarnings("keep run")    public List infoList() {        List list = new ArrayList();        return list;
    }
}

Java 1.5 開始自帶的標準註解,包括 @Override、@Deprecated 和 @SuppressWarnings:

1.1.1 內置註解 - @Override

我們先來看一下這個註解類型的定義:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

從它的定義我們可以看到,這個註解可以被用來修飾方法,並且它只在編譯時有效,在編譯後的 class 文件中便不再存在。這個註解的作用我們大家都不陌生,那就是告訴編譯器被修飾的方法是重寫的父類的中的相同簽名的方法,編譯器會對此做出檢查,
若發現父類中不存在這個方法或是存在的方法簽名不同,則會報錯。

1.1.2 內置註解 - @Deprecated

這個註解的定義如下:

@Documented@Retention(RetentionPolicy.RUNTIME)@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

從它的定義我們可以知道,它會被文檔化,能夠保留到運行時,能夠修飾構造方法、屬性、局部變量、方法、包、參數、類型。這個註解的作用是告訴編譯器被修飾的程序元素已被 “廢棄”,不再建議用戶使用。

1.1.3 內置註解 - @SuppressWarnings

這個註解我們也比較常用到,先來看下它的定義:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {    String[] value();
}

它能夠修飾的程序元素包括類型、屬性、方法、參數、構造器、局部變量,只能存活在源碼時,取值爲 String[]。它的作用是告訴編譯器忽略指定的警告信息,它可以取的值如下所示:

76gTPZ

1.2 元註解

上述內置註解的定義中使用了一些元註解(註解類型進行註解的註解類),在 JDK 1.5 中提供了 4 個標準的元註解:@Target,@Retention,@Documented,@Inherited, 在 JDK 1.8 中提供了兩個新的元註解 @Repeatable 和 @Native。

1.2.1 元註解 - @Target

Target 註解的作用是:描述註解的使用範圍(即:被修飾的註解可以用在什麼地方) 。

Target 註解用來說明那些被它所註解的註解類可修飾的對象範圍:

public enum ElementType { 
    TYPE, // 類、接口、枚舉類 
    FIELD, // 成員變量(包括:枚舉常量) 
    METHOD, // 成員方法 
    PARAMETER, // 方法參數 
    CONSTRUCTOR, // 構造方法 
    LOCAL_VARIABLE, // 局部變量 
    ANNOTATION_TYPE, // 註解類 
    PACKAGE, // 可用於修飾:包 
    TYPE_PARAMETER, // 類型參數,JDK 1.8 新增 
    TYPE_USE // 使用類型的任何地方JDK 1.8 新增 }

1.2.2 元註解 - @Retention & @RetentionTarget

Reteniton 註解的作用是:描述註解保留的時間範圍(即:被描述的註解在它所修飾的類中可以被保留到何時) 。

Reteniton 註解用來限定那些被它所註解的註解類在註解到其他類上以後,可被保留到何時,一共有三種策略,定義在 RetentionPolicy 枚舉中。枚舉如下:

public enum RetentionPolicy { 
    SOURCE,    // 源文件保留
    CLASS,       // 編譯期保留,默認爲該值,CLASS
    RUNTIME   // 運行期保留可通過反射去獲取註解信息}

我們測試下這三種策略,在定義註解類的時候什麼區別:

@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy {  // 源文件保留策略}@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy {  // 編譯器保留策略}@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy {  // 運行期保留策略}

上面已經定義好了三個註解類,我們再用這三個註解類再去註解方法,如下:

public class RetentionTest { 
	@SourcePolicy
	public void sourcePolicy() {
	} 
	@ClassPolicy
	public void classPolicy() {
	} 
	@RuntimePolicy
	public void runtimePolicy() {
	}
}

通過執行 javap -verbose RetentionTest 命令獲取到的 RetentionTest 的 class 字節碼內容如下。

{  public retention.RetentionTest();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial   #1            // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void sourcePolicy();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0

  public void classPolicy();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0
    RuntimeInvisibleAnnotations:
      0: #11()

  public void runtimePolicy();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 15: 0
    RuntimeVisibleAnnotations:
      0: #14()}

從 RetentionTest 的字節碼內容我們可以得出以下兩點結論:

1.2.3 元註解 - @Documented

Documented 註解的作用如下:使用 javadoc 工具爲類生成幫助文檔,並確認是否保留註解信息。

以下代碼在使用 Javadoc 工具可以生成 @DocAnnotation 註解信息。

import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Target; 
@Documented@Target({ElementType.TYPE,ElementType.METHOD})public @interface DocAnnotation { 
	public String value() default "default";
}
@DocAnnotation("some method doc")public void testMethod() {  // 測試方法的文檔註解}

1.2.4 元註解 - @Inherited

Inherited 註解的作用:被它修飾的 Annotation 將具有繼承特性。父類使用了被 @Inherited 修飾的 Annotation,則子類將自動具備該註解。

我們來測試下這個註解:

@Inherited@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE,ElementType.METHOD})
public @interface InheritedAnnotation {    String [] values();    int number();
}
@InheritedAnnotation(values = {"brand"}, number = 100)
public class UserInfo {
}class Customer extends UserInfo {	@Test
    public void testMethod(){        Class clazz = Student.class;        Annotation[] annotations = clazz.getAnnotations();        for (Annotation annotation : annotations) {            System.out.println(annotation.toString());
        }
    }
}
xxx.InheritedAnnotation(values=[brand], number=100)

雖然 Customer 類沒有顯示地被註解 @InheritedAnnotation,但是它的父類 UserInfo 被註解,而且 @InheritedAnnotation 被 @Inherited 註解,因此 Customer 類自動繼承註解

1.2.4 元註解 - @Repeatable (Java8)

Repeatable 是可重複使用的意思,允許在同一聲明的類型 (類,屬性,或方法) 中,可以多次使用同一個註解

JDK8 之前要想實現註解重複使用,需要組合模式,編寫和可讀性都不是很好

public @interface Pet {     String myPet();
}public @interface Pets {    Pet[] value();
}public class RepeatAnnotationOV {    @Pets({@Pet(myPet="dog"),@Pet(myPet="cat")})
    public void workMethod(){
    }
}

由另一個註解來存儲重複註解,在使用時候,用存儲註解 Authorities 來擴展重複註解。

Java 8 中的做法:

@Repeatable(Pets.class)
public @interface Pet {     String myPet();
}public @interface Pets {    Pet[] value();
}public class RepeatAnnotationNV {    @Pet(role="dog")    @Pet(role="cat")
    public void workMethod(){ }
}

不同的地方是,創建重複註解 Authority 時,加上 @Repeatable, 指向存儲註解 Authorities,在使用時候,直接可以重複使用 Authority 註解。從上面例子看出,java 8 裏面做法更適合常規的思維,可讀性強一點

1.2.5 元註解 - @Native (Java8)

使用 @Native 註解修飾成員變量,則表示這個變量可以被本地代碼引用,常常被代碼生成工具使用。對於 @Native 註解不常使用,瞭解即可

1.3 註解與反射接口

定義註解後,如何獲取註解中的內容呢?反射包 java.lang.reflect 下的 AnnotatedElement 接口提供這些方法。這裏注意:只有註解被定義爲 RUNTIME 後,該註解才能是運行時可見,當 class 文件被裝載時被保存在 class 文件中的 Annotation 纔會被虛擬機讀取。

AnnotatedElement 接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通過反射獲取了某個類的 AnnotatedElement 對象之後,程序就可以調用該對象的方法來訪問 Annotation 信息。我們看下具體的相關接口

1.4 自定義註解

當我們理解了內置註解, 元註解和獲取註解的反射接口後,我們便可以開始自定義註解了。這個例子我把上述的知識點全部融入進來, 代碼很簡單:

package com.helenlyn.common.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/**
 * <p>Description: 水果供應者註解        </p>
 * <p>Copyright: Copyright (c) 2021 </p>
 * <p>Company: helenlyn Co., Ltd.             </p>
 *
 * @author brand
 * @date 2021/5/16 16:35
 * <p>Update Time:                      </p>
 * <p>Updater:                          </p>
 * <p>Update Comments:                  </p>
 */@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface FruitProvider {    /**
     * 供應商編號
     * @return
     */
    public int id() default -1;    /**
     * 供應商名稱
     * @return
     */
    public String name() default "";    /**
     * 供應商地址
     * @return
     */
    public String address() default "";
}
package com.helenlyn.common.dto;import com.helenlyn.common.annotation.FruitColor;import com.helenlyn.common.annotation.FruitName;import com.helenlyn.common.annotation.FruitProvider;/**
 * <p>Description:               </p>
 * <p>Copyright: Copyright (c) 2021 </p>
 * <p>Company: helenlyn Co., Ltd.             </p>
 *
 * @author brand
 * @date 2021/5/16 16:28
 * <p>Update Time:                      </p>
 * <p>Updater:                          </p>
 * <p>Update Comments:                  </p>
 */public class AppleDto {   @FruitName("Apple")
    private String appleName;    @FruitColor(fruitColor=FruitColor.Color.RED)
    private String appleColor;    @FruitProvider(id=1,)
    private String appleProvider;
}
/**
 * <p>Description: FruitInfoUtil註解實現 </p>
 * <p>Copyright: Copyright (c) 2021 </p>
 * <p>Company: helenlyn Co., Ltd.             </p>
 *
 * @author brand
 * @date 2021/5/16 16:37
 * <p>Update Time:                      </p>
 * <p>Updater:                          </p>
 * <p>Update Comments:                  </p>
 */
public class FruitInfoUtil {
    public static String getFruitInfo(Class<?> clazz) {
        String strFruitName = " 水果名稱:";
        String strFruitColor = " 水果顏色:";
        String strFruitProvicer = "供應商信息:";

        Field[] fields = clazz.getDeclaredFields();        for (Field field : fields) {            if (field.isAnnotationPresent(FruitName.class)) {
                FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
                strFruitName += fruitName.value();
                System.out.println(strFruitName);
            } else if (field.isAnnotationPresent(FruitColor.class)) {
                FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class);
                strFruitColor += fruitColor.fruitColor().toString();
                System.out.println(strFruitColor);
            } else if (field.isAnnotationPresent(FruitProvider.class)) {
                FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
                strFruitProvicer = " 供應商編號:" + fruitProvider.id() + " 供應商名稱:" + fruitProvider.name() + " 供應商地址:" + fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }        return String.format("%s;%s;%s;", strFruitName, strFruitColor, strFruitProvicer);
    }
}
2022-07-09 11:33:41.688  INFO 5895 --- [TaskExecutor-35] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: cl-debug-rabbitmq-erp-service-7w0cpa.docker.sdp:9146Hibernate: update UserBasicInfo set personName=? where personCode=?
 水果名稱:Apple
 水果顏色:RED
 供應商編號:1 供應商名稱:helenlyn 貿易公司 供應商地址:福州xx路xxx大樓

2 理解註解的原理

2.1 Java 8 提供了哪些新的註解

// 自定義ElementType.TYPE_PARAMETER註解@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE_PARAMETER)public @interface MyNotEmpty {
}// 自定義ElementType.TYPE_USE註解@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE_USE)public @interface MyNotNull {
}// 測試類public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{  //使用TYPE_PARAMETER類型,會編譯不通過//		public @MyNotEmpty T test(@MyNotEmpty T a){//			new ArrayList<@MyNotEmpty String>();//				return a;//		}

  //使用TYPE_USE類型,編譯通過
  public @MyNotNull T test2(@MyNotNull T a){    new ArrayList<@MyNotNull String>();    return a;
  }
}

2.2 註解支持繼承嗎?

註解是不支持繼承的
不能使用關鍵字 extends 來繼承某個 @interface,但註解在編譯後,編譯器會自動繼承 java.lang.annotation.Annotation 接口. 雖然反編譯後發現註解繼承了 Annotation 接口,請記住,即使 Java 的接口可以實現多繼承,但定義註解時依然無法使用 extends 關鍵字繼承 @interface。區別於註解的繼承,被註解的子類繼承父類註解可以用 @Inherited:如果某個類使用了被 @Inherited 修飾的 Annotation,則其子類將自動具有該註解。

3 註解的應用場景

自定義註解和 AOP - 通過切面實現解耦

筆者曾經在 《基於 AOP 的動態數據源切換》 這篇文章中有個典型的例子,就是使用 AOP 切面來對多數據源進行使用場景的切換,下面展示下如何通過註解實現解耦的。

/**
 * @author brand
 * @Description: 數據源切換註解
 * @Copyright: Copyright (c) 2021
 * @Company: Helenlyn, Inc. All Rights Reserved.
 * @date 2021/12/15 7:36 下午
 */@Target({ ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface DataSource {
    String name() default "";
}
/**
 * @author brand
 * @Description:
 * @Copyright: Copyright (c) 2021
 * @Company: Helenlyn, Inc. All Rights Reserved.
 * @date 2021/12/15 7:49 下午
 */@Aspect@Componentpublic class DataSourceAspect implements Ordered  {    /**
     * 定義一個切入點,匹配到上面的註解DataSource
     */
    @Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)")
    public void dataSourcePointCut() {
    }    /**
     * Around 環繞方式做切面注入
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {        MethodSignature signature = (MethodSignature) point.getSignature();        Method method = signature.getMethod();        DataSource ds = method.getAnnotation(DataSource.class);        String routeKey = ds.name();  // 從頭部中取出註解的name(basic 或 cloudoffice 或 attend),用這個name進行數據源查找。
        String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey();        if (StringUtils.isNotEmpty(dataSourceRouteKey)) {            // StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey);
            routeKey = ds.name();
        }
        DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey);        try {            return point.proceed();
        } finally { // 最後做清理,這個步驟很重要,因爲我們的配置中有一個默認的數據源,執行完要回到默認的數據源。
            DynamicDataSource.clearDataSource();
            DynamicDataSourceRouteHolder.clearDataSourceRouteKey();
        }
    }    @Override
    public int getOrder() {        return 1;
    }
}
/**
     * 無註解默認情況:數據源指向basic
     * @return
     */
    @RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET)
    public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) {        return userInfoService.getUserInfo(userCode);
    }    /**
     * 數據源指向attend
     * @return
     */
    @DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME)
    @RequestMapping(value = "/attend/{user_code}", method = RequestMethod.GET)    public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) {        return userInfoService.getUserInfo(userCode);
    }    /**
     * 數據源指向cloud
     * @return
     */
    @DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME)
    @RequestMapping(value = "/cloud/{user_code}", method = RequestMethod.GET)    public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) {        return userInfoService.getUserInfo(userCode);
    }

除此之外,我們可以看到很多日誌管理、權限管理,也都是也是通過類似的註解機制來實現的,通過註解 + AOP 來最終實現模塊之間的解耦,以及業務與系統層面的解耦 。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/a03X7hjwVrlUsPQn9o-Ecg