Spring Aop详细介绍
什么是 AOP
AOP(Aspect-OrientedProgramming,面向切面编程),可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP 则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而 AOP 技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP 代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
AOP 的基本概念
- (1)Aspect(切面):通常是一个类,里面可以定义切入点和通知
- (2)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用
- (3)Advice(通知):AOP 在特定的切入点上执行的增强处理,有 before,after,afterReturning,afterThrowing,around
- (4)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
- (5)AOP 代理:AOP 框架创建的对象,代理就是目标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接口,后者基于子类
通知方法:
- 前置通知:在我们执行目标方法之前运行(@Before)
- 后置通知:在我们目标方法运行结束之后 ,不管有没有异常**(@After)**
- 返回通知:在我们的目标方法正常返回值后运行**(@AfterReturning)**
- 异常通知:在我们的目标方法出现异常后运行**(@AfterThrowing)**
- 环绕通知:动态代理, 需要手动执行 joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around)
Spring AOP
Spring 中的 AOP 代理还是离不开 Spring 的 IOC 容器,代理的生成,管理及其依赖关系都是由 IOC 容器负责,Spring 默认使用 JDK 动态代理,在需要代理类而不是代理接口的时候,Spring 会自动切换为使用 CGLIB 代理,不过现在的项目都是面向接口编程,所以 JDK 动态代理相对来说用的还是多一些。
advice 的类型
- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
- after return advice, 在一个 join point 正常返回后执行的 advice
- after throwing advice, 当一个 join point 抛出异常后执行的 advice
- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice
关于 AOP Proxy
Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理. 如果需要为一个类实现代理, 那么可以使用 CGLIB 代理.
当一个业务逻辑对象没有实现接口时, 那么 Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.
#彻底理解 aspect, join point, point cut, advice
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的. 下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. 首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 爪哇的小县城里的百姓, point cut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问. 为什么可以这样类比呢?
- join point —> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP 中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
- point cut —> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配 joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据
凶手是个男性, 身高约七尺五寸
, 把符合条件的人抓起来. 在这里凶手是个男性, 身高约七尺五寸
就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问. - advice —> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中,
抓过来审问
这个动作就是对作用于那些满足男性, 身高约七尺五寸
的爪哇的小县城里的百姓
. - aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 aspect.
或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 定语
, 被动句
之类的概念, 那么可以做一个不严谨的类比, 即 joinpoint
可以认为是一个 宾语
, 而 pointcut
则可以类比为修饰 joinpoint
的定语, 那么整个 aspect
就可以描述为: 满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.
@AspectJ 支持
@AspectJ
是一种使用 Java 注解来实现 AOP 的编码风格。
@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持 @AspectJ 的 AOP 风格.
使能 @AspectJ 支持
@AspectJ 可以以 XML 的方式或以注解的方式来使能, 并且不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中.
使用 Java Configuration 方式使能@AspectJ
1 |
|
使用 XML 方式使能@AspectJ
1 | <aop:aspectj-autoproxy/> |
定义 aspect(切面)
当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:
1 |
|
声明 pointcut
一个 pointcut 的声明由两部分组成:
- 一个方法签名, 包括方法名和相关参数
- 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).
在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即:
1 | // 切点表达式 |
这个方法必须无返回值.
这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.
上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 com.xys.service.UserService 下的所有方法的执行.
切点标志符(designator)
AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 “execution(* greetTo(..))” 的切点表达式, execution 就是 标志符, 而圆括号里的 *greetTo(..) 就是操作参数
execution
匹配 join point 的执行, 例如 “execution(* hello(..))” 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符.
within
匹配特定包下的所有 join point, 例如 within(com.xys.*)
表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而within(com.xys.service.*Service)
表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.
this 与 target
this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).
bean
匹配 bean 名字为指定值的 bean 下的所有方法, 例如:
1 | bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法 |
args
匹配参数满足要求的的方法. 例如:
1 |
|
当 NormalService.test 执行时, 则 advice doSomething
就会执行, test 方法的参数 name 就会传递到 doSomething
中.
常用例子:
1 | // 匹配只有一个参数 name 的方法 |
@annotation
匹配由指定注解所标注的方法, 例如:
1 |
|
则匹配由注解 AuthChecker
所标注的方法.
常见的切点表达式
匹配方法签名
1 | // 匹配指定包中的所有的方法 |
匹配类型签名
1 | // 匹配指定包中的所有的方法, 但不包括子包 |
匹配 Bean 名字
1 | // 匹配以指定名字结尾的 Bean 中的所有方法 |
切点表达式组合
1 | // 匹配以 Service 或 ServiceImpl 结尾的 bean |
声明 advice
advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式
. 下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的.
Before advice
1 | /** |
这里, @Before 引用了一个 pointcut, 即 “com.xys.aspect.PointcutDefine.dataAccessOperation()” 是一个 pointcut 的名字. 如果我们在 advice 在内置 pointcut, 则可以:
1 |
|
around advice
around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法.
1 |
|
around advice 和前面的 before advice 差不多, 只是我们把注解 @Before 改为了 @Around 了.
基于注解的 AOP 配置方式
切面类:
1 | package com.enjoy.cap10.aop; |
目标方法:
1 | package com.enjoy.cap10.aop; |
配置类:
1 | package com.enjoy.cap10.config; |
测试类:
1 | public class Cap10Test { |
结果:
1 | @Arount:执行目标方法之前... |
AOP 源码赏析
在这个注解比较流行的年代里,当我们想要使用 spring 的某些功能时只需要加上一行代码就可以了,比如:
- @EnableAspectJAutoProxy 开启 AOP
- @EnableTransactionManagement 开启 spring 事务管理,
- @EnableCaching 开启 spring 缓存
- @EnableWebMvc 开启 webMvc
对于我们使用者而言十分简单便利,然而,其背后所做的事,却远远比一个注解复杂的多了,本篇只是简略的介绍一下@EnableAspectJAutoProxy 背后所发生的那些事,了解其工作原理,才能更好的运用,并从中领略大师的智慧.
废话不多说,先来看一下源码:
1 |
|
英文注解已经很详细了,这里简单介绍一下两个参数,一个是控制 aop 的具体实现方式,为 true 的话使用 cglib,为 false 的话使用 java 的 Proxy,默认为 false,第二个参数控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为 false.
这里核心是@Import(AspectJAutoProxyRegistrar.class);在 AspectJAutoProxyRegistrar 里,核心的地方是
1 | AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); |
一个 AOP 的工具类,这个工具类的主要作用是把 AnnotationAwareAspectJAutoProxyCreator 这个类定义为 BeanDefinition 放到 spring 容器中,这是通过实现 ImportBeanDefinitionRegistrar 接口来装载的,具体装载过程不是本篇的重点,这里就不赘述,我们重点看 AnnotationAwareAspectJAutoProxyCreator 这个类.
首先看看这个类图:
从类图是可以大致了解 AnnotationAwareAspectJAutoProxyCreator 这个类的功能.它实现了一系列 Aware 的接口,在 Bean 装载的时候获取 BeanFactory(Bean 容器),Bean 的 ClassLoader,还实现了 order 接口,继承了 PorxyConfig,ProxyConfig 中主要封装了代理的通用处理逻辑,比如设置目标类,设置使用 cglib 还是 java proxy 等一些基础配置.
而能够让这个类参与到 bean 初始化功能,并为 bean 添加代理功能的还是因为它实现了 BeanPostProcessor 这个接口.这个接口的 postProcessAfterInitialization 方法会在 bean 初始化结束后(赋值完成)被调用。
这里先看一下最顶部的抽象类:AbstractAutoProxyCreator,这个抽象类主要抽象了实现代理的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
// 主要看这个方法,在bean初始化之后对生产出的bean进行包装
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.contains(cacheKey)) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
// wrapIfNecessary
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
// 意思就是如果该类有advice则创建proxy,
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 1.通过方法名也能简单猜测到,这个方法就是把bean包装为proxy的主要方法,
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
// 2.返回该proxy代替原来的bean
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
总结:
- 1)将 AnnotationAwareAspectJAutoProxyCreator 注册到 Spring 容器中
- 2)AnnotationAwareAspectJAutoProxyCreator 类的 postProcessAfterInitialization()方法将所有有 advice 的 bean 重新包装成 proxy
创建 proxy 过程分析
通过之前的代码结构分析,我们知道,所有的 bean 在返回给用户使用之前都需要经过 AnnotationAwareAspectJAutoProxyCreator 类的 postProcessAfterInitialization()方法,而该方法的主要作用也就是将所有拥有 advice 的 bean 重新包装为 proxy,那么我们接下来直接分析这个包装为 proxy 的方法即可,看一下 bean 如何被包装为 proxy,proxy 在被调用方法时,是具体如何执行的
以下是 AbstractAutoProxyCreator.wrapIfNecessary(Object bean, String beanName, Object cacheKey)中的 createProxy()代码片段分析
1 | protected Object createProxy( |
TargetSource 中存放被代理的对象,这段代码主要是为了构建 ProxyFactory,将配置信息(是否使用 java proxy,是否 threadlocal 等),目标类,切面,传入 ProxyFactory 中
创建何种类型的 Proxy?JDKProxy 还是 CGLIBProxy?
1 | // getProxy()方法 |
getProxy()方法
由上面可知,通过 createAopProxy()方法来确定具体使用何种类型的 Proxy,针对于该示例,我们具体使用的为 JdkDynamicAopProxy,下面来看下 JdkDynamicAopProxy.getProxy()方法
1 | final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable// JdkDynamicAopProxy类结构,由此可知,其实现了InvocationHandler,则必定有invoke方法,来被调用,也就是用户调用bean相关方法时,此invoke()被真正调用 |
invoke()方法
以上的代码模式可以很明确的看出来,使用了 JDK 动态代理模式,真正的方法执行在 invoke()方法里,下面我们来看下该方法,来看下 bean 方法如何被代理执行的
1 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
拦截方法真正被执行调用 invocation.proceed()
1 | public Object proceed() throws Throwable { |
总结 4:依次遍历拦截器链的每个元素,然后调用其实现,将真正调用工作委托给各个增强器
总结:
纵观以上过程可知:实际就是为 bean 创建一个 proxy,JDKproxy 或者 CGLIBproxy,然后在调用 bean 的方法时,会通过 proxy 来调用 bean 方法
重点过程可分为:
1)将 AnnotationAwareAspectJAutoProxyCreator 注册到 Spring 容器中
2)AnnotationAwareAspectJAutoProxyCreator 类的 postProcessAfterInitialization()方法将所有有 advice 的 bean 重新包装成 proxy
3)调用 bean 方法时通过 proxy 来调用,proxy 依次调用增强器的相关方法,来实现方法切入