一、简介Spring AOP
APO指的是面向切面编程,与OOP(面向对象)类似,是对某一类事情的集中处理。那么Spring AOP就是这个思想的具体实现。
例如对用户的登录权限进行校验,在没有AOP之前就需要在判断用户是否是登录状态的页面都需要实现或者调用验证用户登录的方法,但是在使用AOP之后,就只需要在某一处进行配置,所有判断用户是否是登录状态的页面就全都可以实现用户登录验证了。
二、Spring AOP的相关概念
切面:定义AOP处理的统一功能,这个功能就叫做切面,比如验证用户登录的功能就可以成为一个切面,切面由切点和通知组成。
连接点:触发AOP(拦截方法)的点就可以称之为连接点。
切点:定义AOP的拦截规则。
通知:规定AOP的执行时机和执行方法,通知可以有前置通知、后置通知、抛出异常后通知、返回数据后通知以及环绕通知。
通俗点来讲:切面就是设想一个大型活动,那么切点就是活动策划书,通知就是活动的具体执行,连接点就是触发活动策划中的一些事件。
三、Spring AOP的具体实现
实现Spring AOP具体可以分为如下几步:
- 在项目中添加Spring AOP框架;
- 定义切面;
- 定义切点;
- 实现通知。
添加Spring AOP框架
Spring Boot框架中并没有Spring AOP框架 ,需要去maven中心库进行引入与自己项目版本相匹配的依赖。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>3.0.5</version> </dependency>
定义切面
定义切面时就需要创建一个类,添加@Aspect注解表示该类是一个切面。
例如定义一个UserAspect类:
@Aspect //表示当前类是一个切面 @Component public class UserAspect { }
定义切点
切点是切面中的一个方法,需要使用@PointCut注解,里面需要传入一个AspectJ表达式,AspectJ是一个第三方库,但是由于比较好用,SpringAOP也是兼容的。
如下为AspectJ表达式:
在AspectJ表达式中也可以使用如下的通配符:
- *:可以匹配任意的内容,可以使用在返回值、包名、类名以及方法名。
- ..:匹配任意字符,常用在方法的参数中,若在类上使用就需要配合*来使用。
- +:用于匹配指定类及其所有的子类。
例如,在UserAspect切面中定义如下的切点:
@Pointcut("execution(* com.example.spring_aop.controller.UserController.*(..))") public void pointCut(){}
实现通知
在controller包下定义UserController类,并定义sayHi方法:
@RestController @RequestMapping("/user") public class UserController { @RequestMapping("/sayhi") public String sayHi(){ return "hi!"; } }
前置通知
表示在执行目标方法之前执行该方法,使用@Before注解,其中需要传入的参数是切点的方法名。
@Before("pointCut()") public void doBefore(){ System.out.println("前置方法已经执行"); }
那么访问UserController的sayhi页面:
并且前置方法也被执行了:
后置通知
在执行目标方法之后执行该方法,使用@After注解,同样需要传入切点的方法名。
@After("pointCut()") public void doAfter(){ System.out.println("后置方法已经执行"); }
那么再次访问sayhi页面就会有如下:
返回数据后通知
在目标方法返回数据后执行该方法,如果没有返回数据该方法就不会执行,该方法的执行通常是在后置通知之前执行,使用@AfterReturning注解,传入的参数同样也是切点的方法名。
那么再次访问sayhi页面之后就出现如下所示:
抛出异常后通知
在目标方法抛出异常后执行该方法,如果没有抛出异常该方法就不会执行,使用@AfterThrowing注解,传入的参数同样也是切点的方法名。
例如将sayhi方法修改如下:
@RequestMapping("/sayhi") public String sayHi() throws Exception{ int num = 1/0; return "hi!"; }
那么再次对sayhi进行访问时就会出现如下:
由于在返回数据之前就已经抛出异常了就没有正常返回数据,因此就不会执行返回数据后的通知。
环绕通知
包裹了被通知的方法,在被通知方法通知之前和调用之后使用自定义方法,使用@Around注解,传入的参数同样也是切点的方法名。
该通知相比之前的通知较为复杂,返回值是Object,传入的参数是ProceedingJoinPoint对象,其中方法体中调用该对象的proceed()方法。
@Around("pointCut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object result = null; System.out.println("环绕通知开始"); try { result = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("环绕通知结束"); return result; }
对sayhi进行访问时就会出现如下所示:
那么利用环绕通知可以求目标方法的执行时间:
@Around("pointCut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object result = null; System.out.println("环绕通知开始"); try { long start = System.currentTimeMillis(); result = joinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println(joinPoint.getSignature().getName()+"方法的执行时间:"+(end-start)+"ms"); } catch (Throwable e) { e.printStackTrace(); } System.out.println("环绕通知结束"); return result; }
运行结果:
但是这样写只适合单线程的情况,可以在环绕方法中定义一个StopWath对象, 调用其start方法以及stop方法来计算目标方法的运行时间。
@Around("pointCut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object result = null; System.out.println("环绕通知开始"); StopWatch stopWatch = new StopWatch(); try { stopWatch.start(); result = joinPoint.proceed(); stopWatch.stop(); System.out.println(joinPoint.getSignature().getName()+"方法的执行时间:"+stopWatch.getTotalTimeMillis()+"ms"); } catch (Throwable e) { e.printStackTrace(); } System.out.println("环绕通知结束"); return result; }
四、Spring AOP的实现原理
不引入AOP,就是前端和后端直接进行交互,但是在引入AOP之后,AOP就是中间的代理商,进行动态代理,Spring对AOP仅支持方法级别的拦截,AOP在运行期开始代理。
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理:
JDK Proxy(JDK动态代理);
GGLIB Proxy:默认情况下使用GGLIB Proxy进行动态代理,GGLIB Proxy通过继承代理对象来实现动态代理,所以GGLIB Proxy不能代理被final修饰的类,就需要使用JDK Proxy进行代理。