内容


AOP@Work

用 AspectJ 和 Spring 进行依赖项插入

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

系列内容:

此内容是该系列 # 部分中的第 # 部分: AOP@Work

敬请期待该系列的后续内容。

此内容是该系列的一部分:AOP@Work

敬请期待该系列的后续内容。

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

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

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

什么是依赖项插入?

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

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

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

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

从协作到合约

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

解析依赖项的方法在不同的场景中各有不同。例如,在单元测试用例中,对象的执行环境是测试用例本身,所以测试设置代码有责任直接满足依赖项。在集成测试或应用程序在生产环境时,代理 负责寻找满足对象依赖项的资源,并把它们传递给对象。代理的角色通常是由轻量级容器扮演的,例如 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 配置
  <beans>      
      <bean name="RemoteExceptionHandlingAspect"
        class="org.aspectprogrammer.dw.RemoteExceptionHandling"
        factory-method="aspectOf">
        <property name="exceptionHandler">
          <ref bean="RemoteExceptionHandler"/>
        </property>
      </bean>
      
      <bean name="RemoteExceptionHandler" 
        class="org.aspectprogrammer.dw.DefaultRemoteExceptionHandler">
      </bean>
  </beans>

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

域对象的依赖项插入

配置单体方面就像在 Spring 容器中配置其他 bean 一样简单,但是对于拥有其他生命周期的方面来说,该怎么办呢?例如 perthispertarget 甚至 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. 服务插入器配置
    <beans>  
      <bean name="AccountOperationValidationServiceInjector"
        class="org.aspectprogrammer.dw.
                  AccountOperationValidationServiceInjector"
        factory-method="aspectOf">
        <property name="service">
        <ref bean="AccountOperationValidationService"/>
          </property>
      </bean>
      
      <bean name="AccountOperationValidationService" 
        class="org.aspectprogrammer.dw.
                 DefaultAccountOperationValidationService">
      </bean>
    </beans>

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

重复插入

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

这个场景的基本技术包括字段级插入或 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 目录下的测试用例是良好的起点。


下载资源


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=102393
ArticleTitle=AOP@Work: 用 AspectJ 和 Spring 进行依赖项插入
publish-date=01232006