深入理解Spring AOP-基于API编程

PointCut

AOP标准中的Joinpoit可以有很多类型:构造方法调用、字段的设置和获取、方法调用和执行等。而Spring AOP中只支持方法执行类型的Joinpoint,不过这已经够我们用了。

Spring AOP中通过接口org.springframework.aop.Pointcut来表示所有连接点Joinpoit的抽象。Pointcut接口的代码定义如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

    Pointcut TRUE = TruePointcut.INSTANCE;

}

ClassFilter将用来匹配目标对象,MethodMatcher用来匹配将被执行织入操作的相应方法。TruePointcut表示匹配所有对象。

public interface ClassFilter {

    /**
     * 当织入的目标对象的Class类型和Pointcut所规定的类型相同时,
     * 该方法返回true
     */
    boolean matches(Class<?> clazz);

    /**
     * 匹配所以类的ClassFilter实例.
     */
    ClassFilter TRUE = TrueClassFilter.INSTANCE;

}

MethodMatcher接口的代码定义如下:

public interface MethodMatcher {

    /**
     * 判断方法是否匹配,静态的MethodMatcher调用
     */
    boolean matches(Method method, Class<?> targetClass);

    /**
     * 判断MethodMatcher是否是动态的,如果是动态的该方法返回TRUE,将会调用3个参数的matches方法。
     * 如果是静态的,该方法返回FALSE,将会调用2个参数的matches方法。
     */
    boolean isRuntime();

    /**
     * 判断是否匹配方法,动态的MethodMatcher调用
     * 
     */
    boolean matches(Method method, Class<?> targetClass, Object... args);


    /**
     * 匹配所有方法的MethodMatcher实例
     */
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

根据是否需要捕捉目标方法执行时的参数,可以将MethodMatcher分为动态和静态两种。在MethodMatcher类型的基础上,Pointcut可以分为两类,即StaticMethodMatcherPointcutDynamicMethodMatcherPointcut。因为StaticMethodMatcherPointcut具有明显的性能优势,所以,Spring为其提供了更多支持。

PointCut部分继承图

如果想要自定义PointCut我们可以根据实现需求 ,可以选择继承StaticMethodMatcherPointcut或者继承DynamicMethodMatcherPointcut

Joinpoint和Pointcut的区别:这两个概念差不多,可以把他们当成一回事。一个Pointcut可以包含多个Joinpoint.

Advice(增强)

Spring中的Advice实现全部基于AOP Alliance规定的接口。

Advice部分继承类图

按照增强(advice)在目标对象方法连接点的位置,可以将增强分为以下五类:

  1. 前置增强:org.springframework.aop.BeforeAdvice,在目标方法执行前执行;
  2. 后置增强:org.springframework.aop.AfterReturningAdvice,在目标方法调用后执行;
  3. 环绕增强:org.aopalliance.intercept.MethodInterceptor,截取目标类方法的执行,并在前后添加横切逻辑;
  4. 抛出异常增强:org.springframework.aop.ThrowsAdvice,目标方法抛出异常后执行;
  5. Introduction增强:org.springframework.aop.introductioninterceptor

Spring AOP中的AfterReturningAdvice只有在方法正常返回时才会执行,且不能更改方法的返回值。所以要想实现类似资源清理的横切工作,无法使用AfterReturningAdvice,而Spring AOP并没有提供After Finally Advice。如果要想实现资源清理的工作我们可以借助Around Advice,它在Spring AOP的API编程实现中没有对应的实现类,不过可以借助MethodInterceptor来实现Around Advice。下面来看看如何定义一个Around Advice

/**
 * 通过MethodInterceptor来实现Around Advice
 */
public class PerformanceMethodInterceptor implements MethodInterceptor{

    private final Logger logger = LoggerFactory.getLogger(PerformanceMethodInterceptor.class);

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        try {
            stopWatch.start();
            return invocation.proceed();
        } catch (Exception e){
            // do nothing
        } finally {
            stopWatch.stop();
            if (logger.isInfoEnabled()){
                logger.info(stopWatch.toString());
            }
        }
        return null;
    }
}

异常抛出增强类的定义接口是ThrowsAdvice,它是一个标志接口,内部没有定义任何方法。不过我们在编写ThrowsAdvice的实现类时,必须要定义如下方法:

/**
 * 1. 方法名必须是afterThrowing
 * 2. 前三个参数(method,args,target)是可选的,不过必须是要么同时存在,要么同时不存在
 * 3. 第四个参数必须存在,可以是Throwable或者其任何子类
 * 4. 可以存在多个符合规则的afterThrowing,Spring会自动选择最匹配的
 */
public void afterThrowing(Method method,Object[] args,Object target,Throwable t)

