编程点滴 2018-04-30
在之前的文章里我们讨论过AOP技术的应用基础。在常用的几种方法里,我们比对了decorator pattern,InvocationHandler, CGLib和AspectJ。这些方法都是为了在现有设计和代码的基础上去增加新的功能而不需要对原有的部分做什么改动。但是从前面技术应用的实例来看,光有这些东西直接应用到实现里还是显得不太友好。于是spring AOP框架里就提供了一些相关的脚手架来方便我们使用它们。那么,我们该怎么使用这些基础代码呢?这些具体的基础代码里又是怎么实现的以达到我们使用方便的目的呢?这里针对常用的几个部分进行讨论。
刚看spring aop的时候,很多人会被里面提到的几个概念给弄迷糊。比如JointPoint, Advice, Pointcut, Aspect, Weaving, Target, Introduction。这些东西看起来比较含糊又有点不好理解。其实,抛开这些概念的表面,我们从一个创造者的角度来看他们就相对来说简单一些了。
我们首先来看AOP的基本思想,它无非就是想在程序执行的某些点中间增加一些可以定义的逻辑。那么现在就有这么几个点需要考虑。一个是怎么定义程序中执行的点?是某个方法吗?应该怎么来描述这个程序中具体的点呢?另外一个,我们希望在特定的点增加什么样的逻辑呢?这些逻辑该定义在哪里?我们要添加的逻辑和程序执行的点应该耦合在一起吗?如果应该是松耦合的,那么该用什么办法把他们给结合在一起呢?还有一个就是,我们添加的逻辑和程序执行点之间的执行关系,我们是希望在这个点之前执行?还是之后?所有这些都需要通过某种方式用机器可以理解执行的方式来描述出来。
在进一步澄清这些具体的概念之前,我们先看一个示例:
假设我们定义有如下的类Agent:
package com.yunzero; public class Agent { public void speak() { System.out.println("Bond"); } }
类Agent很简单,就定义了一个speak方法。里面输出一个简单的字符串。
接着,我们定义一个AgentDecorator类,它相当于是对Agent类的增强:
package com.yunzero; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; public class AgentDecorator implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("James "); Object retVal = invocation.proceed(); System.out.println("!"); return retVal; } }
这个类实现了一个特定的接口MethodInterceptor。在具体的实现里,我们加入了自定义的逻辑,也就是在invocation.proceed()方法的前后输出两行字符串。在上述代码里我们可以看到,我们相当于在方法的执行过程中间做的增强。而具体是对哪个方法做了增强呢?这里没有明确去指定哪个方法,而是对所有的方法都做了这个增强,我们在后续会接着讨论。
现在,我们再看怎么把原有代码和增强部分给结合起来:
package com.yunzero; import org.springframework.aop.framework.ProxyFactory; public class App { public static void main( String[] args ) { Agent target = new Agent(); ProxyFactory pf = new ProxyFactory(); pf.addAdvice(new AgentDecorator()); pf.setTarget(target); Agent proxy = (Agent) pf.getProxy(); target.speak(); System.out.println(""); proxy.speak(); } }
在实现里,我们创建了一个ProxyFactory对象,通过addAdvice将前面定义的AgentDecorator加入进去。然后通过setTarget方法将原有的Agent对象加入到其中。最后,通过getProxy方法返回一个同样Agent类型的对象,但是它却不一样了。在执行上述代码之后,会有如下的输出:
Bond James Bond !
从上述代码的实现过程我们可以看到。我们通过程序的方式定义好原有的对象,这个对象有自己的实现和功能。像Agent对象,这就是前面提到的Target。所以Target无非就是一个我们需要去增强功能的目标对象。
而上述我们实现的AgentDecorator则是增强的逻辑,它本身是对所有方法的实现做的一个增强。相当于前面提到的Advice。我们可以尝试修改一下原有的代码,比如说增加一个如下的类:
package com.yunzero; public class Chatter { public void chat() { System.out.println("Knock knock"); } }
然后在后面的组合代码里改成如下:
package com.yunzero; import org.springframework.aop.framework.ProxyFactory; public class App { public static void main( String[] args ) { Chatter target = new Chatter(); ProxyFactory pf = new ProxyFactory(); pf.addAdvice(new AgentDecorator()); pf.setTarget(target); Chatter proxy = (Chatter) pf.getProxy(); target.chat(); System.out.println(""); proxy.chat(); } }
我们仅仅是把原有的Agent对象换成Chatter对象,它对应的输出则变成:
Knock knock James Knock knock !
可见这里增强业务的逻辑和原有的业务逻辑是松耦合的,他们可以自由组合。我们在程序里切入的点,在前面Agent对象里是speak方法,而在Chatter对象里是chat方法。它就是我们程序的切入点,也就是之前概念里提到的Pointcut。当然,这里似乎没有显式的指定Pointcut。
综合前面的代码,我们其实只是定义了原有的逻辑和增强逻辑,然后通过ProxyFactory对象将他们组合在一起。可见,具体实现逻辑的组合的细节就隐藏在ProxyFactory里了。我们先针对这几个基本的概念进一步讨论一下,后续再来探索一下ProxyFactory的实现细节。
在前面的讨论里,我们已经知道,advice是在某个切入点执行的代码。在示例代码里也显示了通过实现MethodInterceptor接口的一种Advice。在实际的框架里,根据我们的需要有很多种Advice。它们详情如下面的列表:
Advice Name | Interface | 描述 |
Before | org.springframework.aop.MethodBeforeAdvice | 在切入点之前执行自定义的逻辑。在Advice中出现异常的时候会后续的执行将会被跳过。 |
After-Returning | org.springframework.aop.AfterReturningAdvice | |
After(finally) | org.springframework.aop.AfterAdvice | 这个Advice会在被切入的方法执行失败或者抛异常的情况下也执行。 |
Around | org.aopalliance.intercept.MethodInterceptor | 这是通过AOP Alliance标准的实现,因为是对原有方法的一个深度定义,我们甚至可以跳过原来的实现用以替换成我们自己的实现。 |
Throws | org.springframework.aop.ThrowsAdvice | 在被切入的方法出现异常的时候执行。 |
Introduction | org.springframework.aop.IntroductionInterceptor |
我们在实际的使用中可以根据需要选择实现的接口就可以了。Advice接口以及相关的各种扩展接口的类关系如下图:
从上图的结构中也可以看到,在绝大多数的情况下Advice的执行情况都给定义好了。
在前面的讨论里有提到,spring里的切入点就是某个方法。前面的示例只是给了一个笼统的对所有方法都进行织入的做法。在具体的实现里,我们还需要根据不同的要求来定义程序的切入点。比如说,有的时候我们需要指定针对某些特定的类下面的方法作为切入点。有的时候我们需要匹配某些符合特定命名规则的方法,等等。所有这一切都表明了,我们需要有比较丰富的描述手段来支持这些需求。
在spring里,有专门的接口Pointcut来定义这一部分的描述。像Pointcut接口的定义如下:
public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher(); Pointcut TRUE = TruePointcut.INSTANCE; }
这里定义了两个方法,一个返回ClassFilter对象,一个返回MethodMatcher对象。从名字上来看,这应该是分别用于过滤类名字和方法名字的。
ClassFilter和MethodMatcher的定义分别如下:
@FunctionalInterface public interface ClassFilter { boolean matches(Class<?> clazz); ClassFilter TRUE = TrueClassFilter.INSTANCE; }
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; }
很显然,ClassFilter里的matches方法就是根据class对象来判断,如果符合给定的要求,就返回true。否则就是false。而MethodMatcher里有两个matches方法,后面一个方法多了一系列的可选参数。这个方法可以用在动态运行的时候,根据方法调用的参数值进行检查判断。而前一个方法则是根据静态的方法属性和class对象来判断。
既然要实现查找和定位执行点就是要实现Pointcut接口,而实现这个接口就牵涉到返回ClassFilter和MethodMatcher对象。总的来说,要自己手工实现这一系列的接口还是比较麻烦的。所以spring里提供了一系列的实现。如下表:
实现类 | 描述 |
org.springframework.aop.support.annotation.AnnotationMatchingPointcut | 通过查找在类或者方法上定义的特定annotation来定位执行点。 |
org.springframework.aop.aspectj.AspectJExpressionPointcut | 那个AspectJ weaver来检查aspectJ的pointcut表达式。这是一种看起来有点像正则表达式的描述形式。 |
org.springframework.aop.support.ComposablePointcut | 将多个切入点组合到一起的pointcut。本身相当于一个辅助功能。 |
org.springframework.aop.support.ControlFlowPointcut | 符合某个方法里面控制流的切入点。 |
org.springframework.aop.support.DynamicMethodMatcherPointcut | 作为构造动态切入点的基类。 |
org.springframework.aop.support.JdkRegexpMethodPointcut | 通过构造正则表达式的方式来查找匹配切入点。 |
org.springframework.aop.support.NameMatchMethodPointcut | 通过简单的名字匹配来查找切入点。 |
org.springframework.aop.support.StaticMethodMatcherPointcut | 作为构造静态切入点的基类。 |
这些详细的Pointcut与子类的关系结构如下图:
有了这些描述之后,我们再来看看该怎么具体应用Pointcut接口的实现。
假设我们有两个类GoodGuitarist 和GreatGuitarist,它们都实现接口Singer:
public interface Singer { void sing(); }
public class GoodGuitarist implements Singer { @Override public void sing() { System.out.println("Who says I can't be free \n From all of the things that I used to be"); } }
public class GreatGuitarist implements Singer { @Override public void sing() { System.out.println("I shot the sheriff, \n But I did not shoot the deputy"); } }
这部分非常简单。然后我们定义一个Advice:
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; public class SimpleAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println(">> Invoking " + invocation.getMethod().getName()); Object retVal = invocation.proceed(); System.out.println(">> Done\n"); return retVal; } }
结合前面的讨论可以看到,这是一个Around advice。它主要打印当前调用的方法名。我们再定义一个Pointcut,这次是通过继承StaticMethodMatcherPointcut:
import java.lang.reflect.Method; import org.springframework.aop.ClassFilter; import org.springframework.aop.support.StaticMethodMatcherPointcut; public class SimpleStaticPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, Class<?> targetClass) { return ("sing".equals(method.getName())); } @Override public ClassFilter getClassFilter() { return cls -> (cls == GoodGuitarist.class); } }
里面的实现也很直白。找到sing方法。并且在getClassFilter里判断要求是GoodGuitarist class的类型。
然后,我们将它们两者结合起来使用:
public class App { public static void main( String[] args ) { GoodGuitarist johnMeyer = new GoodGuitarist(); GreatGuitarist ericClapton = new GreatGuitarist(); Singer proxyOne; Singer proxyTwo; Pointcut pc = new SimpleStaticPointcut(); Advice advice = new SimpleAdvice(); Advisor advisor = new DefaultPointcutAdvisor(pc, advice); ProxyFactory pf = new ProxyFactory(); pf.addAdvisor(advisor); pf.setTarget(johnMeyer); proxyOne = (Singer)pf.getProxy(); pf = new ProxyFactory(); pf.addAdvisor(advisor); pf.setTarget(ericClapton); proxyTwo = (Singer)pf.getProxy(); proxyOne.sing(); proxyTwo.sing(); } }
这里,我们定义了一个DefaultPointcutAdvisor,然后将前面构造好的Pointcut和Advice传入其中。ProxyFactory然后调用了addAdvisor。最后执行的时候输出如下:
>> Invoking sing Who says I can't be free From all of the things that I used to be >> Done I shot the sheriff, But I did not shoot the deputy
可见,虽然都设置了target,但是只有GoodGuitarist的sing方法被加入了这些内容,而另外一个类没有。这里就引出了另外一个东西,就是我们刚才看到的Advisor。我们定义了Advice,表示要加入的内容是什么。我们也定义了Pointcut,表示在哪个地方加。而Advisor就表示它们的一个组合。就是在哪个点做什么这么个意思。这里的Advisor就相当于书面上定义的aspect。
在spring里,为了实现支持不同的Advice和Pointcut的组合,它里面也定义了一系列的Advisor。整体的结构如下图所示:
这些新定义的Advisor实现是为了使得前面定义Advice, Pointcut再将它们组合起来的过程更加便利一些。
前面讨论了半天,基本上明白了Advice, Pointcut, Advisor的含义以及应用场景。从使用的角度来说,我们无非就是定义好它们,然后通过调用ProxyFactory的对象来设置Advice, Pointcut,target或者Advisor。那么,ProxyFactory到底是怎么做的才实现了对我们定义的这些对象的组合呢?我们进一步探索一下。
我们首先看一下ProxyFactory相关的类结构图:
我们在getProxy方法的时候返回的对象就好像有了特殊的功能。而从类结构图里可以看到,前面继承的接口Adviced就有设置Advisor, TargetSource等操作。这些就为了后面的设置打下基础。既然是在getProxy方法的时候发现它的变化,我们就从这个方法跟进去看一下。
在ProxyFactory里,getProxy方法的定义如下:
public Object getProxy() { return createAopProxy().getProxy(); }
而createAopProxy方法的实现如下:
protected final synchronized AopProxy createAopProxy() { if (!this.active) { activate(); } return getAopProxyFactory().createAopProxy(this); }getAopProxyFactory的方法返回了一个引用的接口AopProxyFactory: