Spring項目中自定義註解的使用

1. 準備工作

首先這裏創建了一個簡單的springboot項目:
包結構
各個類的內容如下所示:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private Integer id;

    private String name;

}
@Component
public class UserDao {

    public User findUserById(Integer id) {
        if(id > 10) {
            return null;
        }
        return new User(id, "user-" + id);
    }
}
@Service
public class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User findUserById(Integer id) {
        return userDao.findUserById(id);
    }
}

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("user/{id}")
    public User findUser(@PathVariable("id") Integer id) {
        return userService.findUserById(id);
    }
}

2. 使用註解執行固定的操作

現在我們已經有了這樣的一個簡單的web項目了,直接訪問localhost:8080/user/6後,顯然會得到一個如下的json串

{
  "id": 6,
  "name": "user-6"
}

但是我們不滿足於此,這個項目也未免太簡陋了,現在我們就來爲它增加一個日誌的功能(不要說使用log4j等日誌框架,我們的目的是學習自定義註解)

假設我們現在的目的是,在調用controller中的findUser方法前,先在控制檯輸出一句話。好了那就開始做吧,我們先創建一個annotation包,裏面創建我們自定義的註解類KthLog

package com.example.demo.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface KthLog {

    String value() default "";
}

這裏註解類上的三個註解稱爲元註解,其分別代表的含義如下:

  • @Documented:註解信息會被添加到Java文檔中
  • @Retention:註解的生命週期,表示註解會被保留到什麼階段,可以選擇編譯階段、類加載階段,或運行階段
  • @Target:註解作用的位置,ElementType.METHOD表示該註解僅能作用於方法上

然後我們可以把註解添加到方法上:

    @KthLog("這是日誌內容")
    @RequestMapping("user/{id}")
    public User findUser(@PathVariable("id") Integer id) {
        return userService.findUserById(id);
    }

這個註解目前是沒有任何作用的,因爲我們僅僅是對註解進行了聲明,並沒有在任何地方來使用這個註解,註解的本質也是一種廣義的語法糖,最終還是要利用Java的反射來進行操作

不過Java給我們提供了一個AOP機制,可以對類或方法進行動態的擴展,想較深入地瞭解這一機制的可以參考我的這篇文章:從源碼解讀Spring的AOP

我們創建切面類,如下:

@Component
@Aspect
public class KthLogAspect {
    
    @Pointcut("@annotation(com.example.demo.annotation.KthLog)")
    private void pointcut() {}
    
    @Before("pointcut() && @annotation(logger)")
    public void advice(KthLog logger) {
        System.out.println("--- Kth日誌的內容爲[" + logger.value() + "] ---");
    }
}

其中@Pointcut聲明瞭切點(這裏的切點是我們自定義的註解類),@Before聲明瞭通知內容,在具體的通知中,我們通過@annotation(logger)拿到了自定義的註解對象,所以就能夠獲取我們在使用註解時賦予的值了。這裏如果對於切點和通知等概念不瞭解的,建議先去查閱一些aop的知識再回來看本文較好,本文更注重於實踐,而不是概念的講解

然後我們現在再來啓動web服務,在瀏覽器上輸入localhost:8080/user/6(使用JUnit單元測試也可以),會發現控制檯成功輸出:圖1

3. 使用註解獲取更詳細的信息

剛纔我們使用自定義註解實現了在方法調用前輸出一句日誌,但是我們並不知道這是哪個方法、哪個類輸出的,如果有兩個方法都加上了這個註解,且value的值都一樣,那我們該怎麼區分這兩個方法呢?比如現在我們給UserController類中添加了一個方法:

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @KthLog("這是日誌內容")
    @RequestMapping("user/{id}")
    public User findUser(@PathVariable("id") Integer id) {
        return userService.findUserById(id);
    }

    @KthLog("這是日誌內容")
    @RequestMapping("compared")
    public void comparedMethod() { }
}

如果我們調用comparedMethod()方法,顯然會得到和剛纔一樣的輸出結果,這時候我們就需要對註解做進一步改造,其實很簡單,只需要在切面類的advice()方法中添加一個JoinPoint參數即可,如下:

    @Before("pointcut() && @annotation(logger)")
    public void advice(JoinPoint joinPoint, KthLog logger) {
        System.out.println("註解作用的方法名: " + joinPoint.getSignature().getName());
        
        System.out.println("所在類的簡單類名: " + joinPoint.getSignature().getDeclaringType().getSimpleName());
        
        System.out.println("所在類的完整類名: " + joinPoint.getSignature().getDeclaringType());
        
        System.out.println("目標方法的聲明類型: " + Modifier.toString(joinPoint.getSignature().getModifiers()));
    }

