前言
在 Spring 框架中有一个 AOP 子框架,自己创建了一套独有的表达式语言,通过这些表达式语言,可以轻松控制业务类中某些方法横切或植入新功能,以达到低耦合的效果。Spring AOP 底层用的就是动态代理,不用写动态代理代码。
目标类有实现业务接口就默认用jdk动态代理,目标类没有实现业务接口就用cglib,有业务接口也可以通过指定配置项来使用cglib。
在 Spring Boot 中加入 AOP 是不需要在启动类加任何注解的,因为在AOP的默认配置属性中,spring.aop.auto 属性默认是开启的,也就是说只要引入了 AOP 依赖后,默认已经增加了 @EnableAspectJAutoProxy。
AOP 的若干名词
- 切面:就是新的业务模型,也叫切面类或者新功能类,它的实例化对象为切面对象。以注解@Aspect的形式放在类上方,声明一个切面。
- 目标对象:就是旧的业务模型实例化的对象,被一个或者多个切面所通知的对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个代理对象。
- 连接点:用代理对象调用目标方法的那句话的位置就叫连接点,但前提一定用代理对象调用才是连接点。
- 切点:其实就是筛选出的连接点,匹配连接点的断言,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。
- 通知:需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。包括如下五个方面:
@Before:在切点之前执行
@After:在切点方法之后执行
@AfterReturning:切点方法返回后执行
@AfterThrowing:切点方法抛异常执行
AOP的体系可以梳理为下图:
Spring Boot 如何整合 AOP
在实际开发中对于横向公共的逻辑需要抽取出来,这时候就需要使用AOP,比如日志的记录、权限的验证等等,这些功能都可以用注解轻松的完成。
- 添加依赖
<!--springboot集成Aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义需要的切面
package com.example.canal.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Component // 放到容器中
@Aspect // 标识为切面
@Slf4j // 引入 lombok 依赖,方便使用log
public class ControllerAspect {
/**
* (* com.example.canal.*.*(..))
* 第一个 * 代表所有的方法
* 第二个 * 前面 com.example.canal.* 下面的所有类
* 第三个 * 代表以及子类 最后面括号里面的
* .. 代表所有参数
*
* 定义切入点,切入点为 com.example.canal 下的所有函数
*/
@Pointcut("execution(* com.example.canal.yang.*.*(..))")
private void yangCut() {}
// 方法用 execution 指定,注解用 @annotation 指定,注解的类型是 @interface,算是一种特殊的接口
// @Pointcut("@annotation(com.chy.mall.annotation.UploadObs)")
/**
* 前置通知,在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。
*/
@Before("yangCut()")
public void before(JoinPoint joinPoint) {
// 接收到请求,RequestContextHolder来获取请求信息,Session信息
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("URL : " + request.getRequestURL().toString());
log.info("HTTP_METHOD : " + request.getMethod());
log.info("IP : " + request.getRemoteAddr());
// getSignature() 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "."
+ joinPoint.getSignature().getName());
// getArgs() 获取传入目标方法的参数对象
log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
System.out.println("正在执行前置通知");
}
/**
* 后置通知,在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
*/
@After("yangCut()")
public void after() {
System.out.println("正在执行后置通知");
}
/**
* 后置返回通知,可传递目标方法的返回值,在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 obj 目标方法的返回值
*/
@AfterReturning(value = "yangCut()", returning = "obj")
public void afterReturning(Object obj) {
System.out.println("目标方法的返回值是:" + obj);
}
/**
* 后置异常通知,可传递异常对象,在连接点抛出异常后执行。
*/
@AfterThrowing(value = "yangCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
log.info(joinPoint.getSignature().getName());
System.out.println("异常信息:" + e.getMessage());
}
/**
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
*/
// @Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 前增强
System.out.println("正在执行前置通知");
// 调用目标方法
Object object = proceedingJoinPoint.proceed();
// 后增强
System.out.println("正在执行后置通知");
// 返回目标方法的返回值
return object;
}
}