首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件架构设计 >

Spring AOP 学习札记-引子

2012-09-20 
Spring AOP 学习笔记-引子引子在过去的几年里,AOP(面向方面编程) 已成为Java领域的热门话题,对JAVA开发人

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).

热点排行