然後我們再來執行一遍剛纔的流程,看看會輸出什麼結果:
圖2
現在我們再將這些內容放到日誌中,順便修改一下日誌的格式,如下:

    @Before("pointcut() && @annotation(logger)")
    public void advice(JoinPoint joinPoint, KthLog logger) {
        System.out.println("[" 
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName() 
                + "]-日誌內容-[" + logger.value() + "]");
    }

圖3

4. 使用註解修改參數和返回值

我們把之前添加的compare()方法刪去,現在我們的註解需要對方法的參數作出修改,以findUser()方法爲例,假設我們傳入的用戶id是從1開始計數,後端則是從0開始計數,我們的@KthLog註解的開發者喜歡“多管閒事”,想要幫助其他人減輕一點壓力,那該怎麼做呢?

在這個應用場景中,我們需要做的有兩件事:將傳入的id減1,給返回的user類中的id加1。這就涉及到如何拿到參數的問題。因爲我們需要管理方法執行前和執行後的操作,所以我們使用@Around環繞註解,如下:

    @Around("pointcut() && @annotation(logger)")
    public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) {
        System.out.println("["
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName()
                + "]-日誌內容-[" + logger.value() + "]");
        
        Object result = null;
        
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        
        return result;
    }

這裏除了將@Before改爲@Around之外,還將參數中的JoinPoint改爲了ProceedingJoinPoint,不過不用擔心,JoinPoint能做的ProceedingJoinPoint都能做。這裏通過調用proceed()方法,執行了實際的操作,並獲取到了返回值,那麼接下來對於返回值的操作相信就不用我再多說了,現在問題就是如何獲取到參數

ProceedingJoinPoint繼承了JoinPoint接口,在JoinPoint中,存在一個getArgs()方法,用於獲取方法參數,返回的是一個Object數組,與之匹配的則是proceed(args)方法,這兩個方法結合起來,就能夠實現我們的目的:

    @Around("pointcut() && @annotation(logger)")
    public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) {
        System.out.println("["
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName()
                + "]-日誌內容-[" + logger.value() + "]");

        Object result = null;

        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if(args[i] instanceof Integer) {
                args[i] = (Integer)args[i] - 1;
                break;
            }
        }

        try {
            result = joinPoint.proceed(args);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        if(result instanceof User) {
            User user = (User) result;
            user.setId(user.getId() + 1);
            return user;
        }
        return result;
    }

這裏爲了代碼的魯棒性做了兩次參數類型校驗,接着我們重新執行之前的測試,這裏爲了讓結果更明顯,我們在UserDao處添加一些輸出,來顯示實際執行的參數和返回的值各自是什麼:

@Component
public class UserDao {

    public User findUserById(Integer id) {
        System.out.println("查詢id爲[" + id + "]的用戶");
        if(id > 10) {
            return null;
        }
        User user = new User(id, "user-" + id);
        System.out.println("返回的用戶爲[" + user.toString() + "]");
        return user;
    }
}

現在我們訪問http://localhost:8080/user/6,來看控制檯打印的結果:
圖4
我們發現在url上輸入的6,在後端被轉換成了5,最終查詢的用戶也是id爲5的用戶,說明我們參數轉換成功了,然後我們來看瀏覽器得到的響應結果:
圖5
返回的用戶id是6,而不是後端查詢的5,說明我們對返回值的修改也成功了

5. 總結

在Web項目(這裏特指Spring項目)中使用自定義註解開發,其原理還是依賴於Spring的AOP機制,這一點就與我們普通的Java項目有所區別。當然,如果是開發其他框架而需要使用自定義註解時,則需要自己實現一套機制,不過原理本質上都是大同小異,無非是將一些模板操作進行了封裝

通過自定義的註解,我們不僅能夠在方法執行前後進行擴展,同時還可以獲取到作用方法的方法名,所在類等信息,更重要的是還能夠修改參數和返回值,這幾點應用下來基本就囊括了絕大部分自定義註解的功能。瞭解到這裏,完全就能夠自己動手來寫一個自定義註解來簡化我們的項目

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章