Spring AOP 学习笔记-引子
引子
在过去的几年里,AOP(面向方面编程) 已成为Java领域的热门话题,对JAVA开发人员来说,现在已经能够找到许多关于AOP的文章、讨论和实现了。AOP通常被称为实现横切关注点的工具,这意味着你可以使用AOP来将独立的逻辑片段模块化,也就是我们熟知的关注点,并将这些关注点应用于应用程序的多个地方。
AOP和OOP并不相互抵触,它们是可以相辅相成的两个设计模型,SpringAOP是实现AOP的一种技术,而Spring AOP也是Spring中一些子框架或子功能所依赖的核心。
在本文中,首先会讲解两种截然不同的AOP类型,静态的和动态的。在静态AOP中,如AspectJ的AOP,横切逻辑会在编译时应用到你的代码上,若非修改代码并重新编译的话你是不能改变它的。而使用动态AOP时,如Spring的AOP,横切逻辑是在运行时被动态加入的,这允许你无需重新编译代码就能修改横切的使用。这两种AOP相互补充,将它们结合使用将会在我们的应用中形成强有力的组合。
Spring项目中有很多能集各种设计模式、编码技巧为一体的编码艺术,在灵活应用Spring的同时,若能把Spring项目里面的精华、设计思想、编码技巧等吸纳过来,这对于程序员来说将会是一件非常有意义的事。
从代理机制初探AOP我们暂且把AOP放到一边,先从一个简单例子来看一个议题,这个例子当中包含日志(Logging)动作,程序中常需要为某些动作或事件记下记录,以便在事后检查程序运作过程,或是作为出错时的信息。
来看一个最简单的例子,当你需要在执行某些方法时留下日志信息,可能会如下编写:
package inside.aop;import java.util.logging.Level;import java.util.logging.Logger;public class HelloSpeaker { private Logger logger=Logger.getLogger(this.getClass().getName()); public void hello(String name){ //方法执行开始时留下记录 logger.log(Level.INFO,"hello method start.............."); //程序主要功能 System.out.println("hello"+name); //程序执行完毕时留下记录 logger.log(Level.INFO,"hello method end.............."); }}
在HelloSpeaker类中,当执行hello()时,你希望方法在开始执行和执行完毕时都能留下记录,最简单的作法就是如以上的程序设计,在方法执行的前后加上日志动作,然而日志的这几行程序代码横切入(Cross-cutting)HelloSpeaker类中,对于HelloSpeaker类来说,日志的这几个动作并不属于HelloSpeaker业务逻辑,这无疑是HelloSpeaker增加了额外的职责(违反了面向对象设计的类的单一职责原则)。
可以使用代理(Proxy)机制来解决这个问题,在这里讨论两种代理方法:静态代理(Static Proxy)与动态代理(Dynamic Proxy)。
静态代理
在静态代理的实现中,代理对象与被代理对象必须实现同一个接口,在代理对象中可以实现日志等相关服务,并在需要的时候在调用被代理的对象,如此,被代理对象当中就可以仅保留与业务相关的职责。
重新设计HelloSpeaker类,首先定义一个IHello接口:
package inside.aop;public interface IHello { public void hello(String name);}
package inside.aop;public class HelloSpeaker implements IHello{ public void hello(String name){ //程序主要业务逻辑 System.out.println("hello"+name); }}
可以看到,在HelloSpeaker类中现在没有任何日志的程序插入其中,日志服务的实现将被放置代理之中,代理对象同样也要实现IHello接口,例如:
package inside.aop;import java.util.logging.Level;import java.util.logging.Logger;public class HelloProxy implements IHello { private Logger logger=Logger.getLogger(this.getClass().getName()); private IHello helloObject; public HelloProxy(IHello helloObject){ this.helloObject=helloObject; } public void hello(String name) { //日志服务 logger.log(Level.INFO,"hello method start.............."); //执行业务逻辑 helloObject.hello(name); //日志服务 logger.log(Level.INFO,"hello method end.............."); }}
在HelloProxy 类的hello()方法中,要真正实现业务逻辑前后可以安排日志服务,下面我们编写一个测试程序来看看如何使用代理对象。
package inside.aop;public class StaticProxyTest { public static void main(String[] args) { IHello proxy=new HelloProxy(new HelloSpeaker()); proxy.hello("aop"); }}
程序中调用执行的是代理对象,构造代理对象时必须给它一个被代理对象,记得在操作取回代理对象时,必须转换操作接口为IHello接口,下面是实际执行的结果。
2010-9-29 19:10:04 inside.aop.HelloProxy hello信息: hello method start..............hello,aop2010-9-29 19:10:04 inside.aop.HelloProxy hello信息: hello method end..............
这是静态代理的基本范例,然而正如你看到的,代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每种方法进行代理,静态代理在程序规模较大时就无法胜任了,根据这个设计思想在JDK1.3之后就加入了动态代理的功能。在这里介绍静态代理的目的,是为了了解代理的基本原理。
动态代理
在JDK1.3之后加入了可协助开发动态代理功能的API,从此不必为特定的对象和方法编写特定的代理对象。使用代理对象,可以使用一个处理者(Handler)服务于各个对象。首先,一个处理者的类设计必须实现java.lang.reflect.InvocationHandler接口,下面用实例来进行说明,设计一个LogHandler类:
package inside.aop;import java.lang.reflect.Proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.logging.Level;import java.util.logging.Logger;public class LogHandler implements InvocationHandler { private Logger logger=Logger.getLogger(this.getClass().getName()); private Object target; public Object bind(Object target){ this.target=target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object retValue=null; logger.log(Level.INFO,"method start.........."); retValue=method.invoke(target, args); logger.log(Level.INFO,"method end.........."); return retValue; }}
主要的概念是使用Proxy.newProxyInstance()静态方法建立一个代理对象,建立代理对象时必须告知要代理的接口,之后就可以操作所建立的代理对象。在每次操作时会执行InvocationHandler的invoke()方法,invoke()方法会传入被代理对象的方法名称与执行参数,实际上要执行的方法会交由method.invoke()。如果对上面代码不清楚的地方,请读者自行查阅相关的JDK动态代理的只是。
要实现动态代理,同样必须定义所要代理的接口,此处使用之前定义的IHello接口,以及HelloSpeaker类。测试程序如下所示:
package inside.aop;public class DynamicProxyTest { public static void main(String[] args) { LogHandler logHandler=new LogHandler(); IHello proxy=(IHello) logHandler.bind(new HelloSpeaker()); proxy.hello("AOP"); }}
?????? 来看一下执行结果,如下所示:
2010-9-29 19:42:36 inside.aop.LogHandler invoke信息: method start..........hello,AOP2010-9-29 19:42:36 inside.aop.LogHandler invoke信息: method end..........
LogHandler不再服务于特定对象接口,而HelloSpeaker也不用插入任何有关日志的动作,它不用意识到日志动作的存在。
现在回到AOP的议题上,那么我们之前的例子和AOP有什么关系?
HelloSpeaker本身的职责是显示文字,却要插入日志动作,这使得HelloSpeaker的职责加重,用AOP的术语来说,日志的程序代码横切(Cross-cutting)入HelloSpeaker的程序执行流程中,日志这样的动作在AOP中称之为横切关注点(Cross-cuttingconcern)。
使用代理对象将日志等于业务逻辑无关的动作或任务提取出来,设计成一个服务对象,这样的对象称之为切面(Aspect)。
AOP中的Aspect所指的像日志等这类的动作或服务,将这些动作(Cross-cuttingconcerns)设计为通用、不介入特定业务对象的一个职责清楚地Aspect对象,这就是所谓的Aspect-oriented programming,缩写名称即为AOP。
从上面的几个例子中可以看出,在好的设计之下,Aspect可以独立于应用程序之外,在必要的时候,可以介入应用程序之中提供服务,在不需要相关服务的时候,又可以将这些Aspect直接从应用程序中脱离出来,而应用程序本身不需要修改任何一行程序代码。
AOP术语????? Cross-cutting concern(横切关注点)
在上面的例子中,日志的动作原先被横切(Cross -cutting)入至HelloSpeaker本身所负责的业务流程中,另外类似于日志这类的动作,如安全(Security)检查、事务(Transaction)等系统层面的服务(Service),在一些应用程序之中常被见到安插到各个对象的处理流程之中,这些动作在AOP的术语称为Cross-cuttingconcerns。
Aspect(切面)
将散落在各个业务逻辑之中的Cross-cutting concerns收集起来,设计成各个独立可重用的对象,这些对象称为Aspect。例如,在我们的例子中,将日志的动作设计为一个LogHandler类,LogHandler类在AOP的术语就是Aspect的一个具体事例。
在AOP中着重于Aspect的辨认,使之从业务流程中独立出来。在需要该服务的时候,织入(weave)至应用程序之上;在不需要服务的时候,也可以马上从应用程序中剥离,且应用程序中的可重用组件不用做任何修改。
另一方面,对于应用程序中的可重用组件来说,按照AOP的设计方式,它不用知道提供服务的对象是否存在,具体地说,与服务相关的API不会出现在可重用的应用程序组件之上,因而可提高这些组件的可重用性,你可以把这些组件应用至其他的应用程序之中,不会因为加入了某些服务而与目前的应用程序框架发生耦合。
在Spring AOP中,一个方面是由一个实现Advisor(通知者)接口的类来表示。Spring提供了一些使用方便的Advisor接口的接口类,这样不用在自己的程序中创建各种各样不同的Advisor实例。
Advice(增强或通知)
Advice不管怎么翻译成建议、通知或者增强,都不能直接反映其内容。笔者认为通知稍微能够体现出Advice的本质。
Aspect当中Cross-cutting concerns的具体实现称之为Advice。以日志的动作而言,Advice中会包含日志程序代码是如何实现。Advice中包含了Cross-cutting concerns的行为或所要提供的服务。
换一种说法,通知(Advice)是指在定义好的切入点处,所要执行的程序代码。
JoinPoint(连接点)
Advice在应用程序执行时加入业务流程的点或时机称之为Joinpoint,具体来说,就是Advice在应用程序中被执行的时机。Spring只支持方法的Joinpoint,执行时机可能是某个方法被执行之前或之后(或两者都有),或是方法中某个异常发生的时候。
Spring AOP中最明显的简化之一就是它只支持一种类型的连接点:方法调用。我们可以用它来完成大多数用到AOP的日常编程任务。
Pointcut(切入点)
Pointcut定义了感兴趣的Joinpoint,当调用的方法符合Pointcut表示式时,将Advice织入至应用程序上提供服务。切入点指一个或多个连接点,可以理解成一个点的集合。切入点的描述比较具体,而且一般会跟连接点上下文环境结合。
Target(目标对象)
对于一个现存的类,Introduction可以为其增加行为,且不用修改该类的程序,具体来说,可以为某个已编写或编译完的类,在执行时期动态地加入一些方法或行为,而不用修改或新增任何一行程序代码。
在Spring中,引入被认为是一种特殊的通知。
Interceptor(拦截器)
在之前的静态代理和动态代理中,已经使用了实际的程序范例介绍过的代理机制,Spring的AOP主要是通过动态代理来完成的,可用于代理任何的接口。另一方面,Spring也可以使用CGLIB代理,可以代理类。
Weave(织入)
?????? Advice被应用至对象之上的过程成为织入(Weave),在AOP中织入的方式有几个时间点:编译时期(Compile time)、类加载时期(Classloadtime)、执行时期(Runtime).