Spring3.X学习笔记-SpringAOP增强与切面

上一篇文章介绍了SpringAOP的基础知识以及所依赖的底层Java技术,本篇来说下SpringAOP的增强类型以及切面类型。

SpringAop的增强类型

AOP联盟为增强定义了org.aopaliance.aop.Advice接口,下图为增强接口继承关系图:
这里写图片描述
带《spring》标识的接口是Spring所定义的扩展增强接口;带《aopalliance》标识的接口则是AOP联盟定义的接口。按照增强在目标类方法的连接点位置,可以分为以下5类:

  • 前置增强: org.springframework.aop.BeforeAdvice代表前置增强,因为Spring只支持方法级别的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在方法执行前实施增强,而BeforeAdvice是为了将来版本扩展需要而定义的;
  • 后置增强: org.springframework.aop.AfterReturningAdvice代表后增强,表示在目标方法执行后实施增强;
  • 环绕增强: org.springframework.intercept.MethodInterceptor代表环绕增强,表示在目标方法执行前后实施增强;
  • 异常抛出增强: org.springframework.aop.ThrowsAdvice代表抛出异常增强,表示在目标方法抛出异常后实施增强;
  • 引介增强: org.springframework.aop.IntroductionInterceptor代表引介增强,表示在目标类中添加一些新的方法和属性。

在了解了基础信息后,下面就来分别看下各种增强的用法。通过使用 “保证服务生使用礼貌用语的例子” 来说明各种增强的使用方式。

前置增强

服务生接口

package com.hhxs.bbt.advice;

public interface Waiter {

    /**
     * 欢迎顾客
     * @param name
     */
    void greetTo(String name);

    /**
     * 对顾客提供服务
     * @param name
     */
    void serveTo(String name);
}

现在,来看一个未参加过培训的服务生的服务情况:

package com.hhxs.bbt.advice;
import org.springframework.stereotype.Component;

@Component
public class NaiveWaiter implements Waiter {

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

    @Override
    public void serveTo(String name) {
        System.out.println("serving " + name + "...");
    }
}

NaiveWaiter只是简单的向顾客打招呼,直接提供服务。下面,我们使用前置增强对NaiveWaiter的服务进行规范,让他们在提供服务之前,必须先对顾客使用礼貌用语。

package com.hhxs.bbt.advice;

import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

@Component
public class GreetingBeforeAdvice implements MethodBeforeAdvice {

    // 在目标类方法调用前执行
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String clientName = (String)args[0];
        System.out.println("How are you! Mr." + clientName + ".");
    }
}

上述代码中,我们看到了MethodBeforeAdvice接口的唯一方法before(Method method, Object[] args, Object obj) throws Throwable,method为目标类的方法;args为目标类方法的入参;而obj为目标类实例。当该方法发生异常时,将阻止目标类方法的执行。

下面编写测试例子,看下具体的执行情况:

package com.hhxs.bbt.advice;
import org.springframework.aop.BeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class TestBeforeAdvice {

    public static void main(String[] args) {
        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("张三");
        proxy.serveTo("李四");
    }
}

运行上面的代码,控制台输出信息如下,我们可以看到,通过前置增强在方法前面引入了礼貌用语。

How are you! Mr.张三.
greet to 张三...
How are you! Mr.李四.
serving 李四...

解剖ProxyFactory
在TestBeforeAdvice中,我们使用org.springframework.aop.framework.ProxyFactory代理工厂将GreetingBeforeAdvice的增强织入到目标类NaiveWaiter中。有没有发现,调用方式与上一节介绍的JDK代理和CgLib代理很相似?不错,ProxyFactory的内部就是使用了JDK代理或CGLib技术。下图为AopProxy类结构:
AopProxy类结构
其中,Cglib2AopProxy使用CGLib代理技术创建代理,而JdkDynamicAopProxy使用JDK代理技术创建代理。如果通过ProxyFactory的setInterfaces(Class[] interfaces)指定针对接口进行代理,ProxyFactory就使用JdkDynamicAopProxy;如果针对类的代理,则使用Cglib2AopProxy。此外,还可以通过ProxyFactory的setOptimize(true)方法,让ProxyFactory启动优化代理方式,这样,针对接口的代理也会使用CglibAopProxy。需要注意的是,用户可以通过调用ProxyFactory的addAdvice(Advice)方法添加多个增强,增强的调用顺序和添加顺序一致。

