Spring AOP:底层API

上一章介绍了Spring AOP的概念和基本用法,本章探寻SpringAOP更底层API,一窥Spring AOP的实现原理。在使用层面,上一章介绍的知识完全足够了,并不推荐在实际项目中使用本章介绍的API。

本章对应的官方原文档的地址

PointCut API

通过上一章我们知道,@PoinCut注解可以定义的切点,可在多个通知之间复用。因此在实现层面,切点和通知也是完全解耦的。切点通过接口org.springframework.aop.Pointcut来定义。

public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

PoingCut接口被分成了两个部分,一个部分叫ClassFilter,一个部分叫MethodMatcher,通过名字很容猜到用途。这样的设计一方面进一步方便重用,一方面也能提高切点的执行效率。

ClassFilter定义如下,判断一个类与切点是否匹配,如果该方法总是返回true,说明切点和任意类型都匹配。

public interface ClassFilter {
    boolean matches(Class clazz);
}

相对来说,MethodMatcher更复杂一些:

public interface MethodMatcher {
    boolean matches(Method m, Class targetClass);
    boolean isRuntime();
    boolean matches(Method m, Class targetClass, Object[] args);
}

第一个matches方法基于类型和方法签名,它在AOP代理创建的时候就可以执行;第二个方法则指示是否这是一个运行时的匹配器;如果前两个都返回true,那么才需要在运行时执行第二个matches方法,它匹配来了方法运行时参数值。

对于isRuntime=false的匹配器,我们称之为“静态(static)”的,永远不需要执行运行时的匹配,效率更高;如果可能,我们要尽量使用静态的匹配器。

接口Pointcut的组合

多个Pointcut实现可以组合在一起形成一个新的Pointcut对象,实现“交(intersectiono)”、“并(union)"的功能,”交“意味着,组合后的Pointcut匹配等价于所有的Pointcut匹配;”并“意味着,组合后的Pointcut匹配等价于任意Pointcut匹配。

可以通过org.springframework.aop.support.Pointcuts提供的工具方法创建组合,或者使用ComposablePointcut类型。

AspectJ Expression

Spring最重要的Pointcut实现是org.springframework.aop.aspectj.AspectJExpressionPointcut,它基于AspectJ提供的工具库解析切点表达式生成。

本人没有研究过这块,试着推断一下其基本原理:

  • 切点表达式的每个PCD都可以对应为一个PointCut,最终的表达式实现可以通过组合PointCut来实现;
  • ClassFilter是PCD的类型部分(包名和类名),如果PCD没有没有类型限定,简单返回true即可;
  • MethodMatcher是表达式的方法模式部分,表达式如果没有限定参数运行时类型,那么就是静态的。

其他Pointcut实现

Spring提供了一些内置的Point方便重用。

JdkRegexpMethodPointcut

这是一个静态的匹配器,classFiter部分总是返回true,通过jdk的正则表达式来匹配方法名。配置如下:

<bean id="settersAndAbsquatulatePointcut"
        class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

patterns属性是一个正则表达式集合,每个正则表达式对应一个方法名模式,注意它匹配的是完全限定方法名(包括类名前缀在内)。

ControlFlowPointcut

一个动态的PointCut,可以依据当前调用栈的情况来匹配,这是一个非常耗性能的Poincut。

StaticMethodMatcherPointcut

一个静态PointCut的基类,依据方法名来匹配。

Advice

在API层面,Advice指切面在连接点上执行的操作。在Spring里面Adivce是一个bean,可以同时服务多个目标对象,也可以被单个目标对象所独享。Sping支持前面所介绍的所有Advice类型(Before,Around,After),还可以扩展自定义的Advice类型。

Around Advice

Around通知使用方法拦截器来实现的,接口定义如下:

public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

public interface Interceptor extends Advice {
}

MethodInvocation参数封装了方法执行的所有信息,比如method,参数,proxy。一个简单的拦截器实现如下:

//打印方法执行的轨迹
public class DebugInterceptor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}

MethodInvocation.proceed()沿着着执行链前进一步(连接点上可能有多个方法拦截器,包括自己在内,形成一个执行链);如果所有拦截器都调用prcoceed且返回它的返回值,那么连接点的返回值最终返回给调用者;技术上,拦截器可以返回不同的值给调用者,这是一种危险的行为。

大家可能会觉得奇怪,为啥不直接把invoke方法定义在Advice接口里,而是增加了两级接口继承。仔细看代码的话,会发现Interceptor和Advice定义在org.aopalliance.aop这个package里;这是AOP联盟定义的公共接口,各种AOP框架都应当遵守,Spring AOP也不例外。

Before Advice

接口定义如下:

public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method m, Object[] args, Object target) throws Throwable;
}

Before通知在连接点之前执行一些逻辑,因此不需要MethodInvocation参数,也没有返回值。

如果这个Advice抛出异常,那么会中断当前的拦截链,如果异常是一个RuntimeException或与目标方法的签名一致,那么异常被抛给调用者;否则异常被包裹成一个RuntimeException。

Throw Advice

