第七章 Spring AOP基础

Aspect Oriented Programming,我们无法通过抽象父类的方式来消除一些重复的横切代码,AOP是通过横向抽取机制来进行抽象的。AOP的 一些术语如下:

  • 连接点(JoinPoint):程序执行的某个位置,如:类初始化前、类初始化后、方法调用前、方法调用后、方法抛出异常后。但是Spring仅支持 方法的连接点,即只能在方法的调用前、方法调用后、方法抛出异常时、方法调用前后这些程序执行点织入增强。一个连接点由两个信息确认:一是用方法表示的程序 执行点;二是用相对位置表示的方位。 Spring用切点对执行点进行定位,而方位则在增强类型中定义。
  • 切点(CutPoint):每个程序都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序中客观存在的事物。切点相当于一些匹配条件,AOP 通过切点来定位某些特定的连接点。Spring中切点通过Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责解析切点所设定 的查询条件,找到对应的连接点(确切的说是执行点,因为切点只定位到某个方法上,而连接点还包含方位信息)。
  • 增强(Advice):在Spring中,增强除了用于描述一段特定逻辑的程序代码以外,还拥有一个和连接点相关的信息——即执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。正因为增强既包含用于 添加到目标连接点上的一段执行逻辑,又包含用于定位连接点的方位信息,所以Spring提供的增强接口都是包含方位名的,如BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。只有结合切点和增强,才能 确定特定的连接点并实施增强逻辑。
  • 目标对象(Target):增强逻辑的织入目标类,如果没有AOP,则目标业务类需要自己实现所有的逻辑,包括那些公共的横切逻辑。在AOP的帮助下,性能监控和事务管理等这些横切逻辑就可以使用AOP动态织入到 特定的连接点上。
  • 引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为 这个接口的实现类(注意这个和反射不太一样)。
  • 织入(Weaving):织入是指将增强添加到目标类的具体连接点的过程。AOP将目标类和增强或者引介天衣无缝的编织到一起,根据不同的实现技术,AOP有以下三种织入方式:
    • 编译期织入,这要求使用特殊的Java编译器

    • 类装载期织入,这要求使用特殊的类装载器

    • 动态代理织入,在运行期为目标类添加增强生成子类的方式。

      Spring使用动态代理织入,而AspectJ使用编译期织入和类装载期织入。

  • 代理(Proxy):一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类 既可能是和原类具有相同接口的类(基于JDK的动态代理),也可能是原类的子类(基于CGLib的动态代理)。
  • 切面(Aspect):切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻 辑织入切面所指定的连接点中。AOP的工作重心在于如何将增强应用于目标对象的连接点上。包括两项工作:
    • 第一,如何通过切点和增强定位到连接点
    • 第二,如何在增强中编写切面的代码

动态代理

Spring AOP使用了两种代理机制:一种是基于JDK的动态代理,一种是基于CGLib的动态代理。因为JDK本身只支持基于接口的代理,而 不支持类的代理。

使用JDK创建的代理有一个限制,即它只能为接口创建代理实例,这一点可以从Proxy的接口方法newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)中看出来,第二个参数就是需要代理实例实现的接口列表。尽管面 向接口编程是一个好的思想,但是也不是所有业务方法的类都需要先写一个接口,然后再写一个Impl。对于这种类,动态创建代理实例就需要 CGLib了,CGLib采用底层的字节码技术,为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。

可以关注一下CGLib为某个类动态代理创建子类的代码,其实明面上看着比JDK的要简洁,例如:

