Spring AOP 是 AO P技术在 Spring 中的具体实现,它是构成 Spring 框架的另一个重要基石。(本文通过阅读《精通 Spring+4.x++ 企业应用开发实战》一书的总结)
在开始文章前,有几个问题需要思考一下:
AOP 是 Aspect Oriented Programing 的简称,最初被称为“面向方面编程”,这个翻译向来为人所诟病。但是我们更倾向于用“面向切面编程”的译法,因为它更达意。
按照软件重构思想的理念,如果多个类中出现相同的代码,则应该考虑定义一个父类,将这些相同的代码提取到父类中。比如 Horse、Pig、Camel 这些对象都有 run() 和 eat() 方法,通过引入一个包含这两个方法的抽象的 Animal 父类,House、Pig、Camel 就可以通过继承 Animal 复用 run() 和 eat()。通过引入父类消除多个类中重复代码的方式在大多数情况下是可行的,但世界并非永远这样简单,请看如下代码:
public class UserService {
private TransactionManager manager;
private PerformanceMonitor pmonitor;
private UserDao userDao;
public void removeUser(String userId) {
pmonitor.start();
manager.beginTransaction();
userDao.removeUser(userId);//①
manager.commit();
pmonitor.end();
}
public void addUser(User user) {
pmonitor.start();
manager.beginTransaction();
userDao.addUser(user);//②
manager.commit();
pmonitor.end();
}
}
上面代码斜体部分是方法性能监听代码,它在方法调用前启动,在方法调用返回前结束,并在内部记录性能监视的结果信息。而黑色粗体的代码是事务开始和事务提交的代码。发现①、②处的业务代码淹没在重复化非业务性的代码之中,性能监视和事务管理这些非业务性代码葛藤缠树般包围着业务性代码。
假设将 UserService 业务类看成一段圆木,将 addUser() 和 removeUser() 方法分别看成圆木的一截,会发现性能监视和事务管理的代码就好像一个年轮,而业务代码是圆木的树心,这也正是横切代码概念的由来。
我们无法通过抽象父类的方式消除如上所示的重复性横切代码,因为这些横切逻辑依附在业务类方法的流程中,它们不能转移到其他地方去。
AOP 独辟蹊径,通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案。对于习惯了纵向抽取的开发者来说,可能不太容易理解横向抽取方法的工作机制,因为Java语言本身不直接提供这种横向抽取的能力。暂把具体实现放在一旁,先通过图解的方式归纳出AOP的解决思路。
从上图可以看出,AOP 希望将这些分散在各个业务逻辑中的相同代码通过横向切割的方式抽取到一个独立的模块中,还业务逻辑类一个清新的世界。当然,将这些重复性的横切逻辑独立出来还是很容易的,但如何将这些独立的逻辑融合到业务逻辑中完成和原来一样的业务流程,才是事情的关键,这也正是 AOP 要解决的主要问题。
特定点是程序执行的某个特定的位置,如类开始初始化前、类初始化后、类的某个方法调用前/调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点被称为“连接点”。Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时及方法调用前后这些程序执行点织入增强。我们知道,黑客供给系统需要找到突破口,没有突破口就无法进行攻击。从某种程度上来说,AOP 也可以看成一个黑客(因为它要向目前类中嵌入额外的代码逻辑),连接点就是 AOP 向目标类打入楔子的候选锚点。
连接点由两个信息确定:一是用方法表示的程序执行点;二是相对位置表示的方位。如在 Test.foo() 方法执行前的连接点,执行点为 Test.foo(),方位为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中定义。
每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。但在为数众多的连接点中,如何定位某些感兴趣的连接点呢? AOP 通过“切点”定位特定的连接点。借助数据库查询的概念来理解切点和连接点的关系再适合不过了:连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系一个切点可以匹配多个连接点。
在 Spring 中,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP 的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。确切地说,应该是执行点而非连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体的连接点上,还需要提供方位信息。
增强是织入目标类连接点上的一段程序代码,是不是觉得 AOP 越来越像黑客了,这不是往业务类中装入木马吗?我们大可按照这一思路去理解增强,因为这样更形象易懂。在 Spring 中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。正因为增强既包含用于添加到目标连接点上的一段执行逻辑,又包含用于定位连接点的方位信息,所以 Spring 所提供的增强接口都是带方位名的,如BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。如BeforeAdvice表示方法调用前的位置,而 AfterReturningAdvice 表示访问返回后的位置。所以只有结合切点和增强,才能确定特定的连接点并实施增强逻辑。
增强逻辑的织入目标类。如果没有 AOP,那么目标业务类需要自己实现所有的逻辑,就如 UserService 类所示。在 AOP 的帮助下,UserService 只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑可以只用 AOP 动态织入特定的连接点上。
引介是一种特殊的增强,它为类增加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过 AOP 的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
织入是将增强添加到目标类的具体连接点上的过程。AOP 就像一台织布机,将目标类、增强或者引介天衣无缝地编制到一起。我们不能不说“织入”这个词太精辟了。根据不同的实现技术,AOP 有 3 种织入方式。
Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
一个类被 AOP 织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。
切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。Spring AOP 就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中。
AOP 的工作重心在于如何将增强应用于目标对象的连接点上。这里包括两项工作:第一,如何通过切点和增强定位到连接点上;第二,如何在增强中编写切面的代码。
JDK的动态代理主要涉及java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。(Java动态代理可参考http://blog.csdn.net/dilixinxixitong2009/article/details/78687289一文)
使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点可以从Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)中看得很清楚:第二个入参interfaces就是需要代理实例实现的接口列表。在实际开发中,难道对一个简单业务表的操作也需要老老实实地创建5个类(领域对象类、DAO接口、DAO实现类、Service接口和Service实现类)吗?对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK动态代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这项空缺。
CGLib采用底层的字节码技术,可以作为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。
AOP联盟为增强定义了org.aopalliance.aop.Advice接口,Spring支持5中类型的增强,先来了解一下增强接口继承关系图:
带 <
下面是一个服务员提供服务接口:
public interface Waiter {
void greetTo(String name);
void serveTo(String name);
}
定义一个服务生类:
public class NaiveWaiter implements Waiter {
public void greetTo(String name) {
System.out.println("green to " + name + "...");
}
public void serveTo(String name) {
System.out.println("serving " + name + "...");
}
}
定义一个前置增强:
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] objects, Object o) throws Throwable {
String clientName = (String) objects[0];
System.out.println("how are you! Mr." + clientName);
}
}
BeforeAdvice 是前置增强的接口,方法前置增强的 MethodBeforeAdvice 接口是其子类。Spring 目前只提供方法调用的前置增强,在以后的版本中可能会看到 Spring 提供的其他类型的前置增强,这正是 BeforeAdvice 接口存在的意义。MethodBeforeAdvice 接口仅定义了唯一的方法:before。当该方法发生异常是,将阻止目标方法的执行。
接下来看看测试类:
public class BeforeAdviceTest {
@Test
public void before() {
Waiter waiter = new NaiveWaiter();
BeforeAdvice advice = new GreetingBeforeAdvice();
//Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
//设置目标类
pf.setTarget(waiter);
//为代理目标增加增强
pf.addAdvice(advice);
//生成代理实例
Waiter proxy = (Waiter) pf.getProxy();
proxy.greetTo("John");
proxy.serveTo("Tom");
}
}
输出结果:
how are you! Mr.John
green to John...
how are you! Mr.Tom
serving Tom...
正如期望看到的一样,前置增强在每个方法都得到实现。
Spring定义了org.springframework.framework.AopProxy接口,并提供了两个实现类,如下图所示:
其中,CglibAopProxy 使用 CGLib 动态代理技术创建代理,而 JDKDynamicAOPProxy 使用JDK动态代理技术创建代理。如果通过ProxyFactory的setInterfaces(Class[] interfaces)方法指定目标接口代理,则ProxyFactory使用JdkDynamicAopProxy;如果是针对类的代理,则使用 CglibAopProxy。此外,还可以通过 ProxyFactory的setOptimize(true) 方法让 ProxyFactory 启动优化代理方式,这样,针对接口的代理也会使用 CglibAopProxy。
BeforeAdviceTest 使用的是 CGLib 动态代理技术,当指定针对接口进行代理是,将使用 JDK 动态代理技术。
//指定对接口进行代理
pf.setInterfaces(waiter.getClass());
如果指定启用代理优化,则ProxyFactory还将使用CglibAopProxy代理。
//指定对接口进行代理
pf.setInterfaces(waiter.getClass());
//启用优化
pf.setOptimize(true);
ProxyFactory 通过 addAdvice(Advice) 添加一个增强,用户可以使用该方法增加多个增强。多个增强形成一个增强链,它们的调用顺序和添加顺序一致,可以通过 addAdvice(int, Advice) 方法将增强添加到增强链的具体位置(第一个位置为 0)。
使用 ProxyFactory 比直接使用 CGLib 或 JDK 动态代理技术创建代理省了很多事,可以通过 Spring 的配置以“很 Spring 的方式”声明一个代理。
ProxyFactoryBean 是 FactoryBean 接口实现类,它负责实例化一个 Bean。ProxyFactoryBean 负责为其他 Bean 创建代理实例,它在内部使用 ProxyFactory 来完成这项工作。下面进一步了解一下 ProxyFactoryBean 的几个常用的可配置属性。
interceptorNames是 String[] 类型的,它接收增强 Bean 的名称而非增强 Bean 的实例。这是因为 ProxyBeanFactory 内部在生成代理类时,需要使用增强 Bean 的类,而非增强 Bean 的实例,以织入增强类中所写的横切逻辑代码,因而可以说增强是类级别的。对于属性是字符数组类型且数组元素是 Bean 名称的配置,最好使用
当然,对于希望尽量简化配置文件的开发者来说,也可以采用逗号方式进行配置(字符串数组编辑器支持这种配置),如下:
p:interceptorNames="greetingAdvice,greetingAftrerAdvice"
环绕增强允许在目标类方法调用前后织入横切逻辑,它综合实现了前置、后置增强的功能。
public class GreetingInterceptor implements MethodInterceptor {
//①截取目标类方法的执行,并在前后添加横切逻辑
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
//②目标方法入参
Object[] args = methodInvocation.getArguments();
//③在目标方法执行前调用
System.out.println("how are you! " + args[0]);
//④通过反射机制调用目标方法
Object obj = methodInvocation.proceed();
//⑤在目标方法执行后调用
System.out.println("谢谢光临");
return obj;
}
}
Spring 直接使用 AOP 联盟所定义的 MethodInterceptor 作为环绕增强的接口。该接口拥有唯一的接口方法 Object invoke(MethodInvocation) throws Throwable。MethodInvocation 不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,通过 MethodInvocation 的 getArguments() 方法可以获取目标方法的如参数组,通过 proceed() 方法反射调用目标实例相应的方法。通过在实现类中定义横切逻辑,可以很容易地实现前后的增强。
运行结果:
how are you! John
green to John...
谢谢光临
可见,环绕增强达到了前置和后置增强的联合效果。
引介增强是一种比较特殊的增强类型,它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的。通过引介增强,可以为目标类添加一个接口的实现,即原来目标类未实现某个接口,通过引介增强可以为目标类实现某个接口的代理。这种功能富有吸引力,因为它能够在横向上定义接口的实现方法,思考问题的角度发生很大的变化。
Spring 定义了引介增强接口 Introductioninterceptor,该接口没有定义任何方法,Spring 为该接口提供了 DelegatingIntroductionInterceptor 实现类。一般情况下,通过扩展该实现类定义自己的引介增强类。如果对所有的业务类都织入了性能监视的增强,由于性能监视会影响业务系统的性能,所以是否启用性能监视应该是可控的,即维护人员可以手工打开或关闭性能监视的功能,可以通过引介增强来实现这一诱人的功能。
首先定义一个用于标识目标类是否支持性能监视的接口,代码如下:
public interface Monitorable {
void setMonitorActive(boolean active);
}
该接口仅包括一个 setMonitorActive(boolean active )方法,希望通过该接口方法控制业务类性能监视功能的激活和关闭状态。
ControllablePerformanceMonitor 为目标类引入性能监视的可控功能:
public class ControllablePerformanceMonitor
extends DelegatingIntroductionInterceptor
implements Monitorable{
private ThreadLocal monitorStatusMap = new ThreadLocal();
public void setMonitorActive(boolean active) {
monitorStatusMap.set(active);
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object obj = null;
//对于支持性能监视可控代理,通过判断其状态决定是否开启性能监控功能
if(monitorStatusMap.get() != null && monitorStatusMap.get()) {
System.out.println("开始性能监控");
obj = super.invoke(mi);
System.out.println("结束性能监控");
} else {
obj = super.invoke(mi);
}
return obj;
}
}
ControllablePerformanceMonitor 在扩展 DelegatingIntroductionInterceptor 的同时,还必须实现 Monitorable 接口,提供接口方法的实现。在 ① 处定义了一个 ThreadLocal 类型的变量,用于保存性能监视开关状态。之所以使用 ThreadLocal 变量,是因为这个控制状态代理类变成非线程安全的实例,为了解决单实例线程安全的问题,通过 ThreadLocal 让每个线程单独使用一个状态。
在 ③ 处覆盖了父类中的 invoke() 方法,该方法用于拦截目标方法的调用,根据监视开关的状态有条件地对目标实例方法进行性能监视。
下面通过 Spring 的配置,将这个引介增强织入业务类中:
②
引介增强的配置与一般的配置有比较大的区别:首先,需要指定引介增强所实现的接口,如 ① 处所示,这里的引介增强实现了 Monitorable 接口;其次,由于只能通过为目标类创建子类的方式生成引介增强的代理,所以必须将 ProxyTargetClass 设置为true。
如果没有对 ControllablePerformanceMonitor 进行线程安全的特殊处理,就必须将 singleton 属性设置为 true,让 ProxyFactoryBean 产生 prototype 作用域类型的代理。这就带来一个严重的性能问题,因为 CGLib 动态代理创建代理的性能很低,而每次通过 getBean() 方法从容器中获取作用域类型为 prototype 的 Bean 时都将返回一个新的代理实例,所以这种性能的影响是巨大的,这也是为什么在代码中通过 ThreadLocal 对 ControllablePerformanceMonitor 的开关状态进行线程安全化的处理的原因。通过线程安全化处理后,就可以使用默认的 singleton Bean 作用域,这样创建代理的动作仅发生一次。
接下来的代码对织入性能监视控制接口业务类方法的调用情况:
public class IntroduceTest {
@Test
public void introduce() throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resource = resolver.getResources("classpath:com/smart/bean1.xml");
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(resource);
//默认情况没有开启织入
Waiter waiter = (Waiter) factory.getBean("waiter");
waiter.greetTo("John");
//开启织入
Monitorable monitorable = (Monitorable) waiter;
monitorable.setMonitorActive(true);
waiter.greetTo("John");
}
}
运行结果:
green to John...
开始性能监控
green to John...
结束性能监控