AOP技术基础
简介
我们在使用一些OOP相关的编程语言的时候,就会受到很重的OOA/D思想影响。关于各种职责的划分、各种设计模式的思想和变化等等。只为了能够更加轻松灵活的实现各种业务和需求变化。 当然,在一定程度上,当我们使用OOP的时候,会发现处理某些特定的问题单纯用这些思想还是有点不足。一个典型的场景就是AOP(Aspect Oriented Programming)。在我之前的一些文章里有讨论过AOP结合一些框架的应用,但是对于它的一些比较原生的思想却讨论的比较少。这里想从一些更基础的层面去探讨AOP的思想和实现技术。
OOP和AOP
正如我们在OOP学习里所了解到的,OOP的目的是为了提供一个更加灵活可扩展可重用的方案来解决实际应用的问题。但是在某些实际应用的场景下,光用OOP不一定能找到一个合适的方法。这些场景更多的是牵涉到一些业务的切入。比如在很多应用的具体实现里,我们需要对很多具体的行为进行权限验证。这样,在几乎大多数牵涉到一些权限限制下的操作,我们都需要做这么一个检查。在一些应用里,我们需要记录它们执行的日志信息,有的是在它们某个执行点之前,有的在之后。在某些情况下,我们需要记录跟踪应用的执行情况。这些都是在原有实现的基础上引入的新的需求和变化。从下图中,我们可以看到,我们要引入的变化和现有业务的实现的关系。
那么,为了适应这些变化,我们该怎么做呢?一种办法就是针对每种需要应用到具体业务的场景都来加入具体调用相关的代码。这种方式有一个不足的地方就是,它需要在多个地方重复。从追求软件尽量可重用的基础上,这部分代码其实和原有的代码在业务逻辑上很可能不是一个层面的,这样的实现就显得有点丑陋。另外,这种方式也不够灵活,如果有什么变化的话,我们还是要修改原来的这块代码。哪怕是改方法调用什么的。所以,最理想的方式就是我们原来的代码什么都不用动,我们可以通过某种方式将我们需要添加的内容织入到原有的代码中。
现在,我们就先来探讨一下没有框架支持的情况下,有哪些思路可以达到这个目的。
几种实现AOP的思路
简单代码重复
这种方法最简单直接,只要在每个需要重复的地方重复代码就可以了。当然,既然我们希望能寻找更好的方式,这种方式是不可接受的。
Decorator模式
在我之前的一篇文章里有讨论过这个模式。如果我们希望在不破坏使用者对目标对象方法签名的基础上,一种可行的办法就是通过decorator pattern。它的类模式如下图:
在这种实现里,我们的原有业务对象就相当于是类ConcreteComponent的对象。它必须继承一个抽象类或者实现一个接口。同时,我们需要有一个Decorator类,它也继承自同一个父类,但是它本身也有一个指向抽象父类的引用。同时,它本身并不实现任何具体的细节。它只是保持了一个和父类同样的方法签名以保持兼容,而具体的实现在实现它的子类里。
我们结合一个实例来讨论这种方式的应用。假设我们有一个类Hello:
package com.wrox.Hello; public class Hello implements HelloInterface { @Override public void sayHello() { System.out.println("hello"); } }
它实现了接口HelloInterface:
package com.wrox.Hello; public interface HelloInterface { void sayHello(); }
这就相当于前面uml图里的ConcreteComponent和Component。
当然,为了能够有一个可以实现增强效果的地方,我们需要定义一个抽象类Decorator:
package com.wrox.Hello; public abstract class Decorator implements HelloInterface{ protected HelloInterface hello; public Decorator(HelloInterface hello) { this.hello = hello; } }
它的主要职责就是保留一个指向抽象接口的引用,并定义一个增强效果的构造函数。当然,这个效果是怎么增强的,在详细的代码里我们就会看到了。
有了这个Decorator之后,如果我们希望在Hello的方法执行之前做一些操作,我们就需要定义一个扩展的类BeforeHello:
package com.wrox.Hello; public class BeforeHello extends Decorator { public BeforeHello(HelloInterface hello) { super(hello); } @Override public void sayHello() { System.out.println("Something happens before sayHello!"); hello.sayHello(); } }
它的实现比较直接,就是在调用自己引用的hello对象之前,先执行一个自定义的输出。
同样的,为了在Hello的方法之后执行一些任务,我们也定义了一个AfterHello:
package com.wrox.Hello; public class AfterHello extends Decorator { public AfterHello(HelloInterface hello) { super(hello); } @Override public void sayHello() { hello.sayHello(); System.out.println("After hello triggered!"); } }
从使用的角度来说,现在我们有了两个可以增强原有属性的类了,一个BeforeHello, 一个AfterHello。现在,假设我们希望在Hello的方法执行之前和之后都执行给定的行为,我们需要在具体的代码里如下操作:
package com.wrox.Hello; public class App { public static void main( String[] args ) { HelloInterface hello = new AfterHello(new BeforeHello(new Hello())); hello.sayHello(); } }
可以看到,我们最开始定义的Hello对象就相当于一个被包裹的核,我们通过一层层的构造函数来包装它,最终达到了增强原有特性的效果。如果执行上述代码,我们将看到如下的输出:
Something happens before sayHello! hello After hello triggered!
综合来看,这种使用Decorator pattern的方式还是太费劲了。一个是它需要对我们增强效果的目标对象定义一个实现的接口。或者我们将这个目标对象作为父类来处理。另外一个,我们需要定义Decorator类和具体的属性扩展类。还要包含有相关的增强功能的构造函数。
Proxy和InvocationHandler
还有一种方法就是我们使用Proxy和InvocationHandler来实现这个效果。假设在原有Hello和HelloInterface接口的基础上,我们要增强它的效果。我们可以定义一个如下的类EnhancedHandler:
package com.wrox.Hello; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class EnhancedHandler implements InvocationHandler { private Object obj; public EnhancedHandler(Object obj) { this.obj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if(methodName.equals("sayHello")) { System.out.println("Something happends before sayHello!"); Object result = method.invoke(obj, args); System.out.println("After hello triggered!"); return result; } return method.invoke(obj, args); } }
在这个类里,我们实现了接口InvocationHandler。然后,我们通过反射的方式,通过判断方法名的方式,在程序里调用我们增强的这部分代码。
具体使用这部分的代码如下:
package com.wrox.Hello; import java.lang.reflect.Proxy; public class App { public static void main( String[] args ) { HelloInterface h = new Hello(); HelloInterface proxyH = (HelloInterface)Proxy.newProxyInstance(EnhancedHandler.class.getClassLoader(), new Class<?>[] {HelloInterface.class}, new EnhancedHandler(h)); proxyH.sayHello(); } }
这样,我们将得到前面同样的输出。当然,我们通过这种方式也达到了增强原有代码的效果。只是这里依然有同样的限制,比如说我们要对接口进行代理。这就限制了原有的类里头必须只能是实现某个接口的,单独的类就不行。另外,在增强的部分也不灵活,我们需要判断自己想要增强的方法名,如果想要引入新的方法的时候,这里的变动影响比较大。
那么,针对这个只能对接口的代理,有没有什么更好的办法呢?我们可以看看下面这种方式。
CGLIB
使用CGLIB的话,我们需要在程序里引入对这个类库的依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.6</version> </dependency>
我们首先要定义一个类HelloProxy:
package com.wrox.Hello; import java.lang.reflect.Method; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class HelloProxy implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable { String methodName = method.getName(); if (methodName.equals("sayHello")) { System.out.println("Something happends before sayHello!"); proxy.invokeSuper(object, objects); System.out.println("After hello triggered!"); return object; } proxy.invokeSuper(object, objects); return object; } }
这里需要实现接口MethodInterceptor。这里的方法也很类似,它是通过反射,根据方法的名字来判断。然后我们再调用相应的增强。
而使用这部分的代码如下:
package com.wrox.Hello; import net.sf.cglib.proxy.Enhancer; public class App { public static void main( String[] args ) { HelloProxy proxy = new HelloProxy(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(FlatHello.class); enhancer.setCallback(proxy); FlatHello hello = (FlatHello)enhancer.create(); hello.sayHello(); } }
因为这里是对普通的类进行增强的,我们可以用一个通用的类来尝试。这里我用了另外一个类FlatHello。它和Hello的差别就在于它没有实现任何接口,仅仅是定义了一个同样的方法实现。
AspectJ
我们从前面几种实现功能增强的方法里都可以看到,它们是通过在运行时动态的反射来达到这个增强效果。而这里采用aspectJ却是另外一种方式,它是在编译的时候生成代码到现有的class文件里。这种方式可以实现切入内容和原有实现更加松的耦合。具体它的实现增强我们在后面会详细讨论。
要实现使用AspectJ的效果,我们需要下载AspectJ这个工具。它的下载地址如下(www.eclipse.org/aspectj) 。下载到本地之后,执行java -jar aspectj-1.x.x.jar 命令后会进行安装。在我这边的本地环境里,aspectJ安装的目录是~/aspectj1.8。我们为什么要下载aspectJ这个工具呢?因为我们前面提到过,它是通过编译的方式将内容织入到目标对象里,所以它提供了一个工具来实现对源代码的编译。
现在,我们先来定义一个简单的对象Hello:
public class Hello { public void sayHello() { System.out.println("Hello AspectJ!"); } public static void main(String[] args) { Hello h = new Hello(); h.sayHello(); } }
这部分的代码非常简单,就是输出一个字符串。
然后我们再创建一个文件TxAspect.aj
public aspect TxAspect { void around():call(void Hello.sayHello()) { System.out.println("Start transaction..."); proceed(); System.out.println("End transaction..."); } }
需要注意的是,这里保存的文件是后缀名为.aj的,而不是.java的。因为这里是利用了aspectJ里自定义的一些语法和文件类型。
上面的代码里有几个特殊的地方,一个是它用aspect来表示它是一个切面。而around方法表示在方法的执行前后来执行我们目前的内容。call方法的参数部分指明了是Hello对象里的sayHello方法。proceed()方法表示执行原来的sayHello方法。
现在我们来编译这些代码:
ajc -cp ~/aspectj1.8/lib/aspectjrt.jar Hello.java TxAspect.aj
需要注意一点,因为我们是用命令行来编译代码。这里程序编译的时候依赖一个类库aspectjrt.jar。我们可以在环境变量里配置它,也可以在命令行里指定它。而ajc相当于是对java默认编译器javac的增强。所以我们要用它来编译带.aj后缀的文件。
在运行程序代码的时候我们也需要指定它的类库。
java -cp ~/aspectj1.8/lib/aspectjrt.jar:. Hello
这时候,程序的输出如下:
Start transaction... Hello AspectJ! End transaction...
这个方法看起来比较神奇,我们没有对原有的代码做任何的修改,只需要在外面定义一个aspect文件,里面描述好需要切入的方法和执行的内容就可以了。那么,既然前面我们提到过,我们采用aspectJ的时候是将这些切入的内容给编译到class文件里了。我们也可以进一步看看这些内容是怎么给织入进去的。
在前面编译好的文件里,我们会发现有Hello.class和TxAspect.class两个文件。我们用java的反编译工具打开这些class文件看看。有一个比较简单的java反编译工具可以试一下jd(jd.benow.ca)。我们选择最简单的jar包来运行这个程序:
java -jar jd-gui-1.4.0.jar
我们将Hello.class文件打开,会发现它对应如下的内容:
import java.io.PrintStream; import org.aspectj.runtime.internal.AroundClosure; public class Hello { public void sayHello() { System.out.println("Hello AspectJ!"); } private static final void sayHello_aroundBody1$advice(Hello target, TxAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure) { System.out.println("Start transaction..."); AroundClosure localAroundClosure = ajc$aroundClosure;sayHello_aroundBody0(target); System.out.println("End transaction..."); } public static void main(String[] args) { Hello h = new Hello(); Hello localHello1 = h;sayHello_aroundBody1$advice(localHello1, TxAspect.aspectOf(), null); } private static final void sayHello_aroundBody0(Hello paramHello) { paramHello.sayHello(); } }
这里面多了一个sayHello_aroundBody1$advice方法,在main方法里最后执行的就是这个方法。可见,我们新切入的内容就被aspectj的编译器给加进去了。
总结
这样,我们在这里就探讨了要实现AOP技术可以采用的技术手段。从实现的角度来说,我们可以采用decorator pattern的方法,这种方式需要对目标对象做一些修改和扩展。这样带来的限制还是比较多的,而且很不灵活。如果采用InvocationHandler的方式来实现的话,它需要目标对象实现某个接口。然后在它的实现方法里通过反射来判断目标对象的切入方法。而CGLIB的方式则也是需要实现一个methodInterceptor接口,但是它不要求增强的目标对象实现接口。只是在它的具体实现里,也是通过反射来查找目标方法再进行处理。前面的这几种方法都是在运行时对目标对象进行增强。所以相对来说他们的性能方面会稍微差一些。
除了以上的方法,还有一种就是采用AspectJ的方式。它是通过采用一套aspectJ专门的语法来描述切入的点和执行内容。同时,它通过对javac的增强ajc来对目标代码进行编译。这些代码被直接编译到class文件中。所以它的性能相对来说会高一点。
以上是从底层实现的角度来看待AOP。当然,从宏观的角度来考虑AOP的话,无非就是以下几个点:1. 我们要切入什么?很多时候无非就是一些日志、安全验证、应用执行跟踪等需求。2. 在哪个地方切入?我们一般是在目标方法之前、之后或者中间。具体的应用里还包含一些其他场景。3. 怎么定义和关联我们需要切入的内容和切入点?一种就是前面的采用硬编码判断,一种就是采用特定的语法来解析处理。当然,在结合spring框架的时候,它里面已经做了很多的增强。在后续的文章里我们会对这些增强做一个进一步的分析讨论。
参考材料
https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/index.html
http://www.cnblogs.com/xrq730/p/7003082.html