Spring AOP 学习笔记-引子

引子

在过去的几年里, AOP( 面向方面编程 ) 已成为 Java 领域的热门话题,对 JAVA 开发人员来说,现在已经能够找到许多关于 AOP 的文章、讨论和实现了。 AOP 通常被称为实现横切关注点的工具,这意味着你可以使用 AOP 来将独立的逻辑片段模块化,也就是我们熟知的关注点,并将这些关注点应用于应用程序的多个地方。

AOPOOP 并不相互抵触,它们是可以相辅相成的两个设计模型, Spring AOP 是实现 AOP 的一种技术,而 Spring AOP 也是 Spring 中一些子框架或子功能所依赖的核心。

在本文中,首先会讲解两种截然不同的 AOP 类型,静态的和动态的。在静态 AOP,AspectJAOP ,横切逻辑会在编译时应用到你的代码上,若非修改代码并重新编译的话你是不能改变它的。而使用动态 AOP 时,如 SpringAOP ,横切逻辑是在运行时被动态加入的,这允许你无需重新编译代码就能修改横切的使用。这两种 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);
}

        然后让实现业务逻辑的 HelloSpeaker 类实现 IHello 接口,例如:

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,aop
2010-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() 静态方法建立一个代理对象,建立代理对象时必须告知要代理的接口,之后就可以操作所建立的代理对象。在每次操作时会执行 InvocationHandlerinvoke() 方法, 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,AOP
2010-9-29 19:42:36 inside.aop.LogHandler invoke
信息: method end..........

LogHandler 不再服务于特定对象接口,而 HelloSpeaker 也不用插入任何有关日志的动作,它不用意识到日志动作的存在。

现在回到 AOP 的议题上,那么我们之前的例子和 AOP 有什么关系?

HelloSpeaker 本身的职责是显示文字,却要插入日志动作,这使得 HelloSpeaker 的职责加重,用 AOP 的术语来说,日志的程序代码横切 (Cross-cutting)HelloSpeaker 的程序执行流程中,日志这样的动作在 AOP 中称之为横切关注点 (Cross-cutting concern)

使用代理对象将日志等于业务逻辑无关的动作或任务提取出来,设计成一个服务对象,这样的对象称之为切面 (Aspect)

AOP 中的 Aspect 所指的像日志等这类的动作或服务,将这些动作 (Cross-cutting concerns) 设计为通用、不介入特定业务对象的一个职责清楚地 Aspect 对象,这就是所谓的 Aspect-oriented programming ,缩写名称即为 AOP

从上面的几个例子中可以看出,在好的设计之下, Aspect 可以独立于应用程序之外,在必要的时候,可以介入应用程序之中提供服务,在不需要相关服务的时候,又可以将这些 Aspect 直接从应用程序中脱离出来,而应用程序本身不需要修改任何一行程序代码。

AOP 术语

      Cross-cutting concern( 横切关注点 )

在上面的例子中,日志的动作原先被横切 (Cross -cutting) 入至 HelloSpeaker 本身所负责的业务流程中,另外类似于日志这类的动作,如安全 (Security) 检查、事务 (Transaction) 等系统层面的服务 (Service) ,在一些应用程序之中常被见到安插到各个对象的处理流程之中,这些动作在 AOP 的术语称为 Cross-cutting concerns

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(目标对象)

         一个 Advice 被应用的对象或目标对象,在基于拦截器机制实现的 AOP 框架中,位于拦截器链上最末端的对象实例。一般情况下,拦截器末端都包含一个目标对象,通常也是实际业务对象。

       Introduction(引入)

对于一个现存的类, Introduction 可以为其增加行为,且不用修改该类的程序,具体来说,可以为某个已编写或编译完的类,在执行时期动态地加入一些方法或行为,而不用修改或新增任何一行程序代码。

Spring 中,引入被认为是一种特殊的通知。

Interceptor(拦截器)

         拦截器是用来实现对连接点进行拦截,从而在连接点或后加入自定义的切面模块功能。在大多数 JAVA 的框架实现中,都是使用拦截器来实现字段访问及方法调用的拦截 (Interception) 。所以作用于同一个连接点的多个拦截器组成的一个拦截器链 (Interceptor  chain) ,链接上的每个拦截器通常会调用下一个拦截器。 Spring  AOP 就是采用拦截器来实现。

       Proxy(代理)

在之前的静态代理和动态代理中,已经使用了实际的程序范例介绍过的代理机制, SpringAOP 主要是通过动态代理来完成的,可用于代理任何的接口。另一方面, Spring 也可以使用 CGLIB 代理,可以代理类。

Weave(织入)

       Advice 被应用至对象之上的过程成为织入 (Weave) ,在 AOP 中织入的方式有几个时间点:编译时期 (Compile time) 、类加载时期 (Classload time) 、执行时期 (Runtime).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值