摘要
AOP是通过对程序结构的另一种思考,补充了OOP。在OOP中,最关键的模块是类,而在AOP中最关键的模块是切面。Spring AOP是基于代理的原理实现的。Spring AOP使用JDK的动态代理或者CGLIB来创建代理实例。
一、什么是AOP
AOP(Aspect-oriented Programming,面向切面编程)是通过对程序结构的另一种思考,补充了OOP(Object-oriented Programing,面向对象编程)。在OOP中,最关键的模块是类,而在AOP中最关键的模块是切面。AOP关注的是横跨不同类的逻辑的封装。
AOP是Spring框架的一个核心组件,Spring AOP的使用有两种风格,分别是:Schema(XML)和@AspectJ
AOP在Spring中的应用有:
- 提供了声明式的企业级服务(如最重要的声明式事务管理)
- 让用户自定义切面对OOP设计补充。
AOP的一些概念
- Aspect(切面):横跨不同类的关注部分的封装抽象
- Join Point(连接点):程序运行中的一些节点,比如方法的执行、异常。在Spring中,Join Point 通常代表一个方法的执行
- Advice(增强):在Join Point上的处理,类型可以有"around"、"before"、"after"。Spring将Advice构建成拦截器,围绕着Join Point维护着一条拦截链
- Pointcut(切点):和Join Point相对应起来,Pointcut是一个匹配规则,匹配上Pointcut的Join Point会在上面执行Advice
- Introduction(引入):为一个类型添加方法或字段,Spring AOP 允许引入新的接口(和对应实现)到目标对象上
- Target Object(目标对象,Advised Object):织入Advice后的对象,因为Spring AOP使用动态代理实现后,目标对象通常是一个代理对象
- Weaving(织入):链接Aspect和其它对象并生成Advice Object的工程。
二、AOP使用
以下是分别使用@AspectJ和Schema风格定义一个AOP的例子,此例子用于在服务执行出错时,重试执行服务。(高并发下的乐观锁异常处理)
要注意的是下面所执行的服务是幂等的才可以重试的。后面会展示如何通过定义一个幂等注解,并对幂等注解进行过滤配置。
1、@AspectJ风格
@AspectJ注解风格是从AspectJ项目中引入的,但是AOP还是只使用Spring AOP,而不是依赖AspectJ的编译器和织入器。
使用Java代码启用Aspect:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
或者使用XML配置启用Aspect:
<!-- 声明自动为spring容器中那些配置@Aspect切面的bean创建代理 -->
<aop:aspectj-autoproxy/>
定义Aspect、Pointcut和Advice:
// 注解Aspect
@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;
}
// 定义Pointcut和Advice
@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;
}
}
配置Aspect对应的Bean:
<!-- 配置Bean -->
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
定义幂等注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
为前面的Aspect添加幂等注解过滤:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}
2、Schema(XML)支持
定义Aspect、Pointcut和Advice:
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;
}
// 环绕增强,用于失败重试
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;
}
}
配置Aspect、Pointcut和Advice:
<aop:config>
<!-- 定义切面 -->
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<!-- 定义切入点 -->
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<!-- 定义环绕增强 -->
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<!-- 定义Bean -->
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
定义幂等注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
为前面的Aspect添加幂等注解过滤:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
3、两种AOP声明风格的对比
Aspect同样是一个面向切面的框架,Spring通过Aspect无缝地将AOP和IoC整合在一起。
使用Spring AOP还是AspectJ
Spring AOP 在开发中不需要引入AspectJ的编译器和织入器,如果想在Spring的Bean引入增强。如果不是管理Spring容器,则需要AspectJ了。
在Spring AOP中使用@AspectJ还是XML
- 所有切面、切点、增强都写在一个或几个配置文件里便于维护,且不需要修改Java代码;
- XML风格AOP仅支持"singleton"切面实例模型,而采用AspectJ风格的AOP则没有这个限制;
- XML风格的AOP不支持命名切入点的的声明,而AspectJ没有这个限制(@Pointcut);
参考:简单说说SpringAOP与Aspectj的不同,以及使用SpringAOP所需的最小jar依赖
三、AOP的代理机制
Spring AOP是基于代理的原理实现的。Spring AOP使用JDK的动态代理或者CGLIB来创建代理实例。JDK动态代理是JDK的内置代理,而CGLIB则是一个开源的类定义库(重新打包到spring-core中)。
关于代理的概念,参考:27--静态代理模式和JDK、CGLIB动态代理
Spring AOP使用ProxyFactory来创建代理,类似代码如下:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.adddInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy(); // 返回jdk或者cglib生成的代理对象
// this is a method call on the proxy!
pojo.foo();
}
}
如果代理对象实现了至少一个接口,则会使用JDK动态代理。如果代理对象没有实现任何接口,则会使用CGLIB进行代理。
使用CGLIB的一些限制:
- final类和方法不能使用增强(子类不能不能重写final方法)
- 从Spring 4.0开始,代理对象的构造函数不再被调用两次,因为CGLIB代理实例通过Objenesis创建。只有JVM不允许构造函数绕过时,才可能看到来自Spring AOP支持的双重调用和相应的调用日志。
强制使用CGLIB:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
<!-- 或者 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>