AOP@Work:用AspectJ和Spring进行依赖项插入

面向方面开发人员可以采用的高级技术

简介:依赖项插入和面向方面编程是互补的技术,所以想把它们结合在一起 使 用是很自然的。请跟随 Adrian Colyer 一起探索两者之间的关系,并了解怎样 才 能把它们组合在一起,来促进高级的依赖项插入场景。

依赖项插入和面向方面编程(AOP)是两个关键的技术,有助于在企业应用程 序中简化和纯化域模型和应用程序分层。依赖项插入封装了资源和协调器发现的 细节,而方面可以(在其他事情中)封装中间件服务调用的细节 —— 例如,提 供事务和安全性管理。因为依赖项插入和 AOP 都会形成更简单、更容易测试的 基 于对象的应用程序,所以想把它们结合在一起使用是很自然的。方面可以帮助把 依赖项插入的能力带到更广的对象和服务中,而依赖项插入可以用来对方面本身 进行配置。

在这篇文章中,我将介绍如何把 Spring 框架的依赖项插入与用 AspectJ 5 编写的方面有效地结合在一起。我假设您拥有基本的 AOP 知识(如果没有这方 面 知识 ,可以在 参考资料 中找到一些良好的起点),所以我的讨论将从对基于 依 赖项插入的解决方案中包含的关键角色和职责的分析开始。从这里,我将介绍如 何通过依赖项插入配置单体(singleton)方面。因为配置非单体方面与配置域 对 象共享许多公共内容,所以后面我会研究一个应用于这两者的简单解决方案。总 结这篇文章时,我会介绍如何为多个高级依赖项插入场景使用方面,其中包括基 于接口的插入和重复插入。

请参阅 下载 获得文章的源代码,参阅 参考资料 下载 AspectJ 或 Spring 框架,运行示例需要它们。

什么是依赖项插入?

在 Domain-Driven Design 一书中,Eric Evans 讨论了如何把对象与建立对 象的配置和关联的细节隐藏起来:

对象的大部分威力在于对象内部复杂的配置和关联。应当对对象进行提炼, 直 到与对象的意义或者在交互中支持对象的作用无关的东西都不存在为止。这个中 间循环的责任很多。如果让复杂对象负责自己的创建,就会出现问题。

Evans 接着提供了一个汽车引擎的示例:它的众多部件一起协作,执行引擎 的 职责。虽然可以把引擎块想像成把一组活塞插入气缸,但是这样的设计会把引擎 明显地弄复杂。相反,技工或机器人装配引擎,引擎本身只考虑自己的操作。

虽然这个示例是我从书中介绍用于复杂对象创建的工厂 概念一节中取出的, 但是我们也可以用这个概念解释依赖项插入技术的动机。

从协作到合约

针对这篇文章的目的,可以把依赖项插入想像成对象和对象的执行环境之间 的 合约。对象(执行 ResourceConsumer、 CollaboraTor 和 ServiceClient 的其 中一个角色或全部角色)同意不出去搜索自己需要的资源、它与之协作的合作伙 伴或它使用的服务。相反,对象提供一种机制,让这些依赖项可以提供给它。接 下来,执行环境同意在对象需要它的依赖项之前,向对象提供所有的依赖项。

解析依赖项的方法在不同的场景中各有不同。例如,在单元测试用例中,对 象 的执行环境是测试用例本身,所以测试设置代码有责任直接满足依赖项。在集成 测试或应用程序在生产环境时,代理 负责寻找满足对象依赖项的资源,并把它 们 传递给对象。代理的角色通常是由轻量级容器扮演的,例如 Spring 框架。不管 依赖项是如何解析的,被配置的对象通常不知道这类细节。在第二个示例中,它 可能还不知道代理的存在。

代理(例如 Spring 框架)有四个关键职责,在整篇文章中我将不断提到这 些 职责,它们是:

确定对象需要配置(通常因为对象刚刚创建)