package com.smart.proxy;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor {
	private Enhancer enhancer = new Enhancer();

	public Object getProxy(Class clazz) {
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
	}

	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采用动态创建子类的方式生成代理对象,所以不能对目标类中的final和private方法进行代理,当然final类也不可以。CGLib创建的代理对象 的性能比JDK创建的更高,但是创建时间也花费较多,需要考虑在不同的业务场景使用不同的动态代理方式。

创建增强

Spring使用增强类定义横切逻辑,同时Spring只支持方法连接点,增强还包括在方法的哪一点加入横切代码的方位信息。增强既包含横切 逻辑,又包含部分连接点的信息(方位)。

AOP联盟为增强定义了org.aopalliance.aop.Advice接口,Spring支持5种类型的增强,包括ThrowsAdvice、BeforeAdvice、 MethodBeforeAdvice、AfterReturningAdvice、DynamicIntroductionAdvice。

前置增强

BeforeAdvice代表前置增强,因为Spring只支持方法级别的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在目标方法 执行前实施增强,BeforeAdvice留作将来扩展使用。可以看一个前置增强的例子,如下代码:

先看下Waiter接口:

public interface Waiter {
   void greetTo(String name);
   void serveTo(String name);
}

再看一个具体的Waiter的实现:

public class NaiveWaiter implements Waiter {

	public void greetTo(String name) {
        System.out.println("greet to "+name+"...");
	}
	public void serveTo(String name){
        System.out.println("serving "+name+"...");
	}
}

假设我们现在要对NaiveWaiter类中的方法实施前置增强,确保在方法调用前做一些其他事情,那么我们看看怎么做,首先新写一个类,实现 MethodBeforeAdvice接口,如下:

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class GreetingBeforeAdvice implements MethodBeforeAdvice {
	public void before(Method method, Object[] args, Object obj) throws Throwable {
        String clientName = (String)args[0];
        System.out.println("How are you!Mr."+clientName+".");
	}
}

可以看到,MethodBeforeAdvice接口只提供了一个方法,before(Method method, Object[] args, Object obj),其中,method代表目标类中需要被实 施前置增强的方法(所以你大可以在这个方法内部,增加个判断method是不是你想要增强的方法,如果不这样做,那么会默认对目标类中的所有方法实施这个前置增强); args代表方法的入参,obj代表目标类实例。在before方法内部定义你想要实施的前置增强逻辑,我们看一下这个自定义的前置增强类GreetingBeforeAdvice怎么 用到目标类中去的,大体上,也有两种方案,第一是基于代码,第二种基于配置:先看下下面的测试方法:

@Test
public void before() {
    Waiter target = new NaiveWaiter();
    BeforeAdvice  advice = new GreetingBeforeAdvice();
    
    //Spring提供的代理工厂
    ProxyFactory pf = new ProxyFactory();
    pf.setInterfaces(target.getClass().getInterfaces());
    pf.setOptimize(true);
    pf.setTarget(target);
    pf.addAdvice(advice);

    System.out.println(pf);

    Waiter proxy = (Waiter)pf.getProxy(); 
    proxy.greetTo("John");
    proxy.serveTo("Tom");

}

在上面的方法中,使用了一个代理工厂(org.springframework.aop.framework.ProxyFactory)将GreetingBeforeAdvice的增强织入到目标类NaiveWaiter 中。ProxyFactory内部就是使用JDK或CGLib动态代理技术将增强应用到目标类中的,然后通过getProxy()方法获取一个proxy,当然,如之前所说,这个proxy可能 是和原目标类具有相同接口的类(JDK动态代理),也可能是原目标类的子类(CGLib)。

Spring定义了接口org.springframework.aop.framework.AopProxy,并提供了两个final的实现类:

  • CglibAopProxy:使用CGLib动态代理技术创建代理
  • JdkDynamicAopProxy:使用JDK动态代理技术创建代理

如果通过ProxyFactory的setInterfaces(Class… interfaces)方法指定目标接口进行代理,则ProxyFactory会使用JdkDynamicAopProxy;如果是针对类进行代理,则使用CGLib;

如果通过ProxyFactory的setOptimize(true)方法启动优化,即使前面指定了目标接口,也会使用CglibAopProxy。

ProxyFactory通过addAdvice()方法添加一个增强,可以多次添加不同增强。其调用顺序就和添加顺序保持一致。

通过ProxyFactory来实施增强是比较简单清晰的,我个人也更倾向于这种方法;当然,我们是可以通过xml配置的方式来达到上述代码所实现的功能的,如下:

<bean id="greetingAdvice" 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="greetingAdvice"/>

ProxyFactoryBean是FactoryBean接口的实现类,还记得FactoryBean的功能吗?用来实例化一个Bean。ProxyFactoryBean负责为其他Bean创建代理实例,它在内部使用ProxyFactory来完成 这项工作。下面是ProxyFactoryBean的几个常用的可配置属性:

  • target:代理的目标对象
  • proxyInterfaces:代理的目标接口,可以多个
  • interceptorNames:需要织入的目标对象的Bean列表,配置中的顺序对应调用的顺序
  • singleton:返回的代理是否为单例,默认是
  • optimize:当设置为true时,强制使用CGLib代理。对于Singleton的代理,推荐使用CGLib,相反则推荐使用JDK动态代理
  • proxyTargetClass:是否对类进行代理(而不是对接口进行代理),当设置为true时,使用CGLib动态代理。如果设置了这个属性,就不用设置proxyInterfaces了,即使设置了接口也没用。

测试代码如下,就是读这个配置xml文件,然后getBean即可:

public class AdviceTest {

	@Test
	public void advice() {
        String configPath = "com/smart/advice/beans.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
        Waiter waiter = (Waiter)ctx.getBean("waiter");
        waiter.greetTo("John");
	}
}

后置增强

后置增强在目标类方法调用后执行,其实和前置一样,可以看如下代码:

public class GreetingAfterAdvice implements AfterReturningAdvice {

	public void afterReturning(Object returnObj, Method method, Object[] args,
            Object obj) throws Throwable {
        System.out.println("Please enjoy yourself!");
	}
}

通过实现AfterReturningAdvice接口来定义后置增强的逻辑,具体表现在afterReturning(Object returnObj, Method method, Object[] args, Object obj)方法内部。其中,returnObj是 目标实例方法返回的结果,method为目标类的方法(同样可以在这里进行一些筛选判断),args为method方法的入参,obj为目标类实例。

后置增强也一样,可以通过ProxyFactory在代码里织入,也可以一样通过配置的方式。代码就不贴了,很简单。

环绕增强

环绕增强就是前置和后置增强的结合版,它在一个方法里实现了前置+后置的逻辑,如下所示代码:

public class GreetingInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
        Object[] args = invocation.getArguments();  //目标方法入参
        String clientName = (String)args[0];
        System.out.println("How are you!Mr."+clientName+".");    //在目标方法执行前调用
		
        Object obj = invocation.proceed();    //通过反射调用目标方法
		
        System.out.println("Please enjoy yourself!");   //在目标方法执行后调用
		
        return obj;
	}
}

