1.什么是 AOP?
AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的集中处理
AOP是一种思想,而Spring AOP是一个实现了AOP的思想框架,他们的关系和IOC与DI类似
2.为什要用AOP?
想象⼀个场景,我们在做后台系统时,除了登录和注册等⼏个功能不需要做⽤户登录验证之外,其他⼏乎所有⻚⾯调⽤的前端控制器(Controller)都需要先验证⽤户登录的状态,那这个时候我们要怎么处理呢?
我们之前的处理⽅式是每个 Controller都要写⼀遍⽤户登录验证,然⽽当你的功能越来越多,那么你要写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会增加代码修改和维护的成本。那有没有简单的处理⽅案呢?
有人说可以抽取一个公共方法出来,每次需要验证的时候去调用这个方法就好了
这样做是可以的,但是还存在一个问题,如果登陆方法的参数发生改变,那么每个调用者都需要跟着修改,这样就会变得复杂,代码之间耦合严重,并且随着Controller层的业务越来越多,后期代码维护的时间和成本也大大提高,所以我们要尽量避免这种情况
开发的三个阶段:
- 初级阶段:每个方法都实现
- 中级阶段:抽取公共方法
- 高级阶段:采用AOP的方式
所以,对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑 AOP来统⼀处理了
除了统⼀的⽤户登录判断之外,AOP 还可以实现:
- 统⼀⽇志记录
- 统⼀⽅法执⾏时间统计(在性能优化阶段,监控流量,接口的响应时间等甚至每个方法的响应时间,为整个项目的性能进行优化)
- 统⼀的返回格式设置 (对于接口的返回格式,基本上都是code,message,data)
- 统⼀的异常处理
- 事务的开启和提交等
也就是说使⽤AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object OrientedProgramming,⾯向对象编程)的补充和完善
3.Spring AOP 应该怎么学习呢?
Spring AOP 学习主要分为以下 3 个部分:
- 学习 AOP 是如何组成的?也就是学习 AOP 组成的相关概念。
- 学习 Spring AOP 使⽤。
- 学习 Spring AOP 实现原理。
3.1 AOP 组成
1 切⾯(Aspect)
切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义,一个切面可以有多个切点,一个连接点可以符合多个切点的规则
切⾯是包含了:通知、切点和切⾯的类,相当于 AOP 实现的某个功能的集合
2 连接点(Join Point)
应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为
连接点相当于需要被增强的某个 AOP 功能的所有⽅法
3 切点(Pointcut)
Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice
切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,⽽连接点就是表中⼀条⼀条 的数据)
4 通知(Advice)
通知:定义了切⾯是什么,何时使⽤,其描述了切⾯要完成的⼯作,还解决何时执⾏这个⼯作的问题
AOP是对于同一类事情(范围)的集中处理(处理的内容是什么)
对于AOP而言,处理的内容就是通知,切⾯的⼯作被称之为通知
Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:
- 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
- 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤
- 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
- 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
- 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。
在jointPoint.proceed()前后都可以通知
注意:返回结果需要自己定义
4.我们就以用户登陆验证来举例:
1.首先我们需要创建一个springMVC的项目
参考博客:
spring项目的创建
2.在pop.xml中添加以下配置
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-bo
ot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.创建一个UserController类,模拟用户需要执行的一些方法
/**
* 用户需要调用的一些方法
*/
@Slf4j // 用来打印日志的
@RestController
@RequestMapping("/user")
public class UserController {
// 获取用户信息
@RequestMapping("/get")
public String getInfo() {
return "获取信息";
}
// 注册
@RequestMapping("/reg")
public String reg() {
return "注册";
}
// 登录
@RequestMapping("/log")
public String log() {
return "登录";
}
}
4.定义切面和切点
切面就是具体要处理的某一类问题,(比如用户登录权限验证就是一个具体的问题)切点的作用就是制定一组规则,(比如在这里我们就要制定哪些方法可以走到切面的类里面来,就是拦截那些需要验证用户身份的操作)
@pointcut()注解内的切点表达式说明
1.AspectJ ⽀持三种通配符
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
2.表达式示例
@Slf4j // 用来打印日志的
@Component // 将此类交给spring容器来管理
@Aspect // 表示此类为一个切面
public class LoginAspect {
// 定义一个切点
@Pointcut("execution(* com.example.springaop.controller.UserController.* (..))")
public void pointcut() {
//其中 pointcut ⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识”的作⽤,标识下⾯的通知⽅法具体指的是哪个切点(因为切点可能有很多个)
}
}
其中 pointcut ⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识”的作⽤,标识下⾯的通知⽅法具体指的是哪个切点(因为切点可能有很多个)
5.此时我们来测试一下通知方法注解
通知里就是要定义被切点拦截过来的方法具体要执行的业务,比如用户登陆的权限验证就是具体要执行的业务,在SpringAOP中,可以在方法上加以下注解,该方法就会变为通知方法,在满足条件后就会被调用
1.前置通知使⽤ @Before
通知方法在目标方法(就是连接点)执行之前调用
@Before("pointcut()")
public void doBefore() {
log.info("doBefore...");
}
此时我们在浏览器去搜索(假装登陆操作)
我们查看日志就可以发现在登陆操作之前,执行了Before注解的方法
2.后置通知使⽤ @After
该方法会在连接点(也就是目标方法)返回之后调用执行,或者抛出异常之后也会调用
@After("pointcut()")
public void doAfter() {
log.info("doAfter...");
}
此时去浏览器搜索之后发现在登陆操作之后,执行了@After注解的方法
3.返回之后通知使⽤ @AfterReturning
这个通知方法和@After都实在目标方法返回之后才调用,那么这两个的先后执行顺序是怎么样的呢?
我们来测试一下
@AfterReturning("pointcut()")
public void doAfterReturning() {
log.info("doAfterReturning...");
}
在浏览器操作后,查看日志发现@doAfterRuturning注解的方法比@After注解的方法先执行,但是@After还可以在抛出异常时调用,而且一般这两个不会同时使用
4.抛异常后通知使⽤ @AfterThrowing
该方法会在连接点抛出异常后调用
这个方法和@After注解,都拥有这个功能,那么这两个方法执行的先后顺序如何呢?
我们来测试一下
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
log.info("doAfterThrowing...");
}
我们查看日志发现,@AfterThrowing同样是比@After注解的方法先执行
5.环绕通知使⽤ @Around(使用最多)
该方法包裹了连接点(也就是目标方法),在连接点通知之前和调用之后执行自定义的行为
注意:环绕通知的返回结果需要自己定义
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {// 传入当前的连接点
// 定义返回结果
Object oj = null;
log.info("环绕通知执行之前...");
try {
// 调用目标方法
oj = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后...");
return oj;
}
此时我们查看日志,可以看见在连接点通知之前和调用之后都执行了环绕通知自定义的行为,(在最前面和最后面)
6.ProceedingJoinPoint 常用方法
方法名称 | 方法作用 |
toString | 连接点所在位置的相关信息 |
toShortString | 连接点所在位置的简短相关信息 |
toLongStirng | 连接点所在位置的全部相关信息 |
getThis | 返回AOP代理对象,也就是com.sun.proxy.$Proxy18 |
getTarget | 返回目标对象(定义方法的接口或类) |
getArgs() | 返回被通知方法的参数列表 |
getSignature | 返回当前连接点签名,其getName()方法返回方法的FQN |
6.实际应用
在没有AOP的情况下,我们在每个需要他的方法中都需要去验证用户登陆的身份,他的缺点就是:
- 1.每个方法都要单独写用户登陆验证的方法,即使封装成公共方法,也一样要传参调用和判断,这样代码的耦合性太高,不符合面向对象软件设计模式中的依赖倒置原则
- 2.添加的控制器越来越多,需要调用的方法越来越多,后期维护的时间和成本也大大提高
- 3.这些登陆方法与接下来要实现的业务没有任何逻辑关系,所以使用AOP来实现刻不容缓
这样我们就可以使用切点制定一组规则,(设置连接点)拦截需要验证身份的方法,使得他们在执行前都调用一下身份验证,就像这样:
@Around("pointcut()")
public Object VerifyIdentity(ProceedingJoinPoint joinPoint) {
Object oj = null;
log.info("身份验证的方法");
try {
// 执行目标方法
oj = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
return oj;
}
我们在浏览器分别操作,登录,注册,获取信息的场景
查看日志发现在每一次操作之前都调用了身份验证的方法,
这样我们就实现了使用AOP进行统一的用户判断,是不是方便了许多呢,但是我们发现,登录和注册的方法并不需要验证登陆身份,我们却没有将其排除?
随之而来又有了新的问题:
- 使用AOP的方式获取HttpSession对象不知道该如何去获取
- 我们要对⼀部分方法进行拦截,⽽另⼀部分方法不拦截,如注册方法和登录方法是不拦截的,这样 的话排除方法的规则很难定义,甚⾄没办法定义
那我们又该如何解决呢?
对于以上问题 Spring 中提供了具体的实现拦截器HandlerInterceptor