Java核心知識體系2:註解機制詳解
1 Java註解基礎
註解是JDK1.5版本開始引入的一個特性,用於對程式程式碼的說明,可以對包、類、介面、欄位、方法參數、局部變數等進行註解。
它主要的作用有以下四方面:
- 生成javadoc文檔,通過在程式碼裡面標識元數據生成javadoc文檔。
- 編譯期的檢查,通過標識的元數據讓編譯器在編譯期間對程式碼進行驗證。
- 編譯時動態處理,編譯時通過程式碼中標識的元數據動態處理,比如動態生成程式碼。
- 運行時動態處理,運行時通過程式碼中標識的元數據動態處理,比如使用反射技術注入實例。
註解的常見分類有三種:
- Java自帶的標準註解,包括 @Override、@Deprecated和@SuppressWarnings,分別代表 方法重寫、某個類或方法過時、以及忽略警告,用這些註解標明後編譯器就會進行檢查。
- 元註解,元註解是用於定義註解的註解,包括@Retention、@Target、@Inherited、@Documented 等6種
- @Retention:指定其所修飾的註解的保留策略
- @Document:該註解是一個標記註解,用於指示一個註解將被文檔化
- @Target:用來限制註解的使用範圍
- @Inherited:該註解使父類的註解能被其子類繼承
- @Repeatable:該註解是Java8新增的註解,用於開發重複註解
- 類型註解(Type Annotation):該註解是Java8新增的註解,可以用在任何用到類型的地方
- 自定義註解,可以根據自己的需求定義註解,並可用元註解對自定義註解進行註解。
接下來我們通過這三種分類來逐一理解註解。
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:
@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[]。它的作用是告訴編譯器忽略指定的警告資訊,它可以取的值如下所示:
參數 | 作用 | 原描述 |
---|---|---|
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 |
1.2 元註解
上述內置註解的定義中使用了一些元註解(註解類型進行註解的註解類),在JDK 1.5中提供了4個標準的元註解:@Target,@Retention,@Documented,@Inherited, 在JDK 1.8中提供了兩個新的元註解 @Repeatable和@Native。
1.2.1 元註解 – @Target
Target註解的作用是:描述註解的使用範圍(即:被修飾的註解可以用在什麼地方) 。
Target註解用來說明那些被它所註解的註解類可修飾的對象範圍:
- packages、types(類、介面、枚舉、註解類)
- 類成員(方法、構造方法、成員變數、枚舉值)
- 方法參數和本地變數(如循環變數、catch參數)
在定義註解類時使用了@Target 能夠更加清晰的知道它能夠被用來修飾哪些對象,它的取值範圍定義在ElementType 枚舉中。枚舉資訊如下:
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 的位元組碼內容我們可以得出以下兩點結論:
- 編譯器並沒有記錄下 sourcePolicy() 方法的註解資訊
- 編譯器使用 RuntimeInvisibleAnnotations 去記錄 classPolicy()方法的註解資訊
- 編譯器使用 RuntimeVisibleAnnotations 去記錄 runtimePolicy()方法的註解資訊
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註解:
@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資訊。我們看下具體的先關介面
- boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
判斷該程式元素上是否包含指定類型的註解,存在則返回true,否則返回false。注意:此方法會忽略註解對應的註解容器。 T getAnnotation(Class annotationClass)
返回該程式元素上存在的、指定類型的註解,如果該類型註解不存在,則返回null。- Annotation[] getAnnotations()
返回該程式元素上存在的所有註解,若沒有註解,返回長度為0的數組。 T[] getAnnotationsByType(Class annotationClass)
返回該程式元素上存在的、指定類型的註解數組。沒有註解對應類型的註解時,返回長度為0的數組。該方法的調用者可以隨意修改返回的數組,而不會對其他調用者返回的數組產生任何影響。getAnnotationsByType方法與 getAnnotation的區別在於,getAnnotationsByType會檢測註解對應的重複註解容器。若程式元素為類,當前類上找不到註解,且該註解為可繼承的,則會去父類上檢測對應的註解。T getDeclaredAnnotation(Class annotationClass)
返回直接存在於此元素上的所有註解。與此介面中的其他方法不同,該方法將忽略繼承的注釋。如果沒有注釋直接存在於此元素上,則返回nullT[] getDeclaredAnnotationsByType(Class annotationClass)
返回直接存在於此元素上的所有註解。與此介面中的其他方法不同,該方法將忽略繼承的注釋- Annotation[] getDeclaredAnnotations()
返回直接存在於此元素上的所有註解及註解對應的重複註解容器。與此介面中的其他方法不同,該方法將忽略繼承的註解。如果沒有注釋直接存在於此元素上,則返回長度為零的一個數組。該方法的調用者可以隨意修改返回的數組,而不會對其他調用者返回的數組產生任何影響。
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)
@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;
}
- 用反射介面獲取註解資訊
在 FruitInfoUtil 中進行測試:
/**
* <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大樓
2 理解註解的原理
2.1 Java 8 提供了哪些新的註解
- @Repeatable
- ElementType.TYPE_USE
- ElementType.TYPE_PARAMETER
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;
}
}
2.2 註解支援繼承嗎?
註解是不支援繼承的
不能使用關鍵字extends來繼承某個@interface,但註解在編譯後,編譯器會自動繼承java.lang.annotation.Annotation介面。 雖然反編譯後發現註解繼承了Annotation介面,請記住,即使Java的介面可以實現多繼承,但定義註解時依然無法使用extends關鍵字繼承@interface。 區別於註解的繼承,被註解的子類繼承父類註解可以用@Inherited: 如果某個類使用了被@Inherited修飾的Annotation,則其子類將自動具有該註解。
3 註解的應用場景
自定義註解多喝AOP – 通過切面實現解耦
筆者曾經在 《基於AOP的動態數據源切換》 這篇文章中有個典型的例子,就是使用AOP切面來對多數據源進行使用場景的切換,下面展示下如何通過註解實現解耦的。
- 自定義Annotation,映射的目標範圍為 類型和方法。
/**
* @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 "";
}
- 編寫AOP實現,切面程式碼,以實現對註解的PointCut,切點攔截
/**
* @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;
}
}
- 測試,在Control中寫三個測試方法
/**
* 無註解默認情況:數據源指向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來最終實現模組之間的解耦,以及業務與系統層面的解耦 。