在Spring中配置

虽然使用ProxyFactory比直接使用CGLib或JDK代理技术创建代理省事很多,但是正如大家预想的一样,还可以通过Sping配置的方式来声明代理。

// spring1.xml代码片段
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:component-scan base-package="com.hhxs.bbt.advice" />

    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.hhxs.bbt.advice.Waiter" />
        <property name="target" ref="naiveWaiter" />
        <property name="interceptorNames">
            <list>
                <value>greetingBeforeAdvice</value>
            </list>
        </property>
    </bean>

</beans>
package com.hhxs.bbt.advice;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class BeforeAdviceClient {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/advice/spring1.xml");
        Waiter waiter = (Waiter) context.getBean("waiter");
        waiter.greetTo("张三");
        waiter.serveTo("李四");
    }
}

下面介绍下ProxyFactoryBean的几个常用的可配置属性:
- target: 代理的目标对象;
- proxyInterfaces: 代理所要实现的接口,可以是多个接口。该属性还有一个别名属性interfaces;
- interceptorNames: 需要植入目标对象的Bean列表,采用Bean的名称指定。这些Bean必须是实现;了org.aopalliance.intercept.MethodInterceptororg.springframework.aop.Advisor的Bean,配置中的顺序对应调用的顺序;
- singleton: 返回的代理是否是单实例,默认为单实例;
- optimize: 当设置为true时,强制使用CGLib代理。对于单实例的代理,推荐使用CGLib,对于其他作用域的代理,最好使用JDK代理。原因是CGLib创建代理时速度慢,而创建出的代理对象运行效率高,而使用JDK代理的表现正好相反;
- proxyTargetClass: 是否对类进行代理,设置为true时,使用CGLib代理。

后置增强

后置增强在目标类方法调用后执行,接着上面的例子,增加服务后用语。

import org.springframework.aop.AfterReturningAdvice;
public class GreetingAfterAdvice implements AfterReturningAdvice {

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

其中returnValue为目标实例方法返回的结果;method为目标类的方法;args为目标实例的方法的入参;而target为目标类实例。

// spring2.xml代码片段
...
<bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.hhxs.bbt.advice.Waiter" />
        <property name="target" ref="naiveWaiter" />
        <property name="interceptorNames">
            <list>
                <value>greetingBeforeAdvice</value>
                <value>greetingAfterAdvice</value>
            </list>
        </property>
    </bean>
...
public class AfterAdviceClient {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/advice/spring2.xml");
        Waiter waiter = (Waiter) context.getBean("waiter");
        waiter.greetTo("张三");
        waiter.serveTo("李四");
    }
}

运行上面的代码,控制台输出信息如下:

How are you! Mr.张三.
greet to 张三...
Please enjoy yourself!
How are you! Mr.李四.
serving 李四...
Please enjoy yourself!

环绕增强

环绕增强允许在目标类方法调用前后织入横切逻辑,综合实现了前置、后置增强两者的功能。下面我们使用环绕增强实现上面的功能。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class GreetingInterceptor implements MethodInterceptor {

    @Override
    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联盟所定义的 MethodInterceptor 作为环绕增强接口。该接口唯一的接口方法invoke(MethodInvocation invocation) throws Throwable,其中 MethodInvocation 不但封装目标方法及入参数组,还封装了目标方法所在的实例对象。

// spring3.xml代码片段
...
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.hhxs.bbt.advice.Waiter" />
        <property name="target" ref="naiveWaiter" />
        <property name="interceptorNames">
            <list>
                <value>greetingInterceptor</value>
            </list>
        </property>
    </bean>
...
public class AroundAdviceClient {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/advice/spring3.xml");
        Waiter waiter = (Waiter) context.getBean("waiter");
        waiter.greetTo("张三");
        waiter.serveTo("李四");
    }
}