接口org.springframework.aop.ThrowsAdvice没有定义任何方法,仅仅充当一个标签,它意味着对象实现了以下形式的方法:

public interface org.springframework.aop.ThrowsAdvice {
	void afterThrowing([Method, args, target], subclassOfThrowable)
}

advice的的method、args、target是可选的,实现者依据需求实现所需的形式,比如:

public void afterThrowing(Exception ex);
public void afterThrowing(RemoteException)
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)

一个advice还可实现多个afterThrowing,来拦截不同的异常。

比如:

public class CombinedThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

AfterReturning Advice

接口定义:

public interface AfterReturningAdvice extends Advice {
    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

AfterReturning如果抛出异常,只会沿着拦截链传播,不会影连接点方法调用的正常返回。

Introduction Advice

Spring将Introduction(引入)也实现为方法拦截器

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

implementsInterface给出拦截器是否实现了某个接口。拦截器实现也需要实现MethodInterceptor里的invoke方法,而且必须提供方法的实现体,不能调用MethodInvocation.proceed方法。

Advisor

Advisor实际是对Advice及相关过滤器的结合,可以这样理解:Advisor=Advice+PointCut。这个类的存在纯粹是实现层面的事,不必硬要和AOP的概念相对应。 如果硬要从概念上下定义的话,我认为Advisor是一个包含单个通知和切点的小切面。

最常用的Advisor实现是org.springframework.aop.support.DefaultPointcutAdvisor

使用ProxyFactoryBean创建代理

前面讲了Spring AOP API的各个部件,最终ProxyFactoryBean将这些部件组合起来,创建AOP代理对象。ProxyFactoryBean也是一个FactoryBean,在刚开始接触Spring的时候,我们就学习了FactoryBean的概念,它的职责是创建另一个bean。ProxyFactoryBean的职责,是创建某个bean的代理bean。

ProxyFactoryBean属性

ProxyFactoryBean有一些重要的属性,继承自ProxyConfig:

  • proxyTargetClass:boolean值,是否要创建类的代理而不是接口代理;基于类的代理使用CGLIB技术,基于接口的代理使用JDK动态代理技术;
  • optmize:是否优化CGLIB创建的代理类,对JDK代理无效,一般不用设置;
  • frozen:将ProxyConfig设置为frozen,防止后续意外修改它;
  • exposeProxy:是否将当前的proxy引用暴露给目标对象,如果值为true,proxy引用被放入一个ThreadLocal变量,目标对象可以在被代理的方法中通过AopContext.currentProxy()来读取;
  • proxyInterfaces:代理的接口列表;
  • interceptorNames:一组应用到代理的advice、interceptor、Advisor名字,还可以使用*通配符;
  • singleton: 是否单例;

JDK或CGLIB代理

JDK代理是基于接口的,即代理对象实现和目标对象一样的接口,以实现方法拦截;而CGLIB代理是基于class的,它创建目标对象类型的子类对象,overwrite对应的方法来实现方法拦截。

如果目标对象没有实现任何接口,那么Spring只能创建CGLIB代理,即使ProxyFactoryBean的proxyTargetClass=false。
如果目标对象实现了至少一个接口,那么:

  • 如果proxyTargetClass=true,那么使用CGLIB代理;
  • 如果proxyInterfaces设置了一个或多个接口名,那么使用JDK代理,代理实现设置的所有接口;
  • 如果proxyInterfaces没有设置,那么使用JDK代理,代理实现所有目标对象实现的接口。

注1:如果bean实现了至少一个接口,那么切面只能切入定义在接口里的方法,如果想切入未定义在接口里的方法,只能强制proxyTargetClass=true。
注2:Spring推荐使用JDK代理,遵循面向接口编程的原则;
注3:JDK代理和CGLIB代理在性能上无明显差别,不必列入考虑因素。

其他代理工厂类

再介绍Spring两个常见的代理工厂类,分别是ScopedProxyFactoryBean和TransactionProxyFactoryBean。
ScopedProxyFactoryBean实现了对非单例作用域bean的代理,关于bean的作用域以及为什么需要代理机制,在Spring基础部分已经讲过,这里终于找到了它的实现类。

TransactionProxyFactoryBean则是为Spring事务服务的,等以后研究Spring事务的时候再分析。

ScopedProxyFactoryBean和TransactionProxyFactoryBean并不是从ProxyFactoryBean继承的,在命名上容易造成混淆。

自动代理机制

Auto-Proxy机制让我们不必使用ProxyFactoryBean来手动创建代理,而是用特殊的BeanPostProcessor来为context内的bean创建代理。

先看BeanNameAutoProxyCreator,这是依据bean名字配置来创建AOP代理的工具,使用方式如下:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

再看DefaultAdvisorAutoProxyCreator,这是更常用更强大的一个类,它自动检测Spring容器内所有的advisor,并切入到所有合适的bean,因此只需要将DefaultAdvisorAutoProxyCreator定义为一个bean即可,不需要任何其他配置。

示例代码

源文档有一个基于xml配置的JDK代理示例,本人改成了Java配置版本,并重新整体,并用它来展示多个功能点。完整代码位于示例代码的aop_api子工程中

首先创建接口及实现类:

package beans;

public interface Person {
    void setName(String name);
    void setAge(int age);
}

public class PersonImpl implements Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

一个AdviceConfig类,创建可重用的Advice,一个Advice加一个Advisor;Advisor使用了RegexpMethodPointcutAdvisor类型,它使用正则表达式来匹配通知和连接点:

@Configuration
public class AdviceConfig {

