序言
按照软件重构思想的理念,若多个类中出现相同的代码,则应该考虑定义一个父类,将这些相同的代码提取到父类当中。
举个例子,Cat、Dog、Duck这些对象都有run()和eat()方法,通过引入一个包含这两个方法的抽象的 Animal 父类,Cat、Dog、Duck就可以通过继承Aninmal来复用run()和eat()方法。
虽然上述做法在大多数情况下是可行的,但世界并非永远这么简单,请看下面的代码:
1 | public class UserServiceImpl implements UserService { |
若你学过 Mybatis 框架,你会知道sqlSessionFactory和SqlSession是什么东西。
当然若是没学过也不要紧,你只需要知道SqlSession可以进行事务处理。
你肯定知道事务是什么。
在上述代码中,每当执行一个插入操作后,成功则提交事务,失败则回滚事务。
在代码中,我们会发现一个现象——业务代码只有分割线里面的代码。
当需要删除一个用户的时候,开发人员又需要写开启事务的代码,提交事务的代码,回滚事务的代码。但是,具体删除用户的业务代码,却仅仅只有 2 行呀。
显而易见:若如此进行开发,业务代码将频繁淹没在重复化非业务性的代码之中
思考,这样写代码非常麻烦且浪费时间呢?
确实如此。
假设将上面的业务类看成一段实木,将类里的各种方法分别看成圆木的一截,会发现事务管理的代码像一个年轮,可能含有的其他逻辑(如访问控制)也像一个个年轮,而业务代码是圆木的树心,这也正是横切代码概念的由来。

虽然我们无法通过抽象父类的方式消除如上图的重复性横切代码,因为这些横切逻辑依附在业务类方法的流程中,它们不能转移到其他地方去。
但是,AOP 独辟蹊径,通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案。
对于习惯了纵向抽取的开发者来说,可能不太容易理解横向抽取方法的工作机制,因为 Java 语言本身不直接提供这种横向抽取的能力。暂把具体实现放在一旁,先通过图解的方式归纳出 AOP 的解决思路,如下图:

从上图可以看出,AOP 希望将这些分散在各个业务逻辑代码中的相同代码通过横向切割的方式抽取到一个独立的模块中,还业务逻辑类一个清新的世界。
当然,将这些重复性的横切逻辑独立出来是很容易的,但如何将这些独立的逻辑融合到业务逻辑中以完成和原来一样的业务流程,才是事情的关键,这也正是 AOP 要解决的主要问题。
什么是 AOP?
AOP 是Aspect Oriented Programing的简称,即“面向切面编程”。
AOP 术语
各种学科有各种的专业术语,AOP 也有自己的行话。
为了方便后面的学习,需先了解一下与 AOP 相关的几个重要术语。
① 连接点(Joinpoint)
特定点是程序执行过程中的某个特定位置,如:
- 类初始化前
- 类初始化后
- 类的某个方法调用前
- 类的某个方法调用后
- 方法抛出异常后
一个类或一段程序拥有一些具有边界性质的特定点,这些代码中的特定点就被称为“连接点”。
Spring 仅支持方法的连接点,即仅能在方法调用前、调用后、方法抛出异常时这些程序执行点去织入增强。
我们知道,黑客攻击系统需要找到突破口 ,没有突破口就无法进行攻击。
从某种程度上来说,AOP 也可以看成一个黑客(因为它要向目标类中嵌入额外的代码逻辑),而连接点就是 AOP 向目标类打入楔子的候选锚点(黑客踩点)。
连接点由两个信息确定:一是方法表示的程序执行点;二是相对位置表示的方位。
如在Dog.eat()方法执行前的连接点,执行点为Dog.eat(),方位为该方法执行前的位置。
Spring 使用切点对执行点进行定位,而方位则在增强类型中定义。
② 切点(Pointcut)
每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。
但在为数众多的连接点中,如何定位某些感兴趣的连接点呢?
AOP 通过“切点”来定位特定的连接点。
举个栗子:连接点相当于数据库中的记录,而切点相当于查询条件。
切点和连接点并不是一对一的关系哦,因为一个切点是可以匹配多个连接点的。
在 Spring 中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP 的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
准确来讲,这只是执行点而非连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点。而切点只定位到某个方法上,并不决定什么时候执行,执行什么逻辑。
因此,若希望定位到具体的连接点上,还需要提供方位信息,这由 Advice 提供。
③ 增强(Advice)
增强是织入目标类连接点上的一段程序代码,是不是觉得 AOP 越来越像黑客了?这不是往业务类中装入木马吗?
确实如此,我们可以那么理解。
在 Spring 中,增强除了用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。只有结合执行点的方位信息和切点信息,才能找到特定的连接。
正因为增强既包括用于添加到目标连接点上的一段执行逻辑,又包括用于定位连接点的方位信息,所以 Spring 提供的增强接口都是带方位名的,如前置增强BeforeAdvice、后置增强AfterReturningAdvice、异常增强ThrowsAdvice等。
BeforeAdvice表示方法调用前的位置,而AfterReturningAdvice代表访问返回后的位置。
只有结合切点和增强,才能确定特定的连接点并实施增强逻辑。
特殊增强:引介(Introduction)
引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过 AOP 的引介功能,也可以动态地位该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
Advice 为何翻译为增强?
深挖一下下:Advice 不是“通知”的意思嘛?
其实呀!这就像将“how old are you”译为“怎么老是你”一样,明显是一种“望文生义”的译法。
来看几个使用通知的语境:
- 学校通知学生放假日期
- 供水局通知人们明天停水
从上述语境中可以知道,通知者只是把某个消息传达给被通知者,并不会替被通知者做任何事情。而 Spring 的 Advice 必须嵌入类的某连接点上,并完成一段附加的执行逻辑,这明显是去“增强”目标类的功能呀!
④ 目标对象(Target)
目标对象是增强逻辑的织入目标类。
若没有 AOP,则种类相同的目标业务类就需要自己实现所有的重复逻辑,就如前面的事务控制示例。
在 AOP 的帮助下,UserServiceImpl类只需实现具体的程序逻辑,而事务管理和其他逻辑(比如权限控制)则可以使用 AOP 动态织入到特定的连接点上。
⑤ 织入(Weaving)
织入是将增加添加到目标类的具体连接点上的过程。
AOP 就像一台织布机,将目标类、增强或者引介天衣无缝地编织到一起。
根据不同的实现技术,AOP 的织入方式也不同:
- 编译期织入:要求使用特殊的 Java 编译器。
- 类装载器织入:这要求使用特殊的类装载器。
- 动态代理织入:在运行期为目标类添加增强子类的方式
在 Spring 中,则采用动态代理织入方式,而 AspectJ 采用编译期织入和类装载器织入。
⑥ 代理(Proxy)
一个类被 AOP 织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。
根据不同的代理方式(JDK 动态代理或 CGLIB 动态代理),代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。
⑦ 切面(Aspect)
切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。
Spring AOP 就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中。
AOP 的工作重心在于如何将增强应用于目标对象的连接点上,这包括两项工作:
- 如何通过切点和增强定位到连接点上
- 如何在增强中编写切面的代码
注意哦:要弄懂 Spring 的 AOP 需先弄懂什么是动态代理,请参考其他文章。
增强的类型
Spring 使用增强类定义横切逻辑,同时由于 Spirng 仅支持方法连接点,增强还包括在方法的哪一点。
假如横切代码的方位星系,所以增强既包括横切逻辑,又包括部分连接点的信息 自然而然的,增强分为 5 种:
- 前置增强
- 后置增强
- 环绕增强
- 异常抛出增强
- 引介增强
这些增强接口都有一些方法,通过实现这些接口方法,并在接口方法中定义横切逻辑,就可以将它们织入目标类方法的相应连接点位置。
前置增强
”叹人生、不如意事,十常八九。”人不能太懂人情世故,也不能太懂人情世故。古时青楼女子,大多身世凄惨,卖身也是不得已而为之。生活嘛,要想过的舒服,就得赚够足够多的银子。可银子从哪里赚呢?自然是从客人身上来的。如果你是一个涉世未深的年轻姑娘,没啥才艺,自然是难以维持的了日常开销。为了那些绫罗绸缎,自然要学会琴棋书画的才艺,或许!还有打情骂俏的技巧?
闲话少谈,下面开始代码实现:
1 | public interface Girl { |
可以看到,这是一个经验不足的年轻姑娘,只会在门口喊欢迎光临,为了吸引顾客进店,必须的表现的更热情。我们通过BeforeAdvice接口来实现:
1 | import org.springframework.aop.MethodBeforeAdvice; |
BeforeAdvice是前置增强的接口,方法前置增强的MethodBeforeAdvice接口是其子类。MethodBeforeAdvice接口仅定义了唯一的方法:before(Method method, Object[] args, Object target)throws Throwable,其中method为目标类的方法;args为目标类方法的入参;而obj为目标类实例。当该方法发生异常时,将阻止目标类方法的执行。下面我们来测试一下:
1 |
|

&emsp很好,达到了预期效果,但有个问题,其对所有方法都进行了前置增强。
&emsp这个问题和 ProxyFactory 我们后面再讲解。
ProxyFactory
前面我们使用ProxyFactory(代理工厂)将GreetBeforeAdvice的增强织入了目标类YoungGirl中。我们回想一下,它与 JDK 动态代理和 CGLib 动态代理技术是否有一些相似之处?
不错,ProxyFactory内部就是使用 JDK 或 CGLib 动态代理技术将增强应用到目标类中的。
Spring 定义了org.springframework.aop.framework.AopProxy接口,并提供了两个final类型的实现类,如下图:

其中,Cglib2AopProxy使用 CGLib 动态代理技术创建代理,而JdkDynamicAopProxy使用动态代理技术创建代理。
若通过ProxyFactory的setInterfaces(Class[] interfaces)方法指定目标接口进行代理,则ProxyFactory使用JdkDynamicAopProxy;
若是针对类的代理,则使用Cglib2AopProxy。
此外,还可以通过ProxyFactory的setOptimize(true)方法让ProxyFactory启动优化代理方式,这样,针对接口的代理也会使用Cglib2AopProxy。
注意哦:使用 CGLib 动态代理技术时,必须引入 CGLib 类库。
前面GreetBeforeAdviceTest默认所使用的就是 CGLib 动态代理,我们也可以更改代码使用 JDK 动态代理,其运行结果和原来一样,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void before2() {
//1.创建girl和增强
Girl girl = new YoungGirl();
GreetBeforeAdvice beforeAdvice = new GreetBeforeAdvice();
//2.Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(girl.getClass().getInterfaces());//使用JDK动态代理
pf.setOpaque(true);//启动优化
//3.设置代理目标
pf.setTarget(girl);
//4.为代理目标提供增强
pf.addAdvice(beforeAdvice);
//5.生成代理实例
Girl proxy = (Girl) pf.getProxy();
proxy.greetTo("王老板");
proxy.showTo("王老板");
}
不知道你注意没有,上面代码中的ProxyFactory通过addAdvice(Advice)方法添加了一个增强。
用户也可以使用该方法添加多个增强,多个增强将形成一个增强链,它们的调用顺序和添加顺序一致,可以通过addAdvice(int,Advice)方法将增强添加到增强链的具体位置。(起始位置索引为 0)
Spring 为了帮助我们程序员减少工作量,提供了在配置文件里直接配置增强的功能,让我们以很 Spring 的方式声明一个代理:
1 |
|
ProxyFactoryBean是FactoryBean接口的子类,第一篇文章最后有补充到,它负责实例化一个Bean。而ProxyFactoryBean负责为其它Bean创建代理实例,它在内部使用前面的ProxyFactory来完成这项工作。下面为ProxyFactoryBean的几种常用属性:
target:代理的目标对象。proxyInterfaces:代理所要实现的接口,可以是多个接口,别名interfaces。interceptorNames:需要织入目标对象的Bean列表,采用Bean的名称指定。这些Bean必须是实现了org.aopalliance.intercept.MethodInterceptor或org.springframework.aop.Advisor的Bean,配置中的属性对应调用的属性。singleton:返回的代理是否是单例,默认单例。optimize:当设置为true时,强制使用CGLib动态代理。对于singleton的代理,我们推荐使用CGLib;对于其他作用域类型的代理,最好使用JDK动态代理。原因是虽然CGLib创建代理是速度较慢,但其创建处的代理对象运行效率较高;而使用JDK创建代理的表现则正好相反。proxyTargetClass:是否对类进行代理。当设置为true时,使用CGLib动态代理。
测试类:
1 | import org.junit.Test; |

后置增强
后置增强的接口为AfterReturningAdvice,使用起来和前置增强类似。
1.先定义一个实现后置增强接口的类:
1 | import org.springframework.aop.AfterReturningAdvice; |
2.直接定义配置文件,可以指定多个增强,如下面的配置:
1 |
|
3.测试
1 |
|

环绕增强
介绍完前置、后置增强,环绕增强的作用就显而易见了。环绕增强运行在目标类方法调用前后织入横切逻辑,它综合实现了前置、后置增强的功能,我们通过代码实现:
1.定义实现了环绕增强接口的实现类
1 | import org.aopalliance.intercept.MethodInterceptor; |
2.定义配置文件
1 |
|
3.测试类
1 |
|

可见,环绕增强实现了前置增强和后置增强联合的效果。
异常抛出增强
异常抛出增强最适合的应用场景是事务管理,当参与事务的某个DAO发生异常时,事务管理器必须回滚事务。此处不做介绍。
引介增强
引介增强是一种比较特殊的增强类型,它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的。通过引介增强,可以为目标类添加一个接口的实现,即原来目标类未实现某个接口,通过引介增强可以为目标类创建实现某接口的代理。
切面
在介绍增强时,增强被织入目标类的所有方法中。假设我们希望有选择地织入目标类的某些特定方法中,就需要使用切点进行目标连接点的定位。描述连接点是进行AOP编程的最主要的工作,我们再次给出Spring AOP如何定位连接点:
增强提供了连接点方位信息,如织入到方法前面后面等,而切点进一步描述了织入哪些类的哪些方法上。
切点类型
Spring提供了 6 种类型的切点:
- 注解切点:
AnnotationMatchingPointcut实现类表示注解切点。使用它支持在 Bean 中直接通过Java5.0注解标签定义的切点。(掌握) - 表达式切点:
ExpressionPointcut接口主要是为了支持AspectJ切点表达式语法而定义的接口。(掌握) - 静态方法切点:
StaticMethodMatcheerPointcut是静态方法切点的抽象基类,默认情况下它匹配所有的类。它包括 2 个主要的子类,分别是NameMathcMethodPointcut和AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法签名,而后者使用正则表达式匹配方法签名。 - 动态方法切点:
DynamicMethodMatcherPointcut是动态方法切点的抽象基类,默认情况下它匹配所有的类。 - 流程切点:
ControlFlowPointcut实现类表示流程切点。它是一种特殊的切点,它根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点。 - 复合切点:
ComposablePointcut实现类是为创建多个切点而提供的方便操作类。它所有的方法都返回ComposablePointcut类,这样就可以使用连接表达式对切点进行操作。
切面类型
由于增强既包括横切代码,又包括部分连接点信息(方法前,方法后注方位信息),所以可以仅通过增强类就可以生成一个切面。但切点仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点无法制作处一个切面,必须结合增强才能制作出切面。
Spring 使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包括横切代码和连接点信息。
切面可以分为3类:
- 一般切面
- 切点切面
- 引介切面
它们的类图如下:

Advisor:代表一般切面,仅包含一个 Advice。(了解即可)PointcutAdvisor代表具有切点的切面,包含Advice和Pointcut两个类,这样就可以通过类、方法名及方法定位等信息灵活地定义切面的连接点,提供更具适用性的切面。(掌握)IntroductionAdvisor:代表引介切面。引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切点使用ClassFilter进行定义。
下面再来看一下PointcutAdvisor的主要实现体系:

我们只介绍一下AspectJexpressionPointcutAdvisor和AspectJPointcutAdvisor。
AspectJexpressionPointcutAdvisor:用于AspectJ切点表达式定义切点的切面。AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面。
AOP 框架
AOP 工具的设计目标是把横切的问题(如访问控制、事务管理)模块化。使用类似OOP的方式进行切面的编程工作。
位于 AOP 工具核心的是连接点模型,它提供了一种机制,可以定位到需要在哪里发生横切。
AOP 框架种类有很多:
- AspectWerkz
- JBoss AOP
- Spring AOP
- AspectJ(常用)
AspectJ 是一个基于 Java 语言的 AOP 框架,它提供了强大的 AOP 功能。
AspectJ 实现AOP 有两种方式:
- 基于 XML 的声明式 AspectJ
- 基于注解的声明式 AspectJ
基于注解的声明式 AspectJ
我们还是以增强里面的例子,回顾一下代码:
1.使用前面实现类
1 | public class YoungGirl implements Girl { |
2.这次我们使用注解的方式来完成:
1 | import org.aspectj.lang.annotation.Aspect; |
我们发现这个切面没有实现任何特殊的接口,因为我们使用了 @Aspect 注解。
首先,在GreetByAspectJ类前标注了@Aspect注解,这样,第三方处理器就可以通过类是否拥有@Aspect注解判断其是否为一个切面。
其次,在 beforeGreeting()方法前标注了@Before注解,并为该注解提供了成员值"execution(* greetTo(..))"。这里提供了连个信息:@Before注解表示该增强是前置增强,而成员值是一个@AspectJ切点表达式。它的意思是:在目标类的greetTo()方法上织入增强,greetTo()方法可以带任意的入参和任意的返回值。
最后,在greetTo()方法中添加增强逻辑,该横切逻辑在目标方法前调用,可以通过下图描述这种关系:

3.测试类:
1 |
|
AspectJProxyFactory都不用介绍了,和ProxyFactory作用差不多,只是这里它是AspectJ定义的。这里使用了Java代码实现,不过我们一般都是用配置文件来实现,让我们来改造一下它:
1 |
|
1 | //配置文件的测试类: |
不管是 JAVA 还是配置文件的方式,结果都一样的,如下图。

我们会发现这里结果没客户名字,因为我们没有传参。难道使用@AspectJ定义的切面没有办法访问目标对象连接点的信息了嘛?当然不是的,我们等等讲解。
既然用到了注解,也用到了切点表达式,自然就得学会它是如何使用的。
AspectJ 注解
@Before:前置增强,相当于BeforeAdvice@AfterReturning:后置增强,相当于AfterReturningAdvice@Around:环绕增强,相当于MethodInterceptor@AfterThrowing:抛出增强,相当于ThrowsAdvice@After:Final增强,@DeclareParents:引介增强,相当于IntroductionInterceptor。
切点表达式函数
Spring 支持 9 个@AspectJ切点表达式函数,它们用不同的方式描述目标类的连接点。根据对象的不同,大致可以分为 4 种类型。
- 方法切点函数:通过描述目标类方法的信息定义连接点
- 方法入参切点函数:通过描述目标类方法入参的信息定义连接点
- 目标类切点函数:通过描述目标类类型的信息定义连接点
- 通过描述目标类的代理类的信息定义连接点
详见下表:
| 类别 | 函数 | 入参 | 说明 |
|---|---|---|---|
| 方法切点函数 | execution() | 方法匹配模式串 | 表示满足某一匹配模式的所有目标类方法连接点 |
| @annotation() | 方法注解类名 | 表示标注了特定注解的目标类方法连接点 | |
| 方法入参切点函数 | args() | 类名 | 通过判别目标类运行时入参对象的类型定义指定连接点 |
| @args() | 类型注解类名 | 通过判别目标类运行时入参对象的类是否标注特定注解来指定连接点 | |
| 目标类切点函数 | within() | 类名匹配串 | 表示特定域下的所有连接点 |
| target() | 类名 | 若目标类按类型匹配于指定类,则目标类的所有连接点匹配于这个切点 | |
| @within() | 类型注解类名 | 若目标类按类型匹配于某个类 A,且类 A 标注了特定注解,则目标类的所有连接点匹配这个切点 | |
| @target() | 类型注解类名 | 若目标类标注了特定注解,则目标类的所有连接点都匹配该切点 | |
| 代理类切点函数 | this() | 类名 | 代理类按类型匹配于指定类,则被代理的目标类的所有连接点都匹配该切点 |
@annotation()
@annotation() 是最常用的切点函数,其可以匹配添加了相关注解的方法:1
2
3
4
5
6
7
8/**
* 日志注解
*/
(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
public Log {
String value() default "";
}
1 |
|
最后,只要有相关方法添加了@Log注解,就会被 AOP 切入:1
2
3
4
5
6("查询所有评论")
("/comments")
public JsonResult queryList(Long questionId) {
List<CommentDTO> commentDTOList = commentService.queryList(questionId);
return new JsonResult<>(commentDTOList);
}
execution()
execution() 是比较常用的切点函数,其语法如下:
execution 语法示例如下:
execution(public * * (..)):匹配所有目标类的public方法。第一个表示返回类型,第二个表示方法名;而..代表任意入参的变量。execution(* *To(..)):匹配目标类所有以 To 为后缀的方法。
within()
within()函数所指定的连接点最小范围只能是类,而execution()函数所指定的连接点则可以大到包,小到方法入参。所有从某种意义上说,within()函数是execution()函数的子集。它的语法如下:
within 语法示例如下:
within(cn.wk.YoungGirl):匹配目标类YoungGirl的所有方法。如果表达式调整为within(cn.wk.Girl),则Girl接口实现类都不匹配。因为Girl本身是接口,不能被实例化,这样做是无意义的。within(cn.wk.*):匹配cn.wk包中所有类,不包括其子孙包,如cn.wk.aop包。within(cn.wk..*):匹配cn.wk包和其子孙包中所有类。
基于 XML 的声明式 AspectJ(了解)
基于 XML 的声明式 AspectJ 是指通过 XML 文件来定义切面,切入点及通知,所有的切面、切入点和通知都并需定义在<aop:config>元素内。

由于现在基本使用 SpringBoot 进行开发,这种方式基本被弃用了,在较老的 SSM 架构的项目中可能可以看到。
参考
- 陈雄华 林开雄 文建国. 精通 Spring 4.x 企业应用开发 [M]. 电子工业出版社,2017
文章信息
| 时间 | 说明 |
|---|---|
| 2019-03-07 | 初稿 |