Spring系列框架之面向切面编程AOP
⭐️前面的话⭐️
本篇文章将介绍一种特别重要的思想,AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。
1.面向切面编程AOP
1.1什么是AOP?
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
1.2AOP的作用
想象一个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?
如果不使用AOP,我们就需要在每一个Controller层都写一遍验证用户是否已经登录的程序,如果你实现的功能有很多,并且这些功能都需要进行登录验证,那你就需要编写大量重复的代码,非常的麻烦,尽管你可以将登录验证实现的逻辑封装在一个方法中,但是你要在很多地方调用这个方法,还是很麻烦。
如果使用AOP,在进入核心的业务代码之前会做统一的一个拦截,去验证用户是否登录,这样就很方便,仅需做一个拦截工作,再将验证代码一执行即可。
除了登录验证功能之外,还有很多功能也可以使用AOP,比如:
- 统一日志记录与持久化。
- 统一方法执行时间统计。
- 统一数据返回格式。
- 统一处理程序中的异常。
- 统一事务的开启与提交。
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP (Object Oriented Programming,面向对象编程)的补充和完善。
1.3AOP的核心概念
1、横切关注点
想要对哪些方法或类进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
2、切面(aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象,你可以认为切面相当于横切关注点。
3、连接点(joinpoint)
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
4、切入点(pointcut)
提供一组规则,根据规则匹配合法的连接点,满足规则的连接点可以理解为切点,然后可以为切点提供具体的处理(通知)。
5、通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,或者说在切点出所需要执行的代码是什么。
通知包含前置通知,后置通知,返回之后通知,抛异常后通知与环绕通知五类。
在Spring切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会调用对应满足条件的方法:
- 前置通知使用@Before∶通知方法会在目标方法调用之前执行。
- 后置通知使用@After∶通知方法会在目标方法返回或者抛出异常后调用。
- 返回之后通知使用@AfterReturning∶ 通知方法会在目标方法返回后调用。
- 抛异常后通知使用@AfterThrowing∶ 通知方法会在目标方法抛出异常后调用。
- 环绕通知使用@Around∶通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
6、目标对象
代理的目标对象。
7、织入(weaving)
织入(weaving)即代理的生成时机,
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入∶
- 编译期∶切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载器∶切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving.LTW)就支持以这种方式织入切面。
- 运行期∶切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。SpringAOP就是以这种方式织入切面的。
8、引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
2.Spring AOP
面向切面编程是一种思想,Spring AOP是AOP的一种实现。
2.1Spring AOP的使用
SpringAOP使用的主要步骤为:
第一步,在SpringBoot项目中添加AOP相关的依赖。
第二步,定义切面。
第三步,定义切点。
第四步,实现通知。
第一步,在SpringBoot项目中添加AOP相关的依赖,就是在Maven的配置文件中添加aop的依赖。
由于使用Edit Starters插件访问官方的源是找不到有关SpringBoot的AOP依赖,这是因为在idea中,上面只列举了一些常用的依赖,不是所有依赖都在上面,如果找不到我们就去Maven中央仓库中去寻找。
搜索一下,找到这个依赖,然后进去复制依赖信息拷贝到Maven的配置文件中就行。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步,定义切面,在spring boot项目中其实就是加上@Aspect和@Component注解的一个类,这个类就表示一个切面。
//设置切面,这个类就是一个切面
@Aspect
@Component
public class UserAspect {
...
}
第三步,在切面里面定义切点,在Spring中其实本质上就是一个方法,具体说是使用 @Pointcut注解修饰的一个方法,该方法不需要配置任何信息。
//定义切点,设置拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut() {
}
其中@Pointcut注解中的参数是一个AspectJ表达式,它的作用就是设置哪些返回值类型哪些类的哪些方法需要拦截可以指定到参数列表。
第四步,实现通知,本质上就是实现一个方法,只不过在方法上加上不同通知类型的注解即可,如前置通知加上@Before注解,注解的参数为切点方法名。
//前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行Before通知");
}
同理,后置通知也是如此,就是将注解改为@After:
//后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行After通知");
}
以及目标方法返回后通知@AfterReturning:
//返回之后通知
@AfterReturning("pointcut()")
public void doAfterRunning() {
System.out.println("执行AfterRunning通知");
}
我们来验证一下上述设置切面拦截代码的正确性,我们写一个在拦截范围的类以及方法:
启动程序,我们访问页面http://127.0.0.1:8080/user/hello
,看看控制台的输出:
通过运行结果我们也能够看出上面三种通知方式执行的时机以及先后顺序。
2.2AspectJ表达式基本语法
*
∶匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)
..
∶匹配任意字符,可以匹配多个元素,在表示类时,必须和*
联合使用,匹配参数列表时表示匹配所有类型的参数列表。
+
∶ 表示按照类型匹配指定类及其所有子类,必须跟在类名后面,如com.Car+,表示拦截Cat类以及继承Cat类的所有子类。
切点表达式由切点函数组成,其中execution()
是最常用的切点函数,用来匹配方法,语法为∶
execution(<权限修饰符><返回类型><包.类.方法(参数)><异常>)
其中权限修饰符与异常项一般省略,返回类型方法以及参数不可省略,其他项可以省略。
权限修饰符:
- 填写权限修饰符,就只会匹配相应修饰符修饰的方法。
- 省略,权限不作为限制,所有权限修饰符的方法都会匹配。
返回类型,必须参数,不可省略:
- 填写具体返回类型,就匹配相应返回类型的方法。
*
表示匹配所有返回值类型的方法。
包,类,一般情况下要有,但是可以省略:
- 填写包和类,就只匹配你所规定的包或类。
*
表示匹配某目录下所有的包或者类。+
作用在类上,匹配该类以及继承该类的所有子类。
方法,表示需要匹配方法的名字,参数表示需要匹配参数列表的类型,不可省略:
- 指定方法名和参数列表,就只匹配你所限定的方法。
*
可以作用在方法匹配字段上,表示匹配某类中所有的方法。..
可以作用在参数列表上,对参数列表类型不做限制。
异常,可以匹配抛出指定异常的方法,该参数一般省略。
下面来看几个例子,我们来了解一下AspectJ表达式:
execution(* com.cad.demo.User.*(..))
∶匹配User类里的所有方法。
execution(* com.cad.demo.User+.*(..))
∶匹配User类及其子类中的所有方法。
execution(* com.cad.*.*(..))
∶匹配com.cad包下的所有类的所有方法。
execution(* com.cad..*.*(..))
∶匹配 com.cad 包下、子孙包下所有类的所有方法。
execution(* addUser(String,int))
∶ 匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是int。
2.3抛出异常后通知与环绕通知
前面我们已经介绍了前置通知,后者通知以及返回后通知的演示,下面我们继续介绍剩下两种通知,在上面已经实现代码基础上,我们继续添加通知来进行演示。
抛出异常后通知,其实和前面三种通知的用法可以说一模一样,只不过只有当程序出现异常的时候才会执行该通知,写法如下,就是在切面类中实现一个方法,使用@AfterThrowing注解修饰即可:
//抛异常后通知
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("抛出异常后,执行AfterThrowing通知");
}
然后我们再在目标方法中构造一个异常,异常随便写一个异常就行,如算术异常:
我们访问页面http://127.0.0.1:8080/user/world
来看一看控制台输出:
由于出现了异常,方法被强制终止了,没有返回,所以没有返回后通知。
最后还剩下一个环绕通知,环绕通知你可以理解为将前置通知和后置通知一体化了,环绕通知最常见的用法之一就是计算目标方法执行的时间是多少,使用其他通知无法做到,如果使用前置加后置通知进行对目标方法的计时,在单线程下没有问题,但是在多线程下有问题,当一个线程正在计时时,另外一个线程调用了前置通知,此时计时开始的时间就被刷新了,那自然计算得到的目标方法执行时间也就不准确了,而环绕通知使一体化的,不存在类似这种线程安全的问题。
环绕通知相比于其他的三种通知的使用方法较为复杂,首先实现环绕通知的方法必须含有ProceedingJoinPoint
类的参数和使用 @Around注解修饰,表示连接点的执行进度,方法体里面第一步是执行环绕方法的前置通知,然后通过该类对象获取目标方法执行进度并调用,再执行环绕通知的后置通知,最后并返回该目标方法进度。
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object res = null;
System.out.println("执行环绕通知前置通知");
try {
//根据连接点进度获取目标方法,并执行目标方法
res = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕通知后置通知");
return res;
}
我们访问页面http://127.0.0.1:8080/user/hello
来看一看控制台输出:
我们可以基于环绕通知实现对目标方法的计时功能:
实现思路很简单,就是在执行目标方法之前开始计时,执行完目标方法之后结束计时,差值就是方法运行的时间。
计时的方式可以使用时间戳或者spring中的StopWatch
类,后者更准确一点,其实都差不多。
我们可以通过传入的joinPoint
对象获取目标方法的方法名以及具体所在类和包,joinPoint.getSignature().toString()
就能生成目标方法的全部有关名字的信息,我们可以加上一个方法的信息来表示哪一个方法执行的时间。
@Around("pointcut()")
public Object doTime(ProceedingJoinPoint joinPoint) {
Object result = null;
//System.out.println("环绕通知前置通知");
String methodName = "";
long start = 0;
long end = 0;
StopWatch stopWatch = new StopWatch();
try {
//执行拦截方法
start = System.currentTimeMillis();
stopWatch.start();
methodName = joinPoint.getSignature().toString();
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
end = System.currentTimeMillis();
stopWatch.stop();
}
//System.out.println("环绕通知后置通知");
System.out.println(methodName + "执行了" + (end - start) + "ms");
System.out.println(methodName + "执行了" + (stopWatch.getTotalTimeMillis()) + "ms");
return result;
}
运行结果:
2.4Spring AOP的实现原理
Spring AOP是构建在动态代理基础上,因此 Spring对AOP的支持局限于方法级别的拦截。
Spring AOP支持JDKProxy 和CGLIBProxy方式实现动态代理。默认情况下,对于非final
修饰的类,SpringAOP会基于CGLIBProxy生成代理类,CGLIBProxy生成代理类的原理就是继承目标类,被关键字final
修饰的类,由于不能被继承,所以会基于DKProxy生成代理类。
SpringAOP的本质就是生成一个目标对象的代理类,当前端传来请求时,不会将请求直接交给目标对象,而是首先代理类进行处理,如果满足一定的条件,才会将请求交给目标对象。
如果处理请求前需要登录验证,那么代理类会去验证用户账户是否登录,如果用户登录了才会将请求交给目标对象并执行核心业务代码,否则代理类之间返回响应让用户先登录。
参考 & 资料
- 点赞
- 收藏
- 关注作者
评论(0)