异常抛出增强

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

ThrowsAdvice异常抛出增强接口没有定义任何方法,它是一个 标识接口,在运行期Spring使用反射的机制自行判断,但必须采用以下签名形式定义异常抛出的增强方法:void afterThrowing([Method method, Object[] args, Object target], Throwable),方法名必须为 afterThrowing,方法入参规定:前三个入参Method method, Object[] args, Object target要么都提供,要么都不提供,而最后一个入参是Throwable或其子类。

标识接口是没有任何方法和属性的接口,标识接口不对实现类有任何语义上的要求,仅仅表明它的实现类属于一个特定的类型。它非常类似Web2.0中TAG的概念,Java使用它标识某一类对象。主要有两个用途:第一,通过标识接口标识同一类型的类,这些类本身可能并没有具有相同的方法,如Advice接口;第二,通过标识接口使程序或JVM采取一些特殊处理,如java.io.Serializable,它告诉JVM对象可以被序列化。

引介增强

引介增强是一种比较特殊的增强类型,它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的。

Spring定义了引介增强接口 IntroductionInterceptor,该接口没有定义任何的方法,Spring为该接口提供了 DelegatingIntroductionInterceptor实现类,一般情况下,我们通过扩展该实现类定义自己的引介增强类。下面通过对上一篇文章SpringAOP基础里的性能监视例子进行改造,实现业务类性能监视功能的激活和关闭。

public interface Monitorable {
    void setMonitorActive(boolean active);
}
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
public class ControllablePerformanceMonitor extends DelegatingIntroductionInterceptor implements Monitorable {

    private static final long serialVersionUID = 1L;

    private ThreadLocal<Boolean> monitorStatusMap = new ThreadLocal<Boolean>();

    public Object invoke(MethodInvocation mi) throws Throwable {
        Object obj = null;
        if(monitorStatusMap.get() != null && monitorStatusMap.get()) {
            PerformanceMonitor.begin(mi.getClass().getName() + "." + mi.getMethod().getName());
            obj = super.invoke(mi);
            PerformanceMonitor.end();
        } else {
            obj = super.invoke(mi);
        }
        return obj;
    }

    @Override
    public void setMonitorActive(boolean active) {
        monitorStatusMap.set(active);
    }
}
// beans.xml代码片段
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

    <bean id="pmonitor" class="com.hhxs.bbt.introduce.ControllablePerformanceMonitor" />
    <bean id="forumServiceTarget" class="com.hhxs.bbt.introduce.ForumService" />
    <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interfaces="com.iflytek.bbt.introduce.Monitorable" <!-- 引介增强所实现的接口-->
        p:target-ref="forumServiceTarget"
        p:interceptorNames="pmonitor"
        p:proxyTargetClass="true"  /> <!-- 引介增强只能通过目标类创建子类的方式生成引介增强的代理 -->
</beans>
public class TestIntroduce {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/introduce/beans.xml");
        ForumService forumService = (ForumService) ctx.getBean("forumService");
        forumService.removeForum(10);
        forumService.removeTopic(1024);

        Monitorable monitorable = (Monitorable) forumService;
        monitorable.setMonitorActive(true);
        forumService.removeForum(10);
        forumService.removeTopic(1024);
    }
}

SpringAop的切面类型

通过前面的学习,我们知道增强只提供了连接点的方位信息:如织入到方法前面,后面等,而切点进一步描述了织入到哪些类的哪些方法上。