确定对象的依赖项

发现满足这些依赖项的对象

用对象的依赖项对它进行配置

从下面的各种依赖项插入解决方案可以看出,解决这些职责有多种策略。

使用 Spring 进行依赖项插入

在标准的 Spring 部署中,Spring 容器同时负责创建和配置核心应用程序对 象(称为 bean)。因为容器既创建对象,又扮演代理的角色,所以对 Spring 容 器来说,确定 bean 已经创建而且需要配置是件轻而易举的小事。通过查询应用 程序的元模型,可以确定 bean 的依赖项,元模型通常是在 Spring 的配置文件 中用 XML 表示的。

满足 bean 的依赖项的对象是容器管理的其他 bean。容器充当这些 bean 的 仓库,所以可以用名称查询它们(或者在需要的时候创建)。最后,容器用新 bean 的依赖项对其进行配置。这通常是通过 setter 插入完成的(调用新 bean 的 setter 方法,把依赖项作为参数传递进去),虽然 Spring 支持其他形式的 插入,例如构造函数插入和查询方法插入(请参阅 参考资料 学习关于使用 Spring 进行依赖项插入的更多内容。)

方面的依赖项插入

像其他对象一样,方面可以从通过依赖项插入进行的配置中受益。在许多情 况 下,把方面实现为轻量级控制器 是良好的实践。在这种情况下,方面确定什么 时 候应当执行某些行为,但是会委托给协作器去执行实际的工作。例如,可以用异 常处理策略对象配置异常处理方面。方面会探测出什么时候抛出了异常,并委托 处理器对异常进行处理。清单 1 显示了基本的 RemoteException 处理方面:

清单 1. RemoteException 处理方面

public aspect RemoteExceptionHandling {   private RemoteExceptionHandler exceptionHandler;   public void setExceptionHandler(RemoteExceptionHandler aHandler) {     this.exceptionHandler = aHandler;   }   pointcut remoteCall() : call(* *(..) throws RemoteException+);    /**    * Route exception to handler. RemoteException will still    * propagate to caller unless handler throws an alternate    * exception.    */   after() throwing(RemoteException ex) : remoteCall() {    if (exceptionHandler != null)       exceptionHandler.onRemoteException(ex);   }   }

现在我要用依赖项插入,用一个特殊的异常处理策略来配置我的方面。对于 这 个方面,我可以用标准的 Spring 方式,但是有一个警告。一般来说,Spring 既 负责创建 bean,也负责配置 bean。但是,AspectJ 方面是由 AspectJ 运行时 创 建的。我需要 Spring 来配置 AspectJ 创建的方面。对于单体方面最常见的形 式 ,例如上面的 RemoteExceptionHandling 方面,AspectJ 定义了一个 aspectOf () 方法,它返回方面的实例。我可以告诉 Spring 使用 aspectOf() 方法作为 工 厂方法,获得方面的实例。清单 2 显示了方面的 Spring 配置:

清单 2. 方面的 Spring 配置

            class="org.aspectprogrammer.dw.RemoteExceptionHandling"     facTory-method="aspectOf">                          class="org.aspectprogrammer.dw.DefaultRemoteExceptionHandler">     

我想确保我的方面在远程异常抛出之前得到配置。在示例代码中,我用 Spring 的 ApplicationContext 确保了这种情况,因为它会自动地预先实例化 所 有单体 bean。如果我使用普通的 BeanFacTory,然后再调用 preInstantiateSingletons,也会实现同样的效果。

域对象的依赖项插入

配置单体方面就像在 Spring 容器中配置其他 bean 一样简单,但是对于拥 有 其他生命周期的方面来说,该怎么办呢?例如 perthis、pertarget 甚至 percflow 方面?生命周期与单体不同的方面实例,不能由 Spring 容器预先实 例 化;相反,它们是由 AspectJ 运行时根据方面声明创建的。迄今为止,代理 (Spring)已经知道了对象需要配置,因为它创建了对象。如果我想执行非单体 方面的依赖项插入,就需要用不同的策略来确定需要配置的对象已经创建。

