最近在看《Spring 實戰》,說真的第四章《面向切面程式設計的Spring》講的真心很爛,看了幾遍都不清楚到底要表達什麼,也沒有講清楚Spring AOP 和 AspectJ的差別關系,終于讓我找到了一篇文章關于 Spring AOP (AspectJ) 你該知曉的一切,寫的是真好,這裡記錄一下。
接下來舉個我自己項目代碼的例子。
我們知道,很多時候,要判斷目前使用者是否已經登入,也就是進行鑒權,登入後擷取到使用者的userId,友善進行後續操作,鑒權這裡的邏輯,我們可以寫一個Bean去處理,在每一個Controller的方法裡用以下代碼實作:
@Autowired
private UserService userService;
public Object buy(HttpServletRequest request){
Integer userId = userService.getUserId(request);
if(userId == null){
return "鑒權失敗";
}
// 處理邏輯
return "處理成功";
}
這段代碼看着沒有什麼問題,但是如果這類的邏輯多了,就要出現很多重複性的代碼,滿篇的重複邏輯,這個時候面向切面程式設計就要登場了。
這裡我們通過環繞注解
@Around("@annotation(xxxxx)
的方式實作這個切面。
首先實作兩個注解。
@CheckLogin
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckLogin {
/**
* 設定為true會強制傳回 ApiResponse 401
*
* @return 是否強制要求登入
*/
boolean require() default true;
}
這個是一個标注方法的注解,标注的方法會調用鑒權邏輯進行鑒權,注解的參數require代碼是否要求強制登入。
@UserId
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserId {
}
這個是标注方法參數的注解,作用是如果鑒權成功,在切面程式設計的通知中會把标注的參數指派為鑒權後的userId,比如:
@CheckLogin
public ApiResponse ling(@UserId Integer userId){
//處理邏輯
}
實作切面的通知
@Aspect
@Component
public class LoginAspect {
// 鑒權用戶端對象
private final AuthClient authClient;
// 鑒權失敗時傳回值
private static final ApiResponse NOT_LOGIN_RESPONSE = ApiResponse.error(,"您還未登入,請登入後再試");
// 構造方法
@Autowired
public LoginAspect(AuthClient authClient) {
this.authClient = authClient;
}
// 聲明目前切面為環繞型切面,切入點為注解切入,注解CheckLogin标注的方法都進行環繞切入
@Around("@annotation(com.cyf.annotation.CheckLogin)")
public Object requireLogin(ProceedingJoinPoint pjp) throws Throwable {
//擷取切入的方法簽名
MethodSignature signature = (MethodSignature) pjp.getSignature();
//擷取方法
Method method = signature.getMethod();
//擷取注解執行個體
CheckLogin checkLogin = method.getAnnotation(CheckLogin.class);
Integer userId = null;
try {
//進行鑒權操作
userId = authClient.getUserId();
} catch (ThriftCallException e) {
if (!(e instanceof AuthNoPassException)) {
throw e;
}
//如果必須登入
if (checkLogin.require()) {
//如果傳回值是ResponseEntity
if (method.getReturnType().isAssignableFrom(ResponseEntity.class)) {
return ResponseEntity.ok(NOT_LOGIN_RESPONSE);
} else {
return NOT_LOGIN_RESPONSE;
}
}
}
// 擷取切點方法的參數
Object[] args = pjp.getArgs();
// 給切點的userId指派
assignUserId(method, args, userId);
return pjp.proceed(args);
}
//給切點的@userId注解标注的方法指派
private void assignUserId(Method method, Object[] args, Integer userId){
// 擷取方法參數的所有注解
Annotation[][] annotations = method.getParameterAnnotations();
//擷取方法參數的類型
Class<?>[] types = method.getParameterTypes();
for (int i = ; i < annotations.length; i++) {
Annotation[] arr = annotations[i];
for (Annotation a : arr) {
Class<? extends Annotation> c = a.annotationType();
//判斷是@UserId注解
if (c.isAssignableFrom(UserId.class)) {
Class clazz = types[i];
if (clazz == Integer.class) {
args[i] = userId;//給參數指派
return;
} else if (clazz == Optional.class) {
args[i] = Optional.ofNullable(userId);//給Optional參數指派
return;
}
break;
}
}
}
}
}
使用切面
@RestController
@RequestMapping("test")
public class TestApi {
@CheckLogin
public ApiResponse buy(@UserId Integer userId){
// 處理邏輯
return "處理結果";
}
}
是不是變得很簡單,需要鑒權的部分隻需要用@CheckLogin注解标注即可。