AOP(Aspect Oriented Programming):面向切面编程,它是一种编程思想,是对某一类事情的集中处理;它能够在不改原有代码的前提下对其功能进行增强,就是你代码已经写好了,使用 AOP 可以在不改动代码的前提下增强功能,如对于一个功能,可以基于 AOP 完成对该功能执行效率的计算,能够在功能正式执行前或者执行后,添加其他的功能执行,能够在该功能发生异常后,对其异常进行处理。
想象一个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?
如果不使用 AOP,我们就需要在每一个 Controller 层都写一遍验证用户是否已经登录的逻辑,如果你实现的功能有很多,并且这些功能都需要进行登录验证,那你就需要编写大量重复的代码, 这样代码修改和维护的成本也会很高。
但如果使用 AOP,在进入核心的业务代码之前会做统一的一个拦截,去验证用户是否登录,验证通过的就可以继续请求,此时就不需要每一处都写相同的用户登录逻辑了。
除了登录验证功能之外,还有很多功能也可以使用 AOP,比如:
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP (Object Oriented Programming,面向对象编程)的补充和完善,它可以将横切关注点从应用程序的主业务逻辑中分离出来,使得这些关注点可以集中处理,从而提高代码复用性、可维护性和系统可扩展性。
SpringAOP 是一个框架,提供了对 AOP 的实现,与 IOC 与 DI 的关系类似。
在 Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会调用对应满足条件的方法:
AOP 整个组成部分的概念如下图所示,以多个⻚⾯都要访问⽤户登录权限为例:
1️⃣第一步,添加 Spring Boot AOP 依赖支持。
在 SpringBoot 项目中导入 AOP 依赖时可以不设置版本号,SpringBoot 会帮助我们自动适配。
org.springframework.boot spring-boot-starter-aop 2.7.2
2️⃣第二步,定义切面,我们使用@Aspect注解将类标识为切面类,并使用注解 @Component 将类实例化到容器中。
package com.example.demo.common; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect // 定义切面 @Component public class UserAspect { }
3️⃣第三步,定义切点,配置拦截规则,可以使用AspectJ表达式类来进行描述,它的语法格式如下:
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
AspectJ ⽀持三种通配符:
*:匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)。
.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和*联合使⽤。
+:表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如com.cad.Car+,表示继承该类的
所有⼦类包括本身。
其中访问修饰符和异常可以省略,比如下面这个表达式:
任意声明一个方法,不需要具体实现,使用注解@Pointcut修饰,里面的value属性填写AspectJ表达式,这个方法就可以视为连接目标方法的一个切点。
但要注意,这种表达式的书写是非常繁琐的,目前有更好的AOP实现,可以更加灵活的配置,也就是说这里的写法其实并不常用。
代码实现:
@Aspect // 定义切面 @Component public class UserAspect { // 切点 @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut() { } }
4️⃣第四步,创建通知。
有了切点,要对切点处的方法进行相关处理,需要编写具体的增强方法,也就是通知,下面我们需要在切面类中简单编写几个方法来表示通知,通知就是将共性功能抽取出来后形成的方法。
要注意对于环绕通知,由于在实现增强方法时,需要介入到方法执行前和后,那么必须得获取到目标的方法,不然无法控制你实现的那些代码是在目标方法执行前执行的,哪些方法是在目标方法执行后执行的。
对于这项工作,SpringAOP 已经帮我们做了,所有的切点方法都已经加载到ProceedingJoinPoint对象当中,只要调用proceed方法就能够执行目标方法。
package com.example.demo.common; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect // 定义切面 @Component public class UserAspect { // 切点 @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut() { } // 前置通知通知 @Before("pointcut()") public void doBefore() { System.out.println("执行了前置通知"); } // 后置通知 @After("pointcut()") public void doAfter() { System.out.println("执行了后置通知"); } // 环绕通知 @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("环绕通知执行之前"); // 执行目标方法 Object result = joinPoint.proceed(); System.out.println("环绕通知执行之后"); return result; } }
5️⃣第五步,创建连接点。
package com.example.demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/getuser") public String getUser(){ System.out.println("do getUser"); return "get user"; } @RequestMapping("/deluser") public String delUser(){ System.out.println("do delUser"); return "del user"; } }
🍂此时,就可以启动 SpringAOP 了,然后通过浏览器去访问定义的这两个连接点,看通知效果。
🍂我们可以在Controller层再创建一个ArticleController类用来对照,访问一下看此时是否会执行通知。
package com.example.demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/art") public class ArticleController { @RequestMapping("/getart") public String getArticle(){ System.out.println("do getArticle"); return "getArticle"; } }
结果如下:
因为我们设置的拦截规则不包含这个类,也就不会执行通知了。
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截(使用动态代理技术实现方法的调用)。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用 SpringAOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。
下面简单的来说一下原理,首先,要有一个目标对象,在上面的例子中,目标对象就是UserController,然后通过基于这个目标对象,创建一个代理类,并在某一规定的时机生成,这个时机就是织入,最后在这个代理类上加上一些增强的方法,这个过程就叫做引入。
🎯下面又出来了一组相关概念:
目标对象:代理的目标对象。
织入(weaving): 即代理的生成时机,织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入∶
引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
🍂JDK Proxy 与 CGLIB 的区别: