AOP(Aspect Oriented Programming): 面向切面编程,将重复性的横切性质逻辑模块化,织入到目标对象中。
在程序设计中,会遇到一些不能通过纵向继承解决的重复代码,比如事务控制、性能监控等,这些代码并非业务逻辑所需要关注、却又不得不掺杂在业务逻辑中,造成了业务程序不够清晰、简单,并且需要重复去编写。为了解决这个问题,AOP的设计思路独辟蹊径,通过抽取这些横向逻辑到独立模块,然后再在编译、加载或运行时将这些横向逻辑插入到原业务代码中,实现了横向统一逻辑与业务逻辑的解耦,这也是其名称的由来。
AOP有其特定应用场景,不是OOP的替代方案,而是OOP的有益补充。主要应用于横切性逻辑的处理,包括事务控制、性能监控、访问控制等等。
AOP的实现方案有:
上一篇文章我们介绍了Spring IoC的具体细节,而Spring AOP是建立在Spring IoC的基础上的,与Spring IoC、AspectJ有很好的整合,对AspectJ有部分功能的支持。
理解AOP相关的术语是极其重要的,后面的具体实施其实就是定义切面(切点、增强)的过程,即两个方面的内容:1.如何定位连接点;2.如何编写增强代码。
Spring AOP底层原理:基于动态代理技术,具体使用JDK或者是CGLib动态代理
适用场景:JDK为目标类创建实现同一接口的代理对象,适用于接口实现类的代理场景,对于没有实现接口的类代理则无计可施。
使用步骤:
/**
* InvocationHandler接口的实现类,包含有横切逻辑
*/
public class PerformaceHandler implements InvocationHandler {
private Object target;
public PerformaceHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
PerformanceMonitor.begin(target.getClass().getName() + "." + method.getName());
Object obj = method.invoke(target, args);
PerformanceMonitor.end();
return obj;
}
}
/**
* 使用JDK动态代理
*/
@Test
public void proxy() {
// 定义目标类实例
ForumService target = new ForumServiceImpl();
// 定义InvocationHandler的具体实现类,其包含有具体的横切逻辑
PerformaceHandler handler = new PerformaceHandler(target);
// 通过Proxy创建代理对象,可强制转换为目标类的类型
ForumService proxy = (ForumService) Proxy.newProxyInstance(target
.getClass().getClassLoader(),
target.getClass().getInterfaces(), handler);
// 调用代理对象的方法,其中已经包含有横切逻辑
proxy.removeForum(10);
proxy.removeTopic(1012);
}
适用场景:采用为目标类生成子类的方式进行代理,对于没有实现接口的目标类进行代理。
底层技术:基于底层字节码技术,在子类中采用方法拦截技术拦截所有父类方法的调用并顺势织入横切逻辑。
/**
* CGLib动态代理实现
*/
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
// 设置需要创建子类的类(目标类)
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
// 创建子类实例
return enhancer.create();
}
/**
* 拦截父类所有方法调用
* @param obj
* @param method
* @param args
* @param proxy
* @return
* @throws Throwable
*/
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
PerformanceMonitor.begin(obj.getClass().getName() + "." + method.getName());
Object result = proxy.invokeSuper(obj, args);
PerformanceMonitor.end();
return result;
}
}
/**
* 使用CGLib动态代理
*/
@Test
public void proxy() {
//使用CGLib动态代理
CglibProxy cglibProxy = new CglibProxy();
ForumService forumService = (ForumService)cglibProxy.getProxy(ForumServiceImpl.class);
forumService.removeForum(10);
forumService.removeTopic(1023);
}
使用建议:在代理单例对象或具有实例池的对象时,可采用CGLib动态代理,而使用prototype类型对象代理时,使用JDK动态代理。(在SpringAOP中可通过参数配置)
根据不同场景,Spring AOP有多种使用方式:Advisor、@AspectJ、Schema。
Spring AOP中的增强包含有连接点的方位信息,同时目前只支持方法层面的增强,因此根据不同方位提供了五种类型的增强接口,通过实现不同的接口得到横切逻辑不同的触发时机。
增强主要接口的继承关系如图:
编码方式使用增强
测试一个增强或通过编码方式使用增强,可通过ProxyFactory类将增强织入到目标类中创建代理:
@Test
public void before() {
Waiter target = new NaiveWaiter();
BeforeAdvice advice = new GreetingBeforeAdvice();
ProxyFactory pf = new ProxyFactory();
//pf.setInterfaces(target.getClass().getInterfaces());
//pf.setOptimize(true);
pf.setTarget(target);
pf.addAdvice(advice);
Waiter proxy = (Waiter)pf.getProxy();
proxy.greetTo("John");
proxy.serveTo("Tom");
System.out.println(proxy.getClass());
}
ProxyFactory类中依赖了AopProxy接口,该接口的实现类有CglibAopProxy、JdkDynamicAopProxy,分别对应不同的代理技术,具体使用哪一个,通过以下策略控制:
Spring配置方式使用增强
也可在xml配置文件中为目标类添加增强生成代理Bean:
<bean id="greetingBefore" class="com.smart.advice.GreetingBeforeAdvice" />
<bean id="target" class="com.smart.advice.NaiveWaiter" />
<bean id="waiter"
class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyInterfaces="com.smart.advice.Waiter" p:target-ref="target"
p:interceptorNames="greetingBefore" />
配置代理Bean时,class属性指定为ProxyFactoryBean类,实际是使用SpringIoC中的FactoryBean接口功能,为复杂Bean提供灵活的代码实例化的功能。属性包括:
另外,还有singleton、optimize、proxyTargetClass布尔属性值,分别是定是否为单例(默认)、启用CGLib动态代理优化、对类进行代理。
配置好后,启动Spring容器,从容器获取对应Bean即为代理对象的Bean,为此需要将目标Bean配置为其他名称。
后置增强、环绕增强的使用方式大体相似,不同的是接口方法参数定义有所不同,但也都包含了必要的信息。下面简单介绍下抛出异常增强、引介增强的不同之处。
抛出异常时增强
主要应用于事务管理的场景,在抛出异常的情况下回滚事务。ThrowsAdvice是一个标签接口,运行时Spring通过反射机制,查找符合以下规则的方法:
引介增强
引介增强的连接点为类级别,增强类需要实现目标接口的方法(提供增强的实现,也是目标类动态实现接口的默认实现),同时,直接继承DelegatingIntroductionInterceptor,覆盖invoke方法。由于只能通过生成子类的方式创建代理,必须指定proxyTargetClass为true。
前面我们只实现了增强的逻辑,通过Spring提供的FactoryBean为目标类创建代理,此时为目标类所有方法织入了横切逻辑。对应AOP术语,我们尚未指定具体的切点,执行更个性化的增强动作。
切点在Spring中通过Pointcut表示,查看一下接口定义
public interface Pointcut {
Pointcut TRUE = TruePointcut.INSTANCE;
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
可以看到Pointcut引用了ClassFilter、MethodMatcher两个接口,通过这两个接口描述要对哪些目标类进行过滤。其中,MethodMatcher分为静态和动态方法匹配,静态仅对方法签名进行匹配,而动态则支持检查运行时方法的入参值,动态匹配对运行时的性能影响很大。静态、动态可通过isRuntime方法进行区分。
切点划分
切点类型也体现在了切面类型的划分中,下面通过介绍切面,使用具体的切点。
切面划分
切面的概念包含有增强和切点,因为增强中包含了部分连接点的配置信息、也有横切代码逻辑,因此可以将增强也看作一个切面,这也就是在配置代理类时interceptorNames属性可以直接引用advice的原因。
大的分类上,除了只包含Advice的切面外(因为匹配目标类所有方法,一般不会使用),切面还包括切点切面、引介切面,其中切点切面时最常用的切面类型。切点切面具体有6个具体的实现类,都可以对应到Pointcut上,最常用的切面类型为DefaultPointcutAdvisor,动态切点、流程切点、复合切点都通过该实现类得到具体的切面。
具体使用上,虽然每种类型的切面属性上稍有差异,但与只使用advice类似,增加了Advisor的定义、配置,在代理工厂Bean配置时,interceptorNames属性要指定我们配置的advisorBean即可。
自动代理创建
配置代理时,还有一个痛点,目前只能通过ProxyFactoryBean为指定的目标类创建代理,如果我们需要创建代理的目标类很多,岂不是要配置很多类似的Bean,即使通过Spring的父子配置简化,但工作量还是很大的,Spring为我们提供了三类自动创建代理的策略:
类自身方法代理
书中还介绍类自身方法之间调用时无法通过代理对象执行,只能在目标类中直接调用,也就是无法插入增强的逻辑,书中给出了作者的解决方案,思路是通过注入自身Bean调用自己的方法实现,有兴趣的读者可自行查阅。
@AspectJ是我们最常使用也是优先使用的方式,其配置的内容与Advisor本质上是相同的,只不过对我们的程序侵入性更低。
先看使用@AspectJ定义一个切面的示例:
@Aspect
public class OperationLogAspect {
@Pointcut("execution(public * com.iic.service.*.*(..)) && @annotation(com.iic.service.OperationLogCatcher)")
public void recordLog() {
}
@Around("recordLog() && @annotation(com.doumi.logmanager.service.oplog.OperationLogCatcher)")
public Object saveOptLog(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed(); // 执行目标类方法
// operationLogService.addOperationLog(“记录操作历史”);
}
}
示例中应用了以下几个注解:
声明后,类似于ProxyFactory,可以通过AspectJProxyFactory对象编程方式使用生成代理;也可通过配置AnnotationAwareAspectJAutoProxyCreator自动代理创建Bean的方式生成代理Bean(或使用简洁配置方式:aop:aspectj-autoproxy)
切点表达式包括有函数和入参,同时入参支持通配符的使用,表达式之间可通过逻辑运算符构建更复杂的表达式,所支持的切点函数明细如下:
另外,在对于连接点方法入参、返回值、抛出异常的绑定,本文没有详细介绍,简单来说是可以通过这种机制实现切点函数入参的精简,需要保证参数名称的一致,规则引擎会自动解析对应参数的类型。
如果不使用@Aspect注解定义切面,也能通过Schema配置的方式使用切点表达式。在配置文件中应用
使用切点表达式的同时,想引用基于Spring增强接口实现类的方式配置Advisor可使用
Spring AOP为我们提供了多种配置、使用的方式,有基于实现接口的、基于注解配置以及基于XML Schema。新项目中,可采用简洁的注解配置的方式,为了对老项目兼容,可采用Schema的配置。由于考虑到实现Spring提供的接口导致一定程度的代码与框架耦合,所以一般不采用基于Advisor类的配置方式,但有一种情况除外:基于ControlFlowPointcut的流程切面只能使用基于Advisor类的方式。
虽然配置形式很多,但本质上需要指定的内容还是切面、切点、增强这些,理解了AOP的这些基本概念,相信对无论使用何种配置都会做到心中有数。