Spring通过org.springframework.aop.Pointcut接口描述切点,PointcutClassFilterMethodMatcher 构成,它通过 ClassFilter 定位到某些特定类上,通过 MethodMatcher 定位到某些特定方法上。

Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含横切代码和连接点信息。切面可以分为三类:一般切面、切点切面和引介切面。
- Advisor: 代表一般切面,它仅包含一个Advice。Advice代表的横切连接点是所有目标类的所有方法,因为这个横切面太宽泛,所以很少使用;
- PointcutAdvisor: 代表具有切点的切面,它包含Advice和Pointcut两个类,这样就可以通过类、方法名以及方位等信息灵活地定义切面的连接点,提供更具适用性的切面;
- IntroductionAdvisor: 代表引介切面,引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切点适用 ClassFilter 进行定义。

* 下面看下PointcutAdvisor的6个具体实现类:*
- DefaultPointAdvisor: 最常用的切面类型,它可以通过任意Pointcut和Advice定义一个切面,唯一不支持的是引介的切面类型;
- NameMatchMethodPointcutAdvisor: 通过该类可以定义按方法名定义切点的切面;
- RegexpMethodPointcutAdvisor: 对于按正则表达式匹配方法名进行切点定义的切面,可以通过扩展该实现类进行操作;。RegexpMethodPointcutAdvisor 内部通过 JdkRegexpMethodPointcut 构造出正则表达式方法名切点;
- StaticMethodMatcherPointcutAdvisor: 静态方法匹配器切点定义的切面,默认匹配所有的目标类;
- AspectJExpressionPointcutAdvisor: 用于AspectJ切点表达式定义切点的切面,它是Spring 2.0新提供的类;
- AspectJPointcutAdvisor: 用于AspectJ语法定义切点的切面,它是Spring 2.0新提供的类。


下面我们通过实例来了解下具体用法:

静态普通方法名匹配切面

@Component
public class Waiter {

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

    public void serverTo(String name) {
        System.out.println("waiter serving " + name + "...");
    }
}

@Component
public class Seller {

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

Seller拥有一个和Waiter相同名称的方法greetTo()。现在希望通过 StaticMethodMatcherPointcutAdvisor 定义一个切面,在Waiter#greetTo()方法调用前织入一个增强。

// 切面类代码
public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final long serialVersionUID = 1L;

    // 切点方法匹配规则:方法名为greetTo
    public boolean matches(Method method, Class<?> targetClass) {
        return "greetTo".equals(method.getName());
    }

    // 切点类匹配规则:为Waiter的类或子类
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                return Waiter.class.isAssignableFrom(clazz);
            }
        };
    }
}

因为 StaticMethodMatcherPointcutAdvisor 抽象类唯一需要定义的是 matches() 方法。默认情况下匹配所有类,我们可以通过覆盖 getClassFilter() 方法,让它仅匹配Waiter类及其子类。

// 增强类
@Component
public class GreetingBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName() + "." + method.getName());
        String clientName = (String) args[0];
        System.out.println("How are you! Mr. " + clientName + ".");
    }

}

// beans.xml 配置切面: 静态方法匹配切面
...
<context:component-scan base-package="com.hhxs.bbt.advisor" />
<!-- 向切面注入一个前置增强 -->
<bean id="greetingAdvisor" class="com.hhxs.bbt.advisor.GreetingAdvisor"
      p:advice-ref="greetingBeforeAdvice" />

<bean id="parent" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interceptorNames="greetingAdvisor"
        p:proxyTargetClass="true" />

<bean id="waiter_c" parent="parent" p:target-ref="waiter"/>
<bean id="seller_c" parent="parent" p:target-ref="seller"/>
...

StaticMethodMatcherPointcutAdvisor 除了advice属性外,还可以定义另外两个属性:
- classFilter: 类匹配过滤器,在GreetingAdvisor中,我们用编码的方式设定了classFilter;
- order: 切面织入时的顺序,该属性用于定义Orderd接口标识的顺序。

