上一章介绍了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创建代理。