    @Bean
    public Advice debugInterceptor() {
        return new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                System.out.println("debugInterceptor before " + invocation.getMethod().getName() + "(" + invocation.getArguments()[0] + ")");
                Object rValue = invocation.proceed();
                System.out.println("debugInterceptor after " + invocation.getMethod().getName() + "(" + invocation.getArguments()[0] + ")");
                return rValue;
            }
        };
    }

    @Bean
    public Advisor debugAdvisor() {
        RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
        advisor.setPattern(".*setAge");
        advisor.setAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                System.out.println("debugAdvisor before " + invocation.getMethod().getName() + "(" + invocation.getArguments()[0] + ")");
                Object rValue = invocation.proceed();
                System.out.println("debugAdvisor after " + invocation.getMethod().getName() + "(" + invocation.getArguments()[0] + ")");
                return rValue;
            }
        });
        return advisor;
    }
}

ProxyFactoryBean

首先展示ProxyFactoryBean的用法,创建一个ProxyFactoryConfig,里面手动创建ProxyFactoryBean,指定target对象,注入advice:

@Configuration
@Import(AdviceConfig.class)
public class ProxyFactoryConfig {

    @Bean("person")
    public ProxyFactoryBean factoryBean(Advice advice, Advisor advisor, Person person) {
        ProxyFactoryBean factoryBean = new ProxyFactoryBean();
        factoryBean.setTarget(person);
        factoryBean.setInterfaces(Person.class);

        factoryBean.addAdvice(advice);
        factoryBean.addAdvisor(advisor);

        //设置advice和advisor的bean names,效果和addAdvice,addAdvisor一样
        //factoryBean.setInterceptorNames("debugInterceptor","debugAdvisor");

        //解除注释,使用CGLIB proxy
        //factoryBean.setProxyTargetClass(true);
        return factoryBean;
    }

    @Bean("PersonImpl")
    public Person person() {
        return new PersonImpl();
    }
}

执行代码如下,可以观察到可以观察到,名叫person的这个bean,它的实际类型是com.sun.proxy.$Prox**,它不可以被转换成PersonImpl类型,使用JDK代理,必须要遵循面向接口编程的原则:

	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProxyFactoryConfig.class);
	Person person =  (Person)context.getBean("person");
	System.out.println("bean class is "+person.getClass().getCanonicalName());
	person.setAge(10);
	person.setName("anni");

CGLIB代理示例

只要在上面示例的ProxyFactoryBean配置中,加一句factoryBean.setProxyTargetClass(true);就可以了。此时名叫person的这个bean,它的实际类型是beans.PersonImpl$$EnhancerBySpringCGLIB$$***,是PersonImpl的子类。

如果我们将PersonImpl.setAge的方法设置为final,那么将会得到这样的告警:

Unable to proxy interface-implementing method [public final void beans.PersonImpl.setAge(int)] because it is marked as final: Consider using interface-based JDK proxies instead!

Final method [public final void beans.PersonImpl.setAge(int)] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance

它的意思是,因为setAge方法是final,所以CGLIB创建的子类无法覆盖它,也就无法代理它。因此,此时在person bean上调用setAge方法,修改的是代理对象的字段,而不是目标对象的字段,这肯定不是咱们想要的行为。所以此类告警应当引起重视

AutoProxy

将上面的ProxyFactoryConfig替换成以下配置类即可,使用强大DefaultAdvisorAutoProxyCreator,它自动将检测到的Advisor切入合适的bean:

@Configuration
@Import(AdviceConfig.class)
public class AutoProxyConfig {

    @Bean
    public DefaultAdvisorAutoProxyCreator autoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();
    }
    @Bean
    public Person person() {
        return new PersonImpl();
    }
}

总结

AOP技术涉及的基本概念”切点“和”通知“,分别对应为PointCut和Advice类型,Advisor则是PointCut+Advice的结合。并不存在一个叫做Aspect的类,因为Advisor已经够用了;从概念上分析的,如果有Aspect这个类,也不过是一组Advisor的集合罢了。

Spring将Adivce织入目标方法的手段是使用代理技术,有两种代理技术,一种是JDK动态代理,一种是CGLIB代理;前者是JDK自带的代理技术,需要目标bean至少实现一个java接口;后者需要CGLib库(Spring AOP自带),通过生成目标类型的子类来生成代理。这两种代理技术都是运行时代理技术,不需要引入额外的编译工具或Java类加载器。

Spring最终将AOP代理的生成机制落地到两种我们熟悉的技术,一种是FactoryBean技术,我们可以使用ProxyFactoryBean来手动生成代理;另一种是BeanPostProcessor,他实现了auto-proxy机制,自动检测context内定义的advisor,并为适用的bean创建代理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值