最近在看《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(401,"您还未登录,请登录后再试");
// 构造方法
@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 = 0; 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注解标注即可。