领域对象依赖注入功能的新改进

技术 | Ramnivas Laddad | 2008年1月24日 | ...

Spring 的依赖注入(DI)机制允许配置在应用上下文中定义的 bean。如果你想将相同的想法扩展到非 bean 对象呢?Spring 对领域对象 DI 的支持利用 AspectJ 织入技术将 DI 扩展到任何对象,即使它是由 Web 或 ORM 框架创建的。这使得创建具有丰富领域行为的对象成为可能,因为领域对象现在可以与注入的对象协作。在这篇博客中,我将讨论 Spring Framework 在这一领域的最新改进。

领域对象 DI 背后的核心思想非常简单:通过 AspectJ 织入的切面选择与符合特定规范的任何对象的创建反序列化对应的连接点。这些连接点的通知将依赖项注入到正在创建或反序列化的对象中。当然,细节决定成败。例如,如何选择与反序列化对应的连接点?或者如何确保每个对象只注入一次依赖项?通过提供一些预先编写好的切面,Spring 使开发人员免受这些细节的影响。

目前,大多数 Spring 用户使用 @Configurable 注解来指定可配置的类。随着即将发布的 Spring 2.5.2 中的最新改进(自每夜构建 379 开始可用),你有了更多选项,使此功能更加强大。新改进遵循“让简单的事情更简单,让复杂的事情成为可能”的原则。根据你对 AspectJ 的熟悉程度和预期的设计复杂性,其中一种选项会非常适用。图 1 显示了新的切面层次结构,它使得简单性与灵活性的结合成为可能。

Domain Object Dependency Injection Aspects

图 1:领域对象依赖注入切面的继承层次结构。

那么,这些切面分别提供什么?我们从下往上看。

让简单的事情更简单:AnnotationBeanConfigurerAspect

AnnotationBeanConfigurerAspect 使领域对象 DI 无需任何用户 AspectJ 代码。因此,对于许多开发人员来说,它是最简单的选择。使用此切面,你可以使用 @Configurable 注解标注需要依赖注入的类。例如,你可以将 Order 类标注如下
 
@Configurable
public class Order {
    private transient MailSender mailSender;
    
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

接下来,你指示 Spring 如何配置 Order 类型的对象。配置说明遵循原型 bean 的标准 bean 定义,如下所示


<context:spring-configured/>
    
<bean class="example.Order" scope="prototype">
    <property name="mailSender" ref="externalMailSender"/>
</bean>
    
<bean id="externalMailSender" ...>
    ...
</bean>

现在,对于任何 Order 对象的创建或反序列化,Spring 都将使用 externalMailSender bean 设置创建对象的 mailSender 属性。

Spring 2.5 引入了一种新的基于注解的配置选项,可以消除或减少伴随的 XML 配置。基于 @Configurable 注解的 DI 也因此受益。例如,你可以如下所示将 mailSender 属性标记为 @Autowired

 
@Configurable
public class Order {
    private transient MailSender mailSender;
    
    @Autowired
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

你甚至可以通过直接标注字段本身来去掉 setter 方法,将上面的代码简化为

 
@Configurable
public class Order {
    @Autowired private transient MailSender mailSender;
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

无论哪种情况,伴随的 XML 配置都可以简化为如下所示(注意使用了 <context:annotation-config/>)


<context:spring-configured/>
    
<context:annotation-config/>
    
<bean id="externalMailSender" ...>
    ...
</bean>

有关此领域对象 DI 选项的更多详细信息,请参阅使用 AspectJ 为 Spring 领域对象注入依赖项

让复杂的事情成为可能:AbstractInterfaceDrivenDependencyInjectionAspect

AnnotationBeanConfigurerAspect 的基础切面 AbstractInterfaceDrivenDependencyInjectionAspect 使用接口而不是注解来标记可配置的类。虽然这看起来是一个相当表面的变化,但它提供了一些有趣的选项,例如使用领域接口和注解指定依赖注入、通过绕过反射提高注入性能以及利用多个切面配置一个对象。

在设计层面,此切面配置任何类型实现了 ConfigurableObject 接口的领域对象。虽然让类型直接实现 ConfigurableObject 接口无疑是有效的选择,但一个更优雅的替代方案是在另一个切面(AbstractInterfaceDrivenDependencyInjectionAspect 的子切面将是合乎逻辑的选择)中使用 declare parents 语句。该语句将声明一个可配置的类实现了 ConfigurableObject 接口。这使得你的领域类不受 Spring 特定构件的影响,同时又能从 DI 机制中受益。让我们来看一个这种用法的例子。

考虑前面章节中的 Order 类。与其使用 @Configurable,不如让它实现一个领域特定的 MailSenderClient 接口,这表明它使用了 MailSender

 
public class Order implements MailSenderClient {
    private transient MailSender mailSender;
            
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
            
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

接下来,你编写 AbstractInterfaceDrivenDependencyInjectionAspect 的一个子切面,将依赖项注入到任何 MailSenderClient 对象中。

 
public aspect MailClientDependencyInjectionAspect extends 
    AbstractInterfaceDrivenDependencyInjectionAspect {
    private MailSender mailSender;
    
    declare parents: MailSenderClient implements ConfigurableObject;
            
    public pointcut inConfigurableBean() : within(MailSenderClient+);
    
    public void configureBean(Object bean) {
        ((MailSenderClient)bean).setMailSender(this.mailSender);
    }
            
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
}

切面中使用了两个 AspectJ 构造

