Spring AOP 面向切面编程
**概述:**AOP(Aspect Oriented Programming)面向切面编程。
**实现:**动态代理,就是面向切面编程最主流的实现。而Spring AOP 是Spring 框架的高级技术,旨在管理bean 对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
步骤:
1、导入依赖:
在pom.xml里面导入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、编写 aop 程序:针对于特定方法根据业务需要,进行编程
@Component // 表名当前类是配置类 交给IOC容器进行管理
@Aspect // 表名当前类是 切面类
@Slf4j //日志 注解
public class TimeAspect{
@Around("execution(* com.my.service.*.*(..))") //第一个* 返回值任意 第二个* 类名或者接口名 第三个* 代表方法名 表名针对于所有service包下的所有方法
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//proceedingJoinPoint 固定参数 调用原始方法执行
Long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed(); //调用原始方法开始执行 object原始方法的返回值
Long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature()+"执行耗时:{}ms",end-begin); //proceedingJoinPoint.getSignature()得到原始方法的签名
return object;
}
}
**AOP场景:**记录操作日志、权限控制、事务管理…
**优势:**代码无侵入、减少重复代码、提高开发效率、维护方便…
核心概念:
- 连接点: JoinPoint , 可以被AOP控制的方法(暗含方法执行时的相关信息)。
- 通知: Advice , 指那些重复的逻辑,也就是共性功能,(最终体现为一个方法)。
- 切入点: PointCut , 匹配连接点的条件,通知仅会在切入点方法执行时被调用。
- 切面: Aspect , 描述通知与切入点的对应关系(通知 + 切入点)
- **目标对象:**Target , 通知所应用的对象
AOP执行流程:
在方法执行时会创建一个代理对象,代理对象在运行时就会根据切点,加入通知,对方法进行增强
- 配置阶段
- 依赖加载:在项目启动阶段,当添加了 Spring AOP 相关依赖(如
spring - boot - starter - aop
)后,Spring 容器会加载必要的 AOP 库和配置。 - 启用 AOP:在 Spring 配置类中(如果是 Spring Boot 项目,很多配置是自动完成的),通过
@EnableAspectJAutoProxy
注解开启 AOP 功能。这个注解会触发 Spring 对 AspectJ 切面的自动代理机制,它告诉 Spring 去扫描带有@Aspect
注解的类,并将它们注册为切面。 - 组件扫描:Spring 容器会扫描被
@Component
注解标记的切面类。例如,一个自定义的切面类@Aspect @Component
会被扫描并加载到容器中。
- 依赖加载:在项目启动阶段,当添加了 Spring AOP 相关依赖(如
- 代理对象创建阶段
- 目标对象识别:当 Spring 容器加载了目标对象(被切面增强的业务对象)后,它会根据配置决定是否需要为这个目标对象创建代理。如果目标对象的方法被切面中的切点所匹配,就会触发代理对象的创建。
- 代理方式选择:
- JDK 动态代理:如果目标对象实现了接口,Spring 默认会使用 JDK 动态代理。JDK 动态代理通过反射机制在运行时生成代理类,这个代理类实现了目标对象的接口。例如,假设有一个
UserService
接口和它的实现类UserServiceImpl
,如果要对UserServiceImpl
进行 AOP 增强,并且UserServiceImpl
实现了UserService
接口,那么就会生成一个实现UserService
接口的代理类。 - CGLIB 代理:如果目标对象没有实现接口,Spring 会使用 CGLIB 代理。CGLIB 通过字节码增强技术在运行时生成目标对象的子类作为代理对象。例如,对于一个没有实现任何接口的
SomeBusinessClass
,Spring 会生成一个SomeBusinessClass
的子类作为代理对象。
- JDK 动态代理:如果目标对象实现了接口,Spring 默认会使用 JDK 动态代理。JDK 动态代理通过反射机制在运行时生成代理类,这个代理类实现了目标对象的接口。例如,假设有一个
- 代理对象生成:无论是 JDK 动态代理还是 CGLIB 代理,生成的代理对象都包含了对目标对象的引用,并且代理对象会拦截对目标对象方法的调用。
- 运行时增强阶段(方法调用阶段)
- 切点匹配:当代理对象拦截到对目标对象方法的调用时,它首先会检查当前调用的方法是否与切面中的切点表达式相匹配。例如,在一个切面中有切点表达式
execution(* com.example.service..*.*(..))
,如果调用的方法属于com.example.service
包及其子包下的任何类的任何方法,就会匹配成功。 - 通知执行:
- Before 通知:如果匹配成功且切面中有
@Before
通知,那么在目标方法执行之前,代理对象会先执行@Before
通知中定义的逻辑。例如,在一个日志记录切面中,@Before
通知可能会记录方法的开始时间、输入参数等信息。 - Around 通知:对于
@Around
通知,它会完全包裹目标方法的执行。代理对象会先执行@Around
通知中的前置逻辑,然后决定是否调用目标方法。如果调用目标方法,在目标方法执行完成后,还会执行@Around
通知中的后置逻辑。这种通知可以灵活地控制目标方法的执行,比如在事务管理中,@Around
通知可以在方法执行前开启事务,根据方法执行情况决定是否提交或回滚事务。 - After - returning 通知:如果目标方法正常返回,并且有
@After - returning
通知,代理对象会在目标方法返回后执行该通知中的逻辑。例如,对返回的数据进行格式转换或缓存操作。 - After - throwing 通知:如果目标方法抛出异常,并且有
@After - throwing
通知,代理对象会在异常抛出后执行该通知中的逻辑。例如,记录异常信息或者进行异常处理的补偿操作。 - After 通知:无论目标方法是正常返回还是抛出异常,
@After
通知中的逻辑都会在最后执行。它可以用于清理资源等操作,比如关闭数据库连接或者释放文件句柄。
- Before 通知:如果匹配成功且切面中有
- 目标方法执行:在执行完所有符合条件的通知逻辑(根据通知类型和方法执行情况)后,代理对象会调用目标对象的原始方法。如果通知逻辑中有对目标方法执行的控制(如
@Around
通知),则按照通知中的逻辑执行目标方法。 - 返回结果:目标方法执行完成后,其返回返回结果会沿着代理对象和通知的执行路径反向传递。如果有通知对返回结果进行了处理(如
@After - returning
通知),那么经过处理后的结果会最终返回给方法调用者。
- 切点匹配:当代理对象拦截到对目标对象方法的调用时,它首先会检查当前调用的方法是否与切面中的切点表达式相匹配。例如,在一个切面中有切点表达式
通知类型:
- @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都会被执行。
- @Before: 前置通知,此注解标注的通知方法在目标方法前被执行
- @After: 后置通知 , 此主机标注的通知方法在目标方法后被执行,无论是否有异常都会被执行
- @AfterReturning: 返回后通知 , 此注解标注的通知方法在目标方法后被执行,有异常 不会被 执行
- @AfterThrowing: 抛出异常后通知 , 此注解标注的通知方法发生异常后执行,没有异常不会执行
// 注意事项:
@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他同志则不需要考虑目标方法执行
@Around 环绕通知方法的返回值,必须指定为 Object ,来接收原始方法的返回值
@Around 环绕通知调用原始方法,原始方法有异常时,环绕后就 不会 通知
@Order 执行顺序
当一个项目中有多个切面类时,我们可以使用 @Order注解 来指定 切面的执行顺序, 数字越小越先执行,例如 标注@Order(1)
的切面类中的通知会在标注@Order(2)
的切面类中的通知之前执行(在相同类型的通知和相同连接点的情况下)。
注意:当你不定义时,默认的执行顺序和切面类的类名有关
用 @Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行;
目标方法后的通知方法,数字小的后执行;
切入点表达式
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
- 根据方法的签名来匹配: execution(…) ;
- 根据注解来匹配: @Annotation(…) ;
切入点表达式—execution
execution 主要根据方法得返回值,包名、类名、方法名、方法参数等信息来匹配;
//语法
execution(访问修饰符 ? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
访问修饰符可以省略
包名.类名可以省略 不建议省略 !!!
throws 异常可以省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
@Before("execution(public void com.my.service.impl.serviceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint){}
可以使用通配符描述切入点
单个 * ,可以通配任意返回值,包名,类名,方法名,任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*.update*(*)) //update开头,后面什么无所谓
符号 … 可以通配任意层级的包、或任意属性、任意个数的参数
execution(* com.my..service.*(..)) //匹配com.my包及其子包下service目录(包括子目录)中的所有类的所有方法,
例如:全局配置的话可以加上一个切入点表达式
@Pointcut("execution(* com.my.service.impl.serviceImpl.*(..))")
public void pointCut(){} //定义为private的话。外部类就不可以使用了,只有当前类可以使用
通知引用时可以直接引用
例如 @Around("pointCut()")
切入点表达式—@annotation
@annotation 切入点表达式,用于匹配标识有特定注解的方法
@annotation(com.my.aop.MyAnnotation) //作用在有MyAnnotation注解的方法上
自定义注解步骤:
- 创建一个annotation 接口
- 类上面加上 @Retention(RetentionPolicy.RUNTIME) 表明这个注解在运行时生效 , @Target(ElementType.METHOD) 表名当前注解作用在方法上。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation{}
连接点
在Spring 中 用JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名,方法名,方法参数等。
- 对于@Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其他四种通知来说,获取连接点的信息只能使用 JoinPoint ,他是 ProceedingJoinPoint 的父类型
ProceedingJoinPoint (Around 专属连接点)
@Around("execution(* com.my.service.impl.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String calssName = joinPoint.getTarget().getClass().getName(); //获取目标方法 类名
Signature signature = joinPoint.getSignature(); //获取目标方法 签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
Object res = joinPoint.proceed(); //执行原始方法,返回方法执行结果(环绕通知)
return res;
}
JoinPoint (其余四种通知类型)
@Before("execution(* com.my.service.*(..))")
public void before(JoinPoint point){
String className = point.getTarget().getClass().getName(); //获取目标方法 类名
Signature signature = point。getSignature(); //获取目标方法 签名
String methodName = point.getSignature().getName(); //获取目标方法名
Object[] args = point.getArgs(); //获取目标方法运行参数
}
案例:
将增、删、改相关接口的操作日志记录到数据库表中
操作日志:包含操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
思路分析:需要对所有业务类中的增、删、改方法添加统一的功能,使用 AOP 最方便,@Around 环绕通知
步骤
准备:
- 在案例工程中引入 AOP 的起步依赖
编码:
- 自定义注解 @MyLog 仅仅做标识作用
- 定义切面类,完成记录操作日志相关的逻辑
public class Log{
//需要记录如下日志属性
Integer operateUser; // 操作人ID
LocalDateTime operateTime; //操作时间
String calssName; //操作类名
String methodName; //操作方法名
String methodParams; //操作方法参数
String returnValue; //操作方法返回值
Long costTime; //操作耗时
}
下面在切面类中获取以上属性
@Around("@annotation(com.my.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint point) throws ThrowAble{
Log log = new Log();
//记录操作人的ID-当前登录人的id
log.setOperateUser(当前登陆人);
//操作时间
log.setoperateTime(new Date());
//操作类名
log.setClassName(point.getTarget().getClass().getName());
//操作方法名
log.setMethodName(point.getSignature().getName());
//操作方法参数
Object[] args = point.getArgs();
String methodParams = Arrays.toString(args); //将数组转化成为字符串
log.setMethodParams(methodParams);
//操作方法返回值
//记录耗时 原始方法开始之前记录开始时间
Long begin = System.currentTimeMillis();
Object result = point.proceed();
Long end = System.currentTimeMillis(); //结束时间
String resultValue = JSONObject.toJSONString(result); //fastJSON包下的方法 将结果转成JSON字符串
//操作耗时
log.setCostTime(end - begin);
mapper.insert(log);
return resultValue;
}
之后在增删改的controller 接口方法上面加上 @MyLog 注解