【進階之路】自定義註解介紹與實戰

在使用spring框架的時候,我們經常會感嘆註解式編程真是大大簡化了開發的時間,幾個小小的註解,就能解決一系列的配置問題,讓寫程式碼像寫詩一樣輕鬆明快。

我們都知道,在spring框架的前期,大多使用XML配置進行開發。XML配置起來有時候冗長,如實體類的映射,使用XML進行開發會顯得十分複雜。同時註解在處理一些不變的元數據時有時候比XML方便的多,比如spring 聲明式事務管理,如果用XML寫的程式碼會多的多。註解與Java Bean緊密結合,既大大減少了配置文件的體積,又增加了Java Bean的可讀性與內聚性

當然,不管使用註解還是XML,滿足需求的前提下,採用最簡單的方法才是最合適的。

今天我就以一個簡單的例子來給大家講解,如何進行自定義註解,幫助我們使用註解開發項目。

一、元註解

首先,我們定義一個類需要用到元註解。

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface NotifyMonitor {
    String value() default "";
}

1、@Target

@Target註解,是專門用來限定某個自定義註解能夠被應用在哪些Java元素上面的。它使用一個枚舉類型定義如下:


public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    /** 類,介面(包括註解類型)或枚舉的聲明 */
    TYPE,

    /** Field declaration (includes enum constants) */
    /** 屬性的聲明 */
    FIELD,

    /** Method declaration */
    /** 方法的聲明 */
    METHOD,

    /** Formal parameter declaration */
    /** 方法形式參數聲明 */
    PARAMETER,

    /** Constructor declaration */
    /** 構造方法的聲明 */
    CONSTRUCTOR,

    /** Local variable declaration */
    /** 局部變數聲明 */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    /** 註解類型聲明 */
    ANNOTATION_TYPE,

    /** Package declaration */
    /** 包的聲明 */
    PACKAGE,
}

就像我們之前定義的,@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD,ElementType.TYPE})就是可以運用在註解、方法和類上。

2、@Retention

@Retention註解,翻譯為持久力、保持力。即用來修飾自定義註解的生命力。
註解的生命周期有三個階段:

  • 1、Java源文件階段;
  • 2、編譯到.class文件階段;
  • 3、運行期階段。
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     (註解將被編譯器忽略掉)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     (註解將被編譯器記錄在class文件中,但在運行時不會被虛擬機保留,這是一個默認的行為)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *(註解將被編譯器記錄在.class文件中,而且在運行時會被虛擬機保留,因此它們能通過反射被讀取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

我們使用的@Retention(RetentionPolicy.RUNTIME) 是讓註解將被編譯器記錄在.class文件中,而且在運行時會被虛擬機保留,所以它能通過反射被讀取到。

3、@Inherited

在註解上使用@Inherited 表示該註解會被子類繼承,注意,僅針對類,成員屬性、方法並不受此注釋的影響。

對於類來說,子類要繼承父類的註解需要該註解被 @Inherited 標識。

對於成員屬性和方法來說,非重寫的都會保持和父類一樣的註解,而被實現的抽象方法,被重寫的方法都不會有父類的註解。

當@NotifyMonitor註解加在某個類A上時,假如類B繼承了A,則B也會帶上該註解。

我們可以看到,在springboot,很多類也加上了這個註解。

4、@Documented

除了我們在註解類上應用到的之外,@Documented註解的作用是在使用 javadoc 工具為類生成幫助文檔時保留其註解資訊。

如果去掉了這個註解,那麼在生成的工具文檔上就不會出現這個註解,對於一些內部工具類註解來說可有無可。

二、利用AOP實現自定義註解

我們來實現下面這個場景,執行一個任務,如果任務報錯,我們就通過釘釘通知指定的人員讓他進行處理。

要實現這個功能,我們可能會想到try-catch方式。當然,沒有什麼不對,但是如果要在一百個不同的方法中加入這個邏輯,豈不是要實現100次?於是乎,使用自定義註解的方式或許是不錯的主意。

我寫了一個類來實現上訴方法:

@Slf4j
@Aspect
@Component
public class NotifyMonitorAspect {
    @Autowired
    private DingDingOpe dingDingOpe;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
	
    //相比大家對aop都不會陌生
    @Pointcut("@annotation(com.nanju.aop.NotifyMonitor)")
    private void monitor() {}

    /**
     * 處理任務
     point.proceed()是用來執行原來的任務
     dingDingOpe.sendRobotMsg  是自定了一個方法用來通知釘釘
     *
     * @param point
     */
    @Around("monitor()")
    public Object doAround(ProceedingJoinPoint point) {
        String jobName = getJobName(point);
        Object object = null;
        try {
             object = point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            String url = getUrl(jobName);
            dingDingOpe.sendRobotMsg(url, "任務處理失敗:"+"{"+throwable.getMessage()+"}", false);
        }
        return object;
    }


 /**
     * 獲取Job名稱,這個方法就是利用了NotifyMonitor中的value值,根據不同的方法使用不同的通知
     * @param point 切點
     */
    private String getJobName(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        NotifyMonitor jobs = method.getAnnotation(NotifyMonitor.class);
        if ("".equals(jobs.value())){
            return null;
        }
        return jobs.value();
    }
    
     /**
     * 根據Job名稱獲取通知地址,使用了stringRedisTemplate,提前將輸入埋入redis,也可以放在資料庫里,配置通知地址
     * @param notifyMonitor
     *
     */
    private String getUrl(String notifyMonitor) {
        if (Objects.isNull(notifyMonitor)){
            return TextUtils.dealNull(stringRedisTemplate.opsForValue().get("warn:dingdingUrl:"));
        }else{
            return  TextUtils.dealNull(stringRedisTemplate.opsForValue().get("warn:dingdingUrl:"+notifyMonitor));
        }
    }
}

我們測試一下

1、在方法上加上註解@NotifyMonitor

2、調用方法

3、執行成功

我們還可以嘗試一下,在@NotifyMonitor加上value(因為只有一個屬性,所以value=”xxx” 與 “xxx” 等價)

4、執行結果

這樣,一個註解式的任務處理、通知功能就完成了。自定義註解不僅能夠在方法執行前後進行擴展、獲取到實現註解的方法、所在類等資訊、修改參數和返回值,還能夠實現包括執行緒池、分散式鎖、類數據校驗等等你能想到的大部分操作,我在工作中也實現了其中一些功能,減少了大量的重複程式碼,也讓程式碼的可讀性提高了。

了解到這裡,不妨你也自己動手來寫一個自定義註解來簡化我們的項目吧。

大家好,我是練習java兩年半時間的南橘,下面是我的微信,需要之前的導圖或者想互相交流經驗的小夥伴可以一起互相交流哦。