第五章 Spring AOP
概览
本章首先是脱离Spring之外讲了AOP的基础,其次分析了AOP的两种类型:静态AOP,动态AOP,同时还有讲述了AspectJ以及Spring AOP中的代理:JDK动态代理和CGLIB代理以及切入点的高级使用以及AOP的框架服务。
问题
首先还是回忆一下迄今为止所了解的AOP并整理成一句话或者一段话在心里描述出来:Spring AOP 的实际作用类似于拦截器但于拦截器又不太相同,SpringAOP可以指定类以及某个方法并且提供了前置,后置以及环绕等切面,通常作用于日志,事务等需要横向处理的地方,同时在AOP中涉及到两种代理及JDK的动态代理和CGLIB代理。
对于本章的期望如下:
1.JDK动态代理和CGLIB代理的区别,以及在Spring中两者是如何选择的。
2.AOP的两种类型到底有什么不同,体现在什么地方?如何选择
3.AOP的框架服务具体是指的什么?如何操作?
字典
在这一章节中有许多AOP的基础概念需要了解,首先达成领域术语上的一致(以下摘抄原文):
连接点:连接点是应用程序执行期间明确定义的一个点,连接点的典型示例包括方法调用、方法调用本身、类初始化和对象实例化。连接点是AOP的核心概念,并且定义了在应用程序中可以使用AOP插入其他逻辑的点。
通知:在特定连接点执行的代码就是通知,它是由类中的方法定义的。有许多类型的通知,比如前置通知(在连接点之前执行)和后置通知(在连接点之后执行),还有环绕通知
切入点:切入点是用于定义何时执行通知的连接点集合。通过创建切入点,可以更细致地控制如何将通知应用于应用程序的组件中,如前所述,典型的连接点是方法调用,或是特定类型(这里泛指特定的类)中所有方法调用的集合,通常情况下,可以在复杂的关系中插入切入点,从而进一步限制执行通知的时间。
切面*:切面是封装在类中的通知和切入点的组合,这种组合定义了应该包含在应用程序中的逻辑及其应该执行的位置。
织入:织入是在适当的位置将切面插入到应用程序代码中的过程,对于编译时AOP解决方案,织入过程通常在生成时完成。同样,对于运行时AOP的解决方案,织入过程在运行时动态执行,此外,AspectJ还支持另一种称为加载时织入(LTW)的织入机制,在该机制中,拦截底层的JVM类加载器,并在类加载器加载字节码时向其提供织入功能。
目标对象:执行流由AOP进程修改的对象被称为目标对象,通常也会看到目标对象被称为通知(advice)对象。
引入:这是通过引入其他方法或字段来修改对象结构的过程,可以通过引入AOP来使任何对象实现特定的接口,而无须对象的类显式地实现该接口。
AOP Hello World
首先我们来看一段简单的AOP代码,以此来回顾一下AOP的具体实现:
package com.liuningyun.demo.chapter5;
public class Agent {
public void say(){
System.out.println("hello world !");
}
}
package com.liuningyun.demo.chapter5;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class AgentDecorator implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("invoke "+methodInvocation.getMethod());
Object proceed = methodInvocation.proceed();
System.out.println(" End!");
return proceed;
}
}
package com.liuningyun.demo;
import com.liuningyun.demo.chapter5.Agent;
import com.liuningyun.demo.chapter5.AgentDecorator;
import org.springframework.aop.framework.ProxyFactory;
public class Demo {
public static void main(String[] args) {
Agent agent = new Agent();// 目标对象
AgentDecorator agentDecorator = new AgentDecorator(); // 通知
ProxyFactory proxyFactory = new ProxyFactory(); //代理类
//设置通知,在此处将AgentDecorator通知传递给ProxyFactory
proxyFactory.addAdvice(agentDecorator);
//设置目标对象,即需要被织入的对象
proxyFactory.setTarget(agent);
Agent proxy = (Agent) proxyFactory.getProxy(); //获取目标对象的代理类
proxy.say(); //调用代理对象的方法
}
}
如上,一个简单的Agent对象和AgentDecorator对象,其中AgentDecorator实现了一个MethodInterceptor接口,关于MethodInterceptor接口,我们不如来看看Spring的源码:
package org.aopalliance.intercept;
/**
* Intercepts calls on an interface on its way to the target. These
* are nested "on top" of the target.
*
* The user should implement the {@link #invoke(MethodInvocation)}
* method to modify the original behavior. E.g. the following class
* implements a tracing interceptor (traces all the calls on the
* intercepted method(s)):
*
*
* class TracingInterceptor implements MethodInterceptor {
* Object invoke(MethodInvocation i) throws Throwable {
* System.out.println("method "+i.getMethod()+" is called on "+
* i.getThis()+" with args "+i.getArguments());
* Object ret=i.proceed();
* System.out.println("method "+i.getMethod()+" returns "+ret);
* return ret;
* }
* }
*
*
* @author Rod Johnson
*/
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
从源码的注释中我们可以得知该接口的实现可以在目标对象方法调用的前后进行通知,这是一个环绕通知的接口,同时我们不应该忽略一个东西,就是该接口的package并不是来至于Spring,经过一番查阅得知AOP ALLIANCE为AOP实现定义了一组标准的接口,这个包来至于一个叫AOP联盟的组织,具体我们不深究,有兴趣可以去以下地址探索:http://aopalliance.sourceforge.net/ ,在这里我们只需要意识到这么一个东西即可,这说明MethodInterceptor是一个标准的Aop Alliance接口,对于上面main方法的输出如下:
invoke public void com.liuningyun.demo.chapter5.Agent.say()
hello world !
End!
可以看到在say()方法输出helloWorld!的前后我们成功的织入了自己想要的东西,完全符合期望,但是此刻我们似乎应该意识到另外一个问题,即认知中JDK动态代理应该是只能代理接口,而代理类应该需要CGLIB代理才符合预期,在上面的代码中似乎没有任何设置用来选择使用哪一种代理方式,带着这个疑问我进行了进一步的源码跟踪,结果如下:
首先从ProxyFactory开始,从下图我们可以看到ProxyFactory其实继承了一个叫做ProxyCreatorSupport的类:
此时我们再到ProxyCreatorSupport源码中去看看:
public class ProxyCreatorSupport extends AdvisedSupport {
private AopProxyFactory aopProxyFactory;
private List listeners = new LinkedList();
private boolean active = false;
public ProxyCreatorSupport() {
this.aopProxyFactory = new DefaultAopProxyFactory();
}
public AopProxyFactory getAopProxyFactory() {
return this.aopProxyFactory;
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
this.activate();
}
return this.getAopProxyFactory().createAopProxy(this);
}
....省略部分方法
}
我们可以看到有趣的一个地方在于ProxyCreatorSupport的无参构造器里面初始化了一个DefaultAopProxyFactory的默认AOP代理工厂,这时我们再来看看ProxyFactory类:
public class ProxyFactory extends ProxyCreatorSupport {
public ProxyFactory() {
}
public Object getProxy() {
return this.createAopProxy().getProxy();
}
....省略部分代码
}
在ProxyFactory中,我们可以看到实际我们调用getProxy()方法的时候是调用了父类中的createAopProxy()方法去获取代理的,从父类中我们又发现其实它默认是从父类中DefaultAopProxyFactory代理工厂去获取的代理,那么我们最后看看这个代理工厂的源码:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
/**
* Determine whether the supplied {@link AdvisedSupport} has only the
* {@link org.springframework.aop.SpringProxy} interface specified
* (or no proxy interfaces specified at all).
*/
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class>[] ifcs = config.getProxiedInterfaces();
return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
}
}
在这个类中我们似乎可以看到一点熟悉的东西了,在工厂中出现了两个看起来并不陌生的东西ObjenesisCglibAopProxy和JdkDynamicAopProxy,我们可以看到其实在最后获取代理的时候Spring根据代理的目标对象的一些属性来自动选择了是使用JDK动态代理还是CGLIB代理,从上面代码中我们可以看到是否有机会走到new ObjenesisCglibAopProxy() ** 实际上是由isOptimize,isProxyTargetClass和hasNoUserSuppliedProxyInterfaces方法来决定的(手动设置isOptimize或isProxyTargetClass可以强制改变其选择分支),其中isOptimize和isProxyTargetClass默认是返回false,那么我们只需要关注hasNoUserSuppliedProxyInterfaces方法,从该方法的源码中我们其实可以知道这个方法的作用是用来判断目标对象是否有实现接口,如果没有实现接口则ifcs.length == 0 为true,否则将调用一个native方法进行类判断,isAssignableFrom的作用在于判断所实现的接口是否是SpringProxy.class或者其子类,所以实际上ProxyFactory将代理创建过程委托给了DefaultAopProxyFactory的一个实例,该实例又转而委托给了ObjenesisCglibAopProxy和JdkDynamicAopProxy**。
从上面的一个简单示例其实我们可以看出来Spring其实帮我们进行了AOP代理的选择并简单回答了我们阅读本章所期望了解的第一个问题:**JDK动态代理**和**CGLIB代理**的区别,JDK动态代理只支持代理接口,而CGLIB代理则没有这些限制,同时我们可以通过来设置**isOptimize**或**isProxyTargetClass**为true来强制使用CGLIB代理,但是实际上Spring更期望的是我们使用JDK动态代理,因为尽管我们设置了**isOptimize**或**isProxyTargetClass**为true,Spring依然会检查代理目标类是否是接口并路由到**JDK动态代理**上,另外需要注意的是,SpringAOP中唯一的限制就是无法通知**final**类,因为**final**类无法覆盖方法。
Spring AOP所支持的通知(Advice)
在Spring中所有通知都实现了Advice接口,下面将会列出Spring所支持的六种通知(以下摘自原文):
前置通知(MethodBeforeAdvice):通过使用前置通知,可以在连接点执行之前完成自定义处理,在Spring中的连接点通常就是方法调用,所以允许方法执行前执行预处理,前置通知可以完全访问方法调用的目标以及传递给方法的参数,但却无法控制方法本身的执行,另外:如果前置通知抛出异常,那么拦截器链(以及目标方法)的进一步执行将被中止,并且异常将回传拦截器链。
后置返回通知(AfterReturningAdvice):在连接点的方法调用完成执行并返回一个值后执行后置返回通知,后置返回通知可以访问方法调用的目标,传递给方法的参数以及返回值。由于方法在调用后置返回通知时已经执行,因此根本无法控制方法的调用,如果目标方法抛出异常,则不会运行后置返回通知,并且异常将照样传回调用堆栈。
后置通知(AfterAdvice):仅当被通知方法正常完成时才执行后置通知,然而无论被通知方法的结果如何,后置通知都会被执行,即使被通知方法失败并抛出了异常,后置通知也会执行。
环绕通知(MethodInterceptor):在Spring中,环绕通知使用方法拦截器的AOP Alliance标准进行建模,环绕通知允许在方法调用之前和之后执行,并且可以控制允许进行方法调用的点。
异常通知(ThrowsAdvice):异常通知在方法调用返回后执行,但只有在该调用抛出异常时才执行,异常通知只能捕获特定的异常,如果使用异常通知,则可以访问抛出异常的方法,传递给调用的参数以及调用的目标。
引入通知(IntroductionInterceptor):Spring将引入建模为特殊的拦截器,通过使用引入拦截器,可以指定由引入通知引入的方法的实现。
在这里只需要了解目前Spring所支持的几种通知类型即可,根据实际业务场景再深入。
Pointcut 接口
上面的demo中我们给ProxyFactory设置了adivce以及targe,同时我们应该注意到我们代理了整个目标对象,但是在实际使用中不是很建议这种做法,通常我们会使用一个切入点来告知Spring,我们具体需要在哪些场景下以及哪些方法进行通知,在Spring中有如下一个接口:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
该接口标识了一个切入点和一个默认的Pointcut实例,并且提供了两个方法,分别返回一个ClassFilter实例和MethodMatcher实例:
public interface ClassFilter {
/**
* Should the pointcut apply to the given interface or target class?
* @param clazz the candidate target class
* @return whether the advice should apply to the given target class
*/
boolean matches(Class> clazz);
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
可以看到在ClassFilter实例中存在一个matches(Class> clazz) 方法,通过注释我们可以知道该方法是用来判定 Pointcut接口是否适用于该方法的类,入参表示需要检查的类,如果切入点适用于该方法的类则返回true,否则返回false。
public interface MethodMatcher {
boolean matches(Method method, @Nullable Class> targetClass); //静态调用
boolean isRuntime();
boolean matches(Method method, @Nullable Class> targetClass, Object... args);//动态额外调用
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
再来看MethodMatcher实例,在Spring中一共支持两种类型的MethodMatcher,即静态和动态MethodMatcher,具体使用哪一种是由isRuntime()方法决定的,在使用MethodMatcher之前,Spring会先调用isRuntime()来确定MethodMatcher的类型,返回false表示静态,true表示动态。
对于静态切入点,Spring会针对目标上每一个方法调用一次 matches(Method method, @Nullable Class> targetClass);方法,并缓存返回值,在调用方法时只会做一次适配性检查,方法的后续调用将不再调用matches(Method method, @Nullable Class> targetClass);方法。
对于动态切入点,在第一次适配性检查并且matches(Method method, @Nullable Class> targetClass);返回为ture时Spring还会额外调用一次matches(Method method, @Nullable Class> targetClass, Object... args);方法对每个调用方法进行进一步检查,可以看到重载的matches方法多了一个Objects... args参数,这意味着在动态切入点中提供了运行时的方法请求参数,我们可以在matches(Method method, @Nullable Class> targetClass, Object... args);进行入参的适配,比如支付金额大于1000才满足切入点条件。
在这里我们以JDK动态代理为例子尝试找了一下源码,在AdvisedSupport中找到如下代码,并且该方法在JDK动态代理的invoke方法和CGlib的getCallbacks方法中均有调用,在AdvisedSupport中AdvisorChainFactory的默认实现为DefaultAdvisorChainFactory。
/** The AdvisorChainFactory to use */
AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
public List
我们再跟到DefaultAdvisorChainFactory中一探究竟:
public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {
@Override
public List getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class> targetClass) {
// This is somewhat tricky... We have to process introductions first,
// but we need to preserve order in the ultimate list.
List interceptorList = new ArrayList(config.getAdvisors().length);
Class> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
for (Advisor advisor : config.getAdvisors()) {
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
return interceptorList;
}
/**
* Determine whether the Advisors contain matching introductions.
*/
private static boolean hasMatchingIntroductions(Advised config, Class> actualClass) {
for (Advisor advisor : config.getAdvisors()) {
if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (ia.getClassFilter().matches(actualClass)) {
return true;
}
}
}
return false;
}
}
可以看到,在该方法对ProxyConfig中的Advisor进行了遍历并且如上面所说的逻辑进行isRuntime调用并生成MethodMatcher链供后续调用,对于此处的Advisor则是我们在调用ProxyFactory的addAdvice时写入的,具体逻辑如下:
@Override
public void addAdvice(Advice advice) throws AopConfigException {
int pos = this.advisors.size();
addAdvice(pos, advice);
}
/**
* Cannot add introductions this way unless the advice implements IntroductionInfo.
*/
@Override
public void addAdvice(int pos, Advice advice) throws AopConfigException {
Assert.notNull(advice, "Advice must not be null");
if (advice instanceof IntroductionInfo) {
// We don't need an IntroductionAdvisor for this kind of introduction:
// It's fully self-describing.
addAdvisor(pos, new DefaultIntroductionAdvisor(advice, (IntroductionInfo) advice));
}
else if (advice instanceof DynamicIntroductionAdvice) {
// We need an IntroductionAdvisor for this kind of introduction.
throw new AopConfigException("DynamicIntroductionAdvice may only be added as part of IntroductionAdvisor");
}
else {
addAdvisor(pos, new DefaultPointcutAdvisor(advice));
}
}
其实对于静态切入点和动态切入点从上面我就可以得出的是动态切入点会额外调用一个MethodMatcher用来做参数的适配,而静态切入点在第一次调用之后进行了缓存所以相比之下静态切入点的性能优于动态切入点,而动态切入点的灵活性高于静态切入点。
Spring 默认提供的切入点
和上面的通知一样,Pointcut接口通常也不需要我们自己去实现,Spring默认提供了八个该接口的实现类,其中两个用作创建静态和动态切入点的抽象类以及六个具体实现类,其中每个具体类有以下能力:
一起构成多个切入点
处理控制流切入点
执行简单的基于名称的匹配
使用正在表达式定义切入点
使用AspectJ 表达式定义切入点
定义在类或者方法级别查找特定注解的切入点
具体的描述如下:
AnnotationMatchingPointcut:此实现在类或方法上查找特定的Java注解,需要JDK5或更高。
AspectJExpressionPointcut:此实现使用织入器以AspectJ语法评估切入点的表达式。
ComposablePointcut:ComposablePointcut类使用诸如union()和intersection()等操作组合两个或更多切入点。
ControlFlowPointcut:这是一种特殊的切入点,它们匹配另一个方法的控制流中所有方法,即任何作为另一个方法的结果而直接或间接调用方法。
DynamicMethodMatcherPointcut:此实现作为构建动态切入点的基类。
JdkRegexpMethodPointcut:该实现允许使用正则表达式定义切入点。
NameMatchMethodPointcut:通过该实现可以创建一个根据方法名称进行简单匹配的切入点。
StaticMethodMatcherPointcut:此实现作为构建静态切入点的基类。
如上一共八种,具体的代码就不贴出来了,使用前可以找到相应的源码进行阅读,其中AspectJExpressionPointcut实现,其实就是支持我们常用的类似:execution( com.apress.prospring5.ch5..sing (com.apress.prospring5.ch2.common.Guitar))) **这样的表达式,该表达式标明一个切入点的判断条件为:在包com.apress.prospring5.ch5中含有一个入参为com.apress.prospring5.ch2.common.Guitar的sing方法为适配切入点。
AOP框架服务
对于AOP,通常我们提倡配置优于手动,首先我们看看以声明的方式配置AOP,有如下三个选项:
- 使用ProxyFactoryBean:在Spring aop中,当根据定义的Spring bean创建AOP代理时,ProxyFactoryBean提供了一种声明方式来配置Spring的ApplicationContext(以及底层的BeanFactory)。
配置示例:
advice
- 使用Spring aop命名空间:在Spring 2.0中引入了aop名称空间,提供了一种简化的方式来定义Spring中的切面以及DI需求,但实际命名空间的底层实现依然是ProxyFactoryBean,只是换了个壳子而已。
配置示例:
- 使用@AspectJ样式注解:基于AspectJ语法的注解,在初始化ApplicationContext的时候,仍然使用Spring的代理机制。
使用示例(需要配置
@Component
@Aspect
public class AgentDecoratorAdvice{
@Pointcut("execution(* com.liuningyun.demo..say*())")
public void sayExcution(){}
@Before("sayExcution()")
public void beforeAdvice(JoinPoint joinPoint) throws Throwable {
System.out.println("beforeAdvice: " + joinPoint.getSignature().getDeclaringTypeName() + joinPoint.getSignature().getName());
}
@Around("sayExcution()")
public void aroundAdvice(JoinPoint joinPoint) throws Throwable {
System.out.println("AroundAdvice: " + joinPoint.getSignature().getDeclaringTypeName() + joinPoint.getSignature().getName());
}
}
总结:那么回过头来看看Spring的AOP到底是个什么玩意呢?Spring的AOP是一种面向切面的编程方式,并且在SpringAOP按照AOP Alliance实现了一套标准化的接口,SpringAOP中存在两种代理模式,即CGLIB代理和JDK动态代理,CGLIB代理是为代理目标类生成新的字节码文件,生成一个目标类的子类去进行代理(实际是覆盖方法),所以不能代理final类,而JDK动态代理则是代理接口,使用JDK代理的任何对象都至少实现一个接口,生成的代理则是实现了该接口的对象,并且在Spring中这两种代理模式在默认的情况下会根据代理对象的一些属性进行自动选择。同时,在SpringAOP中涉及的几个基础组件Advice,Pointcut以及Advisor其实Spring都有足够多的实现类,提供了足够灵活的切入点以及通知方式,包括静态切入点的启动缓存通知链策略,动态切入点的二次校验规则等,所以SpringAOP其实给了我们很多选择,但是鱼和熊掌不可兼得,性能和灵活,根据实际业务需要选择就好。