非单体方面不是能够从外部配置受益的、在 Spring 容器的控制之外创建的 惟 一对象类型。例如,需要访问仓库、服务和工厂的域实体(请参阅 参考资料) 也 会从依赖项插入得到与容器管理的 bean 能得到的同样好处。回忆一下代理的四 项职责:

确定对象需要配置(通常因为对象刚刚创建)

确定对象的依赖项

发现满足这些依赖项的对象

用对象的依赖项对它进行配置

我仍然想用 Spring 来确定对象的依赖项,去发现满足这些依赖项的对象, 并 用对象的依赖项来配置对象。但是,需要另一种方法来确定对象需要配置。具体 来说,我需要一个解决方案,针对那些在 Spring 的容器控制之外,在应用程序 执行过程中的任意一点上创建的对象。

SpringConfiguredObjectBroker

我把 Spring 配置的对象叫作 SpringConfigured 对象。创建新的 SpringConfigured 对象之后的需求就是,应当请求 Spring 来配置它。Spring ApplicationContext 支持的 SpringConfiguredObjectBroker 应当做这项工作 , 如清单 3 所示:

清单 3. @SpringConfigured 对象代理

public aspect SpringConfiguredObjectBroker  implements ApplicationContextAware {   private ConfigurableListableBeanFacTory beanFacTory;   /**    * This broker is itself configured by Spring DI, which will    * pass it a reference to the ApplicationContext    */   public void setApplicationContext(ApplicationContext aContext) {    if (!(aContext instanceof ConfigurableApplicationContext)) {     throw new SpringConfiguredObjectBrokerException(       "ApplicationContext [" + aContext +      "] does not implement ConfigurableApplicationContext"       );    }    this.beanFacTory =      ((ConfigurableApplicationContext)aContext).getBeanFacTory();    }   /**    * creation of any object that we want to be configured by Spring    */   pointcut springConfiguredObjectCreation(         Object newInstance,         SpringConfigured scAnnotation         )    : initialization((@SpringConfigured *).new(..)) &&     this(newInstance) &&     @this(scAnnotation);   /**    * ask Spring to configure the newly created instance    */     after(Object newInstance, SpringConfigured scAnn) returning      : springConfiguredObjectCreation(newInstance,scAnn)   {    String beanName = getBeanName(newInstance, scAnn);     beanFacTory.applyBeanPropertyValues(newInstance,beanName);    }   /**    * Determine the bean name to use - if one was provided in    * the annotation then use that, otherwise use the class name.    */   private String getBeanName(Object obj, SpringConfigured ann) {    String beanName = ann.value();    if (beanName.equals (“”)) {     beanName = obj.getClass().getName ();    }    return beanName;   }    }

SpringConfiguredObjectBroker 内部

我将依次分析 SpringConfiguredObjectBroker 方面的各个部分。首先,这个方面实现了 Spring 的 ApplicationContextAware 接口。代理方面本身是由 Spring 配置的 (这是它得到对应用程序上下文的引用的方式)。让方面实现 ApplicationContextAware 接口,确保了 Spring 知道在配置期间向它传递一个 到当前 ApplicationContext 的引用。

切点 springConfiguredObjectCreation() 用 @SpringConfigured 标注与任何对象的 初始化连接点匹配。标注和新创建的实例,都在连接点上作为上下文被捕捉到。 最后,返回的 after 建议要求 Spring 配置新创建的实例。bean 名称被用来查 询实例的配置信息。我可以以 @SpringConfigured 标注的值的形式提供名称, 或 者也可以默认使用类的名称。

方面的实现本身可以是标准库的一部分( 实 际上 Spring 的未来发行版会提供这样的方面),在这种情况下,我需要做的全 部工作只是对 Spring 要配置的实例的类型进行标注,如下所示:

  @SpringConfigured("AccountBean")  public class Account {   ...  }