  1. declare parents 语句使得 MailSenderClient 实现了 ConfigurableObject 接口,使其有资格通过 AbstractInterfaceDrivenDependencyInjectionAspect 进行 DI。
  2. The inConfigurableBean() 仅选择 MailSenderClient 的子类型中的连接点,从而将切面的适用范围限制在仅匹配的类型上。

The configureBean() 方法通过直接调用适当的 setter 方法来执行向 bean 的注入。当然,任何其他适合 bean 配置的逻辑,例如调用多参数方法或调用任何初始化方法,也都完全可行。请注意,以这种方式使用的直接调用避免了反射,并且如果领域对象创建速率很高,可以带来显著的性能提升。

你需要配置 MailClientDependencyInjectionAspect 切面实例本身,以注入其依赖项——mailSender 属性。Spring 的做法是为该切面创建一个 bean 并在应用上下文中配置它


<bean class="example.MailClientDependencyInjectionAspect" 
        factory-method="aspectOf">
    <property name="mailSender" ref="externalMailSender"/>
</bean>
    
<bean id="externalMailSender" ...>
    ...
</bean>

围绕这个切面还有一些额外的模式

  • 使用多个切面配置一个对象(例如,每个“客户端”接口一个切面)。
  • 使用领域注解而不是领域接口或 @Configurable 注解来指定可配置的类型。
  • 使用基于 hasmethod() 的类型模式(目前是 AspectJ 5 中的实验性功能,将成为 AspectJ 6 中的正式功能),以避免使用与 DI 相关的类型或注解。
  • 使用基于 AspectJ 的 mixins 为客户端接口提供默认实现,并避免重复的 setter 方法。

然而,这些想法就留待另一篇博客文章来讨论吧。

当你需要时提供灵活性:AbstractDependencyInjectionAspect

最后,这是最灵活的基础切面。此切面要求你对 AspectJ 切入点语言有扎实的理解。然而,除非在极端定制场景(例如自定义反序列化事件)中,否则你不会直接创建此基础切面的子切面。相反,你将使用我们之前讨论过的子切面之一。

该切面声明了子切面可以定义的六个切入点

  1. beanConstruction(Object bean):选择 bean 的构造。典型实现将选择对象初始化连接点。
  2. beanDeserialization(Object bean):选择 bean 的反序列化。典型实现将选择注入对象中必须存在的 readResolve() 方法。如果你使用了非标准的反序列化(不调用 readResolve()),你可以使用此切入点选择适当的替代方法。
  3. inConfigurableBean():选择定义切面可配置的 bean 中的连接点。典型实现将使用带有适当类型模式的 within() 切入点。
  4. preConstructionConfiguration():选择需要在构造之前注入依赖项的 bean 的连接点。此切入点的默认实现不选择任何连接点(bean 将在构造函数运行后注入依赖项)。
  5. mostSpecificSubTypeConstruction():选择与最具体子类型对应的连接点。默认实现使用连接点签名来确定构造函数是否代表被注入 bean 类型层次结构中最具体的构造函数。然后将此信息与 preConstructionConfiguration() 切入点结合使用,以利用 before 或 after 通知来注入依赖项。
  6. leastSpecificSuperTypeConstruction():选择与最不具体的超类型对应的连接点。

此切面还定义了一个抽象方法 configureBean(Object bean),其实现应指定与依赖注入对应的逻辑。

所以,你现在拥有了在应用中启用领域对象 DI 的所有选项。如果你正在进行 DDD 或需要将 DI 扩展到你的领域对象,你应该关注这组新的切面。根据你的具体需求和 AspectJ 知识,你会发现其中一种有助于创建一个优雅的解决方案。

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

保持领先

VMware 提供培训和认证,助你加速发展。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅即可为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部