Spring直接使用AOP联盟定义的org.aopalliance.intercept.MethodInterceptor作为环绕增强的接口。实现其唯一的方法invoke(MethodInvocation invocation),MethodInvocation封装了目标 方法、其入参、以及目标方法所在实例对象,但本质上和前置、后置增强方法的内容是一致的;通过invocation.proceed()方法反射调用目标实例相应的方法。在这个方法前后分别对应前置和后置逻辑。

同样,代码或者配置的方式都可以实现““织入”“的过程。

异常抛出增强

异常抛出增强最常用的场景是事务管理,当参与事务的某个Dao发生异常时,事务管理器就必须回滚事务。如下代码:

public class TransactionManager implements ThrowsAdvice {
	public void afterThrowing(Method method, Object[] args, Object target,
            Exception ex) throws Throwable {
        System.out.println("-----------");
        System.out.println("method:" + method.getName());
        System.out.println("抛出异常:" + ex.getMessage());
        System.out.println("成功回滚事务。");
	}
}

异常抛出增强接口ThrowsAdvice没有定义任何方法,它只是一个标签接口,内部没有任何方法;。但是对于自己定义的方法签名有一定限制

void afterThrowing(Method method, Object[] args, Object target, Exception ex)
  • 方法名必须为afterThrowing
  • 方法入参:前三个入参是可选的,要么都提供,要么都不提供;最后一个入参是Throwable或者其子类。

可以在同一个异常抛出增强中定义多个afterThrowing方法,当目标类方法抛出异常的时候,Spring会选择最匹配的增强方法。

引介增强

引介增强是一种比较特殊的增强类型,它不在目标方法周围织入增强,而是为目标类创建新的方法和属性(注意,此时的目标已经不是方法了),所以引介增强的连接点 是类级别的,而不是方法级别的。引介增强可以为目标类添加一个接口的实现,即本来目标类是没有实现接口A的,通过引介增强可以为目标类创建一个实现了接口A的代理。

Spring定义了引介增强的接口IntroductionInterceptor,这个接口没有定义任何方法,Spring为该接口提供了DelegatingIntroductionInterceptor实现 类,一般情况下我们扩展这个类来定义自己的引介增强类。

创建切面

在前面介绍增强的内容中,我们知道:增强被织入到了目标类的所有方法中(除非你在具体的方法逻辑里增强判断,但是这样很不友好)。因此 增强描述的是连接点的方位信息,如织入到方法的前面or后面等等,而切点进一步描述了织入到哪些类的哪些方法上。

