引言
在企业级服务中,经常面临多个业务类中需要完成一些相同的事情,如日志记录、异常处理、事物管理、安全管理等,这些多个业务类共同关注的点也叫横切关注点( cross-cutting concern )。如果在每个业务类中都加上这些横切关注点逻辑,不仅工作量会很大,而且容易产生冗余代码。这时候为解决横切关注点的面向切面编程(AOP)应运而生,AOP 补充了面向对象编程(OOP)。OOP 中模块化的关键单元是类,而在 AOP 中模块化的单元是切面。切面支持跨多个类型和对象的关注点(例如事务管理)。
Spring 的一个关键组件就是AOP框架,虽然 Spring IOC 容器并不依赖 Spring AOP(这意味着你在不需要的时候可以不必在项目中引入 Spring AOP 依赖),但是AOP补充了Spring IoC,提供了一个非常强大的中间件解决方案。 Spring提供了两种简单而强大的自定义切面的方式:
- 基于Schema的,也就是在XML配置文件中提供AOP配置和基于注解的AOP配置
- 基于注解的AOP配置
这两种方式都提供了丰富的通知功能和使用 AspectJ 切点表达式语言 的支持,但是在织入时仍然使用 Spring AOP。
AOP 在 Spring 框架中使用主要用于:
- 提供声名式企业服务,最重要的服务莫过于声名式事物管理
- 让开发者实现自定义切面,在开发过程中使用AOP补充OOP编程的不足
1. AOP概念
首先,让我们弄清楚一些AOP的核心核心概念和技术术语:
切面(Aspect): 一个跨多个类关注的模块,在企业级Java应用中的事物管理(Transaction Management)就是一个横切关注点的很好例子。在Spring AOP中通过给普通POJO类在XML文件中进行AOP配置后者给普通POJO类添加 @Aspect 注解实现切面的定义。
连接点(Joint Point): 程序执行过程中一个点,例如方法的执行或者异常处理,在 Spring AOP 中,连接点始终代表方法的执行;
通知(Advice): 切面在特定的连接点上发生的行为,不同类型的通知包括Around、Before、After、After Returning等通知(通知类型之后再讨论)。很多AOP框架包含Spring,将通知看成拦截器和维护围绕连接点的拦截器链。
切点(Pointcut): 匹配连接点的正则表达式,通知与切点表达式紧密关联,并且运行在任意匹配切点表达式的连接点上(例如具有指定名字的方法的执行)。连接点与切入点表达式匹配的概念是 AOP 的核心,Spring默认使用 AspectJ 切入点表达式语言。
引入(Introduction): 代表类型声明其他方法或字段,Spring AOP 允许你将新的接口(和相应的实现)引入任何被通知的对象。例如,你可以使用一个 Introduction 来让一个bean实现一个 IsModified 接口,以简化缓存。(Introduction在 AspectJ 社区中称内部类声明。)
目标对象(Target Object): 被一个或多个切面通知的对象,也被称作通知对象。由于Spring AOP是由运行时代理实现的,因此目标对象永远是代理对象。
AOP代理: AOP框架为实现切面逻辑而创建的一个通知方法执行的对象,在Spring框架中,AOP代理是指JDK动态代理或者CGLIB代理。
织入(Weaving): 将切面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。与其他纯Java AOP 框架一样,Spring AOP 在运行时执行织入。
Spring AOP包含以下5种通知:
- 前置通知(Before Advice): 连接点方法执行前的通知,并不能阻止连接点方法流程的执行,除非执行过程中抛出异常;
- 返回通知(After Returning Advice): 连接点正常执行流程之后返回时的通知(不抛出异常的情况下);
- 异常通知(After Throwning Advice): 连接点方法执行过程中抛出异常时的通知;
- 后置通知(After Advice): 无论连接点方法是否发生异常都会执行的通知;
- 环绕通知(Around Advice): 环绕连接点方法执行过程的通知,这是AOP 5种通知中功能最强大的通知 。环绕通知可以自定义连接点方法执行前后过程中的行为。它也能选择是执行连接点方法流程,还是通过返回连接点方法的返回值或抛出异常的方式剪切被通知方法的执行。
虽然环绕通知是5种通知中功能最强大的通知,Spring AOP 也提供了各种类型的通知,但是官方文档还是建议你使用能实现你业务需求最弱功能的通知。例如你仅仅需要拿到一个方法的返回值去更新缓存,你最好使用后置通知。虽然使用环绕通知也能实现相同的业务,但是使用最准确的通知能够简化程序执行,并尽可能地避免潜在的错误。
所有通知参数都是静态类型的,因此你可以使用确定类型的通知参数(例如一个方法执行的返回值类型),而不是对象数组。
匹配切点表达式的连接点概念是AOP中的关键,它将AOP与只提供拦截的旧技术区分开来。切入点使通知能够独立于面向对象的层次结构。 例如,你可以给分布在服务层中的多个业务操作对象加上一个环绕通知以提供声名式事物管理。
2. Spring AOP 的功能和目标
Spring AOP 是基于纯 Java 对象实现的,不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用服务器中使用。Spring AOP 目前只支持方法级别的连接点拦截,通知必须加在 Spring 容器管理的 bean 方法上,属性级别的拦截目前还没有实现。如果你需要属性访问和更新连接点,可以考虑使用 AspectJ 语言。
Spring AOP 的AOP方法与大多数其他AOP框架不同。其目的不是提供最完整的AOP 实现(尽管Spring AOP非常强大)。相反,其目标是提供 AOP 实现和Spring IoC 之间的紧密集成,以帮助解决企业应用程序中的常见问题。
Spring框架的 AOP 功能通常与 Spring IoC 容器一起使用,切面(Aspect)是通过使用普通的 bean 定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的一个重要区别。使用 Spring AOP 不能轻松或有效地完成某些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。然而,我们的经验是,Spring AOP 为企业Java应用程序中大多数适合 AOP 的问题提供了一个优秀的解决方案。
Spring AOP 从未试图与 AspectJ 竞争来提供全面的AOP解决方案。Spring开发团队认为基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争的。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,从而在一致的基于Spring的应用程序体系结构中支持 AOP 的所有使用。这种集成并不影响Spring AOP API或AOP Alliance API, Spring AOP 保持向后兼容。
Spring 框架的一个核心原则就是非侵入性,它的思想就是不强迫你将框架中指定的类或接口引入到你的业务或领域模型中。然而在某些地方,Spring 框架给你引入 Spring 框架依赖到你代码库中的可选项。之所以提供这些选项,是因为在某些场景中,以这种方式阅读或编写某些特定功能的代码可能更容易。然而,Spring 框架几乎总是为你提供这样的选择:你可以自由地做出明智的决定,即哪种选择最适合你的特定用例或场景。
3 AspectJ支持
@AspectJ 指在普通Java类上加上注解使之成为切面类,@AspectJ 注解是作为AspectJ 项目的一部分引入 AspectJ5 版本的。Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的注解。但是 AOP 运行时仍然是纯 Spring AOP,并且不依赖于AspectJ 编译器。
3.1 开启@AspectJ支持
要在 Spring 配置中使用@AspectJ 切面,您需要启用 Spring 对基于@AspectJ 切面配置Spring AOP 的支持,并根据这些切面是否通知自动代理 bean。通过自动代理,如果Spring 确定一个bean 由一个或多个切面通知,它将自动为该 bean 生成一个代理来拦截方法调用,并确保根据需要执行通知。
可以通过 XML 或 java 风格的配置启用@AspectJ 支持。在这两种情况下,你还需要确保 AspectJ 的 aspectjweaver.jar 包位于应用程序的类路径上(版本1.8或更高)。这个库可以从 AspectJ 发行版的lib目录或 Maven 中央存储库中获得。
3.2 使用 Java 配置开启 @AspectJ 支持
使用Java @Configurtion 配置开启 @AspectJ 支持,你需要在Java配置类上添加@EnableAspectJAutoProxyl 注解,示例代码如下:
代码语言:javascript复制@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=false,exposeProxy=false)
public class AspectjConfig{
}
3.3 使用XML配置开启@AspectJ支持
使用基于 XML 配置开启 @AspectJ 支持,在应用上下文 applicationContext.xml 配置文件中添加 aop:aspectj-autoproxy 元素标签,示例代码如下:
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启aspectj自动代理支持-->
<context: conponent-scan base-package="com.example"/>
<aop:aspectj-autoproxy proxy-target-class="false" exposeproxy="false"/>
<!-- 在这里配置bean -->
</beans>
Spring AOP 可以使用JDK动态代理或 CGLIB 动态代理,如果目标对象没有实现任何接口,Spring AOP 会创建基于CGLIB的动态代理;如果目标对象实现了一个或多个接口,那么Spring AOP会创建基于JDK的动态代理。如果需要强制使用CGLIB动态代理,可以将 proxy-target-class 属性设置为true(如果是使用注解风格,则将 @EnableAspectJAutoProxy 注解的 proxyTargetClass 方法值改为true),这样即使目标对象实现了一个或多个接口,Spring AOP也会创建CGLIB动态代理。而 expose-proxy 属性设置为true时(使用 @EnableAspectJAutoProxy 注解时将其 exposeProxy 方法值改为true),则可以从 ApplicationContext 应用上下文中拿到动态代理对象。
4 声名切面
开启了 @AspectJ 支持后,任何在应用上下文中定义并具有 @Aspect 注解的Bean就是一个切面,它会被Spring容器自动发现并用来配置Spring AOP。下面的代码示例展示了配置一个切面最小的配置需要:
(1)在应用上下文中配置一个常规bean定义
代码语言:javascript复制<bean id="myAspect" class="com.example.aspect.AspectBean">
<!-- configure properties of the aspect here -->
</bean>
(2)在com.example.aspect.AspectBean
上添加org.aspectj.lang.annotation.Aspect
注解
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AspectBean {
}
切面类可以和其他类一样具有方法和属性,也可以包含切点、通知和引用声名。
通过组件扫描自动发现切面
你可以在你的 Spring XML 文件中通过一个常规的 bean 定义,也可以通过类路径扫描自动发现注册切面,与其他Spring 管理的Bean一样。注意添加 @Aspect注解对于 Spring 在类路径中自动发现切面还不够,还需要添加 @Component 注解。如下所示:
代码语言:javascript复制@Aspect
@Component
public class AspectBean {
}
5 声名切点
5.1 切点定义
切入点确定感兴趣的连接点,从而使我们能够控制何时执行通知。Spring AOP 只支持 Spring bean 的方法执行连接点,因此可以将切入点看作是与Spring bean上的方法执行相匹配的。切入点声明有两部分:
- 签名:由名称和任何参数组成;
- 切点表达式:它确定我们对哪个方法执行感兴趣。在AOP的 @AspectJ 注释风格中,切入点签名由一个常规方法定义提供,切入点表达式通过使用@Pointcut注解来表示(作为切入点签名的方法必须是void返回类型)。
下面的简易代码示例希望能帮助读者弄清楚切点签名和和切点表达式:
代码语言:javascript复制@Pointcut("execution(* transfer(..))") // 切点表达式
private void anyOldTransfer() { // 切点签名
}
5.2 Spring中支持的切点表达式
Spring AOP 支持以下几种 AspectJ 切点指示器(PCD)用于切点表达式中
5.2.1 常用的切点指示器
- execution: 用于匹配连接点方法执行,这是使用Spring AOP时使用的主要切点指示器,也是控制粒度最小的切点指示器;
- within: 限制匹配连接点目标对象为确定的类;
- this: 限制匹配连接点为具有指定bean引用类型的实例;
- target: 限制匹配连接点目标对象为指定类的实例;
- args: 限制匹配连接点目标对象方法参数为指定类型;
- @target: 限制匹配连接点目标对象头部有指定的注解类;
- @args: 限制匹配连接点目标对象方法参数具有指定类型的注解;
- @within: 限制匹配连接点目标对象具有指定类型的注解;
- @anotation: 限制匹配连接点目标对象头上具有指定类型的注解;
Spring AOP 也支持另外一个命名为bean的切点指示器,它限制匹配连接点为指定名称的bean或一系列bean集合(使用通配符时)的方法,使用示例如下:
bean(idOrNameOfBean)
idOrNameOfBean 字符可以是任何 Spring Bean 的名字,限制通配符支持使用*字符。因此,如果你为你的 Spring bean 建立一些命名约定,你可以编写一个bean 切点指示器表达式来选择它们。与其他切点指示器一样,bean切点指示器也可以使用 && (and),|| (or) 或 !(negation) 等逻辑操作符。
注意: bean 切点指示器只在 Spring AOP 中受支持,而在原生 AspectJ织入中不受支持,它是 AspectJ 定义的标准切点指示器的特定于 spring 的扩展,因此不能在 @Aspect 模型中声明的切面中使用 bean 切点指示器。
5.2.2 联合使用切点指示器
你可以使用&&、|| 或 ! 操作符联合使用多个切点表达式,也可以通过名字来引用切点表达式。下面的代码示例展示了3种切点表达式的使用:
代码语言:javascript复制@Pointcut("execution(public * (..))")
private void anyPublicOperation() {} //匹配任意public访问修饰符修饰的方法
@Pointcut("within(com.xyz.someapp.trading..)") //匹配包名以com.xyz.someapp.trading开头的任意类的所有方法
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //匹配以com.xyz.someapp.trading开头的任意类中任意以public访问修饰符修饰的方法
最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前面所示。当通过名称引用切入点时,应用普通的 Java 可见性规则(你可以在同一类型中看到private 修饰的切入点、层次结构中的 protect 修饰的切入点、任何地方的 public切入点,等等)。可见性不影响切入点匹配。
5.2.3 共享切点定义
在使用企业应用程序时,开发人员通常希望从几个切面引用应用程序的模块和特定的操作集。官方文档建议定义一个“SystemArchitecture”切面,它可以捕获用于此目的的公共切入点表达式。这样一个切面通常类似于下面的例子:
代码语言:javascript复制package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/*
* 匹配定义在web层且目标对象在com.xyz.someapp.web包及其子包中的所有类的任意方法
*/
@Pointcut("within(com.xyz.someapp.web..)")
public void inWebLayer() {}
/*
* 匹配定义在service层且目标对象在com.xyz.someapp.service包及其子包中所有类中的任 *意方法
*/
@Pointcut("within(com.xyz.someapp.service..)")
public void inServiceLayer() {}
/*
*匹配定义在数据访问层且目标对象在com.xyz.someapp.dao包及其子包中所有类中的任意方法
*/
@Pointcut("within(com.xyz.someapp.dao..)")
public void inDataAccessLayer() {}
/*
* 一个业务服务是定义在服务层接口中任意方法的执行
* 这种假定所有接口放在service包中,而实现类在其子包中
* 如果你把所有接口按功能分组(例如服务层接口在com.xyz.someapp.abc.service包和 * com.xyz.someapp.def.service包中,这样你可以这样使用切点表达式:
* "execution(* com.xyz.someapp..service..(..))"
* 同样,你可以使用bean切点指示器如"bean(Service)"书写切点表达式
* 这假定你以同样的风格命名Spring service Bean
*/
@Pointcut("execution( com.xyz.someapp..service..(..))")
public void businessService() {}
/*
*匹配数据库访问层中目标对象在com.xyz.someapp.dao..(..)及其子包中的任意方法
*/
@Pointcut("execution( com.xyz.someapp.dao..(..))")
public void dataAccessOperation() {}
}
你可以在任何需要切点表达式的地方引用在这样一个切面中定义的切点。
5.2.4 切点表达式解读与使用示例
在使用Spring AOP中,开发者最常使用 execution 切点指示器,execution 切点表达式格式如下:
代码语言:javascript复制execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(前面代码段中的rt-type-pattern)、名称模式和参数模式之外,其他所有部分都是可选的
- modifiers-pattern:方法访问修饰符模式,可选;
- ret-type-pattern: 方法返回类型模式,必须要有,*通配符代表任意返回类型;
- declaring-type-pattern:声名类型模式,可选项,有的话使用包含包名的全限定类名;
- name-pattern: 方法名模式,方法名,通配符代表任意方法名;
- (param-pattern):方法参数模式,()代表没有参数,(…)代表0个或多个参数
下面的代码示例展示了一些常用的切点表达式的使用:
代码语言:javascript复制//匹配任意公共方法
execution(public * *(..))
//匹配任意方法名以set字符开头的方法
execution(* set*(..))
//匹配com.xyz.service.AccountService类下任意方法
execution(* com.xyz.service.AccountService.*(..))
//匹配com.xyz.service包及其子包下任意类的所有方法
execution(* com.xyz.service.*.*(..))
//匹配com.xyz.service包下任意接口的所有方法
within(com.xyz.service.*)
//匹配com.xyz.service包及其子包下任意类的所有方法
within(com.xyz.service..*)
//匹配实现AccountService接口的代理的任意方法:
this(com.xyz.service.AccountService)
//匹配实现AccountService接口的目标对象的任意方法:
target(com.xyz.service.AccountService)
//匹配只带一个参数的方法,且该参数为可序列化参数
args(java.io.Serializable)
//匹配具有Transactional的目标对象任意方法
@target(org.springframework.transaction.annotation.Transactional)
//匹配目标对象声名有Transactional的方法
@within(org.springframework.transaction.annotation.Transactional)
//匹配带有个参数的方法,且运行时参数类型绑定有Classified注解
@args(com.xyz.security.Classified)
//匹配Spring容器中id或name属性值为tradeService的bean实例的方法
bean(tradeService)
//匹配Spring容器中id或name属性值以Service结尾的bean实例的方法
bean(*Service)
6 声名通知
通知与切点表达式紧密相连,并在程序运行时执行与切点匹配的前置(before)、后置(After)或环绕(Around)方法。切点明确了在哪里进行代码织入,而通知则确定了何时织入增强逻辑,通知可以是一个切点名的引用,也可以是在某处声名的切点表达式。
6.1 前置通知(Before Advice)
你可以在一个切面中使用@Before注解声名前置通知,示例代码如下:
(1)在Before注解中声名切点表达式
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
//切点方法执行前执行的逻辑
}
}
(2)
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void pointcut() {
}
@Before("pointcut")
public void beforeAdvice(){
//切点方法执行前执行的逻辑
}
}
@Before注解的源码如下:
代码语言:javascript复制package org.aspectj.lang.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Before {
String value(); //起点表达式值或引用
String argNames() default ""; //参数名
}
6.2 正常返回通知
After Returning 通知在匹配的方法正常返回时执行,你可以在切面中使用@AfterReturning 注解声名 After Returning 通知,示例代码如下:
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
//正常返回时执行的逻辑
}
}
returning 属性中的名字必须与通知方法中的参数名一致,当被拦截切点方法执行返回时,返回值会作为与参数名对应的参数传递给通知方法。也可以通过retVal 的类型限制匹配固定类型的返回值,上面的实例中 Object 类型可以匹配任意类型的返回值。
**注意:**在使用返回通知后返回一个完全不同的引用是不可能的
@AfterReturning注解源码如下:
代码语言:javascript复制package org.aspectj.lang.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterReturning {
String value() default ""; //切点表达式引用方法名
String pointcut() default ""; //切达表达式内容
String returning() default ""; //切点方法返回值名
String argNames() default ""; //切点方法参数名
}
6.3 异常通知(After Throwning Advice)
当匹配的连接点方法在程序执行发生异常时会执行异常通知。你可以在切面类中使用 @AfterThrowing 注解声名异常通知,示例代码如下:
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// 抛出异常时执行的逻辑
}
}
通常你需要在发生指定类型的异常时运行异常通知,你也需要在通知体中获取程序抛出的异常信息。你可以使用 throwing 属性限制匹配和绑定异常到通知参数中,使用示例如下:
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// 抛出DataAccessException类型异常时执行的逻辑
}
}
throwing 属性中的名字必须与通知方法参数中的名字一致,异常将传递到通知方法中对应的参数值中。抛出的异常类型可以指定连接点为抛出指定异常类型的方法,本例中限定抛出 DataAccessException 类异常的连接点。
6.4 后置通知(After Advice)
当存在匹配的连接点方法时,后置通知总是会被执行。你可以使用 @After 注解声名最终通知,最终通知可以同时处理正常返回和发生异常时的情况。最终通知通常用来释放资源或者处理类似目的,下面的代码示例展示了如何使用最终通知:
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
//释放版本锁处理
}
}
@After 注解类源码如下:
代码语言:javascript复制package org.aspectj.lang.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface After {
String value(); //切点表达式值或切点方法名应用
String argNames() default ""; //切点方法参数名
}
6.5 环绕通知(Around Advice)
最后一种通知是环绕通知,环绕通知会在匹配的连接点方法周围执行,它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真正执行方法。如果需要以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态,通常会使用Around通知。始终使用最不低强度的通知来满足你的需求(也就是说,如果before通知可以满足需求的话,就不要使用 around 通知)。
通过只用@Around注解来声名环绕通知,通知方法的第一个参数必须是一个ProceedingJoinPoint类型的参数,调用 ProceedingJoinPoint 对象的 proceed() 方法会触发底层方法的执行,proceed 方法也可以传递一个对象数组,当方法执行时,数组中的值用作方法执行的参数。以下代码示例展示如何使用环绕通知:
代码语言:javascript复制import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
环绕通知方法的返回值就是被拦截方法调用时的返回值,例如一个简单的缓存切面在缓存里有值时可以从缓存里返回一个值,没有的话就执行proceed()方法,注意 proceed() 方法在环绕通知体中可以被执行一次、多次或者零次,这些情况都是合法的。
7 获取通知中的参数
Spring AOP 提供了5中通知,这意味着你可以在通知签名中声名你需要的参数(参考前面的正常返回通知和异常通知中的代码示例),而不是一直使用对象数组。我们接下来再看如何获取通知方法中的参数值和其他与上下文相关的参数值。首先,我们来看看如何编写通用的通知,从而找出通知当前通知的方法。
7.1 获取当前连接点(JoinPoint)
任意通知方法都可以声明第一个参数为 org.aspectj.lang.JoinPoint 类型的参数(注意,环绕通知方法需要声明的第一个参数为 ProceedingJoinPoint类型,它是 JoinPoint 接口的子类)。JoinPoint 接口提供了下面这些非常有用的方法:
- Object[] getArgs() : 返回切点方法参数数组
- Object getThis() : 返回切点方法代理对象
- Object getTarget() : 返回切点方法目标对象
- Signature getSignature() : 返回被通知方法的签名(方法完整描述)
- String toString() : 被通知方法转字符串
7.2 给通知方法传递参数
到现在,我们已经学会了如何在通知方法中绑定切点方法的返回值和异常值(使用正常返回通知和异常通知),为了是切点方法的参数值可用,你可以使用args切点指示器绑定形式。如果在 args 表达式中使用参数名代替类型名,则在调用通知方法时将传递相应参数的值作为通知方法的参数值。举个例子可以说明,假设你需要通知一个携带第一个参数为 Account 类型参数的Dao操作,同时你需要在通知方法中访问该 account 参数值,你可以这样写:
代码语言:javascript复制@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
args(account,…) 作为切点表达式的一部分,起着两个左右:第一,限制匹配连接点方法至少携带一个参数,且第一个参数为 Account 类实例;第二,使得携带的 Account 类型参数在通知方法中可用。
另一种编写方法是声明一个切入点,该切入点在匹配连接点时提供Account对象值,然后从通知中引用指定的切入点。用法示例如下:
代码语言:javascript复制@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理对象(this),目标对象(target)和注解(@within, @target, @annotation, and @args)也能以类似的风格绑定。
7.3 通过参数的名确定参数
通知调用中的参数绑定,依赖于切点表达式中声明的参数名匹配通知方法和切点方法签名中声明的参数名。参数名无法通过Java反射获得,因此Spring AOP使用以下策略来确定参数名:
代码语言:javascript复制@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean*
}
如果第一个参数是 JoinPoint,ProcedingJoinPoint 或 JoinPoint.staticPart类型中的一个,你可以你可以将参数名从 argNames 属性值中移除。例如你要修改前置通知接收一个 JoinPont 对象,那么 argNames 属性可以不包含在切点表达式中,示例代码如下:
代码语言:javascript复制@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用argNames属性有点笨拙,因此如果没有指定 argNames 属性,Spring AOP将查看该类的调试信息,并尝试从局部变量表中确定参数名。
7.4 处理参数
我们在前面提到过,我们将描述如何使用在 Spring AOP 和 AspectJ 中一致工作的参数来编写 proceed() 调用。解决方案是确保通知签名按顺序绑定每个方法参数,下面的例子演示了如何做到这一点:
代码语言:javascript复制@Around("execution(List<Account> find*(..)) && "
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && "
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
8 通知顺序
如果多个通知的代码片段发生在同一个切点上将会发生什么?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。在进程进来时,优先级高的通知方法先运行,例如两个前置通知,优先级高的通知先执行;在进程从连接点方法出去时,优先级高的通知后执行,例如两个后置通知方法片段,优先级高的后执行。
当来自不同切面的两个通知逻辑需要在同一个切点上执行时,除非你指定优先级顺序,否则两个通知执行的顺序将是未知的。你可以通过指定不同切面的优先级控制两个切面中通知执行的顺序,在 Spring 项目中通常通过使切面类实现 org.springframework.core.Ordered 接口或者添加 @Order 注解来控制切面的优先级。
8.1 通过实现 Ordered接口定义优先级
代码语言:javascript复制package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered
@Aspect
public class OrderAspect implements Ordered{
private int order = 1;
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Before("execution(public * com.example.service.impl.*Service.save*(..))")
public void beforeAdviceMethod(){
//通知逻辑实现
}
}
8.2 通过添加@Order注解定义优先级
代码语言:javascript复制package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Aspect
@Order(value=1)
public class OrderAspect{
@Before("execution(public * com.example.service.impl.*Service.save*(..))")
public void beforeAdviceMethod(){
//通知逻辑实现
}
}
当定义在同一个切面中的两个同类通知需要运行在同一处切点时,此时通知的顺序是未知的,因为没有办法通过反射来检索javac编译类的声明顺序。解决方法是考虑将这些通知方法分解为每个切面类的每个连接点上的一个通知方法,或者将这些通知片段重构为可以在切面级上排序的独立切面类。
9 引入
引入(在AspectJ中称为类型间声明)使切面能够声明被通知的对象实现给定的接口,并代表这些对象提供接口的实现。
你可以通过使用 @DeclareParents 注解创建引用,这个注解用来声明匹配的类具有一个新的父类。例如,给定一个命名为 UsageTracked 的接口和实现这个接口的实现类,命名为 DefaultUsageTracked 。以下代码示例表明所有com.xzy.myapp.service 包下的实现类都要实现 UsageTracked 接口。
代码语言:javascript复制@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.* ", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由被 @DeclareParents 注解声名的属性类型决定,如上例中 mixin 字段的类型为 UsageTracked,那么 UsageTracked 接口就是要实现的接口。@DeclareParents 注解的value属性值是一个 AspectJ 类型模式。任何匹配类型的 bean 都要实现UsageTracked接口。注意,在前面示例的 before 通知中,服务bean 可以直接用作 UsageTracked 接口的实现。如果以编程方式访问一个bean,你可以编写以下代码:
代码语言:javascript复制UsageTracked usageTracked = (UsageTracked) appContext.getBean("myService");
10 一个使用 Spring AOP 的完整示例
现在我们已经知道怎么单独使用 Spring AOP 的部分功能了,那么现在让我们来综合使用它做些有用的事情。业务层方法有时会因为并发问题(例如获取锁失败),如果操作重试,那么可能在下一次中成功。对于存在这种并发问题的业务层服务,重试解决问题的合适方法(幂等操作,不需要返回给用户来解决冲突)。我们想通过透明地重试操作以避免客户看到乐观锁失败异常 (PessimisticLockingFailureException)。这种需求明显在服务层中很切多个服务类,因此通过切面解决是一个理想的解决方案。
10.1 定义一个切面类
因为我们要进行重试操作,所以需要使用环绕通知,这样就可以多次调用proceed()方法。下面的代码示例展示了一个切面的基本实现:
代码语言:javascript复制@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts ;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意,由于 ConcurrentOperationExecutor 切面实现了Ordered 接口,我们可以设置通知的优先级高于事物切面的通知。最大重试次数(maxRetries属性)和order属性都是通过Spring配置的。主要的行为都发生在 doConcurrentOperation 环绕通知中。注意当每次在 businessService() 方法运行重试逻辑时,程序尝试执行 proceed() 方法,如果因为捕获到 PessimisticLockingFailureExceptio 异常导致失败就再重复执行一次,知道重试次数大于最大重试次数为止。
10.2 注册切面类到Spring容器
将 ConcurrentOperationExecutor 切面类作为 bean 注册到 Spring 管理的容器对应的 Spring XML 配置代码如下:
代码语言:javascript复制<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
10.3 切面改进
为了改进切面,使它只重试幂等操作,我们可以定义以下幂等注解类:
代码语言:javascript复制@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.Method})
public @interface Idempotent {
// marker annotation
}
然后,我们可以使用 @Idempotent 注解来注释服务操作的实现。对切面类的更改是只重试幂等操作,这涉及到细化切入点表达式,以便只匹配幂等操作,如下所示:
代码语言:javascript复制@Around("com.xyz.myapp.SystemArchitecture.businessService() && "
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
11 小结
本文笔者主要参考了 Spring 官方文档中的面向切面编程部分讲解了 Spring AOP 的一些基本概念,以及如何在项目中开启 AspectJ 的支持,讲解了基于注解的风格的切面的定义、切点表达式的定义和5种通知的使用。使用的demo代码基本都是官方文档中的代码片段,在笔者的下一篇文章中将使用基于 SpringBoot 的项目,讲解利用Spring AOP 特性实现用户登录日志记录,接口调用耗时日志记录和一些操作权限验证等功能。
12 参考文章
[1] Spring5官方文档Spring Core部分之Aspect Oriented Programming with Spring (https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop)
推荐阅读
[1] SpringBoot项目中集成第三方登录功能
[2] 改造jeecg-boot项目,解决启动报错,跑通开发环境!
原创不易,欢迎看过文章的小伙伴点个赞和在看,谢谢!
---END---