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

工程 | Ramnivas Laddad | 2008年1月24日 | ...

Spring 的依赖注入 (DI) 机制允许配置在应用程序上下文中定义的 Bean。如果您想将相同的理念扩展到非 Bean,该怎么办?Spring 对领域对象 DI 的支持利用 AspectJ 编织将 DI 扩展到任何对象,即使它是通过 Web 或 ORM 框架创建的。这使得能够创建行为丰富的领域对象,因为领域对象现在可以与注入的对象协作。在本篇博文中,我将讨论 Spring 框架在该领域的最新改进。

领域对象 DI 背后的核心思想非常简单:一个 AspectJ 编织的方面选择与匹配特定规范的任何对象的创建反序列化相对应的连接点。对这些连接点的建议将依赖项注入到正在创建或反序列化的对象中。当然,魔鬼在于细节。例如,您如何选择与反序列化相对应的连接点,或者您如何仅对每个对象注入一次依赖项?通过提供一些预先编写的方面,Spring 使开发人员免于所有这些细节。

目前,大多数 Spring 用户使用@Configurable注释来指定可配置的类。随着即将推出的 Spring 2.5.2 的最新改进,从 nightly build 379 开始可用,您有更多选项,使此功能更加强大。新的改进遵循“使简单的事情简单,复杂的事情成为可能”的原则。根据您对 AspectJ 的熟悉程度和预期的设计复杂性,其中一个选项将非常有用。图 1 显示了新的方面层次结构,它使简单性和灵活性兼得。

Domain Object Dependency Injection Aspects

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

那么每个方面都提供了什么?让我们自下而上地来看。

简单的事情简单:AnnotationBeanConfigurerAspect

AnnotationBeanConfigurerAspect 能够在没有任何用户 AspectJ 代码的情况下启用领域对象 DI。因此,对于许多开发人员来说,它是最简单的选择。使用此方面,您可以使用@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接口当然是一个有效的选择,但一种优雅的替代方案是在另一个方面使用declare parents语句(AbstractInterfaceDrivenDependencyInjectionAspect 的子方面将是一个逻辑选择)。该语句将声明一个可配置的类作为实现ConfigurableObject接口。这使您的领域类免受 Spring 特定工件的影响,同时受益于 DI 机制。让我们看看这种用法的示例。

考虑前面部分中的Order类。您可以让它实现一个特定于领域的MailSenderClient接口,而不是使用@Configurable,这表示它使用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. inConfigurableBean()仅在MailSenderClient 的子类型中选择连接点,从而将方面的适用性限制在仅匹配的类型。

configureBean()方法通过直接调用相应的 setter 来执行注入。当然,任何其他适合 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 的 mixin 为客户端接口提供默认实现并避免重复的 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 社区中所有即将举行的活动。

查看全部