浅析Spring AOP
在正常的业务流程中,往往存在着一些业务逻辑,例如安全审计、日志管理,它们存在于每一个业务中,然而却和实际的业务逻辑没有太强的关联关系。
图1
这些逻辑我们称为横切逻辑。如果把横切的逻辑代码写在业务代码中,散落在各个地方,则会变得非常难以维护,代码也会显得过于臃肿。
Spring AOP为处理这些问题提供了一种很好的方法。
1 AOP术语
1 通知(advice)。通知是指真正执行的目标逻辑。例如为系统记日志,那么通知做的事情,就是记录日志。
2 连接点(joinpoit)。连接点指何时执行advice的代码。系统中如果想要为所有的start()方法执行之前,记录一条"our system is starting"的日志,那么,start()方法被调用的时机,称为一个连接点。
3 切点(pointcut)。切点指何处执行advice的代码。例如指定特定的一个路径下的start()方法被调用时,执行advice的代码,那么,这个特定的路径可以理解为切点。通常我们使用正则表达式定义匹配的类和方法来确定切点。
4 切面(aspect)。 切面是通知、切点和连接点的全部内容。它定义了何时在何处,完成什么功能。
5 织入(weaving)。织入指实际代码执行到切点时,完成对目标对象代理的过程。织入有如下三种方式:
a.编译器织入。需要特殊编译器支持,例如AspectJ的织入编译器,在编译期间将代码织入。
b.类加载时。目标类在载入JVM时将切面引入。需要特殊的classLoader支持,改变目标类的字节码,增加新的动作。
c.运行期。在运行期运用动态代理技术,代理目标类。Spring AOP就是使用这种方式。
2 Spring对AOP的支持
Spring对AOP提供了多种支持,大体上分为2种思路。
1 使用动态代理,在运行期对目标类生成代理对象。代理类封装了目标类,并拦截被通知的方法的调用,再将调用转发给真正的目标类方法调用。在代理类中,可以执行切面逻辑。基于这种模式,仅限于对方法的拦截。如果想要实现更细粒度的拦截,例如特定的字段的修改,则需要使用第2种思路了。
2 使用AspectJ特有的AOP语言。这种方式的成本在于,需要学习AspectJ的语法。
大多数情况下,第一种思路已经可以满足我们的需求。
3 Demo
我们可以使用execution指示器来指示切点。如图2
图2
execution指示器具体指定了哪个包下面哪个类执行什么方法时,会被织入横切逻辑。Spring AOP的demo网上有很多,但是原理其实都一样。这里贴出其中一种。
我们需要2个类,一个类(Biz)用作正常的业务逻辑,一个类用作横切(AdviceTest)。同时写一个类Main测试
1.Biz
1 public class Biz { 2 public void doBiz(){ 3 System.out.println("doing biz"); 4 } 5 }
2.AdviceTest
1 public class AdviceTest implements Advice, MethodInterceptor { 2 3 @Override 4 public Object invoke(MethodInvocation invocation) throws Throwable { 5 advice(); 6 invocation.proceed(); 7 return null; 8 } 9 10 public void advice(){ 11 System.out.println("this is a advice"); 12 } 13 14 }
3.Main
1 public class Main { 2 public static void main(String[] args) { 3 ApplicationContext applicationContext = new ClassPathXmlApplicationContext(new String[]{"config/spring/spring-aop.xml"}); 4 Biz biz = (Biz)applicationContext.getBean("biz"); 5 biz.doBiz(); 6 } 7 }
4.Spring配置
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans 3 xmlns="http://www.springframework.org/schema/beans" 4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 5 xmlns:aop="http://www.springframework.org/schema/aop" 6 xsi:schemaLocation=" 7 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 8 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 9 "> 10 11 <bean id="adviceTest" class="aop.AdviceTest"/> 12 13 <bean id="biz" class="aop.Biz"/> 14 15 <aop:config> 16 <aop:pointcut id="bizPointCut" expression="execution(* aop.Biz.*(..))"/> 17 <aop:advisor pointcut-ref="bizPointCut" advice-ref="adviceTest"/> 18 </aop:config> 19 20 </beans>
执行Main函数代码,输出如下
this is a advice
doing biz
可见,实际的biz逻辑已经经过增强。
4 源码分析
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。在我们的例子中,显然使用的是Cglib。如图3
图3
为什么我们配置了Biz的bean,得到的却不是Biz的实例呢?这和Spring对xml文件的标签解析策略有关。对于AOP相关的bean的解析,在AopNamespaceHandler类里面有相关代码,感兴趣的同学可以去研究下。
我们继续往下面单步调试,发现进入了CglibAopProxy类里面。
1 public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 2 Object oldProxy = null; 3 boolean setProxyContext = false; 4 Class<?> targetClass = null; 5 Object target = null; 6 try { 7 if (this.advised.exposeProxy) { 8 // Make invocation available if necessary. 9 oldProxy = AopContext.setCurrentProxy(proxy); 10 setProxyContext = true; 11 } 12 // May be null. Get as late as possible to minimize the time we 13 // "own" the target, in case it comes from a pool... 14 target = getTarget(); 15 if (target != null) { 16 targetClass = target.getClass(); 17 } 18 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 19 Object retVal; 20 // Check whether we only have one InvokerInterceptor: that is, 21 // no real advice, but just reflective invocation of the target. 22 if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { 23 // We can skip creating a MethodInvocation: just invoke the target directly. 24 // Note that the final invoker must be an InvokerInterceptor, so we know 25 // it does nothing but a reflective operation on the target, and no hot 26 // swapping or fancy proxying. 27 Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); 28 retVal = methodProxy.invoke(target, argsToUse); 29 } 30 else { 31 // We need to create a method invocation... 32 retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); 33 } 34 retVal = processReturnType(proxy, target, method, retVal); 35 return retVal; 36 } 37 finally { 38 if (target != null) { 39 releaseTarget(target); 40 } 41 if (setProxyContext) { 42 // Restore old proxy. 43 AopContext.setCurrentProxy(oldProxy); 44 } 45 } 46 }
无耻的把代码直接贴出来。我们重点关注下下面这一行
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
在本文例子的场景下,会在此创建一个CglibMethodInvocation执行上下文,并执行proceed()方法。
图4
最终执行AdviceTest中invoke()方法的调用。在AdviceTest的invoke方法中,我们可以自定义自己想要执行的增强逻辑invocation.proceed()来执行目标类的方法。
图5
5 总结
Spring AOP为许多与业务无关的逻辑的执行,提供了一种很好的解决思路。本文也只是抛砖引玉,在实际的Spring源码中,还是比较复杂的,还需要细细研究才行。
参考文献:
《Spring In Action》
《Spring源码深度解析》