ThrowsAdvice的实现如下:

public class MyThrowsAdvice implements ThrowsAdvice {

    private Logger logger = LoggerFactory.getLogger(MyThrowsAdvice.class);

    public void afterThrowing(Method method, Object[] args, Object target, Throwable t) {
        logger.error("发送异常啦",t);
    }

    public void afterThrowing(RuntimeException t) {
        logger.error("发生了运行时异常,异常信息:",t);
    }
}

Introduction

除了常见的Advice之外,还有一种特殊的Advice--Introduction。Introduction可以在不改变目标类的情况下,为目标类添加新的属性以及行为。要想为目标对象添加新的属性和行为,必须要先声明对应的接口和实现类,然后可以通过拦截器IntroductionInterceptor实现添加。

Introduction相关类图

下面来演示一下DelegatingIntroductionInterceptor的用法。

public class DelegatingIntroductionInterceptorSample {
    public static void main(String[] args) {
        IDancer dancer = new Dancer();
        DelegatingIntroductionInterceptor interceptor = new DelegatingIntroductionInterceptor(dancer);

        ProxyFactory weaver = new ProxyFactory(new Singer());
        weaver.setInterfaces(new Class[]{IDancer.class,ISinger.class});
        weaver.addAdvice(interceptor);
        Object proxy = weaver.getProxy();
        ((IDancer)proxy).dance();
        ((ISinger)proxy).sing();
    }
}

Aspect

我们知道@Aspect可以用来表示Aspect。不过在针对面向API编程的Spring AOP中,Advisor用来表示Spring中的Aspect。Advisor只能看成是一种特殊的Aspect,因为在Advisor中通常只持有一个Pointcut和一个Advice(实际的Aspect定义中可以有多个Pointcut和多个Advice)。

Advisor可以分为两种:

  • PointcutAdvisor
  • IntroductionAdvisor

大部分Advisor都实现自PointcutAdvisor。下面是部分PointcutAdvisor子类的继承图
部分PointcutAdvisor继承体系

Spring-AOP 切点/切面类型和创建切面

织入

织入就是为了创建代理对象。当有了切入点和横切逻辑(advice)之后,如何在目标对象(或方法)中加入横切逻辑呢?这个时候我们需要借助织入器将横切逻辑织入目标对象当中。

在Spring AOP中,根据一次创建代理的个数,可以分为创建单个代理的织入器和创建多个代理的织入器(即自动代理)。

Spring AOP中创建单个代理的织入器的类有:

  • ProxyFactory
  • ProxyFactoryBean
  • AspectJProxyFactory

AspectJ使用ajc编译器作为织入器;Jboss aop使用自定义的ClassLoader作为织入器。

在介绍织入相关的内容之前,我们先来看一下相关类的继承图。理解了这张图,织入的原理(其实差不多也就是Spring AOP的原理)便能了然于心。
Spring AOP织入(单个)相关类图

ProxyFactory

ProxyFactory的使用示例:

// 创建目标对象
IService target = new ServiceOne();
// 创建增强类对象(advice)
MethodInterceptor advice = new PerformanceMethodInterceptor();

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvice(advice);
/* 或者
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.addAdvisor(advisor);
 */
IService proxy = (IService) proxyFactory.getProxy();
System.out.println(target.getClass());
System.out.println(proxy.getClass());

Spring AOP借助动态代理技术,可以创建基于接口的代理;借助CGLIB,可以创建基于类的代理。一般根据代理对象class的输出System.out.println(proxy.getClass());,可以观察出代理对象是通过什么技术生成的。

class $Proxy0  表示代理对象是通过动态代理生成的;
$$EnhancerByCGLIB$$9e62fc83  表示代理对象是通过cglib生成的;

默认情况下,Spring AOP会使用动态代理基于接口生成代理对象,当出现下列情况会使用CGLIB基于类生成代理对象。

  1. 目标类没有实现任何接口;
  2. ProxyFactory的proxyTargetClass属性值被设置为true;
  3. ProxyFactory的optimize属性值被设置为true。

ProxyFactoryBean

ProxyFactoryBean和ProxyFactory的使用没有太大的区别。一般ProxyFactory是脱离IOC容器来使用,而ProxyFactoryBean则与IOC容器结合使用。

    <!-- 目标对象 -->
    <bean id="targetObject" class="cn.zgc.aop.apis.advice.advices.TargetObject"/>
    <!-- 前置增强 -->
    <bean id="myBeforeAdvice" class="cn.zgc.aop.apis.advice.advices.MyBeforeAdvice"/>
    <!-- 后置增强 -->
    <bean id="myAfterReturningAdvice" class="cn.zgc.aop.apis.advice.advices.MyAfterReturningAdvice"/>
    <!-- 抛出异常增强 -->
    <bean id="myThrowsAdvice" class="cn.zgc.aop.apis.advice.advices.MyThrowsAdvice"/>
    <!-- 环绕增强 -->
    <bean id="myAroundAdvice" class="cn.zgc.aop.apis.advice.advices.MyAroundAdvice"/>

    <bean id="targetProxy" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:proxyTargetClass="true"
          p:target-ref="targetObject"
          p:interceptorNames="myAroundAdvice,myBeforeAdvice,myAfterReturningAdvice"
    />