public class TestStaticMethodAdvisor {

    public static void main(String[] args) {        
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/advisor/beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter_c");
        Seller seller = (Seller) ctx.getBean("seller_c");
        waiter.greetTo("John");
        waiter.serverTo("John");
        seller.greetTo("John");
    }
}

运行以上代码,输出一下信息:

com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...
waiter serving John...
seller greet to John...

静态正则表达式方法匹配切面

StaticMethodMatcherPointcutAdvisor 中,我们仅能通过方法名定义切点,这种描述方式不够灵活。假设目标类中有多个方法,且它们都满足一定的命名规范,若能使用正则表达式进行匹配描述就灵活的多了。RegexpMethodPointcutAdvisor 是正则表达式方法匹配的切面实现类,需要注意的是:匹配模式串匹配的是目标类方法的全限定名,即带类名的方法名。下面我们用这个实现类对上面的实例进行改写:

...
<context:component-scan base-package="com.hhxs.bbt.advisor" />

    <!-- 向切面注入一个前置增强 -->
    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
        p:advice-ref="greetingBeforeAdvice">
        <property name="patterns">
            <list>
                <value>.*greet.*</value>
            </list>
        </property>
    </bean>

    <!-- 通过父<bean>定义公共配置信息 -->
    <bean id="parent" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interceptorNames="greetingAdvisor"
        p:proxyTargetClass="true" />

    <bean id="waiter_c" parent="parent" p:target-ref="waiter"/>
    <bean id="seller_c" parent="parent" p:target-ref="seller"/>
...

运行以上代码,输出一下信息:

com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...
waiter serving John...
com.hhxs.bbt.advisor.Seller.greetTo
How are you! Mr. John.
seller greet to John...

PS: 除了例子中所使用的 patternsadvice 两属性外,还有另外两个属性:
- pattern: 如果只有一个匹配模式串,可以使用该属性进行配置,patterns属性用于定义多个匹配模式串,这些匹配模式串之间是 “或的关系”
- order: 切面在织入时对应的顺序。

动态切面

在低版本中,Spring提供了用于创建动态切面的 DynamicMethodMatcherPointcutAdvisor 抽象类,因为该类在功能上和其他类有重叠,目前已过期。我们可以使用 DefaultPointcutAdvisorDynamicMethodMatcherPointcut 来完成相同的功能。下面直接看例子:

@Component
public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut {

    private static List<String> specialClientList = new ArrayList<String>();

    static {
        specialClientList.add("John");
        specialClientList.add("Tom");
    }

    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                System.out.println("调用getClassFilter()对" + clazz.getName() + "做静态检查.");
                return Waiter.class.isAssignableFrom(clazz);
            }
        };
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        System.out.println("调用matches(method, targetClass)" + targetClass.getName() + "." + method.getName() + "做静态检查.");
        return "greetTo".equals(method.getName());
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object[] args) {
        System.out.println("调用matches(method, targetClass)" + targetClass.getName() + "." + method.getName() + "做动态检查.");
        String clientName = (String) args[0];
        return specialClientList.contains(clientName);
    }
}

GreetingDynamicPointcut 类既有静态切点检查的方法,也有用于动态切点检查的方法。由于动态切点检查会对性能造成很大的影响,应当尽量避免在运行时每次都对目标类的各个方法进行动态检查。Spring采用这样的机制:在创建代理时对目标类的每个连接点使用静态切点检查,如果仅通过静态切点检查就可以知道连接点是不匹配的,则在运行时就不再进行动态检查了;如果静态切点检查是匹配的,在运行时才进行动态切点检查。

// dynamic_beans.xml 代码片段
...
  <context:component-scan base-package="com.hhxs.bbt.advisor" />

    <bean id="dynamicAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
        p:pointcut-ref="greetingDynamicPointcut" p:advice-ref="greetingBeforeAdvice">
    </bean>

    <!-- 通过父<bean>定义公共配置信息 -->
    <bean id="waiter2" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interceptorNames="dynamicAdvisor"
        p:target-ref="waiter"
        p:proxyTargetClass="true" />