Spring通过org.springframework.aop.Pointcut接口描述切点,这个接口由ClassFilter和MethodMatcher构成,它通过 ClassFilter定位到某些特定类上,通过MethodMatcher定位到具体的方法上。

ClassFilter只定义了一个方法 boolean matches(Class<?> var1),参数表示一个类,从而判断这个类是否满足过滤条件。

MethodMatcher较为复杂一点,Spring支持两种方法匹配器:静态方法匹配器和动态方法匹配器。静态匹配器只对方法名、入参及其类型 进行匹配,而动态匹配器会在运行期检查方法入参的值。静态匹配仅会判别一次,而动态匹配会因为每次调用方法的入参都不一样, 每次调用方法都必须判断,因此并不常用。方法匹配器的类型是由其内部的boolean isRuntime()返回值进行决定的。

切点类型

Spring提供了6种切点类型,分别如下:注意,这些里有些是接口,有些是抽象基类,有些是实现类。

  • 静态方法切点:org.springframework.aop.support.StaticMethodMatcherPointcut是静态方法切点的抽象基类,默认情况 下匹配所有的类。这个基类包括两个主要的子类,分别是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者 提供简单的字符串匹配方法签名,后者使用正则表达式匹配方法签名。
  • 动态方法切点:org.springframework.aop.support.DynamicMethodMatcherPointcut是动态方法切点的抽象基类,默认情况 下匹配所有的类。
  • 注解切点:org.springframework.aop.support.annotation.AnnotationMatchingPointcut实现类表示注解切点, 这样我们就可以通过一些注解来标记某些Bean,从而让其达到一些我们想要的切点功能。
  • 表达式切点:org.springframework.aop.support.ExpressionPointcut接口主要是为了支持AspectJ切点表达式语法定义的。
  • 流程切点:org.springframework.aop.support.ControlFlowPointcut实现类表示控制流程切点。它是一种特殊的切点,它 根据程序执行的堆栈信息查看目标方法是否由某一方法直接或间接发起调用,以此判断是否为匹配的连接点。
  • 复合切点:org.springframework.aop.support.ComposablePointcut实现类是为创建多个切点而提供的方便操作类。它的所有 操作方法都返回ComposablePointcut类,从而实现了一个链接表达式对切点进行操作。如Pointcut pc = new ComposablePointcut().union(classFilter).intersection(methodMatcher).intersection(pointcut)。

切面类型

之前我们看到,增强类既包含横切代码逻辑,又包含部分连接点信息(方法前后等),所以可以仅通过一个增强类(Advice)生成一个切面 (Advisor),即对目标类的所有方法织入了增强。但是切点仅包含目标类连接点的部分信息(类和方法的定位),所以仅通过切点(PointCut) 是无法生成一个切面(Advisor)的,必须结合增强才可以制作一个切面。

Spring使用org.springframework.aop.Advisor接口表示切面,一个切面同时包含横切逻辑和连接点信息。切面可以分为如下三类:

  • Advisor:一般切面,它仅包含一个Advice。因为Advice包含了横切代码和连接点信息,所以Advice本身就是一个切面,但是它针对了 目标类的所有方法,所以一般不使用。
  • PointcutAdvisor:切点切面,包含Advice和Pointcut两个类,这样更加灵活。
  • IntroductionAdvisor:引介切面,对应引介增强的特殊的切面。它应用于类层面上。

PointcutAdvisor主要有6个具体的实现类,如下:

  • DefaultPointcutAdvisor:最常用的切面类,它可以通过任意的Pointcut和Advice定义一个切面,不支持引介的切面类型。
  • NameMatchMethodPointcutAdvisor:通过这个类可以定义按方法名定义切点的切面。
  • RegexpMethodPointcutAdvisor:按正则表达式匹配方法名定义切点的切面
  • StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下匹配所有的目标类
  • AspectJExpressionPointcutAdvisor:用于AspectJ切点表达式定义切点的切面
  • AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面

这些Advisor实现类都可以在Pointcut中找到对应的切点,它们都是通过扩展对应的Pointcut实现类并实现PointcutAdvisor接口来 定义的。

attention:不管是静态切面还是动态切面,都是通过动态代理技术实现的。静态切面,是指在生成代理对象时就确定了增强是否需要织入到 目标类的连接点上;而动态切面是指必须在运行期根据方法入参的值来判断增强是否需要织入。