Spring 有三大核心思想,其目的都是为了解耦。
我们日常开发中总能不知不觉用到其中两种,分别是控制反转(Inversion of Control, IOC)和依赖注入(Dependency Injection, DI)。
而面向切面编程(Aspect Oriented Programming, AOP)却时常被人所忽略,但它的作用却不可忽视。
AOP 的实现的目标是保证开发者在不修改原代码的前提下,去为系统中的业务组件添加某种通用功能。
在使用AOP时,我们能常常听到以下术语:Aspect、Pointcut、Advice、JoinPoint、Weaving。
- Pointcut:切点,表示一组JoinPoint,它定义了Advice将要发生的地方,
- JoinPoint:连接点,表示程序执行过程中能够插入切面的点,可以是方法的调用或异常的抛出。
- Advice:增强,包括处理时机和处理内容。通俗的将就是什么时候该做什么事。
- Aspect:切面,由同一类Pointcut和Advice组成。
- Weaving:织入,就是通过动态代理,在目标对象中执行处理内容的过程。
如果术语描述的不太明白,请允许我在介绍AOP之前讲一个故事:
在很久很久以前,有一个国家叫M国,其国力昌盛,但皇帝大限将至,便将皇位传给太子。
不久后,皇帝驾崩,太子正式登基。但太子念其国力昌盛,整日无所作为,朝事荒废。
十几年后,边境事犯,皇帝欲选取文武兼备的人率军去平定叛乱,但此时朝廷人才寥落,只有一人能担此任。
皇帝为了犒劳该将军,在其出征之前,将其亲子委以朝廷命官,辅佐在自己身边。
经过数年苦战,该将军荣耀而归,平定叛乱。皇帝高兴万分,赏其千金,封为万户侯。
经此事后,皇帝重整朝廷,不久后,国家便重回巅峰。
听完如上故事,请思考并类比术语的含义:
Pointcut:文武兼备的人
JoinPoint:上文中的将军
Advice:将军出征前皇帝留任其亲子在身边、将军回来后皇帝对其进行赏赐。
Aspect:文武兼备的人出征这个事件
Weaving:对文武兼备的人出征前后干的事的过程。
本故事只能使你理解它这些术语的含义,并不能描述AOP的目标,也就是解耦,相信解耦大家都清除,这里就不说了。
接下来开始详细介绍AOP的概念以及使用,首先介绍Pointcut,下面是一段官方介绍:
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
上面的例子定义了一个名为'anyOldTransfer'的切入点,它将匹配任何名为'transfer'的方法的执行。
Spring AOP支持以下AspectJ切入点指示符(PCD),用于切入点表达式中:
切入点指示符 | 描述 |
execution | 限制匹配连接点的方法 |
within | 限制匹配连接点的包或者类 |
@within | 限制匹配连接点的类带有指定注解 |
arg | 限制匹配连接点的参数类型 |
@args | 限制匹配连接点的参数带有指定注解 |
target | 限制匹配连接点目标对象的类型 |
@target | 与@within的功能类似,但注解的保留策略须为RUNTIME |
this | 限制匹配连接点的AOP代理类的类型 |
@annotation | 限制匹配连接点的方法带有指定的注解 |
bean | 限制连接点是指定的Bean,或一组命名Bean(使用通配符时) |
看完上述切点表达式不理解的话很正常,下面给出详细介绍:
1、@Pointcut是创建切入点,切入点不用写代码,返回类型为void。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(方法修饰符(可选) 返回类型 类路径 方法名 参数 异常模式(可选))
- 修饰符匹配(modifiers-pattern?)
- 返回值匹配(ret-type-pattern)
- 类路径匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)
- 参数匹配(param-pattern),可以指定具体参数类型,多个参数用","隔开。
- 异常类型匹配(throws-pattern?)
- 以上匹配中后面跟着"?"的表示是可选项。
多个匹配之间我们可以使用链接符 &&
、||
、!
来表示 “且”、“或”、“非”的关系。
但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。
示例:
// 匹配AccountService的任意方法 execution(* com.xyz.service.AccountService.*(..))
// 匹配服务包下的任意方法 execution(* com.xyz.service.*.*(..))
//匹配服务包或其子包下的任意方法 execution(* com.xyz.service..*.*(..))
// 匹配位于service包下任意类型 within(com.xyz.service.*)
// 匹配代理实现AccountSercice接口的任意类 this(com.xyz.service.AccountService)
// 匹配目标对象实现AccountService接口的任意类 target(com.xyz.service.AccountService)
2、Advice是增强通知,其有五种通知类型,分别如下:
- @Before,在目标方法调用前执行
- @After,在目标方法调用后执行
- @AfterReturning,在目标方法返回后调用
- @AfterThrowing,在目标方法抛出异常后调用
- @Around,将目标方法封装起来
首先介绍一下@AfterReturning,官网带参数示例介绍如下:
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属性中使用的名称必须与通知方法中的参数名称相对应。
当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。
另外returning子句也限制了只能匹配返回指定类型的值,如果是Object类型,将可以匹配任何返回值。
其次是@AfterThrowing的带参数示例:
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) { // ... } }
throwing属性中使用的名称必须和通知方法中的参数名称相对应。
当方法抛出异常时,该异常将作为相应的参数传递给通知方法。
另外throwing子句也限制了只能匹配到指定异常类型。
接下来介绍@Around示例:
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; } }
@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}); }
@Around可以在方法执行之前和之后工作,并能决定该方法何时执行以及如何调用。
该通知方法的第一个参数必须为ProceedingJoinPoint类型。
在通知方法中,调用ProceedingJoinPoint的proceed方法来执行匹配的方法。
proceed方法也可能被调用解析Object[]数组,它被用于匹配方法执行的参数。
通知方法返回的值将是匹配方法调用者看到的返回值。
最后,任何通知方法都可以将JoinPoint作为它的第一个参数,@Around除外,它第一个参数必须是ProceedingJoinPoint。
JoinPoint可以提供许多有用的参数,比如getArgs、getThis、getTarget等等。
目前我们已经看到了如何绑定返回值或异常值。如果要使参数用于通知接收,可以使用绑定形式的args。
如果在args表达式中使用参数名替代类型名称,则在调用通知方法时,将相应参数的值作为参数值传递就可以了。
示例如下:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") public void validateAccount(Account account) { // ... }
arg(account,..)切入点表达式有两个作用,第一它将匹配限制为方法需要有至少一个参数。
第二它限制了参数的类型为Account,并且使该对象可以用于通知方法。
另外它也可以用如下方式表示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // ... }
代理对象、目标对象、和注解等都可以像如下示例使用:
首先定义一个注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); }
然后是与@Auditable相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
通知参数和泛型:(不适用于集合泛型)
public interface Sample<T> { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection<T> param); }
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") public void beforeSampleMethod(MyType param) { // Advice implementation }
通知方法中的参数绑定依赖于切入点表达式中使用的名称。
因为参数名称无法通过java反射活动,因此Sping AOP使用如下策略确定参数名称:
@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,ProceedingJoinPoint等,则可以省去参数。
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()") public void audit(JoinPoint jp) { // ... use jp }
JoinPoint、ProceedingJoinPoint类型特别方便,可以通过它的getArgs方法获取参数。
当多个通知都希望在同一连接点运行时,除非另外指定,否则执行顺序是不确定的。
可以通过org.springframework.core.Ordered的Order注解来确定顺序,值越低,优先级越高。
3、引入(Introductions)
这个注解感觉用起来作用不大,就不介绍了。