註解是JDK1.5版本開始引入的一個特性,用於對程式程式碼的說明,可以對包、類、介面、欄位、方法引數、區域性變數等進行註解。
它主要的作用有以下四方面:
註解的常見分類有三種:
接下來我們通過這三種分類來逐一理解註解。
我們先從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:
@Override
:表示當前類中的方法定義將覆蓋父類別中的方法@Deprecated
:表示該程式碼段被棄用,但是可以使用,只是編譯器會發出警告而已@SuppressWarnings
:表示關閉編譯器的警告資訊我們先來看一下這個註解型別的定義:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
從它的定義我們可以看到,這個註解可以被用來修飾方法,並且它只在編譯時有效,在編譯後的class檔案中便不再存在。這個註解的作用我們大家都不陌生,那就是告訴編譯器被修飾的方法是重寫的父類別的中的相同簽名的方法,編譯器會對此做出檢查,
若發現父類別中不存在這個方法或是存在的方法簽名不同,則會報錯。
這個註解的定義如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
從它的定義我們可以知道,它會被檔案化,能夠保留到執行時,能夠修飾構造方法、屬性、區域性變數、方法、包、引數、型別。這個註解的作用是告訴編譯器被修飾的程式元素已被「廢棄」,不再建議使用者使用。
這個註解我們也比較常用到,先來看下它的定義:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
它能夠修飾的程式元素包括型別、屬性、方法、引數、構造器、區域性變數,只能存活在原始碼時,取值為String[]。它的作用是告訴編譯器忽略指定的警告資訊,它可以取的值如下所示:
引數 | 作用 | 原描述 |
---|---|---|
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制裝箱、拆箱操作時候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制對映相關的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制啟用註釋的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制過期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制確在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模組沒有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制與隱藏變數的區域變數相關的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略沒有完整的switch語句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字元 | to suppress warnings relative to non-nls string literals |
null | 忽略對null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics時忽略沒有指定相應的型別 | to suppress warnings relative to un-specific types when using |
restriction | 抑制與使用不建議或禁止參照相關的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable類中沒有宣告serialVersionUID變數 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正確的靜態存取方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子類沒有按最優方法存取內部類的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制沒有進行型別檢查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制沒有許可權存取的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制沒被使用過的程式碼的警告 | to suppress warnings relative to unused code |
上述內建註解的定義中使用了一些元註解(註解型別進行註解的註解類),在JDK 1.5中提供了4個標準的元註解:@Target,@Retention,@Documented,@Inherited, 在JDK 1.8中提供了兩個新的元註解 @Repeatable和@Native。
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 新增
}
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 的位元組碼內容我們可以得出以下兩點結論:
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() {
// 測試方法的檔案註解
}
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類自動繼承註解
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裡面做法更適合常規的思維,可讀性強一點
使用 @Native 註解修飾成員變數,則表示這個變數可以被原生程式碼參照,常常被程式碼生成工具使用。對於 @Native 註解不常使用,瞭解即可
定義註解後,如何獲取註解中的內容呢?反射包java.lang.reflect下的AnnotatedElement介面提供這些方法。這裡注意:只有註解被定義為RUNTIME後,該註解才能是執行時可見,當class檔案被裝載時被儲存在class檔案中的Annotation才會被虛擬機器器讀取。
AnnotatedElement 介面是所有程式元素(Class、Method和Constructor)的父介面,所以程式通過反射獲取了某個類的AnnotatedElement物件之後,程式就可以呼叫該物件的方法來存取Annotation資訊。我們看下具體的先關介面
當我們理解了內建註解, 元註解和獲取註解的反射介面後,我們便可以開始自定義註解了。這個例子我把上述的知識點全部融入進來, 程式碼很簡單:
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)
@Documented
public @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,name="helenlyn 貿易公司",address="福州xx路xxx大樓")
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:9146
Hibernate: update UserBasicInfo set personName=? where personCode=?
水果名稱:Apple
水果顏色:RED
供應商編號:1 供應商名稱:helenlyn 貿易公司 供應商地址:福州xx路xxx大樓
ElementType.TYPE_USE(此型別包括型別宣告和型別引數宣告,是為了方便設計者進行型別檢查)包含了ElementType.TYPE(類、介面(包括註解型別)和列舉的宣告)和ElementType.TYPE_PARAMETER(型別引數宣告), 可以看下面這個例子:
// 自定義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;
}
}
註解是不支援繼承的
不能使用關鍵字extends來繼承某個@interface,但註解在編譯後,編譯器會自動繼承java.lang.annotation.Annotation介面。 雖然反編譯後發現註解繼承了Annotation介面,請記住,即使Java的介面可以實現多繼承,但定義註解時依然無法使用extends關鍵字繼承@interface。 區別於註解的繼承,被註解的子類繼承父類別註解可以用@Inherited: 如果某個類使用了被@Inherited修飾的Annotation,則其子類將自動具有該註解。
筆者曾經在 《基於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)
@Documented
public @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
@Component
public 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來最終實現模組之間的解耦,以及業務與系統層面的解耦 。