...
// 动态切面测试代码
public class TestDynamicAdvisor {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/advisor/dynamic_beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter2");
        waiter.serverTo("Peter");
        waiter.greetTo("Peter");
        waiter.serverTo("John");
        waiter.greetTo("John");
    }

运行以上代码,在控制台输出一下信息:

// Spring在创建代理织入切面时,对目标类中所有方法进行静态切点检查
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.serverTo做静态检查.
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.greetTo做静态检查.
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.toString做静态检查.
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.clone做静态检查.
// 对应waiter.serverTo(“Peter”): 第一次调用serverTo()方法时,执行静态
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.serverTo做静态检查.
waiter serving Peter...
// 对应waiter.greetTo(“Peter”) : 第一次调用greetTo()时,执行静态动态检查
调用getClassFilter()对com.hhxs.bbt.advisor.Waiter做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.greetTo做静态检查.
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.greetTo做动态检查.
waiter greet to Peter...
// 对应waiter.serverTo("John") :第二次调用serverTo()时,不在执行静态
waiter serving John...
// 对应waiter.greetTo("John") :第二次调用greetTo()时,只执行动态检查
调用matches(method, targetClass)com.hhxs.bbt.advisor.Waiter.greetTo做动态检查.
com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...

在定义切点时,切勿忘记同时覆盖getClassFilter()matches(Method method, Class targetClass)方法,通过静态切点检查可以排除掉大部分方法。

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

流程切面

Spring的流程切面由 DefaultPointcutAdvisorControlFlowPointcut 实现。流程切点代表由某个方法直接或间接发起调用的其他方法。来看下面的实例,我们通过一个WaiterDelegat类代理Waiter所有的方法:

public class WaiterDelegate {

    private Waiter waiter;

    public void service(String clientName) {
        waiter.greetTo(clientName);
        waiter.serverTo(clientName);
    }

    public void setWaiter(Waiter waiter) {
        this.waiter = waiter;
    }
}

假设我们需要实现所有由WaiterDelegate#service()方法发起调用的其他方法都织入GreetingBeforeAdvice增强,就必须使用流程切面来完成目标。

// controlflow_beans.xml 代码片段
...
  <context:component-scan base-package="com.hhxs.bbt.advisor" />

    <bean id="controlFlowPointcut" class="org.springframework.aop.support.ControlFlowPointcut">
        <constructor-arg type="java.lang.Class" value="com.hhxs.bbt.advisor.WaiterDelegate"/>
        <constructor-arg type="java.lang.String" value="service"/>
    </bean>

    <bean id="controlFlowAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
        p:pointcut-ref="controlFlowPointcut" p:advice-ref="greetingBeforeAdvice">
    </bean>

    <!-- 通过父<bean>定义公共配置信息 -->
    <bean id="waiter3" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interceptorNames="controlFlowAdvisor"
        p:target-ref="waiter"
        p:proxyTargetClass="true" />
...

ControlFlowPointcut有两个构造函数,分别是ControlFlowPointcut(Class clazz)ControlFlowPointcut(Class clazz, String methodName)。第一个构造函数指定一个类作为流程切点;第二个构造函数指定一个类和某一个方法作为流程切点。

在这里,我们指定WaiterDelegate#service()方法作为切点,表示所有通过该方法直接或间接发起的调用匹配切点。

public class TestControlFlowAdvisor {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/advisor/controlflow_beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter3");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        waiter.serverTo("John");
        waiter.greetTo("John");
        wd.service("John");
    }
}
waiter serving John...
waiter greet to John...
com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...
com.hhxs.bbt.advisor.Waiter.serverTo
How are you! Mr. John.
waiter serving John...

对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否满足流程切点的要求。因此,和动态切面一样,流程切面对性能的影响也很大。

