一、概述
面向切面编程(AOP)对编程结构提供了另一种思考方式,给面向对象编程(OOP)进行了补充。OOP 模块化的关键单元是类(Class),而 AOP 的模块化关键单元是切面(Aspect),切面实现的模块可以横切多个类型和对象,例如事务管理。AOP 框架是 Spring 的关键组件之一,Spring IoC 容器不依赖于 AOP,AOP 给 Spring IoC 提供了一个强大的中间件解决方案。
1. 基本概念
下面是一些 Spring AOP 相关的术语,我们会针对这些概念去讲怎么实现 Spring AOP 相关编程。
1.1 相关术语
- 切面(Aspect):切面横切多个类的模块,可以使用常规类基于 XML 实现,也可以使用
@Aspect
注解来实现。 - 连接点(Join point):程序执行时的一个特定节点,例如一个方法的执行或者一个异常的处理。
- 通知(Advice):切面在特定连接点执行动作,例如前置通知、后置通知、环绕通知等。
- 切点(Pointcut):用于匹配连接点的描述,Pointcut 表达式来匹配哪些连接点需要被执行,而执行的场景和业务则通过通知来定义。
- 目标对象(Target object):被一个或多个切面切入的对象,因为 Spring AOP 通过运行时代理实现,所以通常是代理对象。
- 织入(Weaving):将切面与其他对象连接起来,并创建目标对象的过程,这个过程可以发生在类的编译期、加载期或运行期,Spring AOP 采用动态代理实现,在运行时期实现织入。
1.2 通知类型
- 前置通知(Before advice):在连接点前面执行的通知,它不能终止连接点后续的执行流程,除非抛出一个异常。
- 后置通知(After returning):在连接点正常返回时执行的通知,并且没有抛出异常。
- 环绕通知(Around advice):围绕在连接点执行的通知,环绕通知是功能最强大的通知,它可以在方法调用的前面或后面添加自定义操作,还可以在方法执行返回时或抛出异常时添加操作。
- 最终通知(After (finally) advice):在连接点退出时执行的通知,无论是正常退出还是异常退出。
- 异常通知(After throwing advice):在方法抛出异常时执行的通知。
环绕通知的功能最强大,它也能实现其他通知的功能,但我们在一般情况下,使用前置或后置等特定通知就能完成的功能需求,就不要使用环绕通知,这样可以避免很多错误。
2. 特性与能力
Spring AOP 使用纯 Java 实现,它不是在编译期或类加载期织入,因此它适用于 Servlet 容器或者其他应用服务。目前只支持对方法进行拦截(Spring Bean 里的方法),不支持字段级的拦截功能。
Spring AOP 不同于其他的 AOP 框架,比如 AspectJ,因为它的目的不是提供完完全全的 AOP 实现(尽管 Spring AOP 已经很强大了),而是更好地结合 Spring IoC,来解决企业项目的大多数问题。
二、AOP 实现
Spring AOP 的实现需要定义三个部分,包括切面(Aspect)、切点(Pointcut)和通知(Advice),所以不管使用什么方式来实现 AOP 功能,都不能缺少其中一个部分。
1. 基于XML方式实现
在 Spring 中基于 XML 实现 AOP 功能时,主要使用标签来配置,它下面包含三个标签,包含切点(
)、Advisor(
)和切面(
)的配置,而且这三个标签的配置是有序的。
1.1 定义切面(Aspect)
基于 XML 方式实现定义一个切面,需要在 Spring 容器中定义一个普通的 Java Bean 对象,这个对象的字段和方法描述了所有的状态和行为,要实现一个完整的 AOP 功能,还需要在切面标签里面定义切点和通知。
<aop:config> <aop:aspect id="myAspect" ref="aBean"> ... </aop:aspect> </aop:config> <bean id="aBean" class="..."> ... </bean>
在 Spring 的 XML 配置中,属性里面带有ref
的都是需要配置引用,而不是直接配置值。
1.2 定义切点(Pointcut)
切点可以控制通知需要执行的范围,Spring 只支持对注册为 Bean 类的方法作为切入点,在标签内使用
标签来定义切点。
<aop:config> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> </aop:config>
也可以在标签内使用
标签定义切点。
<aop:config> <aop:aspect id="myAspect" ref="aBean"> <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/> </aop:aspect> </aop:config>
定义切点表达式的方法我们在第五节会详细讲到。
1.3 定义通知(Advice)
Spring AOP 五种通知类型,分别是前置通知、后置通知、环绕通知、最终通知和异常通知,在可以分别定义。
<aop:aspect id="beforeExample" ref="aBean"> <aop:pointcut id="myPointcut" expression="execution(* cn.codeartist.spring.aop.service.*.*(..))"/> <aop:before pointcut-ref="myPointcut" method="doBefore"/> <aop:after-returning pointcut-ref="myPointcut" method="doAfterReturning"/> <aop:around pointcut-ref="myPointcut" method="doAround"/> <aop:after pointcut-ref="myPointcut" method="doAfter"/> <aop:after-throwing pointcut-ref="myPointcut" method="doAfterThrowing"/> </aop:aspect>
在配置通知的标签中,pointcut-ref
属性配置已经定义好的切点。method
属性定义需要执行的方法,这些方法在切面中定义的 Bean 中实现。
1.4 通过定义 Advisor 实现
在 Spring 中,Advisor 是只包含一个通知和切点的切面,切点的定义和之前一样,通知的定义通过注册 Bean 来实现,注入的类根据实现不同的Advice
接口来注册不同的通知类型。
实现接口来定义通知
public class AopBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) { System.out.println("AopBeforeAdvice.before"); } }
配置 Advisor 实现 AOP
<aop:config> <aop:pointcut id="p" expression="execution(* cn.codeartist.spring.aop.service.*.*(..))"/> <aop:advisor advice-ref="aopBeforeAdvice" pointcut-ref="p"/> </aop:config> <bean id="aopBeforeAdvice" class="cn.codeartist.spring.aop.advice.AopBeforeAdvice"/>
2. 基于注解方式实现
Spring 基于注解实现 AOP 只需要将在普通 Java 类上添加注解就可以定义切面,而不需要在 XML 文件中进行配置,在前面我们讲 Spring IoC 的时候就说到过,Bean 的管理和注册也可以使用基于 XML 或基于注解的方式来实现,其原理是一样的,在使用注解实现之前,我们要启用 AspectJ 注解支持。
在 XML 配置中启用
在 Spring 的 XML 配置文件中添加标签来启用。
<aop:aspectj-autoproxy/>
在 Java 配置中启用
在 Spring 的配置类上添加@EnableAspectJAutoProxy
注解来启用。
@Configuration @EnableAspectJAutoProxy public class AppConfig { }
2.1 定义切面(Aspect)
基于注解定义一个切面,只需要将一个在 Spring 容器中注册过的类上加上@Aspect
注解即可。定义了切面的类和普通的类一样,也有字段和方法,同时,它也要包含切点和通知的定义。
在 Spring 容器注册对象
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"> <!-- configure properties of the aspect here --> </bean>
添加注解来实现切面
@Aspect public class NotVeryUsefulAspect { }
2.2 定义切点(Pointcut)
在配置为切面的类中,在方法上使用@Pointcut
注解可以定义切点。
@Aspect public class NotVeryUsefulAspect { @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature }
2.3 定义通知(Advice)
Spring AOP 五种通知类型可以分别通过在方法上添加@Before
、@AfterReturning
、@Around
、@After
和@AfterThrowing
注解来实现。
@Aspect public class NotVeryUsefulAspect { @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature @Before("anyOldTransfer()") public void doAccessCheck() { // 每个通知的注解都需要指定切点,可以是已经定义的切点方法 } @Before("execution(* com.xyz.myapp.dao.*.*(..)") public void doAccessCheck() { // 每个通知的注解都需要指定切点,可以直接编写切点表达式 } @AfterReturning("anyOldTransfer()") public void doAccessCheck() { // ... } @AfterReturning(pointcut="anyOldTransfer()", returning="retVal") public void doAccessCheck(Object retVal) { // 通过returning属性来获取方法的返回值 } @Around("anyOldTransfer()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } @After("anyOldTransfer()") public void doReleaseLock() { // ... } @AfterThrowing("anyOldTransfer()") public void doRecoveryActions() { // ... } @AfterThrowing(pointcut="anyOldTransfer()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // 通过throwing属性来获取抛出的异常 } }
每个通知的注解都需要指定切点,可以是已经定义的切点方法,也可以直接编写切点表达式。@AfterReturning
注解中的returning
属性可以获取方法的实际返回值,@AfterThrowing
注解中的throwing
属性可以获取方法抛出的异常,它们都是通过和方法的参数名称来进行绑定的。
2.4 通过定义 Advisor 实现
和基于 XML 配置定义的方法一样,Advisor 是只包含一个通知和切点的切面,我们可以使用 Spring 提供的 Advisor 接口实现类,也可以自定义实现该接口。
@Bean public AspectJExpressionPointcutAdvisor aspectjExpressionPointcutAdvisor() { AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor(); advisor.setExpression("execution(* cn.codeartist.spring.aop.service.*.*(..))"); advisor.setAdvice(new AopBeforeAdvice()); return advisor; }
例如上面我们使用AspectJExpressionPointcutAdvisor
类来实现 AOP,这种方法可以利用 AOP 来封装一些自定义功能。