学习 Spring AOP 之前,先要了解 AOP 是什么
AOP(Aspect Oriented Programming):面向切面编程,它和 OOP(面向对象编程)类似。
它是一种思想,是对某一类事情的集中处理。
比如用户登录权限的效验,在学习 AOP 之前,在需要判断用户登录的页面,都要各自实现或调用用户验证的方法,学习 AOP 之后,我们只需要在某一处配置一下,那么所有需要判断用户登录的页面就全部可以实现用户登录验证了,不用在每个方法中都写用户登录验证了
AOP 是一种思想,而 Spring AOP 是实现(框架),这种关系和 IOC(思想)与 DI(实现)类似
除了统一的用户登录判断外,AOP 还可以实现
Spring AOP 学习主要分为3个部分
(1)切面(Aspect)
定义 AOP 是针对某个统一的功能的,这个功能就叫做一个切面,比如用户登录功能或方法的统计日志,他们就各是一个切面。切面是由切点和通知组成的
(2)连接点(Join Point)
所有可能触发 AOP(拦截方法的点)就称为连接点
(3)切点(Pointcut)
切点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知,总的来说就是,定义 AOP 拦截的规则的
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)
(4)通知(Advice)
切面的工作就是通知
通知:规定了 AOP 执行的时机和执行的方法
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后悔通知本方法进行调用
举个例子,在一个生产型公司中
通知相当于底层的执行者,切点是小领导制定规则,切面是大领导制定公司的发展方向,连接点是属于一个普通的消费者用户
以多个⻚⾯都要访问⽤户登录权限为例子,AOP 整个组成部分如图所示
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkAm0DT8-1676207159825)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676183469495.png)]](/uploadfile/202405/e30a78b36af8dcc.png)
Spring AOP 实现步骤
接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的
⽅法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。
在中央仓库中搜锁 Spring AOP Maven Repository: Search/Browse/Explore (mvnrepository.com)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uIsDmKWi-1676207159826)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676185167353.png)]](/uploadfile/202405/670c9b706c20c28.png)
在 pom.xml 中添加如下配置:
org.springframework.boot spring-boot-starter-aop
@Aspect // 当前类是一个切面
@Component
public class UserAspect {// 定义一个切点(设置拦截规则)@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")public void pointcut() {}
}
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p4mQspnI-1676207159827)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676186116916.png)]](/uploadfile/202405/1e930885c2f9e2c.png)
实现通知方法也就是在什么时机执行什么方法
@Aspect // 当前类是一个切面
@Component
public class UserAspect {// 定义一个切点(设置拦截规则)@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")public void pointcut() {}// 定义 pointcut 切点的前置通知@Before("pointcut()")public void doBefore() {System.out.println("执行前置通知");}// 后置通知@After("pointcut()")public void doAfter() {System.out.println("执行后置通知");}// 返回之后通知@AfterReturning("pointcut()")public void doAfterReturning() {System.out.println("执行返回之后通知");}// 抛出异常之后通知@AfterThrowing("pointcut()")public void doAfterThrowing() {System.out.println("执行抛出异常之后通知");}
}
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/sayhi")public String sayHi() {System.out.println("sayhi 方法被执行");int num = 10/0;return "你好,java";}@RequestMapping("/sayhi2")public String sayHi2() {System.out.println("sayhi2 方法被执行");return "你好,java2";}
}
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qVSOeHQj-1676207159828)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676189891534.png)]](/uploadfile/202405/571486ae48b92e7.png)
环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为
// 添加环绕通知
@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;
}
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5xizlv7M-1676207159828)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676190278115.png)]](/uploadfile/202405/167fcb678547fb1.png)
Spring AOP 中统计时间用 StopWatch 对象
// 添加环绕通知@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint) {// spring 中的时间统计对象StopWatch stopWatch = new StopWatch();Object result = null;try {stopWatch.start(); // 统计方法的执行时间,开始计时// 执行目标方法,以及目标方法所对应的相应通知result = joinPoint.proceed();stopWatch.stop(); // 统计方法的执行时间,停止计时} catch (Throwable e) {e.printStackTrace();}System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "." +joinPoint.getSignature().getName() +"执行花费的时间:" + stopWatch.getTotalTimeMillis() + "ms");return result;}
AspectJ 表达式语法:SpringAOP & AspectJ
@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")
AspectJ 语法(Spring AOP 切点的匹配语法):
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
AspectJ ⽀持三种通配符
* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
… :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
+ :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的所有⼦类包括本身
修饰符,一般省略
返回值,不能省略
包,通常不省略,但可以省略
类,通常不省略,但可以省略
UserServiceImpl 指定类
*Impl 以 Impl 结尾
User* 以 User 开头
*任意
方法名,不能省略
addUser 固定方法
add* 以 add 开头
*DO 以 DO 结尾
*任意
参数
() 无参
(int) 一个整形
(int,int)两个整型
(…) 参数任意
throws可省略,一般不写
表达式示例
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pDirrfMk-1676207159829)(C:\Users\28463\AppData\Roaming\Typora\typora-user-images\1676192122209.png)]](/uploadfile/202405/fc4ff839f6f628e.png)
Spring AOP 动态代理实现:
默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类
JDK Proxy(JDK 动态代理)
CGLIB Proxy:默认情况下 Spring AOP 都会采用 CGLIB 来实现动态代理,因为效率高
CGLIB 实现原理:通过继承代理对象来实现动态代理的(子类拥有父类的所有功能)
CGLIB 缺点:不能代理最终类(也就是被 final 修饰的类)
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中
在目标对象的生命周期中有多个点可以进行织入
JDK 动态代理就是依靠反射来实现的
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被
代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {//⽬标对象即就是被代理对象private Object target;public PayServiceJDKInvocationHandler( Object target) {this.target = target;}//proxy代理对象@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//1.安全检查System.out.println("安全检查");//2.记录⽇志System.out.println("记录⽇志");//3.时间统计开始System.out.println("记录开始时间");//通过反射调⽤被代理类的⽅法Object retVal = method.invoke(target, args);//4.时间统计结束System.out.println("记录结束时间");return retVal;}public static void main(String[] args) {PayService target= new AliPayService();//⽅法调⽤处理器InvocationHandler handler = new PayServiceJDKInvocationHandler(target);//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建PayService proxy = (PayService) Proxy.newProxyInstance(target.getClass().getClassLoader(),new Class[]{PayService.class},handler);proxy.pay();}
}
public class PayServiceCGLIBInterceptor implements MethodInterceptor {//被代理对象private Object target;public PayServiceCGLIBInterceptor(Object target){this.target = target;}@Overridepublic Object intercept(Object o, Method method, Object[] args, MethodProxymethodProxy)throws Throwable {//1.安全检查System.out.println("安全检查");//2.记录⽇志System.out.println("记录⽇志");//3.时间统计开始System.out.println("记录开始时间");//通过cglib的代理⽅法调⽤Object retVal = methodProxy.invoke(target, args);//4.时间统计结束System.out.println("记录结束时间");return retVal;}public static void main(String[] args) {PayService target= new AliPayService();PayService proxy= (PayService) Enhancer.create(target.getClass(),new PayServiceCGLIBInterceptor(target));proxy.pay();}
}