复合切点切面

在前面的例子中,我们所定义的切面仅有一个切点,有时,一个切点可能难以描述目标连接点的信息。比如上面流程切点的例子中,假如我们希望由WaiterDelegate#service()发起调用且被调用的方法是Waiter#greetTo()时才织入增强,这个切点就是复合切点。Spring为我们提供了 ComposablePointCut,可以将多个切点以并集或交集的方式组合起来,提供起点之间复合运算功能。
下面,我们通过 ComposablePointcut 创建一个流程切点和方法名切点的相交切点,代码如下:

@Component
public class GreetingComposablePointcut {
    public Pointcut getIntersectionPointcut() {
        // 创建一个复合切点
        ComposablePointcut cp = new ComposablePointcut();
        // 创建一个流程切点
        Pointcut pt1 = new ControlFlowPointcut(WaiterDelegate.class,"service");
        // 创建一个方法名切点
        NameMatchMethodPointcut pt2 = new NameMatchMethodPointcut();
        pt2.addMethodName("greetTo");
        return cp.intersection(pt1).intersection((Pointcut)pt2);
    }
}
// composable_beans.xml 代码片段
...
<context:component-scan base-package="com.hhxs.bbt.advisor" />

<bean id="composableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
  p:pointcut="#{greetingComposablePointcut.intersectionPointcut}" p:advice-ref="greetingBeforeAdvice" />

<!-- 通过父<bean>定义公共配置信息 -->
<bean id="waiter4" class="org.springframework.aop.framework.ProxyFactoryBean"
  p:interceptorNames="composableAdvisor" p:target-ref="waiter"
  p:proxyTargetClass="true" />
...
public class TestComposableAdvisor {
    public static void main(String[] args) {        
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/advisor/composable_beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter4");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        waiter.serverTo("Peter");
        waiter.greetTo("Peter");
        wd.service("Peter");
    }
}   

运行以上代码,在控制台输出一下信息:

waiter serving Peter...
waiter greet to Peter...
com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. Peter.
waiter greet to Peter...
waiter serving Peter...

引介切面

引介切面是引介增强的封装器,通过引介切面,我们更容易为现有对象添加任何接口的实现。IntroductionInfo 描述了目标类需要实现的新接口。IntroductionAdvisor 有两个实现类,分别是 DefaultIntroductionAdvisorDeclareParentsAdvisor,前者是引介切面最常用的实现类,而后者是Spring2.0新添的实现类,它用于实现使用AspectJ语言的DeclareParent注解表示的引介切面。
DefulatIntroductionAdvisor拥有三个构造函数:
- DefaultIntroductionAdvisor(Advice advice): 通过一个增强创建的引介切面,引介切面将为目标对象新增增强对象中所有接口的实现;
- DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice, Class clazz): 通过一个增强和一个指定的接口类创建引介切面,仅为目标对象新增clazz接口的实现;
- DefaultIntroductionAdvisor(Advice advice,IntroductionInfo introductionInfo): 通过一个增强和一个IntroductionInfo创建一个引介切面,目标对象需要实现哪些接口,由introductionInfo对象的getInterfaces()表示。

下面,我们通过DefaultIntroductionAdvisor为 前面的引介增强示例 配置切面:

// introduce_beans.xml 代码片段
...
  <bean id="pmonitor" class="com.hhxs.bbt.introduce.ControllablePerformanceMonitor" />
    <bean id="forumServiceTarget" class="com.hhxs.bbt.introduce.ForumService" />
    <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
        p:interfaces="com.hhxs.bbt.introduce.Monitorable"
        p:target-ref="forumServiceTarget"
        p:interceptorNames="pmonitor"
        p:proxyTargetClass="true" />
...
public class TestIntroduceAdvisor {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/advisor/introduce_beans.xml");
        ForumService forumService = (ForumService) ctx.getBean("forumService");
        forumService.removeForum(10);
        forumService.removeTopic(1024);