可以在程序的控制下, 创 建这一类类型的实例(例如,作为数据库查询的结果),而且它们会把 Spring 为它们配置的全部依赖项自动管理起来。请参阅 下载 得到这里使用的 @SpringConfigured 标注的示例。请注意,当我选择为这个示例使用的标注时( 因为提供 bean 名称是非常自然的方式),标记器接口使得在 Java™ 1.4 及以下版本上可以使用这种方法。

就像我在这一节开始时讨论的, SpringConfigured 技术不仅仅适用于域实例,而且适用于在 Spring 容器的控 制 之外创建的任何对象(对于 Spring 本身创建的对象,不需要添加任何复杂性) 。通过这种方式,可以配置任何方面,而不用管它的生命周期。例如,如果定义 percflow 方面,那么每次进入相关的控制流程时,AspectJ 都会创建新的方面 实 例,而 Spring 会在每个方面创建的时候对其进行配置。

基于接口的插 入

迄今为止,我使用了 Spring 容器读取的 bean 定义来确定对象的依赖 项 。这个方案的一个变体采用合约接口,由客户端声明它的要求。假设前一节的 Account 实体要求访问 AccountOperationValidationService。我可以声明一个 接口,如清单 4 所示:

清单 4. 客户端接口

public interface AccountOperationValidationClient {public void setAccountOperationValidationService(AccountOperationValidationService aValidationService);}

现在,需要访问 AccountOperationValidationService 的对象必须实现这个 接口,并把自己声明为客户。使用与前一节开发的方面类似的方面,我可以匹配 实现这个接口的客户对象的所有初始化连接点。由它负责第一个代理职责:确定 什么时候需要配置对象。第二个职责在接口中被明确表达:必须满足的依赖项是 验证服务依赖项。我将用一个方面插入所有客户验证服务的依赖项。方面得到合 适服务的最简单方法就是把服务插入到方面自身!清单 5 显示了一个示例:

清单 5. 服务插入器方面

/** * ensure that all clients of the account validation service* have Access to it */public aspect AccountOperationValidationServiceInjecTor {private AccountOperationValidationService service;/*** the aspect itself is configured via Spring DI*/public void setService(AccountOperationValidationService aService) {this.service = aService;}/*** the creation of any object that is a client of the * validation service*/pointcut clientCreation(AccountOperationValidationClient aClient) :initialization(AccountOperationValidationClient+.new(..)) &&this(aClient);/*** inject clients when they are created*/after(AccountOperationValidationClient aClient) returning :clientCreation(aClient) {aClient.setAccountOperationValidationService(this.service);}}

这个解决方案提供了两级控制。服务本身实际的定义是在 Spring 的配置文 件中提供的,就像清单 6 中的 XML 片段示例一样:

清单 6. 服务插入器配置

class="org.aspectprogrammer.dw.AccountOperationValidationServiceInjecTor"facTory-method="aspectOf">class="org.aspectprogrammer.dw.DefaultAccountOperationValidationService">

服务的客户只需要实现 AccountOperationValidationClient 接口,那么就 会自动用 Spring 定义的服务的当前实例对它们进行配置。

重复插入

在 Spring 中的查询方法插入查询方法插入是 Spring 容器支持的一种高级 特性:由容器覆盖被管理 bean 的抽象或具体方法,返回在容器中查询另一个命 名 bean 的结果。查询通常是非单体 bean。查询依赖项的 bean ,用被查询 bean 类型所声明的返回类型,定义查询方法。Spring 配置文件在 bean 的内部 使用 元素告诉 Spring 在调用查询方法时应当返回什 么 bean 实例。请参阅 参考资料 学习关于这项技术的更多内容。带有 HotSwappable 目标源的 Spring AOP 代理提供了另一种方法。

