AOP简介
一. AOP字面意思
AOP = Aspect Oriented Programming 中文意为: 面向切面编程
要掌握AOP就得从aspect,即切面,入手,理解什么是切面,为什么要切面,如何使用切面。
二. 没有AOP的日子
OOP = Object Oriented Programming 面向对象的编程。
兴起于上个世纪九十年代的面向对象编程理念,仍旧是现在编程思想的主流。面向对象的核心思想是抽像问题域的模型成对象,用程序去模拟对象以解决问题。但是在软件系统中还存在着一些问题,它们不易于,或者不适合于用面向对象地方式来解决,比如事务,安全,日志等。看下面:
public class Tool1 { public void doSomething() { System.out.println("I am doing..."); } } public class Tool2 { public void doSomething () { System.out.println("I am do some other things..."); } } public class Main() { public static void main(String[] args) { Tool1 t1 = new Tool1(); Tool2 t2 = new Tool2(); t1.doSomething(); t2. doSomething (); } }
例1
这个程序很简单,容易看得懂。现在新的要求来了,要求在执行Tool1和Tool2的两个方法前后说一句话。如果用过程式的编程方法,我们只要改一下main方法,让它变成这样,看例2:
public class Main() { public static void main(String[] args) { Tool1 t1 = new Tool1(); Tool2 t2 = new Tool2(); System.out.println("Going to do something..."); t1.doSomething(); System.out.println("Did something..."); System.out.println("Going to do something..."); t2. doSomething (); System.out.println("Did something..."); } }
例2
很好,实现目标了,但这存在以下问题:
- 如果还有其它不是main的调用者使用Tool1和Tool2,我们需要改动所有调用者代码
- 完成同样的事情却将代码散落在多个地方,复制粘贴不是一个编程好习惯
下面我们来解决这两个问题。为了解决第一个问题,我们可以把代码改动由调用者转移到被调用者里,如:
public class Tool1 { public void doSomething() { System.out.println("Going to do something..."); System.out.println("I am doing..."); System.out.println("Did something..."); } } public class Tool2 { public void doSomething () { System.out.println("Going to do something..."); System.out.println("I am do some other things..."); System.out.println("Did something..."); } } public class Main() { public static void main(String[] args) { Tool1 t1 = new Tool1(); Tool2 t2 = new Tool2(); t1.doSomething(); t2. doSomething (); } }
例3
好,这样就不需要改变第个调用者的代码了。但是为了实现同样的功能,还是用了太多的代码,好,我们利用伟大的面向对向的能力吧!
public abstract class abstractTool { public void doSomething() { System.out.println("Going to do something..."); doSomethingReally(); System.out.println("Did something..."); } protected abstract void doSomethingReally(); } public class Tool1 extends abstractTool { public void doSomethingReally() { System.out.println("I am doing..."); } } public class Tool2 extends abstractTool { public void doSomethingReally() { System.out.println("I am do some other things..."); } } public class Main() { public static void main(String[] args) { Tool1 t1 = new Tool1(); Tool2 t2 = new Tool2(); t1.doSomething(); t2.doSomething (); } }
例4
好了,这样实现新功能的代码被集在了一个抽象类的方法里了,没有在散落开来,易于维护了。这样的方案是不是就是完美的了呢?不,还不够,它存在着一个严重问题,
- 必须得修改被调用者的代码
- 强迫被调用者继承于某个类,这样它将不能再继承其它类
这样做显然很不灵活,假如还有Tool3 Tool4 Tooln…,它们有一些需要这样的功能,有一些不需要,甚至于有时有一些需要,有时有一些不需要,怎么办?我们只好每次变动时都做这样的修改,那太痛苦了。好吧,还有更好的方案吗?也许一开始设计得就有些问题吧?嗯,是的,我们采用工厂模式吧,面向接口编程吧!于是就有了下面的代码:
public interface Tool { public void doSomeThing(); } public class Tool1 implements Tool { public void doSomethingReally() { System.out.println("I am doing..."); } } public class Tool2 implements Tool { public void doSomethingReally() { System.out.println("I am do some other things..."); } } public class ToolProxy implements Tool{ private Tool tool; public ToolProxy(tool) { this.tool =tool; } public void doSomething() { System.out.println("Going to do something..."); this.tool.doSomething(); System.out.println("Did something..."); } } public class ToolFactory { public static Tool getTool(String type) { if("Tool1".equals(type)) { return new ToolProxy(new Tool1()); } else if("Tool2".equals(type)) { return new ToolProxy(new Tool2()); } return null; } } public class Main() { public static void main(String[] args) { Tool t1 = ToolFactory.getTool("Tool1"); Tool t2 = ToolFactory.getTool("Tool2"); t1.doSomething(); t2.doSomething (); } }
例5
接口使被调用者不再需要继承于某个类,也使得调用者和被调用者之间解耦。工厂负责组装类,我们可以在工厂里实现通过xml来配置等功能。ToolProxy代理类把新功能性代码集中在一起,便于管理。可惜这个方案还是有些问题的,假如我们不能改动被调用代码,不能强制人家实现某个接口呢?好吧,我们再使用一点花招吧,看下面代码:
public interface Tool { public void doSomeThing(); } public class Tool1Wrapper implements Tool { public Tool1 tool1; public Tool1Wrapper() { tool1 = new Tool1(); } public void doSomething() { tool1.doSomething(); } } public class Tool2Wrapper implements Tool { public Tool2 tool2; public Tool2Wrapper() { tool2 = new Tool2(); } public void doSomething() { tool2.doSomething(); } } public class Tool1{ public void doSomething() { System.out.println("I am doing..."); } } public class Tool2{ public void doSomething() { System.out.println("I am do some other things..."); } } public class ToolProxy implements Tool{ private Tool tool; public ToolProxy(tool) { this.tool =tool; } public void doSomething() { System.out.println("Going to do something..."); this.tool.doSomething(); System.out.println("Did something..."); } } public class ToolFactory { public static Tool getTool(String type) { if("Tool1".equals(type)) { return new ToolProxy(new Tool1Wrapper()); } else if("Tool2".equals(type)) { return new ToolProxy(new Tool2Wrapper()); } return null; } } public class Main() { public static void main(String[] args) { Tool t1 = ToolFactory.getTool("Tool1"); Tool t2 = ToolFactory.getTool("Tool2"); t1.doSomething(); t2.doSomething (); } }
例6
当你看完这段代码后,如果你想要抓狂,那就抓吧,我能够理解。为了实现这点新功能,我们做了如此大的努力!是不是这样就结束了呢?这就是最终的完美方案了吗?显然还不够:
- 代码太长
- 如果有多个类,就需要写多个Wrapper类
- 如果有多个接口,就需要写多个Proxy类
如果这样的代码就能够让你满意,看来你不是足够懒惰的程序员,你很勤快,愿意为实现这样的功能敲打出一堆代码。但是伟大的懒惰程序员们并不没有停下来。
三. AOP是对OOP的补充
当写完上面的代码后,我们该静下来想一想,我们到底想要对程序做什么,为什么会这样麻烦呢?
我们想要什么?我们想要就是能在程序的调用者和被调用者之间切开一个面,加入新的功能!上面的例子,我们就是想在调用Tool1和Tool2的doSomething()方法时,将调用过程切开一个面,分别在进入和出来时放入两句代码而已。而现在的程序语言恰恰缺少这样把调用过程切开的能力,所以我们需要新的语言,新的能力!这个编程的思想就是AOP,这种能力就是AOP的能力,能够这样做的语言就是AOP的语言。
AOP是对OOP的一个补充,正是OOP缺少了这样的能力,而人们又有这样的需要,所以才会有AOP。AOP并不能代替OOP,正如OOP不能代替过程式编程一样。
四. 理解AOP的语义
AOP中最关键的就是aspect,即切面。切面是以前的语言没有定义的新的概念。要理解切面,就要理解切面几个要素:
1. 切谁 即,对谁的调用会被切开,我们想要加入的新的功能正是针对于这个被切的对象的。
2. 切什么地方 被调用者会可能有多个被外部访问的出入口,为了实现我们的功能,需要被切的那个出入口。
3. 添加什么样的功能 将程序切开,目的是在被切开的地方加入新的功能。
理想状况下,所有的对象都能被切,不管这个对象是什么类型的,是不是final的,是不是nested的等;对象的所有地方都能切,不管是在构建时,调用方法时,调用属性时,调用前,调用后等;任意功能都能添加,切开之后,做什么都可以。
AOP有各种不同的实现,不同实现的能力之间的区别就可以通过上面的三条来衡量。功能最强的AspectJ,可以对任意对象的创建,方法调用,属性调用时做切面,加入任意功能。但是一般情况下我们不需要那么强的能力,能够实现对象的方法调用的切入,就已经够用了,spring里大量使用了这样的能力。
为了使用的方便,一般对方法的切入,分为以下几种:
1. Before 在方法调用前执行,不能控制返回值
2. After 在方法调用后执行,可以控制返回值
3. Throw 在方法异常退出时执行
4. Around 兼有以上三种的能力
五. AOP 实现原理举例
实现AOP有很多种不同方法,大体上可以分为三类:
1. 修改由源程序生的class文件,加入新功能
这一类的代表是AspectJ项目。它在java语言的基础上添加的新的语言和语法。AspectJ有自己的编译器,它会在调用java的编译器生成class文件后,修改class文件。这种方法实现的AOP是最高效的,因为功能的添加是在编译器就加入了,也是功能最强的,所有的地方都可以切入,缺点是需要学习新的语法和需要额外的编译步骤。
2. 动态生成类的子类,通过方法覆盖来添加新的功能
Spring使用cglib在运行时生成类的子类来实现一部分AOP。比如对于上面例子中的Tool1类,spring可以在运行时动态生成一个新的类,类似于:
public class Tool1$4535354 extends Tool1{ public void doSomething { System.out.println("Going to do something..."); super.doSomething(); System.out.println("Did something..."); } }
例7
这种方法需要被调用对象的类是可以被继承的,方法是可以被覆盖的,还好这一条基本上都能满足,因为大部情况下我们面对的都是普通的java bean。
3. 借助于接口和动态代理类,添加新功能。
Spring在处理带有接口的类的时候采用这种方式。JDK里有一个类java.lang.reflect.Proxy 可以动态地生
成一个实现了某个接口的代理类。例如,spring实现了一个类似于下面的类:
public class ProxyFactory { public static Object getProxy(Class[] interfaces, Object target) { Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(), interfaces, new MyInvocationHandler(target); ); return proxy; } static class MyInvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Going to do something..."); Object o = method.invoke(target, args); System.out.println("Did something..."); return o; } } }
进一步的,我们还可以修改MyInvocationHanlder使其动态化,在invoke方法里执行抽像化的before after around throw 等建议,这里就写了。有了这个动态代理类,我们就可以不用实现很多的Proxy类了。
六. AOP的标准
为了能够统一对AOP使用的术语和API,国际上出现了AOP相关的组织,比如项目aopalliance等。Spring在使用的aop时就采用这个项目制定的api。关于标准的内容,可以自行去查看相关资料。
七. 在spring中使用AOP
Spring里大量使用AOP,为了能更简单地表达AOP,可以通过xml或annotation对AOP进行描述。详细请看spring的手册。
<tx:advice id="daoTxAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="find*" read-only="true" propagation="REQUIRED" /> <tx:method name="check*" read-only="true" propagation="REQUIRED" /> <tx:method name="*" propagation="REQUIRED" /> </tx:attributes> </tx:advice > <aop:config> <aop:pointcut id="daoOperation" expression="execution(public * com.**.dao.*Dao.*(..))" /> <aop:advisor advice-ref="daoTxAdvice" pointcut-ref="daoOperation" /> <aop:aspect ref="permissionCheckBean" order="0"> <aop:pointcut id="permissionCheckPointCut" expression="execution(public String com.**.action.*Action.execute*())" /> <aop:around method="checkPermission" arg-names="pjp " pointcut-ref="permissionCheckPointCut"/> </aop:aspect> </aop:config>
例9
上面的配置中<aop:pointcut>标签定义了一个bean,它能够说明切什么对象的什么地方。比如,expression=”execution(public String com.**.action.*Action.execute*()) and @annotation(requirePermission) 说明在对方公开的反回值为String的,包名符使表达式com.liba.**.action的,类名符合*Action的,方法名以execute开头的,无参数的,这样的方法进行切入。然后<aop:around>标签则表达了,对于上面那样的方法以around方式切入,切入后,在切开的地方执行permissionCheckBean的 checkPermission方法。