Spring中的面向切面编程
1 背景
如果说面向对象编程(OOP)着眼于将一切事物抽象成对象,从对象彼此之间的联系构建项目的话,那么面向切面编程(AOP)则是从程序执行流的角度去思考这个问题。
对于常见的web应用来说,程序执行流程大致如下:
在这个过程中,各层各司其职:
- controller负责参数校验、请求服务以及视图选择等
- service负责业务逻辑
- ...
但是存在着几个问题:
- 对于通用的操作(接口安全性校验,重试操作),需要的地方都要重复执行
- 通用操作往往不属于业务逻辑,导致代码不清晰
如何才能够让这些通用操作从业务逻辑中独立出去?
面向切面编程(AOP)可以满足这些需求。
- AOP将执行流看作是一系列的
连接点(join point)
构成 -
连接点
可以通过切点(pointcut)
进行匹配 - 对
切点
设定合适的通知(advice)
定义在切点
上的操作 切点 + 通知 = 切面
对于上面的执行流,可以进行如下切面:
2 Spring AOP的使用
- Spring AOP中通过
切点(pointcut)
匹配method,采用@Pointcut
注解的方式; - 通过
通知(advice)
定义切点
的处理方式; -
切点
和通知
定义在切面
中,采用@AspectJ
注解的方式
AOP的术语有很多,目前实现较为完整的是
AspectJ
。Spring AOP只实现了AOP中部分,并且只能针对method
级别进行切点定义。
如果以method
作为程序执行流中的最小粒度,那么需要关注的问题有:
- 如何匹配到对应的method
- method执行前的操作
- method执行后的操作
- method执行前后的操作
2.1 开启Spring AOP
- xml配置文件中添加
<aop:aspectj-autoproxy/>
- pom中添加依赖项
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
2.2 切面
@Aspect
public class SystemArchitecture {
}
2.3 切点
2.3.1 定义
切点由两个部分组成:
- 切点表达式:用来匹配method
- 切点签名:用来给advice调用,用于限制advice的作用范围,可以说是切点表达式的别名
形如:
@Pointcut("") // 切点表达式
public void pointCutSignature(){} // 切点签名
2.3.2 切点表达式
支持的切点表达式类型:execution
,within
,this
,target
,@target
,@args
,@within
,@annotation
。
-
within
匹配指定类型内的方法
// com.anjuke.xiaohao.controller包下所有的方法
@Pointcut("within(com.anjuke.xiaohao.controller..*)")
public void inController(){}
-
args
匹配运行时参数的类型
// 匹配一个参数且运行时类型为java.io.Serializable的
args(java.io.Serializable)
-
@args
匹配运行时
参数带有指定注解的
// 匹配有一个参数且该参数在运行时带有@Classified注解
@args(com.xyz.security.Classified)
-
@annotation
匹配类型定义中有指定注解的
// 匹配有@Transactional注解的
@annotation(org.springframework.transaction.annotation.Transactional)
-
execution
匹配满足一定格式的method
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern 修饰符 public等 【可选】
- ret-type-pattern 返回值类型
- declaring-type-pattern 定义类型 【可选】
- name-pattern(param-pattern) 方法名(参数)
- throws-pattern异常类型
// 所有的public方法
execution(public * *(..))
// 所有set开头的方法
execution(* set*(..))
2.4 通知
2.4.1 定义
通知也由两个部分组成
- 通知类型(
Before
、AfterReturning
、@AfterThrowing
、After
以及Around
) - 通知执行的方法体
@Before("...")
public void beforeDoing() {
// do something
}
2.4.2 Before
Before
类型的advice会在匹配到的方面之前执行,通过@Before
表示,该类型很适合做参数校验
之类的工作。
完成参数校验
之类工作的难点在于如何捕获到请求参数进行逻辑校验,spring AOP有两种方法获取参数:
- 通知函数签名的第一个参数定义为
JoinPoint
(非around)或者ProceedingJoinPoint
(around) - 通过在
@Before
中指定args
JoinPoint 和 args可以共存
@Before("... && args(account,..)")
public void validateAccount(Account account) {
// ...
}
2.4.3 After
After由细分为三种类型,对应方法返回的不同的状态(正常返回@AfterReturning
、异常返回@AfterThrowing
、正常或异常返回@After
)
2.4.3 Around
Around是在方法执行之前和方法执行之后都执行,通过@Around
注解。这种类型主要用于在方法执行前后共享某些数据。例如当并发操作时,经常会有失败的情况,那么这时候可以通过 Around通知实现重试机制
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// do something before method run
retVal = pjp.proceed();
// do something after method run
return retVal;
}
}
3 通过Around断言实现方法的重试
思路:
- 通过java注解表明方法需要重试(注解中可输入重试的次数)
- 对重试注解建立切点,采用Around断言的方式实现重试
- 定义重试的Java注解,方法级别
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RetryMethod {
int times();
}
- 定义重试异常
public class RetryException extends Exception {
public RetryException(Exception e) {
super(e);
}
}
- 定义Around断言
@Aspect
@Component
public class MethodRetryAspect implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.anjuke.xiaohao.aop.PointcutTest.retryPointCut()")
public Object doRetryOperation(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("doRetryOperation");
// 获取注解中指定的重试次数
RetryMethod retryMethod = ((MethodSignature)pjp.getSignature()).getMethod().getAnnotation(RetryMethod.class);
maxRetries = retryMethod.times();
// 开始重试
int numAttempts = 0;
RetryException retryException;
do {
numAttempts++;
try {
System.out.println("doRetryOperation times " + numAttempts);
return pjp.proceed();
}
catch(RuntimeException ex) {
retryException = new RetryException(ex);
}
} while(numAttempts < this.maxRetries);
throw retryException;
}
}