一. 初识SpringAOP
1.1 AOP是什么?
AOP(Aspect Oriented Programming):⾯向切面编程,它是⼀种思想,它是对某⼀类事情的 集中处理。比如在我们之前我的博客系统中所学习的用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法.但是有了AOP之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就可以全部实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了.
AOP是一种思想,Spring AOP是一个框架.SpringAOP是对AOP思想的实现.类似于IoC和DI的关系.
1.2 为什么要使用AOP?
AOP的优势在于可以提高代码的可重用性和可维护性,减少了代码的重复性和耦合度.它能够将通用功能从核心业务逻辑中解耦出来,使得系统更加模块化和灵活。
比如说我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器都需要先验证用户登录的状态,此时我们应该如何处理呢?
我们之前的处理方式是每个 Contraller
都需要写一遍用户登录验证,但是当功能越来越多时,要写的登录验证也越来越多,这些方法又是相同的.这么多的方法就会有代码修改和维护的成本.此时我们对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP
来统一处理.
除了统一的用户登录判断之外,AOP还可以实现:·
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
1.3 AOP组成
-
切面(Aspect)
(类):指的是某一方面的具体内容就是一个切面,比如用户的登录判断就是一个“切面”,而日志的统计记录它有是一个“切面”。 切面是包含了通知,切点和切面的类,相当于AOP实现的某个功能的集合. -
切点(Pointcut)
(方法):定义(一个)拦截规则。 -
通知(Advice)
(方法具体实现代码):执行AOP具体逻辑业务.或者称为增强. -
连接点(Joinpoint)
:所有可能触发切点的点就叫做连接点. 连接点相当于需要被增强的某个 AOP 功能的所有方法。 4.1 前置通知:在目标方法(实际要执行的方法)调用之前执行的通知; 4.2 后置通知:在目标方法调用之后执行的通知; 4.3 环绕通知:在目标方法调用前、后都执行的通知; 4.4 异常通知:在目标方法抛出异常的时候执行的通知; 4.5 返回通知:在目标方法返回的时候执行通知。
二. Spring AOP的实现
1. 添加Spring AOP依赖
在创建好的Spring Boot项目的pom.xml
中添加Spring AOP
的依赖,我们可以从中央仓库中下载.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后点击刷新,触发下载。
2. 定义切面和切点
这里使用注解@Aspect
表示定义切面,即UserAspect
类为切面,使用@Component
注解表示让切面随着框架的启动而启动,这样切面中的切点定义的拦截规则才能生效。
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component //让切面随着框架的启动而启动
public class UserAspect {
//定义切点,此处使用 AspectJ 表达式语法
@Pointcut("execution(* com.exaple.demo.controller.UserController.*(..))")
public void pointcut(){
}
}
上述代码中,pointcut
方法为空方法,它不需要有方法体,此方法名就是起到一个"标识"的作用,标识下面的通知方法具体指的是那个切点。因为一个切面中有很多切点。
上述pointcut
方法上添加的@Pointcut
注解的参数中使用切点表达式定义了具体的拦截规则。
execution(* com.example.demo.controller.UserController.*(..))
切点表达的意思是:拦截UserController类中的所有方法其参数为任意参数并且返回值是任意类型的返回值.
execution
:表示的意思为执行,执行的是后面跟的()中的规则。*
:表示的多个部分组成的,有修饰符和返回值类型。com.example.demo.controller.UserController
表示要拦截com.example.demo.controller包中的UserController类- 类后面跟的
*
表示UserController类中的所有方法。 ..
表示的不定式传参
切点表达式由切点函数组成,其中execution()
是最常见的切点函数用来匹配方法,语法为:
execution(<修饰符><返回值类型><包.类.方法(参数)><异常>)
常见表达式示例:
execution(* com.example.demo.User.*(..))
:匹配User类中的所有方法。execution(* com.example.demo.User .*(..))
:匹配该类的子类包括该类的所有方法execution(* com.example.*.*(..))
:匹配com.example包下的所有类的所有方法execution(* com.example..*.*(..))
:匹配com.example包下,子孙包下所有类的所有方法execution(* addUser(String,int))
:匹配addUser方法,其第一个参数类型是String,第二个参数类型是int。
创建UserController
类,这个类中的方法哪一个要被执行(目标方法)哪一个就是连接点.
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 "getUser";
}
@RequestMapping("/deluser")
public String delUser(){
System.out.println("do delUser");
return "delUser";
}
}
3. 定义相关通知
通知定义的是被拦截的方法具体要执行的业务。比如用户登录权限验证方法就是具体要执行的业务。 Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知使用
@Before
:通知方法会在目标方法(连接点)调用之前执行 - 后置通知使用
@After
:通知方法会在目标方法(连接点)返回或者抛出异常后调用 - 返回之后通知使用
@AfterReturning
:通知方法会在目标方法(连接点)返回后调用 - 抛异常后通知使用
@AfterThrowing
:通知方法会在目标方法(连接点)抛出异常后调用 - 环绕通知使用
@Around
:通知包裹了被通知的方法,在被通知的方法之前和调用之后执行自定义的行为。
- 前置通知和后置通知的实现:
package com.example.demo.common;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
//定义切点,@Pointcut注解的参数中定义了具体的拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//定义前置通知
@Before("pointcut()")//表示这个通知是针对pointcut方法的
public void doBefore(){
System.out.println("执行了前置通知");
}
//定义后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行了后置通知");
}
}
当我们在前端页面中访问UserController类的方法时,后端程序的控制台上每次出现的结果是先执行前置通知,在执行目标方法(连接点),然后执行后置通知。
2. 环绕通知的具体实现
环绕通知方法是具有Object
类型的返回值,需要把方法执行结果返回给框架,框架拿到对象继续执行。
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注解的参数中定义了具体的拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//定义前置通知
@Before("pointcut()")//表示这个通知是针对pointcut方法的
public void doBefore(){
System.out.println("执行了前置通知");
}
//定义后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行了后置通知");
}
//定义环绕通知
@Around("pointcut()")
//环绕通知方法的参数为要执行的连接点,也就是我们在前端访问的目标方法
public Object doAround(ProceedingJoinPoint joinPoint){
System.out.println("环绕通知之前");
Object result = null;
try {
//执行目标方法,它的目标方法就是我们在前端访问的方法
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕通知之后");
return result;
}
}
从执行结果中可以看到环绕通知的执行范围,可以环绕执行通知是最先执行的,然后是执行前置通知,然后再执行目标方法,然后执行后置通知,最后所有的方法执行完成了,环绕通知方法才会执行完成。
三. AOP的实现原理
动态代理是一种设计模式,它允许你在运行时创建代理对象并将方法调用转发给真实对象。在这种模式中,代理对象和真实对象实现了相同的接口,使得代理对象可以用来代替真实对象,并在需要时执行额外的逻辑。
动态代理可以用于许多场景,如拦截方法调用、记录日志、实现事务管理等。通过使用动态代理,你可以在不修改原始对象的情况下,增加或改变其行为。
AOP的实现原理可以通过动态代理来实现。在Java中,有两种常见的动态代理方式:基于接口的代理(JDK动态代理)和基于类的代理(CGLIB动态代理)。
JDK动态代理:
JDK动态代理是基于接口的代理方式。当目标对象实现了接口时,JDK动态代理通过创建一个实现了同样接口的代理类,并将方法调用委托给目标对象。在AOP中,切面被表示为一个包含通知逻辑的代理类,而目标对象则是实现了接口的原始类。
JDK动态代理通过Java的反射机制来调用目标对象的方法,并提供了InvocationHandler
接口让开发人员定义通知逻辑的执行方式。在运行时,当方法被调用时,JDK动态代理会拦截方法调用,并在必要的时候执行切面中定义的通知逻辑。
JDK动态代理通常使用Java.lang.reflect.Proxy
类和Java.lang.reflect.InvocationHandler
接口来实现。
CGLIB动态代理: CGLIB动态代理是基于类的代理方式。当目标对象没有实现接口时,CGLIB动态代理通过创建目标对象的子类,并重写父类中的方法来实现代理功能。在AOP中,切面被表示为一个继承了目标对象的子类,切面的通知逻辑被插入到子类相应的方法中。 CGLIB动态代理使用了字节码生成库来创建目标对象的子类,并通过继承和方法的重写来实现方法拦截和通知逻辑的执行。
不论是JDK动态代理还是CGLIB动态代理,它们都能够在运行时生成代理类并将切面逻辑织入到目标对象中。具体来说,AOP框架(如Spring)在启动时会扫描切面和目标对象,并根据配置信息动态生成代理类。当调用目标对象的方法时,代理类会拦截方法调用,在合适的位置执行切面中的通知逻辑。这样就实现了将横切关注点与核心业务逻辑分离的效果。
需要注意的是,JDK动态代理只能代理实现了接口的类,而CGLIB动态代理可以代理没有实现接口的类。在选择使用哪种代理方式时,可以根据目标对象是否实现了接口进行判断。如果目标对象实现了接口,则可以使用JDK动态代理;如果目标对象没有实现接口,则需要使用CGLIB动态代理。