        Monitorable monitorable = (Monitorable) forumService;
        monitorable.setMonitorActive(true);
        forumService.removeForum(10);
        forumService.removeTopic(1024);
    }
}

自动代理

在前面的例子中,可以看出,每一个需要被代理的 Bean 都需要使用一个 ProxyFactoryBean 进行配置,为每一个目标类手工配置一个切面是比较繁琐的。幸运的是,Spring为我们提供了自动代理机制,让容器为我们自动生成代理。

Spring使用 BeanPostProcessor 来完成这项工作。基于 BeanPostProcessor 的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例。这些代理创建器可以分为以下三类:
- 基于Bean配置名规则的自动代理创建器: 允许为一组特定配置名的Bean自动创建代理实例的代理创建器,实现类为 BeanNameAutoProxyCreator
- 基于Advisor匹配机制的自动代理创建器: 它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中,实现类为 DefaultAdvisorAutoProxyCreator
- 基于Bean中AspjectJ注解标签的自动代理创建器: 为包含AspectJ注解的Bean自动创建代理实例,它的实现类是 AnnotationAwareAspectJAutoProxyCreator,该类是Spring2.0的新增类。

BeanNameAutoProxyCreator

静态正则表达式方法匹配切面中,通过配置两个ProxyFactoryBean分别为waiter和seller的Bean创建代理对象。下面,我们通过BeanNameAutoProxyCreator来完成相同功能:

...
  <context:component-scan base-package="com.hhxs.bbt.advisor" />

    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"
        p:beanNames="*er"
        p:interceptorNames="greetingBeforeAdvice"
        p:optimize="true" />
...

BeanNameAutoProxyCreator有一个beanNames属性,它允许用户指定一组需要自动代理Bean名称,Bean名称可以使用*通配符。当然使用通配符会带来一定风险,在上面的例子中,假设一个其他的Bean名称以“er”结尾,则自动代理创建器也会为该Bean创建代理。所以,保险的方式是直接使用beanid,如value=”waiter,seller”。

public class TestBeanNameAutoProxyCreator {

    public static void main(String[] args) {        
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/autoproxy/beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter");
        Seller seller = (Seller) ctx.getBean("seller");
        waiter.greetTo("John");
        waiter.serverTo("John");
        seller.greetTo("John");
    }
}

运行以上代码,在控制台输出一下信息:

com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...
com.hhxs.bbt.advisor.Waiter.serverTo
How are you! Mr. John.
waiter serving John...
com.hhxs.bbt.advisor.Seller.greetTo
How are you! Mr. John.
seller greet to John...

DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator能够扫描容器中的Advisor,并将Advisor自动织入到匹配的目标Bean中,即为匹配的目标Bean自动创建代理。

静态方法匹配切面中,我们通过ProxyFactoryBean为waiter配置了代理,在这里,我们引入DefaultAdvisorAutoProxyCreator为容器中所有带“greet”方法名的目标Bean自动创建代理:

// beans.xml代码片段
...
<!--通过Advisor自动创建代理-->
<bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
  p:patterns=".*greet.*" p:advice-ref="greetingBeforeAdvice" />

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
...
public class TestBeanNameAutoProxyCreator {

    public static void main(String[] args) {        
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hhxs/bbt/autoproxy/beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter");
        Seller seller = (Seller) ctx.getBean("seller");
        waiter.greetTo("John");
        waiter.serverTo("John");
        seller.greetTo("John");
    }
}

运行以上代码,在控制台输出一下信息:

com.hhxs.bbt.advisor.Waiter.greetTo
How are you! Mr. John.
waiter greet to John...
waiter serving John...
com.hhxs.bbt.advisor.Seller.greetTo
How are you! Mr. John.
seller greet to John...

————本文结束感谢您的阅读————

猜你喜欢

转载自blog.csdn.net/hzygcs/article/details/79367345
今日推荐