一:戏谑前言
删删改改写了很多次,懒得再折腾什么没有狗屁意义的概念。AOP的概念晦涩难懂,根本就不属于人类理解的范畴。这篇文章就说Spring AOP的大概实现原理,面试应该怎么去说,工作中怎么用。其余的仁者见仁智者见智,有更好描述的朋友欢迎补充探讨。一上来看那些见鬼的概念然后纠结不清的都是ZZ
将依赖放在最显眼的位置,它是最靓的一个仔。还有朋友喜欢引用aspectjweaver这个包,但是aspects包含了这个包。所以单纯就AOP实现所需依赖就看个人选择咯
二:AspectJ与Spring AOP
两者都是AOP的实现框架,都有自己的实现原理与使用方法,Spring AOP发展中引用了AspectJ的使用方法,但其最后编织切面与运用程序时还是使用的Spring AOP进行实现。Spring AOP虽然功能强大性比不上AspectJ,具体有啥差别Google一下
三:切面
个人理解就和Java中得类差不多,各式各样的切入点在一个切面里面。Spring AOP支持XML配置与基于AspectJ注解的使用,两种方式下面一一介绍
3.1 XML配置aspect
- 切面配置使用标签<aop:aspect>。id属性唯一标记,ref引用对象可以通过标签<bean>配置,也可以使用注解扫描加入IOC中的对象
- 切面配置包括后面讲的切入点、通知等配置都需要在标签<aop:config>中使用,<aop:config>可以配置多个,当然每个config里面的aspect切面也可以配置多个
3.2 AspectJ创建aspect
首先在SpringMVC配置文件中加如上配置,这里加到SprigMvc配置文件中是为了拦截控制访问层方法。如果将这个配置加入Spring配置文件程序启动不会报错,但是不能拦截Controller的方法,具体原因Google一下
加完配置后直接在切面类上注解@Aspect标记切面类即可,但是注意这个类不具备扫描到IOC容器中的能力。所以需要添加注解@Component或者配置文件中增加<bean>标签配置
四:切入点
Spring AOP重点就在于使用JDK动态代理或者CGLIB动态代理生成代理对象,然后针对代理对象拦截切入。拦截规则的定义就用官网说的专业名词切入点完成配置
4.1 XML配置poinitcut
切入点应该是位于某个切面中存在,试了一下XML配置的时候只能定义一个pointcut,在AspectJ的时候能定义多个。expression属性里属性值为切入点表达式,后面详细介绍该表达式规则
4.2 AspectJ创建pointcut
与XML配置切入点并没有太大区别,同样在一个声明的切面中使用注解@Pointcut标记一个切入点,匹配规则采用AspectJ的切入点表达式。但是注意,这里切入点方法必须返回值必须是void
4.3 类型匹配
看着上面实例有*
以及..
与+
符号,这种符号一看就是与正则表达式类似的类型匹配。其具体含义如下表所示:
符号 | 含义 |
---|---|
* | 任何数量字符 |
.. | 跟在包后表示包以及所有子包,用在参数中表示任意个数类型的参数 |
+ | 指定类型的子类型,只能作为后缀放在类型模式后边 |
4.4 切入点指示符
execution可不是随便瞎写的,Spring AOP支持的切入点指示符如下表所示。切入点指示符作用就是限制切入点拦截类型,比如方法、执行对象类型、注解、参数等。当然Spring AOP不支持完整的AspectJ切入点指示符,最常用的就是execution表示方法
切入点指示符 | 限制类型 | 备注 |
---|---|---|
execution | 方法 | 切入点表达式声明到方法一级 |
within | 类中方法 | 切入点表达式匹配到类一级 |
this | 代理对象 | 切入点表达式只能是完全限定路径,指定代理对象的类型。包括对象实现接口 |
target | 目标对象 | 切入点表达式只能是完全限定路径,指定目标对象的类型,不包括对象实现接口 |
args | 参数 | 切入点表达式只能是完全限定路径,匹配方法参数是给定类型实例 |
@annotation | 注解 | 切入点表达式只能是完全限定路径,匹配方法注解 |
@args | 参数注解 | 切入点表达式只能是完全限定路径,参数具备给定的注解 |
@target | 目标对象注解 | 目标对象类上注解匹配 |
@within | 目标对象注解 | 感受不出和@target的区别 |
4.5 execution详解
仔细观察4.4截图示例中execution与其它切入点指示符的切入点表达式格式不一样,确实作为开发中最常用的切入点指示符格式还不一致那不得拿出来万众瞩目一下?其格式具体分析如下:
- 完整结构:方法权限 返回值类型 方法完全限定(参数) 异常
- 方法权限:可省略,表示方法的访问权限。如public等
- 返回值类型:不可省略,一般使用*表示所有返回值
- 方法完全限定:不可省略,使用上述的切入点表达式
- 参数:不可省略,()表示无参,(…)任何个数类型参数,(*)表示一个任意类型参数
- 异常,可省略,使用throws声明异常全限定名,多个异常逗号隔开。如下图所示
4.6 组合切入点表达
一个切入点表达式限制的范围还是不足以表达,需要多个切入表达式组合。那就可以使用如下图的&& || !对切入表达式进行组合
XML配置的时候因为解析问题,肯定是不能直接使用。那么就可以使用and、or、not表达,如下图所示
五:通知
切入点匹配到连接点,也就可以理解为切入点匹配到执行方法后是不是应该做点什么。这个操作执行的时间、以及具体操作逻辑则可以通过通知进行定义
5.1 通知分类
- @Before:前置通知,匹配方法执行前执行
- @AfterReturning:后置通知,匹配方法执行返回之后执行
- @AfterThrowing:异常通知,匹配方法抛出异常执行
- @Afer:最终通知,匹配方法执行退出之后执行
- @Around:环绕通知,有机会在匹配方法执行前后增加执行逻辑
5.2 前置通知
上图示例XML文件配置前置通知语法,pointcut-ref属性引入切入点的定义,method属性定义前置通知执行切面中的哪个方法,属性值为方法名称
当然如果这个通知不想引入前面定义的切入点,自定义属于本通知的切入点表达式也是允许的。只不过需要将pointcut-ref属性修改为pointcut即可。具体的切入点表达式规则不变
AspectJ的操作与XML配置类似,将XML配置换成注解即可。并没有太大的区别
5.3 后置通知
后置通知的定义与前置通知类似,有一点区别就是因为执行时间的不一致,所以后置通知可以拿到匹配方法的执行返回值。XML中使用属性returning定义返回值名称,然后在通知方法中直接使用,同理AspectJ则在注解中声明returning属性即可。注意这个通知方法的参数名需要与定义的返回值名称一致
5.4 异常通知
异常通知与后置通知类似,可以通过在XML中配置亦或是在AspectJ注解中声明属性throwing获取到匹配方法的具体异常。并根据异常做出处理与操作
5.5 最终通知
最终通知因为执行时间为匹配方法退出之后,所以拿不到方法运行的任何信息,包括返回值、异常等
5.6 环绕通知
环绕通知比较强大,可以决定匹配方法的是否执行。通过对象ProceedingJoinPoint调用方法proceed()即可执行匹配方法,如果匹配方法需要传递执行参数,则使用一个Object类型数组提供。其实这个ProceedingJoinPoint是Joinpoint的子类,请求参数完全可以在对象中获取到。这个Joinpoint在下一小节解释
5.7 通知参数
如果想要针对匹配方法的一些数据进行操作,可以在通知方法的第一个参数中使用Joinpoint对象。这个对象Spring会自动帮助注入,注意的就是这个参数必须位于参数列表的第一个,不然会报错
使用切入点表达式定义绑定参数也是可以的,使用到切入点指示符args。如果是在AspectJ语法中的用法则可以如下图所示
六:执行顺序
同一个切面可以针对同一个匹配方法设置多种通知类型,那么这些通知类型执行顺序是什么样对于程序员而言是非常有必要关注的。当然,当多个切面叠加的时候顺序又是什么样的,通过代码怎么去限定切面的执行顺序等等这些在本节一一阐述
6.1 同一切面执行顺序
进入环绕通知
前置通知
执行匹配方法
完成环绕通知
最终通知
后置通知
如上所示,在同一个切面中针对同一个匹配方法配置了五种通知。最后的根据输出结果总结出如下图所示顺序,当然并没有控制匹配方法抛出异常,如果是匹配方法抛出异常那么最后的结果就是把后置通知与异常通知的执行,并且不会执行环绕通知的匹配方法后续逻辑
进入环绕通知
前置通知
执行匹配方法
最终通知
异常通知
6.2 不同切面
修改一下同一切面的输出代码,然后新增第二个切面。先定义一下切面的执行顺序,定义切面执行顺序的方法下一节阐述。查看最后的执行结果如下所示:
进入环绕111通知
前置111通知
进入环绕222通知
前置222通知
执行匹配方法
最终222通知
异常222通知
最终111通知
异常111通知
对于每个切面来讲,正常执行的逻辑与异常执行的逻辑与上一个小节阐述的并没有差别。这里就采用匹配方法抛出异常后的执行结果作为示例,其实AOP的切面结构可以想一下同心圆,外层最先执行的必定最后结束。只需要关注外层切面执行什么逻辑之后才进入内层切面,总结的执行流程图如下所示:
6.3 切面顺序
import org.springframework.core.annotation.Order;
仔细看前面的示例代码在不同切面通知执行顺序的Demo中有一个Order注解,该注解的作用就是定义切面的执行顺序,规则很简单就是Order越小先执行
。除了这种使用注解的方式之外还能有什么方法?在XML配置中可以指定,通过继承接口实现同样可以指定。需要强调一点阐述的都是切面的执行顺序,同一切面中相同类型通知的执行顺序无法指定,要么进行通知合并,要么安排到不同切面中用切面的顺序指定
在XML配置中则可以通过切面定义标签<aop:aspect>中的order属性指定,效果与注解一致
在切面类上实现import org.springframework.core.Ordered接口,重写getOrder()方法。同样也可以限定切面aspect的执行顺序
七:引入
当一个类想要增加某个功能,并不想在原代码基础上实现接口并重写方法。或者说该接口有一个实现类已经写好了方法的实现,然后有个类就想要这个实现的方法怎么办?AOP编程提供了引入的方式,在切面中定义目标对象并为其实现接口并且提供重写的方法
7.1 定义引入功能接口
7.2 实现引入功能接口
既然不想在某些类中对提供新增功能的直接进行实现,那么必然需要额外的类对接口功能进行实现。注意这里的实现类添加IOC扫描注解,必须将这个实现类交给Spring进行管理
7.3 需要引入功能的类
与引入功能接口实现类一致,需要引入功能的类也应该交给Spring进行统一管理
7.4 引入功能切面
定义切面也就是最后实现新功能引入的关键,主要分为以下几个关键点:
- 使用注解@DeclareParents注解
- 注解属性value定义需要引入新功能类的表达式,支持切入点表达式
- 注解属性defautImpl则提供新功能实现类class对象
- 注解@DeclareParents定义属性类型应该为提供新功能接口
7.5 测试引入功能
测试功能里面涉及到类型强转与方法调用,这里并不会在编译亦或是运行中抛出异常。证明新增功能引入成功且该类也实现了新增功能接口