目录
前言
什么是AOP
什么是Spring AOP?
使用Spring AOP
引入依赖
编程AOP程序
优势
详解Spring AOP
Spring AOP核心概念
切点
连接点
通知
切面
通知类型
结论
切点提取
切面优先级 @Order 注解
切点表达式
execution切点表达式
@annotation
自定义注解
前言
在前面我们介绍了Spring有两大核心:IoC和AOP,我们已经讲解了什么是IoC,以及如何使用IoC,那么本篇我们就来讲解什么是AOP,以及如何使用Spring AOP。
什么是AOP
AOP(Aspect Oriented Programming) 是一种编程范型,即面向切面编程。
什么是面向切面编程?
切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。面向编程旨在通过分离横切关注点来提高代码的模块化和可维护性,允许开发者将那些与业务逻辑无关,但又需要在多个地方使用的功能(如日志记录、事务管理、安全性等)从业务逻辑中分离出来。在前面我们的登录校验,用了拦截器,其实也是对AOP思想的一种实现。统一数据返回格式和统一异常处理也是AOP的一种实现。
结论:AOP是一种思想,是对某一类事情的集中处理。
AOP是一种思想,实现它的方法有很多,如Spring AOP,以及AspectJ、CGLIB等。
接下来我们就来学习一下Spring AOP。
什么是Spring AOP?
Spring AOP是Spring框架对面向切面编程的支持,允许开发者将横切关注点从业务逻辑中分离出来,从而提高代码的模块化、可维护性和可扩展性。
Spring AOP 是基于动态代理实现的,支持通过注解或者XML配置来定义切面逻辑。那什么是动态代理,我们下一篇讲。
我们在前面学的拦截器、统一数据返回格式以及统一异常处理这些还不够吗?
拦截器作用的维度是URL(一次请求和响应),@ControllerAdvice 的应用场景主要是全局异常处理,数据绑定,数据预处理等,而AOP的作用维度更加细致(可以根据包、类、方法名、参数等进行拦截),能够实现更加复杂的业务逻辑。
假如我们现在有一个项目,想要对其中一些业务功能进行优化,那么我们就需要知道它的耗时时长,需要对每个接口都进行添加计算耗时的逻辑,这样就太麻烦了,而且成本高。
但如果我们使用AOP,可以对原始接口不修改的情况下,对特定的方法进行功能增强。
使用Spring AOP
引入依赖
在使用AOP之前,我们需要先引入AOP的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编程AOP程序
我们这里来编写一个AOP类,定义一个记录方法执行耗时的方法。
需要用 @Aspect 注解修饰类,同时我们需要将这个AOP类交给Spring容器来管理,需要用类注解修饰@Component:
package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {//切面是由切点+通知组成的@Around("execution(* com.example.demo.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}
}
- @Aspect:用这个注解修饰,表示这个类是一个切面类;
- @Around:环绕通知,在目标方法执行前后都会被执行。后面的表达式对哪些方法进行增强;
- pjp.proceed():让目标方法执行。
整个方法可以分为三部分:
package com.example.demo.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("/aop")
public class AopController {@RequestMapping("/t1")public String t1(){log.info("t1方法执行");return "t1";}}
我们运行一下:
可以看到,我们对调用的方法不比修改,就可以做出额外的一些工作。
优势
代码不侵入:不修改原始的业务方法,就可以堆原始的业务方法进行功能增强或是功能上的改变;
减少重复代码;
提高开发效率;
方便维护
详解Spring AOP
既然我们知道了Spring AOP的初始用法,那么就下来我们就来学习一下Spring AOP几个核心概念:
- 切点
- 连接点
- 切面
- 通知
Spring AOP核心概念
切点
切点(PointCut),也叫做“切入点”,提供一组规则(使用 Aspect pointCut expression language来描述),告诉程序哪些方法需要增强。
前面代码中在注解@Around后面的 execution(* com.example.demo.controller.*.*(..))
字符串就是切点表达式:
切点表达式中的含义我们后面讲。
连接点
满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法。例如上面例子中也就是在 com.example.demo.controller 包下的所有类都叫做连接点。
在上面的例子中,pjp就是一个连接点:
通过执行该参数的proceed() 方法就可以执行目标方法。
通知
通知指的就是具体做的工作,需要重复执行的逻辑,也就是共性功能(最终体现为一个方法)
我们在上面例子中计算方法执行耗时的逻辑,就是通知。
切面
切面(Aspect)是由切点(PointCut)+通知(Advice)组成的。
切面封装了横切关注点的逻辑,横切关注点指的是那些影响多个类或模块的逻辑,如日志记录、事务管理等。
通过切面就能够描述当前AOP程序需要对哪些方法,在什么时候执行什么样的操作。切面既包含了通知逻辑的定义,也包括了连接点的定义。切面所在的类,我们一般称为切面类(被@Aspect修饰的类)。
通知类型
在Spring AOP中的通知类型,不仅仅只有我们上面例子中用到的 @Around (环绕通知)注解,还有以下其它几种通知类型,总结起来:
- @Around:环绕通知,此注解修饰的通知方法在目标方法执行前后都被执行;
- @Before:前置通知,此注解标注的通知方法在目标方法执行前被执行;
- @After:后置通知,此注解标注的通知方法在目标方法执行后被执行;
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法执行前被执行,有异常不会执行;
- @AfterThrowing:异常后通知,此注解标注的通知方法在目标方法发送异常后执行
我们通过代码来加深一下对这几个通知的理解:
package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {//切面是由切点+通知组成的@Around("execution(* com.example.demo.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}@After("execution(* com.example.demo.controller.*.*(..))")public void afterRecordTime(){log.info("执行After通知");}@Before("execution(* com.example.demo.controller.*.*(..))")public void beforeRecordTime(){log.info("执行Before通知");}@AfterReturning("execution(* com.example.demo.controller.*.*(..))")public void afterReturningRecordTime(){log.info("执行AfterReturning通知");}@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")public void afterThrowingRecordTime(){log.info("执行AfterThrowing通知");}}
测试类:
package com.example.demo.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("/aop")
public class AopController {@RequestMapping("/t1")public String t1(){log.info("t1方法执行");return "t1";}@RequestMapping("/t3")public String t3(){int a = 1/0;return "t3";}
}
运行一下进行测试:
对于t1方法:
我们可以看到,在程序正常运行的情况下,由 @AfterThrowing 修饰的方法并不会被执行、。
同时,我们可以观察到, @Around 标识的通知方法包含两部分,一个“前置逻辑”,一个“后置逻辑”。其中“前置逻辑”会先于 @Before 标识的通知方法执行,“后置逻辑”会晚于 @After 标识的通知方法执行。
对于t3方法:
t3方法中我们给了一个异常, 在程序发生异常的时候,@AfterReturning 标识的方法不会被执行,但@AfterThrowing 标识的通知方法被执行了。
@Arounf环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会被执行(因为此时原始方法调用出现了问题)
结论
如果目标方法中不出现异常:
目标方法中出现异常:
注意:
- @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑;
- @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的;
- 一个切面类可以有多个切点
切点提取
在前面的代码中,我们可以看到,每个通知的切点表达式都是一样的,那么我们有没有办法可以将这些公共的表达式提取出来,需要的使用引入即可?
在Spring中,提供了 @PointCut 注解,可以把公共的切点表达式提取出来。
所以上面的代码我们可以修改为:
package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {@Pointcut("execution(* com.example.demo.controller.*.*(..))")public void pointCut(){}//切面是由切点+通知组成的@Around("pointCut()")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}@After("pointCut()")public void afterRecordTime(){log.info("执行After通知");}@Before("pointCut()")public void beforeRecordTime(){log.info("执行Before通知");}@AfterReturning("pointCut()")public void afterReturningRecordTime(){log.info("执行AfterReturning通知");}@AfterThrowing("pointCut()")public void afterThrowingRecordTime(){log.info("执行AfterThrowing通知");}}
如果存在多个切面类时,其他切面类想要使用这个切点定义时,就需要将访问修饰符修改为public,其他切面类引用这个切点表达式的方法需要使用 全限定类名.方法名() 。如果只想要在该类中使用,就用private修饰。
package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;@Slf4j
@Component
@Aspect
public class Aspectdemo {@After("com.example.demo.aspect.TimeAspect.pointCut()")public void after() {log.info("执行Aspectdemo的after方法");}@Before("com.example.demo.aspect.TimeAspect.pointCut()")public void before() {log.info(" 执行Aspectdemo的before方法");}
}
切面优先级 @Order 注解
那么存在多个切面的时候,它们的执行顺序是怎么的?随机的?其实不是的,如果有多个切面类的多个切入点都匹配到同一个目标方法,当目标方法运行时,会执行以下两个原则:
- 前面通知类型的先后顺序
- 切面类的类名排序
这里怎么不是先执行完Aspectdemo中的通知方法再执行TimeAspect中的通知方法?
切面类中的通知方法的执行顺序,我们可以看成以下这幅图:
那么如果我们想要改变一下切面类的执行顺序,就需要在想要优先执行的切面类上添加 @Order()注解。
可以看到,通过 @Order注解并填入value值,就能够改变切面类的执行顺序,value值越小的切面类优先级越高。
切点表达式
前面我们使用切点表达式来描述切点,下面我们就来介绍一下切点表达式的语法。
切点表达式常见有两种表达方式:
- excution(......):根据方法的签名来匹配;
- @annitation(......):根据注解匹配
execution切点表达式
execution()是最常用的一种切点表达式,用来匹配方法,语法:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中:访问修饰符合异常可以省略。
切点表达式支持通配符表达:
- *:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
包名使用 * 表示任意包(一层包使用一个*)
类名使用 * 表示任意类
返回值使用 *表示任意返回值类型
方法名使用 * 表示任意方法
参数使用 * 表示一个任意类型的参数
.. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
使用 .. 配置包名,表示此包以及此包下的所有子包
可以使用 .. 配置参数,任意个任意类型的参数
示例:
AopController下public修饰,返回类型为String,方法名t1,无参方法:
execution(public String com.example.demo.controller.AopController.t1())
省略访问修饰符:
execution(String com.example.demo.controller.AopController.t1())
匹配所有返回类型:
execution(* com.example.demo.controller.AopController.t1())
匹配AopController下的所有无参方法:
execution(* com.example.demo.controller.AopController.*())
匹配AopController下的所有方法:
execution(* com.example.demo.controller.AopController.*(..))
匹配controller包下所有类的所有方法:
execution(* com.example.demo.controller.*.*(..))
匹配所有包下面的AopController:
execution(* com..controller.AopController.*(..))
匹配com.example.demo包下,子孙包下的所有类的所有方法:
execution(* com.example.demo..*(..))
@annotation
execution表达式更适用有规则的,但如果我们想要匹配多个无规则的方法,比如我们想要增强AopController中t1方法和t2方法,但由于返回值不同,所以使用execution不能同时增强这两个方法。
那么这里就需要用到我们的 @annotation 注解来捕获更多无规则的方法。
自定义注解
那么如何使用 @annotation 注解呢?
我们首先需要创建出一个自定义的注解:
package com.example.demo.aspect;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
这里我们创建了一个简单的注解,参考@Component注解:
- @Target:标识了Annotation所修饰的对象范围,即该注解在什么地方可以用;
- ElementType.TYPE:⽤于描述类、接⼝(包括注解类型)或enum声明
- ElementType.METHOD:描述方法
- ElementType.PARAMETER:描述参数
- ElementType.TYPE_USE:可以标注任意类型
- @Retention 指Annotation 被保留的时间长短,标明注解的⽣命周期:
- RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使⽤。比如 @SuppressWarnings ,以及lombok提供的注解 @Data ,@Slf4j
- RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时⽆法获取。通常⽤于⼀些框架和⼯具的注解
- RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常⽤于⼀些需要在运行时处理的注解,如Spring的 @Controller @ResponseBody
我们自定完注解之后,就需要在切面类中使用@annotation切点表达式定义切点,只对@MyAspect生效。
@annotation中需要填入我们自定义注解的全限定名。
package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Slf4j
@Component
@Aspect
@Order(2)
public class Aspectdemo {@Around("@annotation(com.example.demo.aspect.MyAspect)")public Object around(ProceedingJoinPoint pjp) throws Throwable {log.info("执行Aspectdemo的around方法前");long start = System.currentTimeMillis();Object proceed = pjp.proceed();long end = System.currentTimeMillis();log.info("耗时:{}ms", end - start);log.info("执行Aspectdemo的around方法后");return pjp.proceed();}}
可以看到,我们只要在对应的方法上加上@MyAspect注解,就能够对对应方法进行增强。
以上就是本篇所有内容~
若有不足,欢迎指正~