.迄今为止,我介绍的解决方案都是在对象初始化之后立即配置对象。但是, 在某些情况下,客户需要与之协调的对象在运行的时候变化。例如,通过与系统 进行交互,销售团队可以动态地为在线预订应用程序修改报价策略和座位分配策 略。与报价策略和座位分配策略交互的预订服务需要的策略实现,应当是预订时 的实现,而不是预订服务第一次初始化的时候实现的版本。在这种情况下,可以 把依赖项的插入延迟到客户第一次需要它的时候,并在每次引用依赖项的时候, 将依赖项的最新版本重新插入客户。

这个场景的基本技术包括字段级插入或 getter 方法覆盖。在进入示例之前 ,我要再次强调:我要介绍的插入技术所面向的对象,是在 Spring 容器的控制 之外 创建的。对于 Spring 创建的对象,Spring 容器已经提供了解决这些需求 的简单机制。

字段级插入

在下面的示例中,可以看出如何为延迟插入或重复插入应用字段级插入。字 段的 get 连接点让我可以确定什么时候进行插入,而字段类型可以确定要插入 的依赖项。所以,如果客户声明了这样的一个字段:

private PricingStrategy pricingStrategy;

而在客户的方法中,发现了 下面的代码

this.pricingStrategy.price(…..);

那么代码在运行时的执行会形成 pricingStrategy 字段的 get() 连接点,我可以用它插入当前报价策略实现, 如清单 7 所示:

清单 7. 字段级插入示例

public aspect PricingStrategyInjecTor {private PricingStrategy currentPricingStrategy;public void setCurrentPricingStrategy(PricingStrategy aStrategy) {this.currentPricingStrategy = aStrategy;}/*** a client is trying to Access the current pricing strategy*/pointcut pricingStrategyAccess() : get(PricingStrategy *) &&!within(PricingStrategyInjecTor); // don’t advise ourselves!/*** whenever a client Accesses a pricing strategy field, ensure they* get the latest...*/PricingStrategy around() : pricingStrategyAccess() {return this.currentPricingStrategy;}}

请参阅 下载 获得这个技术的实际效果。

服务定位策略

重复插入的一个替代就是用更常规的技术,用服务定位策略技术实现插入客 户。例如:

public interface PricingStrategyLocaTor {PricingStrategy getCurrentPricingStrategy();}

虽然代价是定义一个额外接口,还会使客户代码更长一些,但是 这项技术对于代码清晰性来说具有优势。

结束语

在这篇文章中,我把依赖项插入看作对象和对象执行的环境之间的合约。对 象不愿意外出寻找自己需要的资源、要协作的合作伙伴或者使用的服务。相反, 对象提供了一种机制,允许把这些依赖项提供给它。然后,在对象需要依赖项之 前,执行环境负责把对象需要的所有依赖项提供给它。

我讨论了依赖项插入解决方案的四个关键职责,这些是代理代表对象获取依 赖项时必须解决的问题。最后,我介绍了满足这些需求的许多不同的技术。显然 ,如果能够 用 Spring 容器初始化并配置对象,那么就应当这么做。对于在 Spring 容器的控制之外创建的对象,例如一些使用非单体实例化模型的域对象 或方面,我推荐使用 @SpringConfigured 标注或类似的东西。这项技术让您可 以把全部配置信息完全转移到外部的 Spring 配置文件中。

在编写这篇文章的示例时,我采用了 AspectJ 5 的最新里程碑版(到 2005 年 10 月)和 Spring 1.2.4。请 下载 完整的工作示例,开始体验我讨论的想 法。testsrc 目录下的测试用例是良好的起点。

代码下载:http://www.ibm.com/developerworks/cn/java/j- aopwork13.html

我们可以沿途用镜头记录彼此的笑脸,和属于我们的风景。

AOP@Work:用AspectJ和Spring进行依赖项插入

相关文章:

你感兴趣的文章:

标签云: