1 理解AOP
1.1 什么是AOP
AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。
那么AOP为何那么重要呢?在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。例如下面这个示意图:
有多少业务操作,就要写多少重复的校验和日志记录代码,这显然是无法接受的。当然,用面向对象的思想,我们可以把这些重复的代码抽离出来,写成公共方法,就是下面这样:
这样,代码冗余和可维护性的问题得到了解决,但每个业务方法中依然要依次手动调用这些公共方法,也是略显繁琐。有没有更好的方式呢?有的,那就是AOP,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
1.2 AOP体系与概念
简单地去理解,其实AOP要做三类事:
在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。
在什么时候切入,是业务代码执行前还是执行后。
切入后做什么事,比如做权限校验、日志记录等。
因此,AOP的体系可以梳理为下图:
一些概念详解:
- Pointcut:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
- Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
- Aspect:切面,即Pointcut和Advice。
- Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
- Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
2 AOP实例
实践出真知,接下来我们就撸代码来实现一下AOP。
- 使用 AOP,首先需要引入 AOP 的依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
2.1 第一个实例
接下来,我们先看一个极简的例子:所有的get请求被调用前在控制台输出一句"get请求的advice触发了"。
具体实现如下:
- 1.创建一个AOP切面类,只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现advice:
packagecom.jingudi.framework.log.log.aspect;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.springframework.stereotype.Component;/**
* @author [email protected]
* @version 1.0
* @description: TODO
* @date 2022-4-10 下午 9:31
*/@Aspect@ComponentpublicclassLogAdvice{// 定义一个切点:所有被GetMapping注解修饰的方法会织入advice@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")privatevoidlogAdvicePointcut(){}@Before("logAdvicePointcut()")publicvoidlogAdvice(){// 这里只是一个示例,你可以写任何处理逻辑System.out.println("get请求的advice触发了");}}
- 2.随便创建一个接口类,内部创建一个get请求 (必须要有@GetMapping):
@ApiOperation("查询用户列表")@GetMappingpublicPageResult<UserEntity>getUserList(QueryUserVo vo){return userService.getUserList(vo);}
2.2 第二个实例
下面我们将问题复杂化一些,该例的场景是:
自定义一个注解PermissionsAnnotation
创建一个切面类,切点设置为拦截所有标注PermissionsAnnotation的方法,截取到接口的参数,进行简单的权限校验
将PermissionsAnnotation标注在测试接口类的测试接口test上
具体的实现步骤:
- 1.使用@Target、@Retention、@Documented自定义一个注解
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfacePermissionAnnotation{}
- 2.创建第一个AOP切面类,,只要在类上加个@Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现第一步权限校验逻辑:
packagecom.jingudi.advice;importcom.alibaba.fastjson.JSONObject;importcom.jingudi.modules.system.dto.DictDetailDto;importlombok.extern.log4j.Log4j;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.JoinPoint;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.Signature;importorg.aspectj.lang.annotation.*;importorg.springframework.core.annotation.Order;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;/**
* @author [email protected]
* @version 1.0
* @description: TODO
* @date 2022-4-10 下午 9:56
*/@Aspect@Component@Order(1)@Slf4jpublicclassPermissionFirstAdvice{// 定义一个切面,括号内写入第1步中自定义注解的路径@Pointcut("@annotation(com.jingudi.annotation.PermissionsAnnotation)")privatevoidpermissionCheck(){}@Before("permissionCheck()")publicvoidbeforeAdvice(JoinPoint joinPoint){// 这里只是一个示例,你可以写任何处理逻辑System.out.println("---------Before触发了----------");// 获取签名Signature signature = joinPoint.getSignature();// 获取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 获取即将执行的方法名String funcName = signature.getName();
log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);// 也可以用来记录一些信息,比如获取请求的 URL 和 IPServletRequestAttributes attributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 获取请求 URLString url = request.getRequestURL().toString();// 获取请求 IPString ip = request.getRemoteAddr();
log.info("用户请求的url为:{},ip地址为:{}", url, ip);}@Around("permissionCheck()")publicObjectpermissionCheckFirst(ProceedingJoinPoint joinPoint)throwsThrowable{// 这里只是一个示例,你可以写任何处理逻辑System.out.println("---------Around触发了----------");//获取请求参数,详见接口类Object[] objects = joinPoint.getArgs();System.out.println(objects);// Integer id = ((JSONObject) objects[0]).getInteger("id");DictDetailDto object1 =(DictDetailDto)objects[0];// 修改入参JSONObject object =newJSONObject();return joinPoint.proceed(objects);}@AfterReturning(pointcut ="permissionCheck()", returning ="result")publicvoidafterReturningAdvice(JoinPoint joinPoint,Object result){// 这里只是一个示例,你可以写任何处理逻辑System.out.println("---------AfterReturning触发了----------");Signature signature = joinPoint.getSignature();String classMethod = signature.getName();
log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);// 实际项目中可以根据业务做具体的返回值增强
log.info("对返回参数进行业务上的增强:{}", result +"增强版");}@AfterThrowing(pointcut ="permissionCheck()", throwing ="ex")publicvoidafterThrowing(JoinPoint joinPoint,Throwable ex){Signature signature = joinPoint.getSignature();String method = signature.getName();// 处理异常的逻辑
log.info("执行方法{}出错,异常为:{}", method, ex);}@After("permissionCheck()")publicvoidafterAdvice(JoinPoint joinPoint){// 这里只是一个示例,你可以写任何处理逻辑System.out.println("---------After触发了----------");Signature signature = joinPoint.getSignature();String method = signature.getName();
log.info("方法{}已经执行完", method);}}
@Order(0)
AOP加载顺序(切面加载顺序)
总结:
- 前置通知 在目标方法执行之前执行执行的通知。
- 环绕通知 在目标方法执行之前和之后都可以执行额外代码的通知。
- 后置通知 在目标方法执行之后执行的通知。
- 异常通知 在目标方法抛出异常时执行的通知。
- 最终通知 是在目标方法执行之后执行的通知。
以上5种都可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。
五种通知的执行顺序:
- 在目标方法没有抛出异常的情况下 前置通知 环绕通知的调用目标方法之前的代码 目标方法 环绕通知的调用目标方法之后的代码 后置通知 最终通知
- 在目标方法抛出异常的情况下 前置通知 环绕通知的调用目标方法之前的代码 目标方法 抛出异常 异常通知 最终通知
- 如果存在多个切面 多切面执行时,采用了责任链设计模式。 切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时, 去执行下一个切面或如果没有下一个切面执行目标方法,从而达成了如下的执行过程:
版权归原作者 Marion158 所有, 如有侵权,请联系我们删除。