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單元測試也可以),會發現控制檯成功輸出:
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()));
}
然後我們再來執行一遍剛纔的流程,看看會輸出什麼結果:
現在我們再將這些內容放到日誌中,順便修改一下日誌的格式,如下:
@Before("pointcut() && @annotation(logger)")
public void advice(JoinPoint joinPoint, KthLog logger) {
System.out.println("["
+ joinPoint.getSignature().getDeclaringType().getSimpleName()
+ "][" + joinPoint.getSignature().getName()
+ "]-日誌內容-[" + logger.value() + "]");
}
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
,來看控制檯打印的結果:
我們發現在url上輸入的6,在後端被轉換成了5,最終查詢的用戶也是id爲5的用戶,說明我們參數轉換成功了,然後我們來看瀏覽器得到的響應結果:
返回的用戶id是6,而不是後端查詢的5,說明我們對返回值的修改也成功了
5. 總結
在Web項目(這裏特指Spring項目)中使用自定義註解開發,其原理還是依賴於Spring的AOP機制,這一點就與我們普通的Java項目有所區別。當然,如果是開發其他框架而需要使用自定義註解時,則需要自己實現一套機制,不過原理本質上都是大同小異,無非是將一些模板操作進行了封裝
通過自定義的註解,我們不僅能夠在方法執行前後進行擴展,同時還可以獲取到作用方法的方法名,所在類等信息,更重要的是還能夠修改參數和返回值,這幾點應用下來基本就囊括了絕大部分自定義註解的功能。瞭解到這裏,完全就能夠自己動手來寫一個自定義註解來簡化我們的項目