自动代理

Spring AOP为我们提供了自动代理机制,让容器为我们自动生成代理,这样我们就不用针对每个目标对象若想生成代理对象,都需要配置相应的ProxyFactoryBean。在Spring内部,通过借助BeanPostProcessor完成自动代理这项工作。

BeanPostProcessor类可以在对象实例化前为其生成代理对象并返回,而不是实例化后的目标对象本身,从而达到代理对象自动生成的目的

常用的自动代理类

  • BeanNameAutoProxyCreator
  • DefaultAdvisorAutoProxyCreator
  • AnnotationAwareAspectJAutoProxyCreator

看看下面的类继承图,可以发现自动代理类都实现了BeanPostProcessor

自动代理类的继承关系图

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator通过beanNames属性指定目标对象集合,通过interceptorNames属性指定advice。只要在IOC容器中注册BeanNameAutoProxyCreator,就能为目标对象创建出代理。

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

    <!-- 通过Bean名称自动创建代理 -->

    <!-- 目标Bean -->
    <bean id="serviceOne" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.ServiceOne"/>
    <bean id="serviceTwo" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.ServiceTwo"/>
    <!-- 增强 -->
    <bean id="privilegeDetectionAdvice" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.PrivilegeDetectionAdvice"/>

    <!-- 自动代理-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames">
            <list>
                <value>service*</value>
            </list>
        </property>
        <property name="interceptorNames">
            <list>
                <value>privilegeDetectionAdvice</value>
            </list>
        </property>
    </bean>
</beans>

DefaultAdvisorAutoProxyCreator

要想使用DefaultAdvisorAutoProxyCreator的话,需要在配置文件中注册(配置)DefaultAdvisorAutoProxyCreator和相关Advisor的bean,并且要想DefaultAdvisorAutoProxyCreator的自动配置生效的话,必须得配置Advisor,因为只有Advisor当中才即包含Pointcut又包含Advice,有了这两个信息之后,DefaultAdvisorAutoProxyCreator就能通过Advisor的信息自动为目标对象生成代理。

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

    <!-- 通过Advisor自动创建代理 -->

    <!-- 目标Bean -->
    <bean id="serviceOne" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.ServiceOne"/>
    <bean id="serviceTwo" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.ServiceTwo"/>
    <!-- 增强 -->
    <bean id="privilegeDetectionAdvice" class="cn.zgc.aop.apis.weave.autoCreateProxy.BeanNameAutoProxyCreator.PrivilegeDetectionAdvice"/>

    <!-- Aspect-->
    <bean id="privilegeAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut">
            <bean class="org.springframework.aop.support.NameMatchMethodPointcut">
                <property name="mappedNames">
                    <list>
                        <value>service*</value>
                    </list>
                </property>
            </bean>
        </property>

        <property name="advice" ref="privilegeDetectionAdvice"/>
    </bean>

    <!-- 自动代理 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

</beans>

AnnotationAwareAspectJAutoProxyCreator

AnnotationAwareAspectJAutoProxyCreator可以自动将@AspectJ注解切面类织入目标Bean中。注意,要想使用@AspectJ需要导入aspectjweaver的jar包。

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

    <!-- 通过配置使用@AspectJ -->

    <!-- 目标Bean -->
    <bean id="service" class="cn.zgc.aop.apis.weave.autoCreateProxy.AnnotationAwareAspectJAutoProxyCreator.ServiceOne"/>
    <!-- 使用了@AspectJ注解的切面类 -->
    <bean class="cn.zgc.aop.apis.weave.autoCreateProxy.AnnotationAwareAspectJAutoProxyCreator.PrivilegeDetectionAspect"/>
    <!-- 自动代理创建器,自动将@AspectJ注解切面类织入目标Bean中 -->
    <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator">
        <!-- true:使用cglib生成代理对象,false(默认):使用动态代理生成代理对象 -->
        <property name="proxyTargetClass" value="true"/>
    </bean>

</beans>

建议

基于API编程的方式对于我们理解Spring AOP的原理很有帮助,我们应该对其要有所了解,但是不推荐再使用这种方式了。

参考
《Spring揭秘 》.王福强

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容