最近在做接口的统一逻辑处理问题的时候学习了一下AOP,觉得很有帮助,故在此整理总结一下,希望对大家有所帮助。
AOP概述
AOP(Aspect Oriented Programming),面向切面思想,与IOC(控制反转)、DI(依赖注入)组成Spring的三大核心思想。既然是核心,那肯定是重要的。那么他为什么重要,以及在实际应用场景中我们可以用它来做什么呢?
不知道大家在开发过程中有没有遇到过这样的系统性需求:统计,权限检验,日志记录等等。可能很多人想到的就是在每一个调用方法的业务逻辑中都写一遍检验或者记录日志或者统计,例如我们需要在调用方法前进行一下权限的校验,在方法结束以后记录一下操作的日志,示意图如下:
这样做也的确是可以满足我们的需求,但是在实际的维护过程中非常不利,有多少业务操作,就要写多少重复的校验和日志记录代码,存在大量的冗余代码,这显然是无法接受的。如果你稍微觉得不妥,在原来的基础上进行一次优化,利用面向对象的思想,将这些冗余的代码抽离出来做成一个公共的方法,示意图如下:
这样的确是可以解决代码冗余和可维护性的问题,但是我们在使用的时候依然需要手动的调用这些公共的方法,看起来也有点不妥。那么有没有一种更好的方法,我们不需要每次都去调用,在调用方法前后就会自动帮我们进行对应的操作呢?答案是肯定的,用AOP就可以,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
AOP体系
AOP的体系大致如下图:
接下来对各个概念做一下解释:
Aspect
切面,包含Pointcut和Advice。在使用AOP进行切面定义的时候,在类上加上@Aspect即可定义一个切面类。例如我们需要定义一个日志切面LogAspect,则可如下定义,@Component 注解表示该类交给 Spring 来管理。
/**
* @Author likangmin
* @create 2020/11/24 17:03
*/
//定义切面类LogAspect
@Aspect
@Component
public class LogAdvice {
}
Pointcut
切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面),分为execution(系统注解,可以用路径表达式指定哪些类织入切面)方式和annotation(自定义注解,以指定被哪些注解修饰的代码织入切面)方式。在实际使用中,用@Pointcut注解定义一个切面,即某件事情的入口,如记录操作日志的入口,切入点定义了事件触发时机。
/**
* @Author likangmin
* @create 2020/11/24 17:03
*/
@Aspect
@Component
public class LogAspect{
/**
* 定义一个切面,拦截 com.kmli.aopexe.controller 包和子包下的所有方法
*/
@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")
public void pointCut() {
}
}
以上表示拦截com.kmli.aopexe.controller 包和子包下的所有方法,进入该包和子包下的所有方法都需要进行记录日志的操作。前面说了两种注解的方式,接下里解释一下两种方式中表达式的具体含义:
execution方式
以代码中execution(* com.kmli.aopexe.controller….(…))为例:
- 第一个 *号的位置:表示返回值类型,星号表示所有类型;
- com.kmli.aopexe.controller…: 表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包;
- 第二个 * 号的位置:表示类名,* 表示所有类;
- *(…):星号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
annotation方式
这种方式是针对某个注解来定义切面,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
/**
* @Author likangmin
* @create 2020/11/24 17:03
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {
}
使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping不同注解有各种特定处理逻辑的场景。
Advice
处理,包括处理时机(在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等)和处理内容(要做什么事,比如校验权限和记录日志)。
处理内容没有什么需要说明,主要看看处理时机的几个注解:
@Before
使用@Before 注解指定的方法在切面切入目标方法之前执行,这个时候我们可以做一些处理,如记录当前调用日志,获取用户请求的URL以及用户的IP等等信息。还是以上面的日志记录为例介绍一下使用方法(参数JointPoint对象很重要):
/**
* @Author likangmin
* @create 2020/11/24 17:03
*/
@Aspect
@Component
@Slf4j
public class LogAdvice {
/**
* 定义一个切面,拦截 com.mutest.controller 包下的所有方法
*/
@Pointcut("execution(* com.kmli.aopexe.controller..*.*(..))")
public void pointCut() {
}
/**
* 在定义的切面方法之前执行该方法
* @param joinPoint jointPoint
*/
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("====doBefore方法进入了====");
// 获取签名
Signature signature = joinPoint.getSignature();
// 获取切入的包名
String declaringTypeName = signature.getDeclaringTypeName();
// 获取即将执行的方法名
String funcName = signature.getName();
log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);
// 也可以用来记录一些信息,比如获取请求的 URL 和 IP
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求 URL
String url = request.getRequestURL()