Spring5-高级教程-二-

75 阅读50分钟

Spring5 高级教程(二)

原文:Pro Spring 5

协议:CC BY-NC-SA 4.0

四、Spring 详细配置和 Spring Boot

在前一章中,我们详细介绍了控制反转(IoC)的概念以及它是如何融入 Spring 框架的。然而,我们实际上只是触及了 Spring Core 的皮毛。Spring 提供了大量的服务来补充和扩展其基本的 IoC 功能。在这一章中,你将详细探讨这些。具体来说,您将看到以下内容:

  • 管理 bean 生命周期:到目前为止,您看到的所有 bean 都相当简单,并且与 Spring 容器完全解耦。在这一章中,我们将介绍一些策略,您可以使用这些策略来使您的 beans 在其生命周期的不同阶段接收来自 Spring 容器的通知。你可以通过实现 Spring 设计的特定接口,指定 Spring 可以通过反射调用的方法,或者使用 JSR-250 JavaBeans 生命周期注释来实现。
  • 让您的 bean“感知 Spring”:在某些情况下,您希望 bean 能够与配置它的ApplicationContext实例进行交互。出于这个原因,Spring 提供了两个接口,BeanNameAwareApplicationContextAware(在第三章的最后介绍),分别允许 bean 获得它的指定名称和引用它的ApplicationContext。本章的这一节介绍了这些接口的实现,并给出了在应用中使用它们的一些实际注意事项。
  • 使用 FactoryBeans:顾名思义,FactoryBean接口意味着可以由充当其他 bean 工厂的任何 bean 来实现。FactoryBean接口提供了一种机制,通过这种机制,您可以轻松地将自己的工厂与 Spring BeanFactory接口集成在一起。
  • 使用 JavaBean s property editors:PropertyEditor接口是在java.beans包中提供的标准接口。PropertyEditor用于在属性值和String表示之间进行转换。Spring 广泛使用PropertyEditor s,主要是读取BeanFactory配置中指定的值,并将它们转换成正确的类型。在本章中,我们将讨论 Spring 提供的一组PropertyEditor以及如何在应用中使用它们。我们还看一下如何实现定制的PropertyEditor
  • 了解关于 Spring ApplicationContext 的更多信息:正如我们所知,ApplicationContextBeanFactory的扩展,旨在用于完整的应用。ApplicationContext接口提供了一组有用的附加功能,包括国际化消息支持、资源加载和事件发布。在这一章中,我们将详细介绍除 IoC 之外ApplicationContext提供的特性。我们还提前一点向您展示了ApplicationContext如何在您构建 web 应用时简化 Spring 的使用。
  • 使用 Java 类进行配置:在 3.0 之前,Spring 只支持带有 beans 注释的 XML 基本配置和依赖配置。从 3.0 开始,Spring 为开发人员提供了另一种选择,使用 Java 类配置 Spring ApplicationContext接口。我们来看看 Spring 应用配置中的这个新选项。
  • 使用 Spring Boot:通过使用 Spring Boot,Spring 应用配置变得更加实用。这个 Spring 项目使得创建独立的、生产级的、基于 Spring 的应用变得容易,您可以“直接运行”
  • 使用配置增强:我们展示了使应用配置更容易的特性,比如概要文件管理、环境和属性源抽象等等。在本节中,我们将介绍这些特性,并展示如何使用它们来满足特定的配置需求。
  • 使用 Groovy 进行配置:Spring 4.0 的新增功能是用 Groovy 语言配置 bean 定义,这可以作为现有 XML 和 Java 配置方法的替代或补充。

Spring 对应用可移植性的影响

本章中讨论的大多数特性都是 Spring 特有的,在许多情况下,其他 IoC 容器中没有这些特性。尽管许多 IoC 容器提供了生命周期管理功能,但它们可能是通过一组不同于 Spring 的接口来实现的。如果应用在不同 IoC 容器之间的可移植性非常重要,那么您可能希望避免使用一些将应用耦合到 Spring 的特性。

但是,请记住,通过设置约束——意味着您的应用可以在 IoC 容器之间移植——您就失去了 Spring 提供的丰富功能。因为您可能会做出使用 Spring 的战略选择,所以尽最大能力使用它是有意义的。

注意不要凭空制造出可移植性的需求。在许多情况下,应用的最终用户并不关心应用是否可以在三个不同的 IoC 容器上运行;他们只是想让它跑起来。根据我们的经验,试图在您选择的技术中可用的最小公分母特性上构建应用通常是一个错误。这样做通常会使您的应用从一开始就处于不利地位。但是,如果您的应用需要 IoC 容器的可移植性,不要把这看作是一个缺点——这是一个真实的需求,因此,您的应用应该满足这个需求。在《专家一对一:没有 EJB 的 J2EE 开发》( Wrox,2004)中,Rod Johnson 和 jürgen h ller 将这些类型的需求描述为幻影需求,并对它们以及它们如何影响您的项目进行了更详细的讨论。

尽管使用这些特性可能会将您的应用耦合到 Spring 框架,但实际上您是在更大范围内增加应用的可移植性。假设您使用的是一个免费的开源框架,没有特定的供应商关系。使用 Spring 的 IoC 容器构建的应用可以在 Java 运行的任何地方运行。对于 Java 企业应用,Spring 为可移植性开辟了新的可能性。Spring 提供了许多与 JEE 相同的功能,还提供了抽象和简化 JEE 其他方面的类。在许多情况下,可以使用 Spring 构建一个 web 应用,该应用运行在一个简单的 servlet 容器中,但是具有与面向成熟的 JEE 应用服务器的应用相同的复杂程度。通过耦合到 Spring,您可以用 Spring 中的等效特性替换许多特定于供应商或依赖于特定于供应商的配置的特性,从而提高应用的可移植性。

Bean 生命周期管理

任何 IoC 容器(包括 Spring)的一个重要部分是,beans 可以以这样一种方式构造,即它们可以在生命周期的某些点接收通知。这使您的 beans 能够在其生命周期的某些点上执行相关的处理。通常,有两个生命周期事件与 bean 特别相关:初始化后和销毁前。

在 Spring 的上下文中,一旦 Spring 完成了对 bean 的所有属性值的设置,并完成了您配置它执行的任何依赖检查,就会引发后初始化事件。销毁前事件在 Spring 销毁 bean 实例之前触发。然而,对于具有原型范围的 beans,预销毁事件不会被 Spring 触发。Spring 的设计是,初始化生命周期回调方法将在对象上被调用,而不管 bean 的作用域,而对于具有原型作用域的 bean,销毁生命周期回调方法将不会被调用。Spring 提供了三种机制,bean 可以使用这三种机制来挂钩这些事件并执行一些额外的处理:基于接口的、基于方法的和基于注释的机制。

使用基于接口的机制,您的 bean 实现了一个特定于它想要接收的通知类型的接口,Spring 通过接口中定义的回调方法通知 bean。对于基于方法的机制,Spring 允许您在ApplicationContext配置中指定 bean 初始化时要调用的方法的名称,以及 bean 销毁时要调用的方法的名称。对于注释机制,您可以使用 JSR-250 注释来指定 Spring 在构造之后或销毁之前应该调用的方法。

在这两种情况下,机制实现了完全相同的目标。接口机制在 Spring 中被广泛使用,因此每次使用 Spring 的一个组件时,您不必记得指定初始化或销毁。但是,在您自己的 bean 中,使用基于方法的机制或注释机制可能会更好,因为您的 bean 不需要实现任何特定于 Spring 的接口。尽管我们说过,可移植性并不像许多书让你相信的那样重要,但这并不意味着当存在一个非常好的替代方案时,你应该牺牲可移植性。也就是说,如果您以其他方式将您的应用耦合到 Spring,使用接口方法允许您指定一次回调,然后忘记它。如果您正在定义许多需要利用生命周期通知的相同类型的 bean,那么使用接口机制可以避免为 XML 配置文件中的每个 bean 指定生命周期回调方法的需要。使用 JSR-250 注释也是另一个可行的选择,因为它是由 JCP 定义的标准,并且您也没有耦合到 Spring 的特定注释。只需确保运行应用的 IoC 容器支持 JSR-250 标准。

总的来说,选择哪种机制来接收生命周期通知取决于您的应用需求。如果您关心可移植性,或者您只是定义一个或两个需要回调的特定类型的 beans,请使用基于方法的机制。如果您使用注释类型的配置,并且确定您使用的是支持 JSR-250 的 IoC 容器,请使用注释机制。如果您不太关心可移植性,或者您正在定义许多需要生命周期通知的相同类型的 bean,那么使用基于接口的机制是确保您的 bean 总是收到它们所期望的通知的最佳方式。如果您计划在许多不同的 Spring 项目中使用一个 bean,您几乎肯定希望该 bean 的功能尽可能独立,因此您肯定应该使用基于接口的机制。

图 4-1 显示了 Spring 如何管理其容器中 beans 的生命周期的高级概述。

A315511_5_En_4_Fig1_HTML.jpg

图 4-1。

Spring beans life cycle

挂钩到 Bean 创建

通过知道何时被初始化,bean 可以检查它是否满足了所有需要的依赖关系。尽管 Spring 可以为您检查依赖关系,但这几乎是一种要么全有要么全无的方法,它没有提供任何机会将额外的逻辑应用于依赖关系解析过程。考虑一个 bean,它有四个被声明为 setters 的依赖项,其中两个是必需的,另一个在没有提供依赖项的情况下有合适的默认值。使用初始化回调,您的 bean 可以检查它需要的依赖项,根据需要抛出异常或提供默认值。

bean 不能在其构造函数中执行这些检查,因为此时,Spring 还没有机会为它可以满足的依赖项提供值。Spring 中的初始化回调是在 Spring 完成提供它所能提供的依赖项并执行您要求的任何依赖项检查之后调用的。

您不仅限于使用初始化回调来检查依赖关系;您可以在回调中做任何您想做的事情,但是它对于我们描述的目的是最有用的。在许多情况下,初始化回调也是触发 bean 响应其配置时必须自动采取的任何操作的地方。例如,如果您构建一个 bean 来运行调度任务,初始化回调提供了启动调度程序的理想位置——毕竟,配置数据是在 bean 上设置的。

您将不必编写 bean 来运行调度任务,因为这是 Spring 可以通过其内置的调度特性或通过与 Quartz 调度程序的集成自动完成的。我们将在第十一章中对此进行更详细的介绍。

创建 Bean 时执行方法

正如我们前面提到的,接收初始化回调的一种方法是在 bean 上指定一个方法作为初始化方法,并告诉 Spring 使用这个方法作为初始化方法。如前所述,当您只有几个相同类型的 beans 时,或者当您想让您的应用与 Spring 分离时,这种回调机制非常有用。使用这种机制的另一个原因是使您的 Spring 应用能够与以前构建的或由第三方供应商提供的 beans 一起工作。指定回调方法就是在 bean 的<bean>标签的init-method属性中指定名称。以下代码示例显示了一个具有两个依赖项的基本 bean:

package com.apress.prospring5.ch4;

import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class Singer {
    private static final  String DEFAULT_NAME  =  "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void init() {
        System.out.println("Initializing bean");

       if (name == null) {
            System.out.println("Using default name");
            name = DEFAULT_NAME;
       }

       if (age == Integer.MIN_VALUE) {
           throw new IllegalArgumentException(
            "You must set the age property of any beans of type " +  Singer.class);
       }
    }

    public String toString()  {
        return "\tName: " + name + "\n\tAge: " + age;
    }

    public static void main(String... args) {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        getBean("singerOne", ctx);
        getBean("singerTwo", ctx);
        getBean("singerThree", ctx);

        ctx.close();
    }

    public static Singer getBean(String beanName,
         ApplicationContext ctx)  {
        try {
            Singer bean = (Singer) ctx.getBean(beanName);
            System.out.println(bean);
            return  bean;
        } catch (BeanCreationException ex) {
            System.out.println("An error occured in bean configuration: "
                    +  ex.getMessage());
            return null;
        }
    }
}

注意,我们已经定义了一个方法init(),作为初始化回调。init()方法检查 name 属性是否已经设置,如果没有,它使用保存在DEFAULT_NAME常量中的默认值。The init()方法还检查是否设置了age属性,如果没有,抛出IllegalArgumentException

SimpleBean类的main()方法试图使用自己的getBean()方法从GenericXmlApplicationContext获得三个Singer类型的 beans。注意,在getBean()方法中,如果成功获得了 bean,其详细信息将被写入控制台输出。如果在init()方法中抛出异常(如果没有设置age属性,就会出现这种情况),那么 Spring 会在BeanCreationException. The getBean()方法中包装该异常,捕捉这些异常,并向控制台输出写入一条消息,通知我们该错误,并返回一个null值。

下面的配置片段显示了一个ApplicationContext配置,它定义了在前面的代码片段(app-context-xml.xml)中使用的 beans:

<?xml version="1.0" encoding="UTF-8"?>

<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-lazy-init="true">

    <bean id="singerOne"
        class="com.apress.prospring5.ch4.Singer"
        init-method="init" p:name="John Mayer"  p:age="39"/>

    <bean id="singerTwo"
        class="com.apress.prospring5.ch4.Singer"
        init-method="init" p:age="72"/>

    <bean id="singerThree"
        class="com.apress.prospring5.ch4.Singer"
        init-method="init" p:name="John Butler"/>
</beans>

正如您所看到的,三个 bean 中每一个的<bean>标签都有一个init-method属性,它告诉 Spring 一旦完成 bean 的配置就应该调用init()方法。singerOne bean 的nameage属性都有值,所以它通过init()方法时完全没有变化。singerTwo bean 的name属性没有值,这意味着在init()方法中,name属性被赋予默认值。最后,singerThree bean 没有age属性的值。在init()方法中定义的逻辑将此视为错误,因此抛出IllegalArgumentException。还要注意,在<beans>标签中,我们添加了属性default-lazy-init="true"来指示 Spring 仅在应用请求 bean 时实例化配置文件中定义的 bean。如果我们不指定它,Spring 会在ApplicationContext的引导过程中尝试初始化所有的 beans,在singerThree的初始化过程中会失败。

当配置文件中的所有 beans 都具有相同的init-method配置时,可以通过在<beans>元素上设置default-init-method属性来简化文件。豆子可以是不同的类型;它们的唯一条件是拥有一个名为default-init-method属性值的方法。因此,前面的配置也可以写成这样:

<beans ...
    default-lazy-init="true" default-init-method="init">

    <bean id="singerOne"
        class="com.apress.prospring5.ch4.Singer"
          p:name="John Mayer" p:age="39"/>

    <bean id="singerTwo"
        class="com.apress.prospring5.ch4.Singer"
         p:age="72"/>

    <bean id="singerThree"
        class="com.apress.prospring5.ch4.Singer"
         p:name="John Butler"/>
</beans>

运行前面的示例会产生以下输出:

Initializing bean
        Name: John Mayer
        Age: 39
Initializing bean
Using default name
        Name: Eric Clapton
        Age: 72
Initializing bean
An error occured in bean configuration: Error creating bean
with name 'singerThree' defined in class path
resource spring/app-context-xml.xml: Invocation of init method failed;
nested exception is java.lang.IllegalArgumentException:
You must set the age property of any beans of type class
 com.apress.prospring5.ch4.Singer

从这个输出中,您可以看到singerOne已经用我们在配置文件中指定的值进行了正确配置。对于singerTwo,使用了name属性的默认值,因为在配置中没有指定任何值。最后,对于singerThree,没有创建 bean 实例,因为init()方法由于缺少age属性的值而引发了一个错误。

如您所见,使用初始化方法是确保正确配置 beans 的理想方法。通过使用这种机制,您可以充分利用 IoC 的优势,而不会失去手动定义依赖关系所获得的任何控制权。对初始化方法的唯一约束是它不能接受任何参数。您可以定义任何返回类型,尽管 Spring 会忽略它,您甚至可以使用静态方法,但是该方法必须不接受任何参数。

当使用静态初始化方法时,这种机制的好处被否定了,因为您不能访问 bean 的任何状态来验证它。如果您的 bean 使用静态状态作为节省内存的机制,并且您使用静态初始化方法来验证该状态,那么您应该考虑将静态状态转移到实例状态,并使用非静态初始化方法。如果您使用 Spring 的单例管理功能,最终效果是相同的,但是您有了一个测试更简单的 bean,并且您还可以在必要时创建具有自己状态的 bean 的多个实例。当然,在某些情况下,您需要使用跨 bean 的多个实例共享的静态状态,在这种情况下,您总是可以使用静态初始化方法。

实现 InitializingBean 接口

Spring 中定义的InitializingBean接口允许您在 bean 代码中定义希望 bean 接收 Spring 已完成配置的通知。与使用初始化方法时一样,这使您有机会检查 bean 配置以确保它是有效的,同时提供任何默认值。InitializingBean接口定义了一个方法afterPropertiesSet(),它与上一节中介绍的init()方法的作用相同。下面的代码片段显示了使用InitializingBean接口代替初始化方法重新实现前面的示例:

package com.apress.prospring5.ch4;

import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class SingerWithInterface implements InitializingBean {
    private static final String DEFAULT_NAME = "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void afterPropertiesSet() throws Exception {
        System.out.println("Initializing bean");

        if (name == null) {
            System.out.println("Using  default  name");
            name =  DEFAULT_NAME;
        }

        if (age == Integer.MIN_VALUE) {
            throw new IllegalArgumentException(
                    "You must set the age property of any beans of type "
                    +  SingerWithInterface.class);
        }
    }

    public String toString()  {
        return "\tName: " +  name +  "\n\tAge: " +  age;
    }

    public static void main(String... args) {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        getBean("singerOne", ctx);
        getBean("singerTwo", ctx);
        getBean("singerThree", ctx);

        ctx.close();

    }

    private static SingerWithInterface getBean(String beanName,
            ApplicationContext ctx) {
        try {
            SingerWithInterface bean =
                (SingerWithInterface) ctx.getBean(beanName);
            System.out.println(bean);
            return bean;
        } catch (BeanCreationException ex) {
            System.out.println("An error occured in bean configuration: "
                    + ex.getMessage());
            return null;
        }
    }
}

如您所见,本例中没有太多变化。除了明显的类名变化,唯一的区别是这个类实现了InitializingBean并且初始化逻辑已经移到了afterPropertiesSet()方法中。在下面的代码片段中,您可以看到这个示例(app-context- xml.xml)的配置:

<beans ... default-lazy-init="true">

    <bean id="singerOne"
          class="com.apress.prospring5.ch4.SingerWithInterface"
          p:name="John Mayer" p:age="39"/>

    <bean id="singerTwo"
          class="com.apress.prospring5.ch4.SingerWithInterface"
          p:age="72"/>

    <bean id="singerThree"
          class="com.apress.prospring5.ch4.SingerWithInterface"
          p:name="John Butler"/>
</beans>

同样,这里介绍的配置代码和上一节中的配置代码没有太大区别。值得注意的区别是省略了init-method属性。因为SimpleBeanWithInterface类实现了InitializingBean接口,Spring 知道调用哪个方法作为初始化回调,因此不需要任何额外的配置。此示例的输出如下所示:

Initializing bean
        Name: John Mayer
        Age: 39
Initializing bean
Using default  name
        Name: Eric Clapton
        Age: 72
Initializing bean
An error occured in bean configuration: Error creating bean with name 'singerThree'

 defined in  class path resource spring/app-context-xml.xml: Invocation of
 init method failed; nested exception is java.lang.IllegalArgumentException:
 You must set the age property of any beans of type class
 com.apress.prospring5.ch4.SingerWithInterface

使用 JSR-250 @PostConstruct 注释

另一个可以达到同样目的的方法是使用 JSR-250 生命周期注释。从 Spring 2.5 开始,还支持 JSR-250 注释来指定 Spring 应该调用的方法,如果类中存在与 bean 的生命周期相关的相应注释的话。以下代码示例显示了应用了@PostConstruct注释的程序:

package com.apress.prospring5.ch4;

import javax.annotation.PostConstruct;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class SingerWithJSR250 {
    private static final  String DEFAULT_NAME  =  "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name)  {
        this.name = name;
    }

    public void setAge(int  age) {
        this.age = age;
    }

    @PostConstruct
    public void init() throws Exception {
        System.out.println("Initializing bean");

       if (name  ==  null) {
            System.out.println("Using default name");
            name =  DEFAULT_NAME;
        }

        if (age == Integer.MIN_VALUE) {
            throw new IllegalArgumentException(
                    "You must set the age property of any beans of type " +
                    SingerWithJSR250.class);
        }
    }

    public String toString()  {
        return "\tName: " + name + "\n\tAge: " +  age;
    }

    public static void main(String... args) {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-annotation.xml");
        ctx.refresh();

        getBean("singerOne", ctx);
        getBean("singerTwo", ctx);
        getBean("singerThree", ctx);

        ctx.close();
    }

    public static SingerWithJSR250 getBean(String beanName,
         ApplicationContext ctx) {
        try {
            SingerWithJSR250 bean  =
                (SingerWithJSR250) ctx.getBean(beanName);
            System.out.println(bean);
            return  bean;
        } catch (BeanCreationException ex) {
            System.out.println("An error occured in bean configuration: "
                    + ex.getMessage());
            return null;
        }
    }
}

该程序与使用init-method方法相同;只需在init()方法之前应用@PostConstruct注释。请注意,您可以为该方法指定任何名称。在配置方面,由于我们使用了注释,我们需要将上下文名称空间中的<context:annotation-driven>标记添加到配置文件中。

<?xml version="1.0" encoding="UTF-8"?>

<beans 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"

       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context

          http://www.springframework.org/schema/context/spring-context.xsd"

       default-lazy-init="true">

    <context:annotation-config/>

    <bean id="singerOne"
        class="com.apress.prospring5.ch4.SingerWithJSR250"
        p:name="John Mayer"  p:age="39"/>

    <bean id="singerTwo"
        class="com.apress.prospring5.ch4.SingerWithJSR250"
        p:age="72"/>

    <bean id="singerThree"
        class="com.apress.prospring5.ch4.SingerWithJSR250"
            p:name="John Butler"/>
</beans>

运行该程序,您将看到与其他机制相同的输出。

Initializing bean
        Name: John Mayer
        Age: 39
Initializing bean
Using default name
        Name: Eric Clapton
        Age: 72
Initializing bean
An error occurred in bean configuration: Error creating bean with name 'singerThree':
 Invocation of init method failed; nested exception is
 java.lang.IllegalArgumentException: You must set the age property of any beans
 of type class com.apress.prospring5.ch4.SingerWithJSR250

这三种方法各有利弊。使用初始化方法的好处是保持应用与 Spring 的解耦,但是您必须记住为每个需要它的 bean 配置初始化方法。使用InitializingBean接口,您可以为 bean 类的所有实例指定一次初始化回调,但是您必须耦合您的应用才能做到这一点。使用注释,您需要将注释应用到方法中,并确保 IoC 容器支持 JSR-250。最后,您应该让应用的需求来决定使用哪种方法。如果可移植性是一个问题,使用初始化或注释方法;否则,使用InitializingBean接口来减少您的应用所需的配置数量,以及由于错误配置而导致的错误蔓延到您的应用的可能性。

A315511_5_En_4_Figa_HTML.jpginit-method@PostConstruct配置初始化时,用不同的访问权限声明初始化方法是有好处的。Spring IoC 应该只在 bean 创建时调用一次初始化方法。后续调用将导致意外结果甚至失败。通过初始化方法private可以禁止外部附加调用。Spring IoC 将能够通过反射调用它,但是不允许在代码中进行任何额外的调用。

使用@Bean 声明初始化方法

声明 bean 初始化方法的另一种方式是为@Bean注释指定initMethod属性,并将初始化方法名设置为其值。该注释用于在 Java 配置类中声明 beans。尽管 Java 配置将在本章的稍后部分讨论,但是 bean 初始化部分属于这里。对于这个例子,使用初始的Singer类,因为配置是外部的,就像使用init-method属性一样。我们将只写一个配置类和一个新的main()方法来测试它。另外,default-lazy-init="true"将被每个 bean 声明上的@Lazy注释所取代。

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.Singer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.GenericApplicationContext;

import static com.apress.prospring5.ch4.Singer.getBean;

public class SingerConfigDemo {

        @Configuration
        static class SingerConfig{

                @Lazy
                @Bean(initMethod = "init")

                Singer singerOne() {
                        Singer singerOne = new Singer();
                        singerOne.setName("John Mayer");
                        singerOne.setAge(39);
                        return  singerOne;
                }

                @Lazy
                @Bean(initMethod =  "init")

                Singer singerTwo() {
                        Singer singerTwo = new Singer();
                        singerTwo.setAge(72);
                        return singerTwo;
                }

                @Lazy
                @Bean(initMethod = "init")

                Singer singerThree()  {
                        Singer singerThree = new Singer();
                        singerThree.setName("John Butler");
                        return singerThree;
                }
        }

        public static void main(String args) {
                GenericApplicationContext ctx  =
                    new AnnotationConfigApplicationContext(SingerConfig.class);

                getBean("singerOne", ctx);
                getBean("singerTwo", ctx);
                getBean("singerThree", ctx);

                ctx.close();
        }

}

运行这段代码将会产生到目前为止观察到的相同结果,如下所示:

Initializing bean
        Name: John Mayer
        Age: 39
Initializing bean
Using default name
        Name: Eric Clapton
        Age: 72
Initializing bean
An error occurred in bean configuration: Error creating bean with name 'singerThree'

 defined in com.apress.prospring5.ch4.config.SingerConfigDemo$SingerConfig:
 Invocation of init method failed; nested exception is
 java.lang.IllegalArgumentException: You must set the age  property of any beans
  of type class com.apress.prospring5.ch4.Singer

了解解决方案的顺序

所有初始化机制都可以在同一个 bean 实例上使用。在这种情况下,Spring 首先调用用@PostConstruct注释的方法,然后调用afterPropertiesSet(),接着调用配置文件中指定的初始化方法。这种顺序是有技术原因的,通过遵循图 4-1 中的路径,我们可以注意到 bean 创建过程中的以下步骤:

  1. 首先调用构造函数来创建 bean。
  2. 依赖项被注入(调用 setters)。
  3. 既然 bean 已经存在并且提供了依赖关系,那么就要咨询预初始化的BeanPostProcessor基础设施 bean,看它们是否想从这个 bean 中调用任何东西。这些是特定于 Spring 的基础设施 bean,它们在创建后执行 bean 修改。@PostConstruct注释是由CommonAnnotationBeanPostProcessor注册的,所以这个 bean 将调用用@PostConstruct注释的方法。这个方法在 bean 构造完成之后,类投入使用之前执行,在 bean 实际初始化之前(在afterPropertiesSetinit-method之前)执行 1
  4. InitializingBeanafterPropertiesSet在依赖项注入后立即执行。在设置了所有提供的 bean 属性并满足了BeanFactoryAwareApplicationContextAware之后,afterPropertiesSet()方法被BeanFactory调用。
  5. 最后执行init-method属性,因为这是 bean 的实际初始化方法。

如果您有一个以特定方法执行一些初始化的现有 bean,但是您需要在使用 Spring 时添加更多的初始化代码,那么理解不同类型的 bean 初始化的顺序会很有用。

钩住豆破坏

当使用一个包装了DefaultListableBeanFactory接口的ApplicationContext实现时(比如通过getDefaultListableBeanFactory()方法的GenericXmlApplicationContext,你可以通过调用ConfigurableBeanFactory.destroySingletons()BeanFactory发出信号,告诉他你想要销毁所有的单例实例。通常,您在应用关闭时这样做,它允许您清理 beans 可能保持打开的任何资源,从而允许您的应用正常关闭。这个回调还提供了一个完美的地方,可以将内存中存储的任何数据刷新到持久存储中,并允许 beans 结束它们可能已经启动的任何长时间运行的进程。

为了让您的 bean 接收到已经调用了destroySingletons()的通知,您有三种选择,都类似于接收初始化回调的可用机制。销毁回调通常与初始化回调一起使用。在许多情况下,在初始化回调中创建和配置资源,然后在销毁回调中释放资源。

销毁 Bean 时执行方法

要指定在 bean 被销毁时调用的方法,只需在 bean 的<bean>标记的destroy-method属性中指定方法的名称。Spring 在销毁 bean 的单例实例之前调用它(Spring 不会为那些具有 prototype 作用域的 bean 调用这个方法)。下面的代码片段提供了一个使用destroy-method回调的例子:

package com.apress.prospring5.ch4;

import java.io.File;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.GenericXmlApplicationContext;

public class DestructiveBean implements InitializingBean {
    private File file;
    private String filePath;

    public void afterPropertiesSet() throws Exception {
        System.out.println("Initializing  Bean");

        if (filePath == null) {
            throw new IllegalArgumentException(
                    "You must specify the filePath property of"
                        + DestructiveBean.class);
        }

        this.file = new File(filePath);
        this.file.createNewFile();

        System.out.println("File exists: " +  file.exists());
    }

    public void destroy() {
        System.out.println("Destroying  Bean");

        if(!file.delete()) {
            System.err.println("ERROR: failed  to delete file.");
        }

        System.out.println("File exists: " + file.exists());
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public static void main(String... args) throws Exception {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        DestructiveBean bean = (DestructiveBean) ctx.getBean("destructiveBean");

        System.out.println("Calling destroy()");
        ctx.destroy();

        System.out.println("Called destroy()");
    }
}

这段代码定义了一个destroy()方法,在这个方法中,创建的文件被删除。main()方法从GenericXmlApplicationContext中检索一个DestructiveBean类型的 bean,然后调用它的destroy()方法(它将依次调用被ApplicationContext包装的ConfigurableBeanFactory.destroySingletons(),指示 Spring 销毁它管理的所有单例。初始化和销毁回调都向控制台输出写入一条消息,通知我们它们已被调用。在下面的代码片段中,您可以看到destructiveBean bean ( app-context-xml.xml)的配置:

<?xml version="1.0" encoding="UTF-8"?>

<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="destructiveBean"
        class="com.apress.prospring5.ch4.DestructiveBean"
        destroy-method="destroy"

        p:filePath=
   "#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>

注意,我们已经通过使用destroy-method属性将destroy()方法指定为销毁回调。filePath 属性值是通过使用 SpEL 表达式构建的,在文件名test.txt前连接系统属性java.io.tmpdirfile.separator,以确保跨平台兼容性。运行此示例会产生以下输出:

Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()

可以看到,Spring 首先调用初始化回调,DestructiveBean实例创建File实例并存储。接下来,在调用destroy()的过程中,Spring 遍历它所管理的单例集,在本例中只有一个,并调用任何指定的销毁回调。在这里,DestructiveBean实例删除创建的文件,并将消息记录到屏幕上,表明它不再存在。

实现 DisposableBean 接口

与初始化回调一样,Spring 提供了一个接口,在本例中为DisposableBean,它可以由 beans 作为接收销毁回调的机制来实现。DisposableBean接口定义了一个方法destroy(),这个方法在 bean 被销毁之前被调用。使用这种机制与使用InitializingBean接口接收初始化回调是正交的。下面的代码片段显示了实现DisposableBean接口的DestructiveBean类的修改实现:

package com.apress.prospring5.ch4;

import java.io.File;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.GenericXmlApplicationContext;

public class DestructiveBeanWithInterface implements InitializingBean, DisposableBean {
    private File file;
    private String filePath;

    @Override
    public void afterPropertiesSet()  throws Exception {
        System.out.println("Initializing  Bean");

        if (filePath == null) {
            throw new IllegalArgumentException(
                    "You must  specify the filePath property of " +
                    DestructiveBeanWithInterface.class);
        }

        this.file = new File(filePath);
        this.file.createNewFile();

        System.out.println("File exists: " +  file.exists());
    }

    @Override
    public void destroy() {
        System.out.println("Destroying  Bean");

        if(!file.delete()) {
            System.err.println("ERROR: failed  to delete file.");
        }

        System.out.println("File exists: " +  file.exists());
    }
    public void setFilePath(String filePath)  {
        this.filePath =  filePath;
    }

    public static void main(String... args) throws Exception {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        DestructiveBeanWithInterface bean =
            (DestructiveBeanWithInterface) ctx.getBean("destructiveBean");

        System.out.println("Calling destroy()");
        ctx.destroy();
        System.out.println("Called destroy()");
    }
}

使用回调方法机制的代码和使用回调接口机制的代码没有太大区别。在这种情况下,我们甚至使用了相同的方法名。此示例的配置如下所示(app-context-xml.xml):

<beans ...>

    <bean id="destructiveBean"
        class="com.apress.prospring5.ch4.DestructiveBeanWithInterface"
        p:filePath=
   "#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>

除了不同的类名,唯一的区别是省略了destroy-method属性。运行此示例会产生以下输出:

Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()

使用 JSR-250 @PreDestroy 注释

定义在销毁 bean 之前调用的方法的第三种方式是使用 JSR-250 生命周期@PreDestroy注释,这是@PostConstruct注释的逆。下面的代码片段是DestructiveBean的一个版本,它在同一个类中同时使用了@PostConstruct@PreDestroy来执行程序初始化和销毁操作:

package com.apress.prospring5.ch4;

import java.io.File;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.context.support.GenericXmlApplicationContext;

public class DestructiveBeanWithJSR250 {
    private File file;
    private String filePath;

    @PostConstruct
    public void afterPropertiesSet()  throws Exception {
        System.out.println("Initializing  Bean");

        if (filePath == null) {
            throw new IllegalArgumentException(
                    "You must specify the filePath property of " +
                    DestructiveBeanWithJSR250.class);
        }

        this.file = new File(filePath);
        this.file.createNewFile();

        System.out.println("File exists: " +  file.exists());
    }

    @

PreDestroy

    public void destroy() {
        System.out.println("Destroying  Bean");

        if(!file.delete()) {
            System.err.println("ERROR: failed  to delete file.");
        }

        System.out.println("File exists: " +  file.exists());
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public static void main(String... args) throws Exception {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-annotation.xml");
        ctx.refresh();
        DestructiveBeanWithJSR250 bean  =
            (DestructiveBeanWithJSR250) ctx.getBean("destructiveBean");

        System.out.println("Calling destroy()");
        ctx.destroy();
        System.out.println("Called destroy()");
    }
}

在下面的代码片段中,您可以看到这个 bean 的配置文件,它使用了<context:annotation-config>标记(app-context-annotation.xml)。

<beans ...>

    <context:annotation-config/>

    <bean id="destructiveBean"
        class="com.apress.prospring5.ch4.DestructiveBeanWithJSR250"
        p:filePath=
    "#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>

使用@Bean 声明销毁方法

为 bean 声明 destroy 方法的另一种方式是为@Bean注释指定destroyMethod属性,并将 destroy 方法名设置为其值。该注释用于在 Java 配置类中声明 beans。尽管 Java 配置将在本章的稍后部分讨论,但是 bean 销毁部分属于这里。对于这个例子,使用初始的DestructiveBeanWithJSR250类,因为配置是外部的,就像使用destroy-method属性一样。我们将只写一个配置类和一个新的main()方法来测试它。另外,default-lazy-init="true"将被每个 bean 声明上的@Lazy注释所取代。

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.DestructiveBeanWithJSR250;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.GenericApplicationContext;

/**
 * Created by  iuliana.cosmina on  2/27/17.
 */
public class DestructiveBeanConfigDemo {

        @Configuration
        static class  DestructiveBeanConfig {

            @Lazy
            @Bean(initMethod = "afterPropertiesSet", destroyMethod = "destroy")
             DestructiveBeanWithJSR250 destructiveBean() {
                 DestructiveBeanWithJSR250 destructiveBean  =
                            new DestructiveBeanWithJSR250();
                 destructiveBean.setFilePath(System.getProperty("java.io.tmpdir") +
                         System.getProperty("file.separator") +  "test.txt");
                   return  destructiveBean;
                 }

        }

        public static void main(String... args) {
                GenericApplicationContext ctx =
                  new AnnotationConfigApplicationContext(DestructiveBeanConfig.class);

                ctx.getBean(DestructiveBeanWithJSR250.class);
                System.out.println("Calling destroy()");
                ctx.destroy();
                System.out.println("Called destroy()");
        }
}

在 bean 配置中也使用了@PostConstruct注释;因此,运行这段代码将产生到目前为止观察到的相同结果。

Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()

销毁回调是一种理想的机制,可以确保您的应用正常关闭,并且不会让资源处于打开或不一致的状态。然而,您仍然必须决定是使用析构方法回调、DisposableBean接口、@PreDestroy注释、XML destroy-attribute属性还是destroyMethod。同样,让您的应用的需求驱动您在这方面的决策;在可移植性成问题的地方使用方法回调,并使用DisposableBean接口或 JSR-250 注释来减少所需的配置量。

了解解决方案的顺序

与创建 bean 的情况一样,您可以在同一个 bean 实例上使用所有机制来销毁 bean。在这种情况下,Spring 首先调用用@PreDestroy注释的方法,然后调用DisposableBean.destroy(),接着调用在 XML 定义中配置的 destroy 方法。

使用关闭挂钩

Spring 中销毁回调的唯一缺点是它们不会自动触发;您需要记住在应用关闭之前调用AbstractApplicationContext.destroy()。当您的应用作为 servlet 运行时,您可以简单地调用 servlet 的destroy()方法中的destroy()。然而,在一个独立的应用中,事情并不那么简单,尤其是当您的应用有多个出口点时。幸运的是,有一个解决方案。Java 允许您创建一个关闭挂钩,这是一个在应用关闭之前执行的线程。这是调用您的AbstractApplicationContextdestroy()方法的完美方式(它被所有具体的ApplicationContext实现扩展)。利用这种机制最简单的方法是使用AbstractApplicationContextregisterShutdownHook()方法。该方法自动指示 Spring 注册底层 JVM 运行时的关闭挂钩。bean 声明和配置与以前一样;唯一改变的是 main 方法:添加了对ctx.registerShutdownHook的调用,对ctx.destroy()close()的调用将被移除。

...
public class DestructiveBeanWithHook

{

   public static void main(String... args) {
        GenericApplicationContext ctx =
           new AnnotationConfigApplicationContext(
               DestructiveBeanConfig.class);

           ctx.getBean(DestructiveBeanWithJSR250.class);
           ctx.registerShutdownHook();

   }
}

运行这段代码将会产生到目前为止观察到的相同结果。

Initializing Bean
File exists: true
Destroying Bean
File exists: false

如您所见,调用了destroy()方法,尽管我们没有编写任何代码在应用关闭时显式调用它。

让你的豆子“感知 Spring”

作为一种实现控制反转的机制,依赖注入相对于依赖查找的最大卖点之一是,您的 beans 不需要知道管理它们的容器的实现。对于使用构造函数或 setter 注入的 bean,Spring 容器与 Google Guice 或 PicoContainer 提供的容器是一样的。但是,在某些情况下,您可能需要一个使用依赖注入来获取其依赖项的 bean,这样它就可以出于其他原因与容器进行交互。这方面的一个例子可能是一个 bean,它自动为您配置一个关闭挂钩,因此它需要访问ApplicationContext。在其他情况下,一个 bean 可能想知道它的名称是什么(也就是在当前的ApplicationContext中分配的 bean 名称),这样它就可以基于这个名称执行一些额外的处理。

也就是说,这个特性实际上是供内部 Spring 使用的。赋予 bean 名称某种业务含义通常不是一个好主意,并且会导致配置问题,因为必须人为地操纵 bean 名称来支持它们的业务含义。然而,我们发现让 bean 在运行时找到它的名字对于日志记录非常有用。假设您有许多相同类型的 beans 在不同的配置下运行。bean 名称可以包含在日志消息中,以帮助您区分产生错误的 bean 和出错时工作正常的 bean。

使用 BeanNameAware 接口

想获得自己名字的 bean 可以实现的BeanNameAware接口只有一个方法:setBeanName(String)。Spring 在配置完 bean 之后,调用任何生命周期回调(初始化或销毁)之前调用setBeanName()方法(参见图 4-1 )。在大多数情况下,setBeanName()接口的实现只是一行代码,它将容器传入的值存储在一个字段中,供以后使用。下面的代码片段显示了一个简单的 bean,它通过使用BeanNameAware获得自己的名称,然后使用这个 bean 名称打印到控制台:

package com.apress.prospring5.ch4;

import org.springframework.beans.factory.BeanNameAware;

public class NamedSinger implements BeanNameAware {
    private String name;

    /** @Implements {@link BeanNameAware#setBeanName(String)} */
    public void setBeanName(String beanName)  {
        this.name = beanName;
    }

    public void sing() {
        System.out.println("Singer " + name + " - sing()");
    }
}

这个实现相当简单。请记住,在通过调用ApplicationContext.getBean()将 bean 的第一个实例返回到您的应用之前,会调用BeanNameAware.setBeanName(),因此不需要检查 bean 名称在sing()方法中是否可用。在这里,您可以看到本例中使用的app-context-xml.xml文件中包含的配置:

<beans ...>
    <bean id="johnMayer"
           class="com.apress.prospring5.ch4.NamedSinger"/>
</beans>

如您所见,利用BeanNameAware接口不需要特殊的配置。在下面的代码片段中,您可以看到一个简单的示例应用,它从ApplicationContext中检索Singer实例,然后调用sing()方法:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;

public class NamedSingerDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        NamedSinger bean = (NamedSinger) ctx.getBean("johnMayer");
        bean.sing();

        ctx.close();
    }
}

此示例生成以下日志输出;注意调用sing()的日志消息中包含了 bean 名称:

Singer johnMayer - sing()

使用BeanNameAware接口真的很简单,当您提高日志消息的质量时,它会派上用场。避免仅仅因为您可以访问 bean 名称就试图赋予它们业务意义;通过这样做,您将您的类耦合到 Spring 来获得一个可以忽略不计的好处。如果您的 bean 需要某种内部名称,让它们用方法setName()实现一个接口,比如Nameable(特定于您的应用),然后使用依赖注入给每个 bean 一个名称。这样,您可以保持用于配置的名称简洁,并且您不需要不必要地操纵您的配置来为您的 beans 赋予具有业务意义的名称。

使用 ApplicationContextAware 接口

在第三章的结尾引入了ApplicationContextAware,以展示如何使用 Spring 来处理需要其他 bean 来运行的 bean,这些 bean 不是使用配置中的构造函数或设置函数注入的(例如depends-on)。

使用ApplicationContextAware接口,您的 beans 可以获得对配置它们的ApplicationContext实例的引用。创建该接口的主要原因是允许 bean 访问应用中 Spring 的ApplicationContext,例如,使用getBean()以编程方式获取其他 Spring beans。但是,您应该避免这种做法,并使用依赖注入来为您的 beans 提供它们的协作者。如果在可以使用依赖注入的情况下,使用基于查找的getBean()方法来获取依赖,那么就会给 beans 增加不必要的复杂性,并且毫无理由地将它们耦合到 Spring 框架。

当然,ApplicationContext不仅仅是用来查豆子的;它执行许多其他任务。正如您之前看到的,这些任务之一是销毁所有的单例,在这样做之前依次通知它们。在上一节中,您看到了如何创建一个关闭挂钩来确保在应用关闭之前指示ApplicationContext销毁所有的单例。通过使用ApplicationContextAware接口,您可以构建一个可以在ApplicationContext中配置的 bean,以自动创建和配置关机钩子 bean。以下配置显示了此 bean 的代码:

package com.apress.prospring5.ch4;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.GenericApplicationContext;

public class ShutdownHookBean implements ApplicationContextAware  {
    private ApplicationContext ctx;

        /** @Implements {@link ApplicationContextAware#s
           etApplicationContext(ApplicationContext)}    }*/
    public void setApplicationContext(ApplicationContext ctx)
        throws BeansException {

        if (ctx instanceof GenericApplicationContext) {
            ((GenericApplicationContext)  ctx).registerShutdownHook();
        }
    }
}

现在,您应该对这些代码的大部分已经很熟悉了。ApplicationContextAware接口定义了一个单独的方法setApplicationContext(ApplicationContext),Spring 调用该方法向 bean 传递对其ApplicationContext的引用。在前面的代码片段中,ShutdownHookBean类检查ApplicationContext是否属于GenericApplicationContext类型,这意味着它支持registerShutdownHook()方法;如果是这样,它将向ApplicationContext注册一个关机挂钩。下面的配置片段显示了如何配置这个 bean 来与DestructiveBeanWithInterface bean ( app-context-annotation.xml)一起工作:

<beans ...">

    <context:annotation-config/>

    <bean id="destructiveBean"
        class="com.apress.prospring5.ch4.DestructiveBeanWithInterface"
        p:filePath=
 "#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>

    <bean id="shutdownHook"
        class="com.apress.prospring5.ch4.ShutdownHookBean"/>
</beans>

请注意,不需要特殊的配置。下面的代码片段显示了一个简单的示例应用,它使用ShutdownHookBean来管理单例 beans 的销毁:

package com.apress.prospring5.ch4;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import org.springframework.context.support.GenericXmlApplicationContext;

public class DestructiveBeanWithInterface {
    private File file;
    private String filePath;

    @PostConstruct
    public void afterPropertiesSet() throws Exception {
        System.out.println("Initializing  Bean");

        if (filePath == null) {
            throw new IllegalArgumentException(
                    "You must specify the filePath property of " +
                    DestructiveBeanWithInterface.class);
        }

        this.file = new File(filePath);
        this.file.createNewFile();

        System.out.println("File exists: " + file.exists());
    }

    @PreDestroy
    public void destroy() {
        System.out.println("Destroying Bean");

        if(!file.delete()) {
        System.err.println("ERROR: failed to delete file.");
        }

        System.out.println("File exists: " +  file.exists());
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public static void main(String... args) throws Exception {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-annotation.xml");
        ctx.registerShutdownHook();
        ctx.refresh();
        ctx.getBean("destructiveBean",
            DestructiveBeanWithInterface.class);
    }
}

您应该对这段代码非常熟悉。当在配置中定义了 Spring bootstraps ApplicationContextdestructiveBean时,Spring 将ApplicationContext的引用传递给shutdownHook bean,以注册关机挂钩。运行此示例会产生预期的以下输出:

Initializing Bean
File exists: true
Destroying Bean
File exists: false

正如你所看到的,即使在主应用中没有对destroy()的调用,ShutdownHookBean也被注册为一个关闭钩子,它在应用关闭之前调用destroy()

工厂设备的使用

使用 Spring 时,您将面临的一个问题是如何创建并注入依赖项,这些依赖项不能简单地通过使用new操作符来创建。为了克服这个问题,Spring 提供了FactoryBean接口,作为不能使用标准 Spring 语义创建和管理的对象的适配器。通常,您使用FactoryBean来创建您不能使用new操作符创建的 beanss,比如那些您通过静态工厂方法访问的 bean,尽管情况并不总是如此。简单地说,FactoryBean是一种充当其他豆子工厂的豆子。像任何普通 bean 一样在您的ApplicationContext中配置FactoryBean s,但是当 Spring 使用FactoryBean接口来满足一个依赖或查找请求时,它不会返回FactoryBean;相反,它调用FactoryBean.getObject()方法并返回调用的结果。

s 在 Spring 发挥了巨大的作用;最显著的用途是创建事务代理,我们将在第九章中介绍,以及从 JNDI 上下文中自动检索资源。然而,FactoryBean不仅对构建 Spring 的内部有用;当您构建自己的应用时,您会发现它们非常有用,因为它们允许您通过使用 IoC 来管理比其他方式更多的资源。

FactoryBean 示例:MessageDigestFactoryBean

通常我们工作的项目需要某种密码处理;通常,这包括生成要存储在数据库中的用户密码的消息摘要或散列。在 Java 中,MessageDigest类提供了创建任意数据摘要的功能。MessageDigest本身是抽象的,通过调用MessageDigest.getInstance()并传入您想要使用的摘要算法的名称,您可以获得具体的实现。例如,如果我们想使用 MD5 算法创建一个摘要,我们使用下面的代码来创建MessageDigest实例:

MessageDigest md5 = MessageDigest.getInstance("MD5");

如果我们想使用 Spring 来管理MessageDigest对象的创建,那么在没有FactoryBean的情况下,我们能做的最好的事情就是在 bean 上有一个属性algorithmName,然后使用一个初始化回调来调用MessageDigest.getInstance()。使用一个FactoryBean,我们可以将这个逻辑封装在一个 bean 中。然后,任何需要一个MessageDigest实例的 beans 都可以简单地声明一个属性messageDigest,并使用FactoryBean来获取实例。下面的代码片段显示了FactoryBean的一个实现,它就是这样做的:

package com.apress.prospring5.ch4;

import java.security.MessageDigest;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;

public class MessageDigestFactoryBean implements
      FactoryBean<MessageDigest>, InitializingBean {
    private String algorithmName = "MD5";

    private MessageDigest messageDigest =  null;

    public MessageDigest getObject()  throws Exception {
        return  messageDigest;
    }

    public Class<MessageDigest> getObjectType() {
        return MessageDigest.class;
    }

    public boolean isSingleton() {
        return true;
    }

    public void afterPropertiesSet() throws Exception {
        messageDigest = MessageDigest.getInstance(algorithmName);
    }

    public void setAlgorithmName(String algorithmName) {
        this.algorithmName = algorithmName;
    }
}

Spring 调用getObject()方法来检索由FactoryBean创建的对象。这是传递给使用FactoryBean作为合作者的其他 beans 的实际对象。在代码片段中,您可以看到MessageDigestFactoryBean传递了在InitializingBean.afterPropertiesSet()回调中创建的存储的MessageDigest实例的克隆。

getObjectType()方法允许你告诉 Spring 你的FactoryBean将返回什么类型的对象。如果事先不知道返回类型,这可以是null(例如,FactoryBean根据配置创建不同类型的对象,这只有在FactoryBean初始化后才能确定),但是如果你指定一个类型,Spring 可以使用它进行自动连接。我们返回MessageDigest作为我们的类型(在本例中,是一个类,但是尝试返回一个接口类型,并让FactoryBean实例化具体的实现类,除非有必要)。原因是我们不知道将返回什么具体类型(这并不重要,因为所有 beans 都将通过使用MessageDigest来定义它们的依赖关系)。

isSingleton()属性允许您通知 SpringFactoryBean是否正在管理单例实例。记住,通过设置FactoryBean<bean>标签的 singleton 属性,您告诉 Spring 关于FactoryBean本身的 singleton 状态,而不是它返回的对象。现在让我们看看FactoryBean是如何在应用中使用的。在下面的代码片段中,您可以看到一个简单的 bean,它维护两个MessageDigest实例,然后显示传递给其digest()方法的消息摘要:

package com.apress.prospring5.ch4;

import java.security.MessageDigest;
public class MessageDigester {
    private MessageDigest digest1;
    private MessageDigest digest2;

    public void setDigest1(MessageDigest digest1) {
        this.digest1 =  digest1;
    }

    public void setDigest2(MessageDigest digest2) {
        this.digest2 =  digest2;
    }

    public void digest(String msg) {
        System.out.println("Using digest1");
        digest(msg, digest1);

        System.out.println("Using digest2");
        digest(msg, digest2);
    }

    private void digest(String msg, MessageDigest digest) {
        System.out.println("Using alogrithm: " +  digest.getAlgorithm());
        digest.reset();
        byte[] bytes = msg.getBytes();
        byte[] out = digest.digest(bytes);
        System.out.println(out);
    }

}

以下配置片段显示了两个MessageDigestFactoryBean类的示例配置,一个用于 SHA1 算法,另一个使用默认(MD5)算法(app-context-xml.xml):

<beans ...>

    <bean id="shaDigest"
        class="com.apress.prospring5.ch4.MessageDigestFactoryBean"
        p:algorithmName="SHA1"/>

    <bean id="defaultDigest"
        class="com.apress.prospring5.ch4.MessageDigestFactoryBean"/>

    <bean id="digester"
        class="com.apress.prospring5.ch4.MessageDigester"
        p:digest1-ref="shaDigest"
        p:digest2-ref="defaultDigest"/>
</beans>

如您所见,我们不仅配置了两个MessageDigestFactoryBean类,还配置了一个MessageDigester,使用两个MessageDigestFactoryBean类来为digest1digest2属性提供值。对于defaultDigest bean,因为没有指定algorithmName属性,所以不会发生注入,将使用类中编码的默认算法(MD5)。在下面的代码示例中,您可以看到一个基本的示例类,它从BeanFactory中检索MessageDigester bean,并创建一个简单消息的摘要:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;

public class MessageDigestDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext  ctx  =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        MessageDigester digester = ctx.getBean("digester",
            MessageDigester.class);
        digester.digest("Hello World!");

        ctx.close();
    }
}

运行此示例会产生以下输出:

Using digest1
Using alogrithm: SHA1
B@130f889

Using digest2
Using alogrithm: MD5
[B@1188e820

如您所见,MessageDigest bean 提供了两个MessageDigest实现,SHA1MD5,尽管在BeanFactory中没有配置MessageDigestbean。这就是FactoryBean在起作用。

当你正在处理不能用new操作符创建的类时,这是一个完美的解决方案。如果您使用通过工厂方法创建的对象,并且希望在 Spring 应用中使用这些类,那么创建一个FactoryBean作为适配器,允许您的类充分利用 Spring 的 IoC 功能。

当使用通过 Java 配置的配置时,使用FactoryBean s 是不同的,因为在这种情况下,编译器限制使用正确的类型设置属性;因此,必须显式调用getObject()方法。在下面的代码片段中,您可以看到一个配置与上一个示例相同的 beans 的示例,但是使用了 Java 配置:

package com.apress.prospring5.ch4.config;

import  com.apress.prospring5.ch4.MessageDigestFactoryBean;
import  com.apress.prospring5.ch4.MessageDigester;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;

public class MessageDigesterConfigDemo {
    @Configuration
    static class MessageDigesterConfig {

        @Bean
        public MessageDigestFactoryBean shaDigest() {
            MessageDigestFactoryBean factoryOne =
                new MessageDigestFactoryBean();
                factoryOne.setAlgorithmName("SHA1");
                return factoryOne;
        }

        @Bean
        public MessageDigestFactoryBean defaultDigest() {
            return new MessageDigestFactoryBean();
        }

        @Bean
        MessageDigester digester() throws Exception {
            MessageDigester messageDigester =  new MessageDigester();
            messageDigester.setDigest1(shaDigest().getObject());
            messageDigester.setDigest2(defaultDigest().getObject());
            return messageDigester;
        }
    }

    public static void main(String... args) {
        GenericApplicationContext ctx  =
            new AnnotationConfigApplicationContext(MessageDigesterConfig.class);

        MessageDigester digester = (MessageDigester) ctx.getBean("digester");
        digester.digest("Hello World!");
        ctx.close();
    }
}

如果运行这个类,将会打印出与之前相同的输出。

直接访问工厂 Bean

假设 Spring 自动满足由某个FactoryBean产生的对象对该FactoryBean的任何引用,您可能想知道是否可以直接访问该FactoryBean。答案是肯定的。

访问FactoryBean很简单:在对getBean()的调用中,在 bean 名称前面加上一个&符号,如下面的代码示例所示:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;

import java.security.MessageDigest;

public class AccessingFactoryBeans {

        public static void main(String... args) {
                GenericXmlApplicationContext  ctx  =
                    new GenericXmlApplicationContext();
                ctx.load("classpath:spring/app-context-xml.xml");
                ctx.refresh();
                ctx.getBean("shaDigest", MessageDigest.class);

                MessageDigestFactoryBean factoryBean  =
                                (MessageDigestFactoryBean) ctx.getBean("&shaDigest");
                try {
                        MessageDigest shaDigest = factoryBean.getObject();
                        System.out.println(shaDigest.digest("Hello world".getBytes()));
                } catch (Exception ex) {
                        ex.printStackTrace();
                }
                ctx.close();
        }
}

运行该程序会生成以下输出:

[B@130f889

Spring 代码中有几个地方使用了这个特性,但是您的应用应该没有理由使用它。FactoryBean的目的是作为一个支持基础设施,允许你在 IoC 环境中使用更多的应用类。避免直接访问FactoryBean并手动调用其getObject(),让 Spring 替你做;如果您手动地这样做,您正在为自己做额外的工作,并且不必要地将您的应用耦合到将来很容易改变的特定实现细节。

使用工厂 bean 和工厂方法属性

有时,您需要实例化由非 Spring 驱动的第三方应用提供的 JavaBeans。您不知道如何实例化该类,但您知道第三方应用提供了一个类,可用于获取您的 Spring 应用需要的 JavaBean 的实例。在这种情况下,可以使用 Spring bean 的<bean>标签中的factory-beanfactory-method属性。

为了了解它是如何工作的,下面的代码片段展示了另一个版本的MessageDigestFactory,它提供了一个返回MessageDigest bean 的方法:

package com.apress.prospring5.ch4;

import java.security.MessageDigest;

public class MessageDigestFactory {
    private String algorithmName = "MD5";

    public MessageDigest createInstance() throws Exception {
       return MessageDigest.getInstance(algorithmName);
    }

    public void setAlgorithmName(String  algorithmName) {
        this.algorithmName = algorithmName;
    }
}

下面的配置片段显示了如何配置工厂方法来获取相应的MessageDigest bean 实例(app-context-xml.xml):

<beans...>

    <bean id="shaDigestFactory"
        class="com.apress.prospring5.ch4.MessageDigestFactory"
        p:algorithmName="SHA1"/>

    <bean id="defaultDigestFactory"
        class="com.apress.prospring5.ch4.MessageDigestFactory"/>

    <bean id="shaDigest"
          factory-bean="shaDigestFactory"
          factory-method="createInstance">
    </bean>

    <bean id="defaultDigest"
          factory-bean="defaultDigestFactory"
          factory-method="createInstance"/>

    <bean id="digester"
        class="com.apress.prospring5.ch4.MessageDigester"
        p:digest1-ref="shaDigest"
        p:digest2-ref="defaultDigest"/>

</beans>

注意,定义了两个摘要工厂 beans,一个使用 SHA1,另一个使用默认算法。然后对于 beanshaDigestdefaultDigest,我们通过factory-bean属性指示 Spring 使用相应的消息摘要工厂 bean 来实例化 bean,并且我们通过factory-method属性指定了用于获得 bean 实例的方法。下面的代码片段描述了测试类:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;

public class MessageDigestFactoryDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
           new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        MessageDigester digester = ctx.getBean("digester",
            MessageDigester.class);
        digester.digest("Hello World!");

        ctx.close();
    }
}

运行该程序会生成以下输出:

Using digest1
Using alogrithm: SHA1 [B@77a57272
Using digest2
Using alogrithm: MD5 [B@7181ae3f

JavaBeans 属性编辑器

如果您不完全熟悉 JavaBeans 的概念,PropertyEditor是一个接口,它将属性值转换为本机类型表示,或者从本机类型表示转换为String。最初,这被认为是一种允许属性值作为String值输入到编辑器中并将其转换为正确类型的方法。然而,因为PropertyEditor本质上是轻量级的类,所以它们在许多环境中都有应用,包括 Spring。

因为基于 Spring 的应用中很大一部分属性值都是在BeanFactory配置文件中开始的,所以它们本质上是String s。然而,设置这些值的属性可能不是String类型的。因此,为了让您不必人工创建大量的String类型的属性,Spring 允许您定义PropertyEditor来管理基于String的属性值到正确类型的转换。图 [4-2 显示了作为spring-beans包一部分的PropertyEditor的完整列表;您可以在任何智能 Java 编辑器中看到这个列表。

A315511_5_En_4_Fig2_HTML.jpg

图 4-2。

Spring PropertyEditors

它们都扩展了java.beans.PropertyEditorSupport,并且可以用于将String文字隐式转换为要注入 beans 的属性值;因此,他们在BeanFactory预先注册。

使用内置的属性编辑器

下面的代码片段显示了一个简单的 bean,它声明了 14 个属性,内置PropertyEditor实现支持的每种类型都有一个属性:

package com.apress.prospring5.ch4;

import java.io.File;
import java.io.InputStream;
import java.net.URL; import java.util.Date; import java.util.List; import java.util.Locale;
import java.util.Properties; import java.util.regex.Pattern; import java.text.SimpleDateFormat;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;

import org.springframework.context.support.GenericXmlApplicationContext;

public class PropertyEditorBean {

)

    private byte[] bytes;                // ByteArrayPropertyEditor

)

    private Character character;         //CharacterEditor
    private Class cls;                   // ClassEditor
    private Boolean trueOrFalse;         // CustomBooleanEditor
    private List<String> stringList;     // CustomCollectionEditor
    private Date date;                   // CustomDateEditor
    private Float floatValue;            // CustomNumberEditor
    private File file;                   // FileEditor
    private InputStream stream;          // InputStreamEditor
    private Locale locale;               // LocaleEditor
    private Pattern pattern;             // PatternEditor
    private Properties properties;       // PropertiesEditor
    private String trimString;           // StringTrimmerEditor
    private URL url;                     // URLEditor

     public void setCharacter(Character character) {
        System.out.println("Setting character: " + character);
        this.character = character;
    }

    public void setCls(Class cls) {
        System.out.println("Setting class: " + cls.getName());
        this.cls = cls;
    }
    public void setFile(File file) {
        System.out.println("Setting file: " + file.getName());
        this.file = file;
    }

    public void setLocale(Locale locale) {
        System.out.println("Setting locale: " + locale.getDisplayName());
        this.locale = locale;
    }

    public void setProperties(Properties properties) {
        System.out.println("Loaded " + properties.size() + " properties");
        this.properties = properties;
    }

    public void setUrl(URL url) {
        System.out.println("Setting URL: " + url.toExternalForm());
        this.url = url;
    }

    public void setBytes(byte... bytes) {
        System.out.println("Setting bytes: " + Arrays.toString(bytes));
        this.bytes = bytes;
    }

    public void setTrueOrFalse(Boolean trueOrFalse) {
        System.out.println("Setting Boolean: " + trueOrFalse);
        this.trueOrFalse = trueOrFalse;
    }

    public void setStringList(List<String> stringList) {
        System.out.println("Setting string list with size: "
            + stringList.size());

        this.stringList = stringList;

        for (String string: stringList) {
            System.out.println("String member: " + string);
        }
    }

    public void setDate(Date date) {
        System.out.println("Setting date: " + date);
        this.date = date;
    }

    public void setFloatValue(Float floatValue) {
        System.out.println("Setting float value: " + floatValue);
        this.floatValue = floatValue;
    }

    public void setStream(InputStream stream) {
        System.out.println("Setting stream: " + stream);
        this.stream = stream;
    }

    public void setPattern(Pattern pattern) {
        System.out.println("Setting pattern: " + pattern);
        this.pattern = pattern;
    }

    public void setTrimString(String trimString) {
        System.out.println("Setting trim string: " + trimString);
        this.trimString = trimString;
    }

    public static class CustomPropertyEditorRegistrar
            implements PropertyEditorRegistrar {
        @Override
        public void registerCustomEditors(PropertyEditorRegistry registry) {
            SimpleDateFormat dateFormatter  =  new SimpleDateFormat("MM/dd/yyyy");
            registry.registerCustomEditor(Date.class,
                     new CustomDateEditor(dateFormatter, true));

            registry.registerCustomEditor(String.class, new StringTrimmerEditor(true));
        }
    }

    public static void main(String... args) throws Exception {
        File file = File.createTempFile("test", "txt");
        file.deleteOnExit();

        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-01.xml");
        ctx.refresh();

        PropertyEditorBean bean =
            (PropertyEditorBean) ctx.getBean("builtInSample");

        ctx.close();
    }
}

在下面的配置示例中,您可以看到用于声明类型为PropertyEditorBean的 bean 的配置,其中为所有先前的属性(app-config-01.xml)指定了值:

<?xml version="1.0" encoding="UTF-8"?>

<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="customEditorConfigurer"
        class="org.springframework.beans.factory.config.CustomEditorConfigurer"
        p:propertyEditorRegistrars-ref="propertyEditorRegistrarsList"/>

    <util:list id="propertyEditorRegistrarsList">
        <bean class="com.apress.prospring5.ch4.PropertyEditorBean$
              CustomPropertyEditorRegistrar"/>
    </util:list>

    <bean id="builtInSample"
          class="com.apress.prospring5.ch4.PropertyEditorBean"
        p:character="A"
        p:bytes="John Mayer"
        p:cls="java.lang.String"
        p:trueOrFalse="true"
        p:stringList-ref="stringList"
        p:stream="test.txt"
        p:floatValue="123.45678"
        p:date="05/03/13"
          p:file="#{systemProperties'java.io.tmpdir'}
                      #{systemProperties'file.separator'}test.txt"
        p:locale="en_US"
        p:pattern="a*b"
        p:properties="name=Chris age=32"
        p:trimString="   String need trimming   "
        p:url="https://spring.io/"
    />

    <util:list id="stringList">
        <value>String member 1</value>
        <value>String member 2</value>
    </util:list>
</beans>

正如您所看到的,虽然PropertyEditorBean上的所有属性都不是String s,但是属性的值被指定为简单的String s。还要注意,我们注册了CustomDateEditorStringTrimmerEditor,因为这两个编辑器在 Spring 中不是默认注册的。运行此示例会产生以下输出:

Setting bytes: [74, 111, 104, 110, 32, 77, 97, 121, 101, 114]
Setting character: A
Setting class: java.lang.String
Setting date: Wed May 03  00:00:00 EET 13
Setting file: test.txt
Setting float value: 123.45678
Setting locale: English (United States)
Setting pattern: a*b
Loaded 1 properties
Setting stream: java.io.BufferedInputStream@42e25b0b
Setting string list with size: 2
String member: String member 1
String member: String member 2
Setting trim string: String need trimming
Setting Boolean: true
Setting URL: https://spring.io/

如您所见,Spring 已经使用内置的PropertyEditor将各种属性的String表示转换为正确的类型。表 4-1 列出了 Spring 可用的最重要的内置PropertyEditor

表 4-1。

Spring PropertyEditors

| 使用 | 描述 | | --- | --- | | `ByteArrayPropertyEditor` | 将`String`值转换成相应的字节表示。 | | `CharacterEditor` | 从`String`值填充`Character`或`char`类型的属性。 | | `ClassEditor` | 从完全限定的类名转换成一个`Class`实例。当使用这个`PropertyEditor`时,注意不要在使用`GenericXmlApplicationContext`时类名的两边包含任何多余的空格,因为这会导致一个`ClassNotFoundException`。 | | `CustomBooleanEditor` | 将字符串转换为 Java 布尔类型。 | | `CustomCollectionEditor` | 将源集合(例如,由`Spring`中的`util`名称空间表示)转换成目标`Collection`类型。 | | `CustomDateEditor` | 将日期的字符串表示转换为`java.util.Date`值。您需要用期望的日期格式在 Spring 的`ApplicationContext`中注册`CustomDateEditor`实现。 | | `FileEditor` | 将一个`String`文件路径转换成一个`File`实例。Spring 不检查文件是否存在。 | | `InputStreamEditor` | 将资源的字符串表示形式(例如,使用`file:D:/temp/test.txt or classpath:test.txt`的文件资源)转换为输入流属性。 | | `LocaleEditor` | 将一个地区的`String`表示,比如`en-GB`,转换成一个`java.util.Locale`实例。 | | `PatternEditor` | 将一个字符串转换成 JDK `Pattern`对象或者反过来。 | | `PropertiesEditor` | 将格式为`key1=value1 key2=value2 keyn=valuen`的`String`转换为配置了相应属性的`java.util.Properties`的实例。 | | `StringTrimmerEditor` | 在注入之前对字符串值执行修整。您需要显式注册这个编辑器。 | | `URLEditor` | 将 URL 的`String`表示转换成`java.net.URL`的实例。 |

这组PropertyEditor为使用 Spring 提供了一个很好的基础,并使得用文件和 URL 等通用组件配置应用变得更加简单。

创建自定义属性编辑器

尽管内置的PropertyEditor涵盖了属性类型转换的一些标准情况,但有时您可能需要创建自己的PropertyEditor来支持您在应用中使用的一个类或一组类。Spring 完全支持注册自定义PropertyEditors;唯一的缺点是java.beans.PropertyEditor接口有很多方法,其中许多与手头的任务无关,即转换属性类型。谢天谢地,JDK 5 或更新版本提供了PropertyEditorSupport类,您自己的PropertyEditor可以扩展这个类,让您只实现一个方法:setAsText()。让我们考虑一个简单的例子,看看如何实现自定义属性编辑器。假设我们有一个只有两个属性firstNamelastNameFullName类,定义如下:

package com.apress.prospring5.ch4.custom;

public class FullName {
    private String firstName;
    private String lastName;

    public FullName(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String toString() {
        return "First name: " + firstName + " - Last name: " + lastName;
    }
}

为了简化应用配置,让我们开发一个定制编辑器,将带有空格分隔符的字符串分别转换为FullName类的名字和姓氏。下面的代码片段描述了自定义属性编辑器的实现:

package com.apress.prospring5.ch4.custom;

import java.beans.PropertyEditorSupport;

public class NamePropertyEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        String[] name = text.split("\\s");

        setValue(new FullName(name[0],  name[1]));
    }
}

编辑器很简单。它扩展了 JDK 的PropertyEditorSupport类并实现了setAsText()方法。在该方法中,我们简单地将String分割成一个字符串数组,以空格作为分隔符。之后,FullName类的一个实例被实例化,在空格字符前传入String作为名字,在空格字符后传入String作为姓氏。最后,通过调用带有结果的setValue()方法返回转换后的值。为了在您的应用中使用NamePropertyEditor,我们需要在 Spring 的ApplicationContext中注册编辑器。以下配置示例显示了一个CustomEditorConfigurerNamePropertyEditor ( app-context-02.xml)的ApplicationContext配置:

<beans ...>

    <bean name="customEditorConfigurer"
  class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="customEditors">
            <map>
                <entry key="com.apress.prospring5.ch4.custom.FullName"
                  value="com.apress.prospring5.ch4.custom.NamePropertyEditor"/>
            </map>
        </property>
    </bean>

    <bean id="exampleBean"
      class="com.apress.prospring5.ch4.custom.CustomEditorExample"
      p:name="John Mayer"/>
</beans>

在这个配置中,您应该注意到两件事。首先,通过使用Map类型的customEditors属性,自定义的PropertyEditor被注入到CustomEditorConfigurer类中。其次,Map中的每个条目代表一个单独的PropertyEditor,条目的关键字是使用PropertyEditor的类的名称。如您所见,NamePropertyEditor的键是com.apress.prospring4.ch4.FullName,这表示这是应该使用编辑器的类。下面的代码片段显示了在前面的配置中注册为 bean 的CustomEditorExample类的代码:

package com.apress.prospring5.ch4.custom;

import org.springframework.context.support.GenericXmlApplicationContext;

public class CustomEditorExample {
    private FullName name;

    public FullName getName() {
        return name;
    }

    public void setName(FullName name) {
        this.name = name;
    }
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
           new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-02.xml");
        ctx.refresh();

        CustomEditorExample bean =
            (CustomEditorExample) ctx.getBean("exampleBean");

        System.out.println(bean.getName());

        ctx.close();
    }
}

前面的代码没什么特别的。运行该示例,您将看到以下输出:

First name: John - Last name: Mayer

这是我们在FullName类中实现的toString()方法的输出,您可以看到通过使用配置的NamePropertyEditor,Spring 正确地填充了FullName对象的名字和姓氏。从版本 3 开始,Spring 引入了类型转换 API 和字段格式化服务提供者接口(SPI ),它们提供了一个更简单且结构良好的 API 来执行类型转换和字段格式化。这对于 web 应用开发尤其有用。类型转换 API 和字段格式化 SPI 将在第十章中详细讨论。

更多 Spring 应用上下文配置

到目前为止,虽然我们正在讨论 Spring 的ApplicationContext,但是我们已经讨论过的大多数特性主要围绕着由ApplicationContext包装的BeanFactory接口。在 Spring 中,BeanFactory接口的各种实现负责 bean 的实例化,为 Spring 管理的 bean 提供依赖注入和生命周期支持。然而,如前所述,作为BeanFactory接口的扩展,ApplicationContext也提供了其他有用的功能。ApplicationContext的主要功能是提供一个更加丰富的框架来构建你的应用。ApplicationContext更了解您在其中配置的 beans(与BeanFactory相比),对于许多 Spring 基础设施类和接口,比如BeanFactoryPostProcessor,它代表您与它们进行交互,减少了使用 Spring 所需编写的代码量。

使用ApplicationContext的最大好处是它允许你以完全声明的方式配置和管理 Spring 和 Spring 管理的资源。这意味着,只要有可能,Spring 都会提供支持类来将ApplicationContext自动加载到您的应用中,这样您就不需要编写任何代码来访问ApplicationContext。实际上,这个特性目前只有在您使用 Spring 构建 web 应用时才可用,这允许您在 web 应用部署描述符中初始化 Spring 的ApplicationContext。在使用单机应用时,也可以通过简单的编码来初始化 Spring 的ApplicationContext

除了提供更侧重于声明性配置的模型之外,ApplicationContext还支持以下特性:

  • 国际化
  • 事件发布
  • 资源管理和访问
  • 附加生命周期界面
  • 改进了基础架构组件的自动配置

在接下来的章节中,我们将讨论除了 DI 之外的一些最重要的特性。

消息源的国际化

Spring 真正擅长的一个领域是对国际化(i18n)的支持。使用MessageSource接口,您的应用可以访问用各种语言存储的String资源,称为消息。对于您希望在应用中支持的每种语言,您需要维护一个消息列表,这些消息对应于其他语言的消息。例如,如果你想用英语和捷克语显示“敏捷的棕色狐狸跳过了懒惰的狗”,你可以创建两个消息,都键入msg;英语的读法是“一只敏捷的棕色狐狸跳过了一只懒惰的狗”,德语的读法是“一只棕色的狐狸跳过了一只懒惰的狗”。

虽然您不需要使用ApplicationContext来使用MessageSource,但是ApplicationContext接口扩展了MessageSource,并为加载消息和使它们在您的环境中可用提供了特殊的支持。消息的自动加载在任何环境中都是可用的,但是自动访问只在某些 Spring 管理的场景中提供,比如当您使用 Spring 的 MVC 框架构建 web 应用时。尽管任何类都可以实现ApplicationContextAware,从而访问自动加载的消息,但我们在本章后面的“在独立应用中使用 MessageSource”一节中建议了一个更好的解决方案

在继续之前,如果您不熟悉 Java 中的 i18n 支持,我们建议您至少查看一下 Javadocs ( http://download.java.net/jdk8/docs/api/index.html )。

消息源的国际化

除了ApplicationContext,Spring 还提供了三个MessageSource实现。

  • ResourceBundleMessageSource
  • ReloadableResourceBundleMessageSource
  • StaticMessageSource

不应该在生产应用中使用StaticMessageSource实现,因为您不能在外部配置它,这通常是您向应用添加 i18n 功能时的主要需求之一。

ResourceBundleMessageSource使用 Java ResourceBundle加载消息。ReloadableResourceBundleMessageSource本质上是相同的,除了它支持底层源文件的计划重载。

所有三个 MessageSource 实现还实现了另一个名为HierarchicalMessageSource的接口,该接口允许嵌套许多MessageSource实例。这是ApplicationContext使用MessageSource实例的关键。

为了利用ApplicationContextMessageSource的支持,您必须在您的配置中定义一个类型为MessageSource的 bean,并使用名称messageSource. ApplicationContext将这个MessageSource嵌套在其自身中,允许您通过使用ApplicationContext来访问消息。这可能很难想象,所以看看下面的例子。以下代码示例显示了一个简单的应用,该应用访问英语和德语区域设置的一组消息:

package com.apress.prospring5.ch4;
import java.util.Locale;

import org.springframework.context.support.GenericXmlApplicationContext;

public class MessageSourceDemo  {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        Locale english = Locale.ENGLISH;
        Locale german = new Locale("de", "DE");

        System.out.println(ctx.getMessage("msg", null, english));
        System.out.println(ctx.getMessage("msg", null,  german));

        System.out.println(ctx.getMessage("nameMsg", new Object[]
         { "John", "Mayer" }, english));
        System.out.println(ctx.getMessage("nameMsg", new Object[]
        { "John", "Mayer" }, german));

        ctx.close();
    }
}

现在还不要担心对getMessage()的调用;我们将很快回到这些问题上。现在,只需知道它们为指定的地区检索一个键控消息。在下面的配置片段中,您可以看到这个应用使用的配置(app-context-xml.xml):

<beans ...>

    <bean id="messageSource"
   class="org.springframework.context.support.ResourceBundleMessageSource"
      p:basenames-ref="basenames"/>

    <util:list id="basenames">
        <value>buttons</value>
        <value>labels</value>
    </util:list>
</beans>

这里,我们根据需要定义一个名为messageSourceResourceBundleMessageSource bean,并用一组名称对其进行配置,以形成其文件集的基础。一个由ResourceBundleMessageSource使用的 Java ResourceBundle,在一组由基本名称标识的属性文件上工作。当查找特定Locale的消息时,ResourceBundle会查找由基本名称和地区名称组合而成的文件。例如,如果基本名称是foo,并且我们在en-GB(英国英语)语言环境中查找消息,ResourceBundle将查找名为foo_en_GB.properties的文件。

在前面的示例中,英语(labels_en.properties)和德语(labels_de_DE.properties)的属性文件的内容如下所示:

#labels_en.properties
msg=My stupid mouth has got me in trouble
nameMsg=My name is {0} {1}
#labels_de_DE.properties
msg=Mein dummer Mund hat mich in Schwierigkeiten gebracht
nameMsg=Mein Name ist {0} {1}

这个例子引发了更多的问题。那些打给getMessage()的电话是什么意思?为什么我们使用ApplicationContext.getMessage()而不是直接访问ResourceBundleMessageSource bean?我们将依次回答这些问题。

使用 getMessage()方法

MessageSource接口为getMessage()方法定义了三个重载。这些在表 4-2 中描述。

表 4-2。

Overloads for MessageSource.getMessage()

| 方法签名 | 描述 | | --- | --- | | `getMessage (String,  Object[], Locale)` | 这是标准的`getMessage()`方法。`String`参数是对应于属性文件中的键的消息的键。在前面的代码示例中,对`getMessage()`的第一次调用使用了`msg`作为键,这对应于`en`地区的属性文件中的以下条目:`msg=The quick brown fox jumped over the lazy dog`。`Object[]`数组参数用于替换消息中的内容。在对`getMessage()`的第三次调用中,我们传入了一个由两个`String`组成的数组。大括号中的数字是占位符,每个数字都被替换为参数数组中相应的条目。最后一个参数`Locale`告诉`ResourceBundleMessageSource`要查看哪个属性文件。尽管示例中对`getMessage()`的第一次和第二次调用使用了相同的键,但是它们返回了不同的消息,这些消息对应于传递给`getMessage()`的`Locale`设置。 | | `getMessage (String, Object[], String, Locale)` | 这个重载的工作方式与`getMessage(String, Object[], Locale)`相同,除了第二个`String`参数,它允许我们在所提供的键的消息对于所提供的`Locale`不可用的情况下传入一个默认值。 | | `getMessage (MessageSourceResolvable, Locale)` | 这个重载是一个特例。我们将在“MessageSourceResolvable 接口”一节中详细讨论它。 |
为什么使用 ApplicationContext 作为消息源?

要回答这个问题,我们需要稍微超前一点,看看 Spring 中的 web 应用支持。总的来说,答案是您不应该使用ApplicationContext作为MessageSource,因为这样做会不必要地将您的 bean 耦合到ApplicationContext(这将在下一节详细讨论)。当你使用 Spring 的 MVC 框架构建一个 web 应用时,你应该使用ApplicationContext

Spring MVC 中的核心接口是Controller。不像 Struts 这样的框架要求你通过从一个具体的类继承来实现你的控制器,Spring 只要求你实现Controller接口(或者用@Controller注释来注释你的控制器类)。话虽如此,Spring 提供了一组有用的基类,您可以用它们来实现自己的控制器。这些基类中的每一个都是ApplicationObjectSupport类的子类(直接或间接),对于任何想要知道ApplicationContext的应用对象来说,这是一个方便的超类。请记住,在 web 应用设置中,ApplicationContext是自动加载的。

ApplicationObjectSupport访问这个ApplicationContext,将其包装在一个MessageSourceAccessor对象中,并通过受保护的getMessageSourceAccessor()方法使其对您的控制器可用。MessageSourceAccessor 提供了大量使用MessageSource实例的便捷方法。为使用MessageSource实例提供了一系列方便的方法。这种形式的自动注射非常有益;它消除了所有控制器暴露一个MessageSource属性的需要。

然而,这并不是在 web 应用中使用ApplicationContext作为MessageSource的最好理由。使用ApplicationContext而不是手动定义的MessageSource bean 的主要原因是 Spring 尽可能将ApplicationContext作为MessageSource暴露给视图层。这意味着当你使用 Spring 的 JSP 标签库时,<spring:message>标签会自动从ApplicationContext读取消息,当你使用 JSTL 时,<fmt:message>标签也会这么做。

所有这些好处意味着,在构建 web 应用时,最好使用ApplicationContext中的MessageSource支持,而不是单独管理MessageSource的一个实例。当您考虑到您需要做的就是配置一个名为messageSourceMessageSource bean 来利用这个特性时,这一点尤其正确。

在独立应用中使用 MessageSource

当您在独立应用中使用MessageSource时,Spring 除了在ApplicationContext中自动嵌套MessageSource bean 之外不提供额外的支持,最好通过使用依赖注入来使MessageSource可用。您可以选择让您的 bean 成为ApplicationContextAware,但是这样做会妨碍它在BeanFactory上下文中的使用。除此之外,您使测试变得复杂,没有任何明显的好处,很明显,您应该坚持在独立设置中使用依赖注入来访问MessageSource对象。

MessageSourceResolvable 接口

当您查找来自MessageSource的消息时,您可以使用实现MessageSourceResolvable的对象来代替一个键和一组参数。这个接口在 Spring 验证库中被广泛使用,用来将Error对象链接到它们的国际化错误消息。

应用事件

另一个BeanFactory中没有的ApplicationContext特性是通过使用ApplicationContext作为代理来发布和接收事件的能力。在本节中,您将了解它的用法。

使用应用事件

事件是从ApplicationEvent派生的类,它本身从java.util.EventObject派生。任何 bean 都可以通过实现ApplicationListener<T>接口来监听事件;ApplicationContext在配置时,自动将实现该接口的任何 bean 注册为监听器。事件是使用ApplicationEventPublisher.publishEvent()方法发布的,因此发布类必须了解ApplicationContext(它扩展了ApplicationEventPublisher接口)。在 web 应用中,这很简单,因为您的许多类都是从 Spring Framework 类派生的,这些类允许通过受保护的方法访问ApplicationContext。在独立应用中,您可以让发布 bean 实现ApplicationContextAware来发布事件。

以下代码示例显示了一个基本事件类的示例:

package com.apress.prospring5.ch4;
import org.springframework.context.ApplicationEvent;

public class MessageEvent extends ApplicationEvent {
    private String msg;

    public MessageEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }

    public String getMessage() {
        return msg;
    }
}

这段代码非常简单;唯一值得注意的一点是,ApplicationEvent有一个单一的构造函数,它接受对事件源的引用。这反映在MessageEvent的构造函数中。在这里,您可以看到监听器的代码:

package com.apress.prospring5.ch4;

import org.springframework.context.ApplicationListener;

public class MessageEventListener
      implements ApplicationListener<MessageEvent> {
    @Override
    public void onApplicationEvent(MessageEvent event) {
        MessageEvent msgEvt = (MessageEvent) event;
        System.out.println("Received: " + msgEvt.getMessage());
    }
}

ApplicationListener接口定义了一个方法onApplicationEvent,当事件发生时,Spring 会调用这个方法。通过实现强类型的ApplicationListener接口,MessageEventListener只对类型MessageEvent(或其子类)的事件感兴趣。如果接收到MessageEvent,它将消息写入stdout。发布事件很简单;这只是创建一个事件类的实例并将其传递给ApplicationEventPublisher.publishEvent()方法,如下所示:

package com.apress.prospring5.ch4

;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Publisher implements ApplicationContextAware {
    private ApplicationContext ctx;

    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.ctx = applicationContext;
    }

    public void publish(String message) {
        ctx.publishEvent(new MessageEvent(this, message));
    }

    public static void main(String... args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(
            "classpath:spring/app-context-xml.xml");

        Publisher pub = (Publisher) ctx.getBean("publisher");
        pub.publish("I send an SOS to the world... ");
        pub.publish("... I hope that someone gets my...");
        pub.publish("... Message in a bottle");
    }
}

这里您可以看到,Publisher类从ApplicationContext中检索自己的一个实例,然后使用publish()方法,向ApplicationContext发布两个MessageEvent实例。Publisher bean 实例通过实现ApplicationContextAware来访问ApplicationContext实例。以下是本例的配置(app-context-xml.xml):

<beans ...>

    <bean id="publisher"
        class="com.apress.prospring5.ch4.Publisher"/>

    <bean id="messageEventListener"
        class="com.apress.prospring5.ch4.MessageEventListener"/>
</beans>

注意你不需要特殊的配置来注册MessageEventListenerApplicationContext;它被 Spring 自动拾起。运行此示例会产生以下输出:

Received: I send an SOS to the world...
Received: ... I hope that someone gets my...
Received: ... Message in a bottle

事件使用的注意事项

在应用的许多情况下,某些组件需要被通知某些事件。通常,您可以通过编写代码来显式通知每个组件,或者通过使用 JMS 之类的消息传递技术来实现这一点。编写代码依次通知每个组件的缺点是,您将这些组件耦合到了发布者,在许多情况下这是不必要的。

考虑这样一种情况,您在应用中缓存产品详细信息,以避免访问数据库。另一个组件允许修改产品细节并保存到数据库中。为了避免使缓存无效,更新组件显式地通知缓存用户详细信息已经改变。在这个例子中,更新组件被耦合到一个实际上与其业务职责无关的组件。更好的解决方案是让更新组件在每次修改产品细节时发布一个事件,然后让感兴趣的组件(如缓存)监听该事件。这样做的好处是保持组件的解耦性,这使得在需要的时候移除缓存或者添加另一个监听器变得很简单,这个监听器有兴趣知道产品细节的变化。

在这种情况下使用 JMS 可能有些矫枉过正,因为使产品在缓存中的条目无效的过程很快,并且不是业务关键的。Spring 事件基础设施的使用给应用增加了很少的开销。

通常,我们将事件用于快速执行的反应性逻辑,而不是主应用逻辑的一部分。在前面的例子中,产品在缓存中的失效发生在对产品细节更新的反应中,它执行得很快(或者应该执行),并且它不是应用主要功能的一部分。对于长期运行并构成主要业务逻辑一部分的流程,建议使用 JMS 或类似的消息传递系统,如 RabbitMQ。使用 JMS 的主要好处是它更适合长时间运行的流程,并且随着系统的增长,如果有必要,您可以将 JMS 驱动的包含业务信息的消息处理放在单独的机器上。

访问资源

通常,应用需要以不同的形式访问各种资源。您可能需要访问存储在文件系统的某个文件中的一些配置数据、存储在类路径上的 JAR 文件中的一些图像数据,或者其他地方的服务器上的一些数据。Spring 提供了一种以独立于协议的方式访问资源的统一机制。这意味着您的应用可以以相同的方式访问文件资源,无论它是存储在文件系统中、类路径中还是远程服务器上。

Spring 资源支持的核心是org.springframework.core.io.Resource接口。Resource接口定义了十种自解释方法:contentLength()exists()getDescription()getFile()getFileName()getURI()getURL()isOpen()isReadable()lastModified()。除了这十个方法之外,还有一个不那么自明的:createRelative()createRelative()方法通过使用一个相对于调用它的实例的路径来创建一个新的Resource实例。您可以提供自己的Resource实现,尽管这超出了本章的范围,但是在大多数情况下,您使用一个内置实现来访问文件(FileSystemResource类)、类路径(ClassPathResource类)或 URL 资源(UrlResource类)。在内部,Spring 使用另一个接口ResourceLoader和默认实现DefaultResourceLoader来定位和创建Resource实例。然而,你通常不会与DefaultResourceLoader交互,而是使用另一个ResourceLoader实现,叫做ApplicationContext。下面是一个示例应用,它使用ApplicationContext访问三个资源:

package com.apress.prospring5.ch4;

import java.io.File;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;

public class ResourceDemo {
    public static void main(String... args) throws Exception{
        ApplicationContext ctx = new ClassPathXmlApplicationContext();

        File file = File.createTempFile("test", "txt");
        file.deleteOnExit();

        Resource res1 = ctx.getResource("file://" + file.getPath());
        displayInfo(res1);

        Resource res2 = ctx.getResource("classpath:test.txt");
        displayInfo(res2);

        Resource res3 = ctx.getResource("http://www.google.com");
        displayInfo(res3);
    }

    private static void displayInfo(Resource res) throws Exception{
        System.out.println(res.getClass());
        System.out.println(res.getURL().getContent());
        System.out.println("");
    }
}

注意,在对getResource()的每次调用中,我们为每个资源传入一个 URI。你会认出我们为res1res3传递的通用file:http:协议。我们为res2使用的classpath:协议是特定于 Spring 的,它指示ResourceLoader应该在类路径中查找资源。运行此示例会产生以下输出:

class org.springframework.core.io.UrlResource
java.io.BufferedInputStream@3567135c

class org.springframework.core.io.ClassPathResource
sun.net.www.content.text.PlainTextInputStream@90f6bfd

class org.springframework.core.io.UrlResource
sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@735b5592

注意,对于file:http:协议,Spring 都返回了一个UrlResource实例。Spring 确实包含了一个FileSystemResource类,但是DefaultResourceLoader根本没有使用这个类。这是因为 Spring 的默认资源加载策略将 URL 和文件视为具有不同协议(file:http:)的相同类型的资源。如果需要FileSystemResource的实例,使用FileSystemResourceLoader。一旦获得了一个Resource实例,您就可以使用getFile()getInputStream()getURL()随意访问内容。在某些情况下,例如当您使用http:协议时,对getFile()的调用会导致FileNotFoundException。因此,我们建议您使用getInputStream()来访问资源内容,因为它可能适用于所有可能的资源类型。

使用 Java 类进行配置

除了 XML 和属性文件配置,您还可以使用 Java 类来配置 Spring 的ApplicationContext。到目前为止,到处都介绍了代码示例,以使您熟悉注释风格的配置。Spring JavaConfig 曾经是一个独立的项目,但是从 Spring 3.0 开始,它使用 Java 类进行配置的主要特性被合并到了核心的 Spring 框架中。在这一节中,我们将展示在使用 XML 配置时,如何使用 Java 类来配置 Spring 的ApplicationContext和它的等价物。

Java 中的应用上下文配置

我们来看看 Spring 的ApplicationContext如何使用 Java 类进行配置;我们将引用在第二章和第三章中给出的消息提供者和呈现者的相同例子。下面的代码概括了消息提供者接口和一个可配置的消息提供者: 2

//chapter02/hello-world/src/main/java/com/apress/prospring5/
//    ch2/decoupled/MessageProvider.java
package com.apress.prospring5.ch2.decoupled;

public interface MessageProvider {
    String getMessage();
}

//chapter03/constructor-injection/src/main/java/com/apress/prospring5/
//    ch3/xml/ConfigurableMessageProvider.java
package com.apress.prospring5.ch3.xml;

import com.apress.prospring5.ch2.decoupled.MessageProvider;

public class ConfigurableMessageProvider implements MessageProvider {
        private String message = "Default  message";

        public ConfigurableMessageProvider() {

        }

        public ConfigurableMessageProvider(String message) {
                this.message = message;
        }

        public void setMessage(String message) {
                this.message = message;
        }

        public String getMessage() {
                return message;
        }
}

下面的代码片段显示了MessageRenderer接口和StandardOutMessageRenderer实现:

//chapter02/hello-world/src/main/java/com/apress/prospring5/
//     ch2/decoupled/MessageRenderer.java
package com.apress.prospring5.ch2.decoupled;

public interface MessageRenderer {
    void render();
    void setMessageProvider(MessageProvider provider);
    MessageProvider getMessageProvider();
}

//chapter02/hello-world/src/main/java/com/apress/prospring5/
//     ch2/decoupled/StandardOutMessageRenderer.java
package com.apress.prospring5.ch2.decoupled;

public class StandardOutMessageRenderer
     implements MessageRenderer {

        private MessageProvider messageProvider;

        public StandardOutMessageRenderer(){
                System.out.println(" -->
                    StandardOutMessageRenderer: constructor called");
        }
        @Override
        public void render() {
                if (messageProvider == null) {
                        throw new RuntimeException(
                          "You must set the property messageProvider of class:"
                                + StandardOutMessageRenderer.class.getName());
                } System.out.println(messageProvider.getMessage());
        }

        @Override

        public void setMessageProvider(MessageProvider provider) {
                System.out.println(" -->
                      StandardOutMessageRenderer: setting the provider");
                this.messageProvider = provider;
        }

        @Override
        public MessageProvider getMessageProvider()  {
                return this.messageProvider;
        }
}

下面的配置片段描述了 XML 配置(app-context-xml.xml):

<beans ...>

    <bean id="messageRenderer"
      class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
      p:messageProvider-ref="messageProvider"/>

    <bean id="messageProvider"
      class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider"
      c:message="This is a configurable message"/>
</beans>

测试这个的类看起来也很熟悉,如下所示:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class JavaConfigXMLExample {

        public static void main(String... args) {
                ApplicationContext ctx = new  ClassPathXmlApplicationContext("
                     classpath:spring/app-context-xml.xml");

        MessageRenderer renderer =
            ctx.getBean("messageRenderer", MessageRenderer.class);
        renderer.render();
        }
}

运行该程序会产生以下输出:

--> StandardOutMessageRenderer: constructor called
--> StandardOutMessageRenderer: setting the provider
This is a  configurable message

为了去掉 XML 配置,app-context-xml.xml文件必须由一个特殊的类代替,这个特殊的类称为配置类,它将用@Configuration进行注释。@Configuration注释用于通知 Spring 这是一个基于 Java 的配置文件。这个类将包含用代表 bean 声明的@Bean定义注释的方法。@Bean注释用于声明一个 Spring bean 和 DI 需求。@Bean标注相当于<bean>标签,方法名相当于<bean>标签内的id属性,实例化MessageRender bean 时,通过调用相应的方法获取消息提供者来实现 setter 注入,这与在 XML 配置中使用<ref>属性是一样的。这些注释和这种类型的配置在前面的章节中已经介绍过了,目的是让您熟悉它们,但是直到现在才详细介绍它们。下面的代码片段描述了与前面介绍的 XML 配置等效的AppConfig内容:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import com.apress.prospring5.ch3.xml.ConfigurableMessageProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MessageProvider messageProvider() {
        return new ConfigurableMessageProvider();
    }

    @Bean
    public MessageRenderer messageRenderer() {
        MessageRenderer renderer = new StandardOutMessageRenderer();
        renderer.setMessageProvider(messageProvider());

        return renderer;
    }
}

下面的代码片段显示了如何从 Java 配置文件初始化ApplicationContext实例:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class JavaConfigExampleOne {
    public static void main(String... args) {
        ApplicationContext ctx = new
            AnnotationConfigApplicationContext(AppConfig.class);

        MessageRenderer renderer =
            ctx.getBean("messageRenderer", MessageRenderer.class);

        renderer.render();
    }
}

在前面的清单中,我们使用了AnnotationConfigApplicationContext类,将配置类作为构造函数参数传入(您可以通过 JDK varargs 特性将多个配置类传递给它)。之后,您可以照常使用返回的ApplicationContext。有时出于测试目的,可以将配置类声明为静态内部类,如下所示:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import com.apress.prospring5.ch3.xml.ConfigurableMessageProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class JavaConfigSimpleExample {

    @Configuration
    static class AppConfigOne {
        @Bean
        public MessageProvider messageProvider() {
            return new ConfigurableMessageProvider();
        }

        @Bean
        public MessageRenderer messageRenderer() {
            MessageRenderer renderer = new StandardOutMessageRenderer();
            renderer.setMessageProvider(messageProvider());

            return renderer;
        }

    }

    public static void main(String... args) {
        ApplicationContext ctx = new
            AnnotationConfigApplicationContext(AppConfig.class);

        MessageRenderer renderer =
            ctx.getBean("messageRenderer", MessageRenderer.class);

        renderer.render();
    }
}

返回的ApplicationContext实例可以照常使用,输出将与 XML 配置的应用的情况相同。

 --> StandardOutMessageRenderer: constructor called
 --> StandardOutMessageRenderer: setting the provider
Default  message

已经了解了 Java 配置类的基本用法,让我们继续了解更多的配置选项。对于消息提供者,假设我们想要将消息外部化到一个属性文件(message.properties)中,然后通过使用构造函数注入将其注入到ConfigurableMessageProvider中。message.properties的内容如下:

message=Only hope can keep me together

让我们看看修改后的测试程序,它通过使用@PropertySource注释加载属性文件,然后将它们注入到消息提供者实现中。

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource(value = "classpath:message.properties")
public class AppConfigOne {

        @Autowired
        Environment env;

        @Bean
        public MessageProvider messageProvider() {
                return new ConfigurableMessageProvider(env.getProperty("message"));
        }

        @Bean(name = "messageRenderer")
        public MessageRenderer messageRenderer()  {
            MessageRenderer renderer = new StandardOutMessageRenderer();
            renderer.setMessageProvider(messageProvider());
            return renderer;
        }

}

在第三章中介绍了配置类,以显示 XML 元素和属性的等价物。可以用与 Bean 作用域、加载类型和依赖关系相关的其他注释来注释 bean 声明。在下面的代码片段中,AppConfigOne配置类增加了 bean 声明的注释:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource(value = "classpath:message.properties")
public class AppConfig {

        @Autowired
        Environment env;

        @Bean
        @Lazy
        public MessageProvider messageProvider() {
                return new ConfigurableMessageProvider(env.getProperty("message"));
        }

        @Bean(name = "messageRenderer")
        @Scope(value="prototype")
        @DependsOn(value="messageProvider")
        public MessageRenderer messageRenderer() {
                MessageRenderer renderer = new StandardOutMessageRenderer();
                renderer.setMessageProvider(messageProvider());
                return renderer;
        }
}

在之前的代码示例中,引入了一些注释,如表 4-3 中所述。通过启用组件扫描并在需要的地方自动连接,使用类似@ Component@Service等原型注释定义的 Beans 可以在 Java 配置类中使用。在下面的例子中,我们将ConfigurableMessageProvider声明为服务 bean。

表 4-3。

Java Configuration Annotations Table

| 注释 | 描述 | | --- | --- | | `@PropertySource` | 该注释用于将属性文件加载到 Spring 的`ApplicationContext`中,它接受位置作为参数(可以提供多个位置)。对于 XML 来说,``也有同样的作用。 | | `@Lazy` | 该注释指示 Spring 仅在被请求时实例化 bean(与 XML 中的`lazy-init="true"`相同)。该注释有一个默认的`value`属性,默认为`true`;因此,使用`@Lazy(value=true)`相当于使用`@Lazy`。 | | `@Scope` | 当所需的作用域不是 singleton 时,这用于定义 bean 作用域。 | | `@DependsOn` | 这个注释告诉 Spring 某个 bean 依赖于其他一些 bean,所以 Spring 将确保这些 bean 首先被实例化。 | | `@Autowired` | 这个注释用在`env`变量上,它属于`Environment`类型。这是 Spring 提供的`Environment`抽象特性。我们将在本章后面讨论它。 |
package com.apress.prospring5.ch4.annotated;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service("provider")
public class ConfigurableMessageProvider implements MessageProvider {

        private String message;

        public ConfigurableMessageProvider(
                        @Value("Love on the weekend")String message) {
                this.message = message;
        }

        @Override
        public String getMessage() {
            return this.message;
        }
}

这里您可以看到配置类

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;

@Configuration

@ComponentScan(basePackages={"com.apress.prospring5.ch4.annotated"})
public class AppConfigTwo {

        @Autowired
        MessageProvider provider;

        @Bean(name = "messageRenderer")
        public MessageRenderer messageRenderer() {
                MessageRenderer renderer =
                      new StandardOutMessageRenderer();
                renderer.setMessageProvider(provider);
                return renderer;
        }
}

@ComponentScan定义了 Spring 应该扫描哪些包来查找 bean 定义的注释。它与 XML 配置中的<context:component-scan>标签相同。执行以下示例中的代码:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class JavaConfigExampleTwo {
    public static void main(String... args) {
        ApplicationContext ctx = new
            AnnotationConfigApplicationContext(AppConfigTwo.class);

        MessageRenderer renderer =
            ctx.getBean("messageRenderer", MessageRenderer.class);

        renderer.render();
    }
}

您将获得以下结果:

 --> StandardOutMessageRenderer: constructor called
 --> StandardOutMessageRenderer: setting the provider
Love on the weekend

一个应用还可以有多个配置类,这些配置类可以用于解耦配置和按用途组织 bean(例如,一个类可以专用于 DAO beans 声明,一个用于服务 bean 声明,等等)。让我们使用另一个名为AppConfigFour的配置类来定义provider bean。通过导入该类定义的 bean,可以从另一个配置类访问该 bean。这是通过用@Import注释目标配置类AppConfigThree来实现的。

//AppConfigFour.java
package com.apress.prospring5.ch4.multiple;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages={"com.apress.prospring5.ch4.annotated"})
public class AppConfigFour { }

package com.apress.prospring5.ch4.multiple;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;

@Configuration

@Import(AppConfigFour.class)

public class AppConfigThree {
        @Autowired
        MessageProvider provider;

        @Bean(name = "messageRenderer")
        public MessageRenderer messageRenderer() {
                MessageRenderer renderer = new StandardOutMessageRenderer();
                renderer.setMessageProvider(provider);
                return renderer;
        }
}

如果在类JavaConfigExampleTwo的 main 方法中,您用AppConfigThree替换了类AppConfigTwo,那么当运行该示例时,会打印出相同的输出。

Spring 混合配置

但是 Spring 能做的远不止这些。Spring 允许混合 XML 和 Java 配置类。当应用带有由于某种原因不能更改的遗留代码时,这是很有用的。要从 XML 文件导入 bean 声明,可以使用@ImportResource注释。在下面的配置片段中,您可以看到在名为app-context-xml-01.xml的 XML 文件中声明的provider bean:

<beans ...>

   <bean id="provider"
            class="com.apress.prospring5.ch4.ConfigurableMessageProvider"
         p:message="Love on the weekend" />

</beans>

下一个代码示例描述了导入 XML 文件中声明的 beans 的类AppConfigFive。如果在classJavaConfigExampleTwo的 main 方法中,我们用AppConfigFive替换了类AppConfigTwo,那么当这个例子运行时,同样的输出被打印出来。

package com.apress.prospring5.ch4.mixed;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration

@ImportResource(value="classpath:spring/app-context-xml-01.xml")

public class AppConfigFive {
        @Autowired
        MessageProvider provider;

        @Bean(name = "messageRenderer")
        public MessageRenderer messageRenderer() {
                MessageRenderer renderer = new StandardOutMessageRenderer();
                renderer.setMessageProvider(provider);
                return renderer;
        }

}

同样,反过来也可以:在 Java 配置类中定义的 beans 可以导入到 XML 配置文件中。在下一个例子中,messageRenderer bean 是在 XML 文件中定义的,它的依赖项,provider bean 是在配置类AppConfigSix中定义的。XML 配置文件app-context-xml-02.xml的内容描述如下:

<beans ...>

    <context:annotation-config/>

    <bean class="com.apress.prospring5.ch4.mixed.AppConfigSix"/>

    <bean id="messageRenderer"
            class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
          p:messageProvider-ref="provider"/>

</beans>

必须声明配置类类型的 bean,并且必须使用<context:annotation-config/>启用对带注释方法的支持。这使得类中声明的 bean 可以配置为 XML 文件中声明的 bean 的依赖项。配置类AppConfigSix非常简单。

package com.apress.prospring5.ch4.mixed

;

import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch4.annotated.ConfigurableMessageProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfigSix {

        @Bean
        public MessageProvider provider() {
                return new ConfigurableMessageProvider("Love on the weekend");
        }
}

创建一个ApplicationContext实例是使用ClassPathXmlApplicationContext来完成的,到目前为止它已经被大量使用。

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch4.mixed.AppConfigFive;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class JavaConfigExampleThree {
    public static void main(String... args) {
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext
                ("classpath:spring/app-context-xml-02.xml");

        MessageRenderer renderer =
            ctx.getBean("messageRenderer", MessageRenderer.class);

        renderer.render();
    }
}

运行前面的代码会产生与前面描述的相同的输出。

A315511_5_En_4_Figa_HTML.jpg应用基础设施服务也可以在 Java 配置类中定义。例如,@EnableTransactionManagement定义我们将使用 Spring 的事务管理特性,这将在第九章中进一步讨论,而@EnableWebSecurity@EnableGlobalMethodSecurity用于启用 Spring Security 上下文,这将在第十六章中详细讨论。

Java 还是 XML 配置?

正如您已经看到的,使用 Java 类可以实现与 XML 相同级别的ApplicationContext配置。那么,你应该用哪一个呢?这种考虑很像是在 DI 配置中使用 XML 还是 Java 注释。每种方法都有自己的优点和缺点。但是,建议是一样的;也就是说,当您和您的团队决定使用哪种方法时,坚持使用它并保持配置风格的持久性,而不是分散在 Java 类和 XML 文件之间。使用一种方法会使维护工作容易得多。

轮廓

Spring 提供的另一个有趣的特性是配置文件的概念。基本上,概要文件指示 Spring 只配置在指定概要文件激活时定义的ApplicationContext实例。在这一节中,我们将演示如何在一个简单的程序中使用概要文件。

使用 Spring 轮廓特征的示例

假设有一个叫FoodProviderService的服务,负责给学校提供食物,包括幼儿园和高中。FoodProviderService接口只有一个名为provideLunchSet()的方法,它为调用学校的每个学生生成午餐套餐。午餐集是一个由Food对象组成的列表,它是一个简单的类,只有一个name属性。下面的代码片段显示了Food类:

package com.apress.prospring5.ch4;

public class Food {
    private String name;

    public Food() {
    }

    public Food(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

而这里是FoodProviderService界面:

package com.apress.prospring5.ch4;

import java.util.List;

public interface FoodProviderService {
    List<Food> provideLunchSet();
}

现在假设午餐套餐有两个提供者,一个是幼儿园的,一个是高中的。他们生产的午餐套餐是不一样的,虽然他们提供的服务是一样的,就是给学生提供午餐。所以,现在让我们创建FoodProviderService的两个实现,使用相同的名称,但是将它们放入不同的包中以标识它们的目标学校。这里显示了两个类:

//chapter04/profiles/src/main/java/com/apress/prospring5/ch4/
        highschool/FoodProviderServiceImpl.java

package com.apress.prospring5.ch4.highschool;

import java.util.ArrayList;
import java.util.List;

import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;

public class FoodProviderServiceImpl implements FoodProviderService {
    @Override
    public List<Food> provideLunchSet() {
        List<Food> lunchSet = new ArrayList<>();
        lunchSet.add(new Food("Coke"));
        lunchSet.add(new Food("Hamburger"));
        lunchSet.add(new Food("French Fries"));

        return lunchSet;
    }
}

//chapter04/profiles/src/main/java/com/apress/prospring5/ch4/
        kindergarten/FoodProviderServiceImpl.java

package com.apress.prospring5.ch4.kindergarten;

import java.util.ArrayList;
import java.util.List;

import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;

public class FoodProviderServiceImpl implements FoodProviderService {
    @Override
    public List<Food> provideLunchSet() {
        List<Food> lunchSet = new ArrayList<>();
        lunchSet.add(new Food("Milk"));
        lunchSet.add(new Food("Biscuits"));

        return lunchSet;

    }
}

从前面的清单中,您可以看到这两个实现提供了相同的FoodProviderService接口,但是在午餐集中产生了不同的食物组合。因此,现在假设一所幼儿园希望供应商为他们的学生提供午餐套餐;让我们看看如何使用 Spring 的概要文件配置来实现这一点。我们将首先浏览 XML 配置。我们将创建两个 XML 配置文件,一个用于幼儿园配置文件,另一个用于高中配置文件。下面的配置片段描述了两个概要文件配置:

<!-- highschool-config.xml -->
<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd"
        profile="highschool">

    <bean id="foodProviderService"
      class="com.apress.prospring5.ch4.highschool.FoodProviderServiceImpl"/>
</beans>

<!-- kindergarten-config.xml -->
<beans 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd"
        profile="kindergarten">

    <bean id="foodProviderService"
      class="com.apress.prospring5.ch4.kindergarten.FoodProviderServiceImpl"/>
</beans>

在前两个配置中,注意在<beans>标签中分别使用了profile="kindergarten"profile="highschool"。它实际上告诉 Spring,只有当指定的概要文件处于活动状态时,文件中的那些 beans 才应该被实例化。现在让我们看看在独立应用中使用 Spring 的ApplicationContext时如何激活正确的配置文件。以下代码片段显示了测试程序:

package com.apress.prospring5.ch4;

import java.util.List;
import org.springframework.context.support.GenericXmlApplicationContext;

public class ProfileXmlConfigExample {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
              new GenericXmlApplicationContext();
        ctx.load("classpath:spring/*-config.xml");
        ctx.refresh();

        FoodProviderService foodProviderService =
            ctx.getBean("foodProviderService", FoodProviderService.class);

        List<Food> lunchSet = foodProviderService.provideLunchSet();

        for (Food food: lunchSet) {
            System.out.println("Food: " + food.getName());
        }

        ctx.close();
    }
}

ctx.load()方法将加载kindergarten-config.xmlhighschool-config.xml,因为我们将通配符作为前缀传递给该方法。在这个例子中,只有文件kindergarten-config.xml中的 beans 会根据profile属性被 Spring 实例化,这个属性是通过传递 JVM 参数-Dspring.profiles.active="kindergarten"激活的。使用这个 JVM 参数运行程序会产生以下输出:

Food: Milk
Food: Biscuits

这正是幼儿园提供者的实现将为午餐集产生的内容。现在,将前面清单中的配置文件参数更改为 high school ( -Dspring.profiles.active="highschool"),输出将更改如下:

Food: Coke
Food: Hamburger
Food: French Fries

您还可以通过调用ctx.getEnvironment().setActiveProfiles("kindergarten")以编程方式设置要在代码中使用的配置文件。此外,您可以通过向您的类添加@Profile注释,使用 Java Config 注册由概要文件启用的类。

使用 Java 配置的 Spring 概要文件

当然,有一种方法可以使用 Java configuration 来配置 Spring profiles,因为不喜欢 XML 配置的开发人员也应该感到满意。前一节中声明的 XML 文件必须替换为等效的 Java 配置类。下面是kindergarten概要文件的配置类:

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.FoodProviderService;

import com.apress.prospring5.ch4.kindergarten.FoodProviderServiceImpl;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration

@Profile("kindergarten")

public class KindergartenConfig {

        @Bean
        public FoodProviderService foodProviderService(){
                return new FoodProviderServiceImpl();
        }
}

如您所见,foodProviderService bean 是使用@Bean注释定义的。使用@Profile注释将该类标记为特定于kindergarten概要文件。显然,除了 bean 类型、配置文件名和类名之外,特定于highschool配置文件的类是相同的。

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.FoodProviderService;

import com.apress.prospring5.ch4.highschool.FoodProviderServiceImpl;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration

@Profile("highschool")

public class HighschoolConfig {

        @Bean
        public FoodProviderService foodProviderService(){
                return new FoodProviderServiceImpl();
        }
}

这些类的使用方式与 XML 文件相同。一个上下文声明使用它们两个,实际上只有其中一个被用来创建ApplicationContext实例,这取决于-Dspring.profiles.active JVM 选项的值。

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

import java.util.List;

public class ProfileJavaConfigExample {

        public static void main(String... args) {
                GenericApplicationContext ctx =
                     new AnnotationConfigApplicationContext(
                                KindergartenConfig.class,

                                HighschoolConfig.class);

                FoodProviderService foodProviderService =
                        ctx.getBean("foodProviderService",
                        FoodProviderService.class);

                List<Food> lunchSet = foodProviderService.provideLunchSet();
                for (Food food : lunchSet) {
                        System.out.println("Food: " + food.getName());
                }
                ctx.close();
        }
}

通过使用kindergarten作为 JVM 选项-Dspring.profiles.active的值运行前面的示例,可以打印出预期的输出。

Food: Milk
Food: Biscuits

还有一个用于配置已用概要文件的注释,它取代了-Dspring.profiles.active JVM 选项,但是这只能用于测试类。由于测试 Spring 应用已经在第十三章中介绍过了,这里就不详细介绍了。但是包含了一些示例代码。

package com.apress.prospring5.ch4.config;

import com.apress.prospring5.ch4.FoodProviderService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static  org.junit.Assert.assertTrue;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={KindergartenConfig.class,
    HighschoolConfig.class})

@ActiveProfiles("kindergarten")

public class ProfilesJavaConfigTest {

        @Autowired FoodProviderService foodProviderService;

        @Test
        public void testProvider(){
                assertTrue(foodProviderService.provideLunchSet() != null);
                assertFalse(foodProviderService.provideLunchSet().isEmpty());

                assertEquals(2, foodProviderService.provideLunchSet().size());
        }

}

正如您自己可能已经发现的,指定使用哪个概要文件来运行这个测试的注释是@ActiveProfiles("kindergarten")。在复杂的应用中,通常会有不止一个概要文件,而且更多的概要文件可以用来组成测试的上下文配置。这个类可以在任何 Java 智能编辑器中运行,并且在执行gradle clean build时自动运行。

使用配置文件的注意事项

Spring 中的 profiles 特性为开发人员创建了另一种方式来管理应用的运行配置,这在以前是在构建工具中完成的(例如,Maven 的 profile 支持)。构建工具依靠传递到工具中的参数将正确的配置/属性文件打包到 Java 归档文件(JAR 或 WAR,取决于应用类型)中,然后部署到目标环境中。Spring 的 profile 特性允许我们作为应用开发人员自己定义配置文件,并通过编程或传入 JVM 参数来激活它们。通过使用 Spring 的概要文件支持,您现在可以使用相同的应用归档文件,并通过在 JVM 启动期间将正确的概要文件作为参数传入来部署到所有环境中。例如,您可以拥有不同概要文件的应用,比如(dev, hibernate)(prd, jdbc)等等,每种组合代表运行环境(开发或生产)和要使用的数据访问库(Hibernate 或 JDBC)。它将应用概要管理引入到编程中。

但是这种方法也有缺点。例如,有些人可能认为,如果不小心处理,将不同环境的所有配置放入应用配置文件或 Java 类并将它们捆绑在一起将容易出错(例如,管理员可能忘记在应用服务器环境中设置正确的 JVM 参数)。将所有概要文件打包在一起也会使包比平常大一点。同样,让应用和配置需求驱动您选择最适合您的项目的方法。

环境和属性资源抽象

要设置激活的配置文件,我们需要访问Environment界面。Environment接口是一个抽象层,用于封装正在运行的 Spring 应用的环境。

除了概要文件之外,Environment接口封装的其他关键信息是属性。属性用于存储应用的基础环境配置,如应用文件夹的位置、数据库连接信息等。

Spring 中的EnvironmentPropertySource抽象特性帮助开发人员从运行平台访问各种配置信息。在抽象下,所有系统属性、环境变量和应用属性都由Environment接口提供服务,Spring 在引导ApplicationContext时会填充这个接口。以下代码片段显示了一个简单的示例:

package com.apress.prospring5.ch4;

import java.util.HashMap;
import java.util.Map;

import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;

public class EnvironmentSample {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
             new GenericXmlApplicationContext();
        ctx.refresh();

        ConfigurableEnvironment env = ctx.getEnvironment();
        MutablePropertySources propertySources = env.getPropertySources();

        Map<String,Object> appMap = new HashMap<>();
        appMap.put("user.home", "application_home");

        propertySources.addLast(new MapPropertySource("prospring5_MAP", appMap));

        System.out.println("user.home: " + System.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));

        System.out.println("user.home: " + env.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + env.getProperty("JAVA_HOME"));

        ctx.close();
    }
}

在前面的代码片段中,在ApplicationContext初始化之后,我们获得了对ConfigurableEnvironment接口的引用。通过该接口,可以获得一个对MutablePropertySources(PropertySources接口的默认实现,它允许对包含的属性源进行操作)的句柄。之后,我们构造一个映射,将应用属性放入映射中,然后用映射构造一个MapPropertySource类(一个从Map实例中读取键和值的PropertySource子类)。最后,通过addLast()方法将MapPropertySource类添加到MutablePropertySources中。运行程序,打印出以下内容:

user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
application.home:  application_home

对于前两行,检索 JVM 系统属性user.home和环境变量JAVA_HOME,和以前一样(通过使用 JVM 的System类)。然而,对于最后三行,您可以看到所有的系统属性、环境变量和应用属性都可以通过Environment接口访问。您可以看到Environment抽象如何帮助我们管理和访问应用运行环境中的各种属性。

对于PropertySource抽象,Spring 将按照以下默认顺序访问属性:

  • 正在运行的 JVM 的系统属性
  • 环境变量
  • 应用定义的属性

例如,假设我们定义了同一个应用属性user.home,并通过MutablePropertySources类将其添加到Environment接口。如果您运行该程序,您仍然会看到user.home是从 JVM 属性中检索的,而不是您的。然而,Spring 允许您控制Environment检索属性的顺序。以下代码片段显示了修订后的版本:

package com.apress.prospring5.ch4;

import java.util.HashMap;
import java.util.Map;

import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;

public class EnvironmentSample {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.refresh();

        ConfigurableEnvironment env = ctx.getEnvironment();
        MutablePropertySources propertySources = env.getPropertySources();

        Map<String,Object> appMap = new HashMap<>();
        appMap.put("application.home", "application_home");

        propertySources.addFirst(new MapPropertySource("prospring5_MAP", appMap));

        System.out.println("user.home: " + System.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));

        System.out.println("user.home: " + env.getProperty("user.home"));
        System.out.println("JAVA_HOME: " + env.getProperty("JAVA_HOME"));

        ctx.close();
    }
}

在前面的代码示例中,我们定义了一个应用属性,也称为user.home,并通过MutablePropertySources类的addFirst()方法将其添加为第一个要搜索的属性。当您运行该程序时,您将看到以下输出:

user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
user.home: application_home
JAVA_HOME: /home/jules/bin/java

前两行保持不变,因为我们仍然使用 JVM System类的getProperty()getenv()方法来检索它们。然而,当使用Environment接口时,您会看到我们定义的user.home属性优先,因为我们将其定义为第一个搜索属性值的属性。

在现实生活中,您很少需要直接与Environment接口交互,而是会使用一个以${}形式的属性占位符(例如${application.home})并将解析后的值注入到 Spring beans 中。让我们来看看实际情况。假设我们有一个类来存储从属性文件加载的所有应用属性。下面显示了AppProperty类:

package com.apress.prospring5.ch4;

public class AppProperty {
    private String applicationHome;
    private String userHome;

    public String getApplicationHome() {
        return applicationHome;
    }

    public void setApplicationHome(String applicationHome) {
        this.applicationHome = applicationHome;
    }

    public String getUserHome() {
        return userHome;
    }

    public void setUserHome(String userHome) {
        this.userHome = userHome;
    }
}

在这里你可以看到application.properties文件的内容:

application.home=application_home
user.home=/home/jules-new

注意,属性文件还声明了user.home property。让我们来看看 Spring XML 配置;参见下面的代码(app-context-xml.xml):

<beans 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder
         location="classpath:application.properties"/>

    <bean id="appProperty" class="com.apress.prospring5.ch4.AppProperty"
          p:applicationHome="${application.home}"
          p:userHome="${user.home}"/>
</beans>

我们使用<context:property-placeholder>标签将属性加载到 Spring 的Environment中,后者被包装到ApplicationContext接口中。我们还使用 SpEL 占位符将值注入到AppProperty bean 中。以下代码片段显示了测试程序:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;
public class PlaceHolderDemo {
    public static void main(String... args) {
        GenericXmlApplicationContext ctx =
            new GenericXmlApplicationContext();
        ctx.load("classpath:spring/app-context-xml.xml");
        ctx.refresh();

        AppProperty appProperty = ctx.getBean("appProperty",
           AppProperty.class);

        System.out.println("application.home: " +
            appProperty.getApplicationHome());
        System.out.println("user.home: " +
           appProperty.getUserHome());

        ctx.close();
    }
}

让我们运行程序,您将看到以下输出:

application.home: application_home
user.home: /Users/jules

您将看到application.home占位符被正确解析,而user.home属性仍然从 JVM 属性中检索,这是正确的,因为它是PropertySource抽象的默认行为。为了指示 Spring 优先考虑application.properties文件中的值,我们将属性local-override="true"添加到<context:property-placeholder>标签中。

<context:property-placeholder local-override="true"
    location="classpath:env/application.properties"/>

属性指示 Spring 用这个占位符中定义的属性覆盖现有的属性。运行该程序,您将会看到现在已经从application.properties文件中检索到了user.home属性。

application.home: application_home
user.home: /home/jules-new

使用 JSR-330 注释的配置

正如我们在第一章中所讨论的,JEE 6 提供了对 JSR-330(Java 依赖注入)的支持,这是一个注释集合,用于在 JEE 容器或其他兼容的 IoC 框架中表达应用的 DI 配置。Spring 也支持和识别这些注释,所以尽管您可能没有在 JEE 6 容器中运行您的应用,您仍然可以在 Spring 中使用 JSR-330 注释。使用 JSR-330 注释可以帮助您轻松地从 Spring 迁移到 JEE 6 容器或其他兼容的 IoC 容器(例如 Google Guice)。

同样,让我们以消息呈现器和消息提供器为例,使用 JSR-330 注释来实现它。为了支持 JSR-330 注释,您需要向项目添加javax.inject 3 依赖项。

下面的代码片段显示了MessageProviderConfigurableMessageProvider的实现:

 //chapter04/jsr330/src/main/java/com/apress/prospring5/
     ch4/MessageProvider.java
package com.apress.prospring5.ch4;

public interface MessageProvider {
    String getMessage();
}
//chapter04/jsr330/src/main/java/com/apress/prospring5/
   ch4/ConfigurableMessageProvider.java
package com.apress.prospring5.ch4;

import javax.inject.Inject;
import javax.inject.Named;

@Named("messageProvider")
public class ConfigurableMessageProvider
    implements MessageProvider {
    private String message = "Default  message";

    public ConfigurableMessageProvider() {
    }

    @Inject

    @Named("message")
    public ConfigurableMessageProvider(String message) {
        this.message = message;
    }

    public void setMessage(String message) {
        this.message =  message;
    }

    public String getMessage() {

        return  message;
    }
}

你会注意到所有的注释都属于javax.inject package,这是 JSR-330 标准。这个类在两个地方使用了@Named。首先,它可以用来声明一个可注入的 bean(与 Spring 中的@Component注释或其他原型注释相同)。在清单中,@Named("messageProvider")注释指定ConfigurableMessageProvider是一个可注入的 bean,并将其命名为messageProvider,这与 Spring 的<bean>标签中的name属性相同。其次,我们通过在接受字符串值的构造函数前使用@Inject注释来使用构造函数注入。然后,我们使用@Named来指定我们想要注入被赋予名称message的值。让我们继续看一下MessageRenderer接口和StandardOutMessageRenderer实现。

 //chapter04/jsr330/src/main/java/com/apress/prospring5/
    ch4/MessageRenderer.java
package com.apress.prospring5.ch4;

public interface MessageRenderer {
    void render();
    void setMessageProvider(MessageProvider provider);
    MessageProvider getMessageProvider();
}
//chapter04/jsr330/src/main/java/com/apress/prospring5/
    ch4/StandardOutMessageRenderer.java
package com.apress.prospring5.ch4;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

@Named("messageRenderer")
@Singleton
public class StandardOutMessageRenderer
    implements MessageRenderer {

    @Inject

    @Named("messageProvider")
    private MessageProvider messageProvider =  null;

    public void render() {
        if (messageProvider == null) {
            throw new RuntimeException(
                "You must set the property messageProvider of class:"
                + StandardOutMessageRenderer.class.getName());
        }

        System.out.println(messageProvider.getMessage());
    }

    public void setMessageProvider(MessageProvider provider) {
        this.messageProvider = provider;
    }

    public MessageProvider getMessageProvider()  {
        return this.messageProvider;
    }
}

在前面的代码片段中,我们使用了@Named来定义它是一个可注入的 bean。请注意@Singleton注释。值得注意的是,在 JSR-330 标准中,bean 的默认作用域是非 singleton,这类似于 Spring 的prototype作用域。因此,在 JSR-330 环境中,如果您希望您的 bean 是单例的,您需要使用@Singleton注释。然而,在 Spring 中使用这个注释实际上没有任何效果,因为 Spring 的 bean 实例化的默认范围已经是 singleton 了。我们把它放在这里只是为了演示,值得注意的是 Spring 和其他 JSR-330 兼容容器之间的区别。

对于messageProvider属性,我们这次使用@Inject进行 setter 注入,并指定名为messageProvider的 bean 应该用于注入。下面的配置片段为应用定义了一个简单的 Spring XML 配置(app-context-annotation.xml):

<beans ...>

    <context:component-scan
         base-package="com.apress.prospring5.ch4"/>

    <bean id="message" class="java.lang.String">
        <constructor-arg value="Gravity is  working against me"/>
   </bean>
</beans>

使用 JSR-330 不需要任何特殊标签;就像普通的 Spring 应用一样配置您的应用。我们使用<context:component-scan>来指示 Spring 扫描 DI 相关的注释,Spring 将识别这些 JSR-330 注释。我们还声明了一个名为message的 Spring bean,用于将构造函数注入到ConfigurableMessageProvider类中。以下代码片段显示了测试程序:

package com.apress.prospring5.ch4;

import org.springframework.context.support.GenericXmlApplicationContext;

public class Jsr330Demo {

        public static void main(String... args) {
                GenericXmlApplicationContext  ctx  =
                    new GenericXmlApplicationContext();
                ctx.load("classpath:spring/app-context-annotation.xml");
                ctx.refresh();

                MessageRenderer renderer = ctx.getBean("messageRenderer",
                    MessageRenderer.class);
                renderer.render();

                ctx.close();
        }
}

运行该程序会产生以下输出:

Gravity is working against me

通过使用 JSR-330,您可以轻松地迁移到其他 JSR-330 兼容的 IoC 容器(例如,JEE 6 兼容的应用服务器或其他 DI 容器,如 Google Guice)。然而,Spring 的注释比 JSR-330 注释更加丰富和灵活。这里强调了一些主要差异:

  • 使用 Spring 的@Autowired批注时,可以指定一个required属性来表示必须满足 DI(也可以使用 Spring 的@Required批注来声明这个要求),但是对于 JSR-330 的@Inject批注就没有这样的等价物了。此外,Spring 提供了@Qualifier注释,允许对 Spring 进行更细粒度的控制,以基于限定符名称执行依赖关系的自动连接。
  • JSR-330 只支持 singleton 和非 singleton bean 作用域,而 Spring 支持更多的作用域,这对 web 应用很有用。
  • 在 Spring 中,您可以使用@Lazy注释来指示 Spring 仅在应用请求时实例化 bean。在 JSR 没有这样的对等物-330。

您还可以在同一个应用中混合搭配 Spring 和 JSR-330 注释。但是,建议您选择其中一种来保持应用的一致风格。一种可能的方法是尽可能多地使用 JSR-330 注释,并在需要时使用 Spring 注释。然而,这给你带来的好处很少,因为在迁移到另一个 DI 容器时,你仍然需要做相当多的工作。总之,推荐使用 Spring 的注释方法,而不是 JSR-330 注释,因为 Spring 的注释更强大,除非你的应用需要独立于 IoC 容器。

使用 Groovy 进行配置

Spring Framework 4.0 的新特性是能够使用 Groovy 语言配置 bean 定义和ApplicationContext。这为开发人员提供了另一种配置选择,可以替换或补充 XML 和/或基于注释的 bean 配置。Spring ApplicationContext可以直接在 Groovy 脚本中创建,也可以从 Java 加载,都是通过GenericGroovyApplicationContext类的方式。首先,让我们通过展示如何从外部 Groovy 脚本创建 bean 定义并从 Java 加载它们来深入了解细节。在前面的章节中,我们介绍了各种 bean 类,为了提高代码的可重用性,我们将在这个例子中使用在第三章中介绍的Singer类。以下代码片段显示了该类的内容:

package com.apress.prospring5.ch3.xml;

public class Singer {
    private String name;
    private int  age;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString()  {
        return "\tName: " + name + "\n\t" + "Age: " + age;
    }
}

正如您所看到的,这只是一个 Java 类,带有几个描述歌手的属性。我们在这里使用这个简单的 Java 类来说明,仅仅因为您在 Groovy 中配置了 bean,并不意味着您的整个代码库都需要用 Groovy 重写。不仅如此,Java 类还可以从依赖项中导入,并在 Groovy 脚本中使用。现在,让我们创建 Groovy 脚本(beans.groovy),它将用于创建 bean 定义,如前面的代码片段所示:

package com.apress.prospring5.ch4

import com.apress.prospring5.ch3.xml.Singer

beans {
    singer(Singer, name: 'John Mayer', age: 39)
}

这个 Groovy 脚本从名为beans的顶级闭包开始,它向 Spring 提供 bean 定义。首先,我们指定 bean 名称(singer),然后作为参数,我们提供类类型(Singer),后跟我们想要设置的属性名称和值。接下来,让我们用 Java 创建一个简单的测试驱动程序,从 Groovy 脚本加载 bean 定义,如下所示:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch3.xml.Singer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericGroovyApplicationContext;

public class GroovyBeansFromJava {
    public static void main(String... args) {
        ApplicationContext context =
            new GenericGroovyApplicationContext("classpath:beans.groovy");
        Singer singer = context.getBean("singer", Singer.class);
        System.out.println(singer);
    }
}

如您所见,ApplicationContext的创建是以典型的方式进行的,但是它是通过使用GenericGroovyApplicationContext类并提供构建 bean 定义的 Groovy 脚本来完成的。

在运行本节中的示例之前,您需要向这个项目添加一个依赖项:groovy-all库。这里显示了项目groovy-config-javabuild.gradle配置文件内容:

apply plugin: 'groovy'

dependencies {
        compile misc.groovy
        compile project(':chapter03:bean-inheritance')
}

compile project(':chapter03:bean-inheritance')行指定章节 3项目必须被编译并作为该项目的依赖项使用。这是包含Singer类的项目。

Gradle 配置文件使用 Groovy 语法,misc.groovy引用父项目build.gradle文件中定义的misc数组的groovy属性。这里显示了该文件内容的一个片段(与 Groovy 相关的配置):

ext {
        springVersion =  '5.0.0.M4'
        groovyVersion =  '2.4.5'
...

        misc = [
                ...
                    groovy: "org.codehaus.groovy:groovy-all:$groovyVersion"
        ]
...
}

运行GroovyBeansFromJava类会产生以下输出:

Name: John Mayer
Age: 39

既然您已经看到了如何通过外部 Groovy 脚本从 Java 加载 bean 定义,那么我们如何仅从 Groovy 脚本创建ApplicationContext和 bean 定义呢?让我们看看这里列出的 Groovy 代码(GroovyConfig.groovy):

package com.apress.prospring5.ch4

import com.apress.prospring5.ch3.xml.Singer
import org.springframework.context.support.GenericApplicationContext
import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader

def ctx = new GenericApplicationContext()
def reader = new GroovyBeanDefinitionReader(ctx)

reader.beans {
    singer(Singer, name: 'John Mayer', age: 39)
}

ctx.refresh()

println ctx.getBean("singer")

当我们运行这个示例时,我们得到和以前一样的输出。这一次我们创建了一个典型的GenericApplicationContext的实例,但是使用了GroovyBeanDefinitionReader,它将用于向传递 bean 定义。然后,和前面的例子一样,我们从简单的 POJO 创建一个 bean,刷新ApplicationContext,并打印Singer bean 的字符串表示。没有比这更简单的了!

正如您可能已经知道的,我们只是触及了 Spring 中 Groovy 支持的皮毛。因为您拥有 Groovy 语言的全部功能,所以在创建 bean 定义时,您可以做各种有趣的事情。因为您拥有对ApplicationContext的完全访问权,所以您不仅可以配置 beans,还可以使用概要文件支持、属性文件等等。请记住,权力越大,责任越大。

Spring Boot

到目前为止,您已经学习了多种配置 Spring 应用的方法。无论是 XML、注释、Java 配置类、Groovy 脚本,还是所有这些的混合,现在您应该对如何实现有了一个基本的概念。但是如果我们告诉你有比这更酷的东西呢?

Spring Boot 项目旨在简化使用 Spring 构建应用的入门体验。Spring Boot 消除了手动收集依赖关系的猜测,并提供了大多数应用所需的一些最常见的功能,如指标和健康检查。

Spring Boot 采取了一种“固执己见”的方法,通过为已经包含适当依赖项和版本的各种类型的应用提供起始项目来实现开发人员简化的目标,这意味着花费更少的时间来开始。对于那些希望完全摆脱 XML 的人来说,Spring Boot 不要求任何配置都用 XML 编写。

在这个例子中,我们将创建一个传统的 Hello World web 应用。与典型的 Java web 应用设置相比,您可能会惊讶地发现这样做只需要很少的代码。通常,我们通过定义需要添加到项目中的依赖项来开始示例。Spring Boot 的简化模型的一部分是为你准备所有的依赖项,例如,当使用 Maven 时,你作为开发者利用一个父 POM 来获得这个功能。当使用 Gradle 时,事情变得更加简单。除了 Gradle 插件和 starter 依赖项之外,不需要任何父插件。在下面的例子中,我们将创建一个 Spring 应用,列出上下文中的所有 bean,然后访问helloWorld bean。这里描述了boot-simple项目的梯度配置:

buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
        maven { url "http://repo.spring.io/release" }
        maven { url "http://repo.spring.io/milestone" }
        maven { url  "http://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/libs-snapshot" }
    }
    dependencies {
        classpath boot.springBootPlugin

    }
}

apply plugin: 'org.springframework.boot'

dependencies {
    compile boot.starter
}

boot.springBootPlugin行引用了父项目的build.gradle文件中定义的boot数组的springBootPlugin属性。此处描述了该文件内容的一个片段(仅与 Spring Boot 相关的配置):

ext {
    bootVersion = '2.0.0.BUILD-SNAPSHOT'

    ...
    boot = [
      springBootPlugin:
            "org.springframework.boot:spring-boot-gradle-plugin:$bootVersion", starter    :
            "org.springframework.boot:spring-boot-starter:$bootVersion", starterWeb    :
            "org.springframework.boot:spring-boot-starter-web:$bootVersion"
    ]
    ...
}

在撰写本文时,Spring Boot 版本 2.0.0 还没有发布。这就是为什么版本是2.0.0.BUILD-SNAPSHOT,我们需要在配置中添加 Spring 快照库 https://repo.spring.io/libs-snapshot 。很有可能在这本书之后,会发布一个官方版本。

Spring Boot 的每个版本都提供了它所支持的依赖项的精选列表。选择必要库的版本,使 API 完全匹配,这是由 Spring Boot 处理的。因此,不需要手动配置依赖项版本。升级 Spring Boot 将确保这些依赖项也得到升级。在前面的配置中,一组依赖项将被添加到项目中,每个依赖项都有适当的版本,这样它们的 API 将是兼容的。在像 IntelliJ IDEA 这样的智能编辑器中,有一个 Gradle Projects 视图,您可以在其中展开每个模块并检查可用的任务和依赖项,如图 4-3 所示。

A315511_5_En_4_Fig3_HTML.jpg

图 4-3。

Gradle Projects view of the boot-simple project

现在设置已经就绪,让我们创建类。这里显示了HelloWorld类:

package com.apress.prospring5.ch4;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class HelloWorld {

        private static Logger logger =
             LoggerFactory.getLogger(HelloWorld.class);

        public void sayHi() {
                logger.info("Hello World!");
        }
}

这没有什么特别或复杂的;它只是一个带有方法和 bean 声明注释的类。让我们看看如何使用 Spring Boot 构建一个 Spring 应用,并创建一个包含这个 bean 的ApplicationContext:

package com.apress.prospring5.ch4;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.Arrays;

@SpringBootApplication
public class Application {

        private static Logger logger = LoggerFactory.getLogger(Application.class);

        public static void main(String args) throws Exception {
                ConfigurableApplicationContext ctx =
                    SpringApplication.run(Application.class, args);
                assert (ctx != null);
                logger.info("The beans you were looking for:");

                // listing all bean definition    names
                Arrays.stream(ctx.getBeanDefinitionNames()).forEach(logger::info);

                HelloWorld hw = ctx.getBean(HelloWorld.class);
                hw.sayHi();

                System.in.read();
                ctx.close();

        }
}

仅此而已。真的。这门课本来可以更小,但是我们想向你们展示如何做一些额外的事情。让我们把它们都包括进去。

  • 检查我们有一个上下文:assert语句用于测试你的假设ctx不是null
  • 设置日志记录:Spring Boot 附带了一组日志记录库,所以我们只需将我们想要使用的配置放在resources目录下。在我们的例子中,我们选择了logback
  • 列出上下文中的所有 bean 定义:使用 Java 8 lambda 表达式,可以在一行中列出上下文中的所有 bean 定义。因此,我们添加了这一行,这样您就可以看到 Spring Boot 为您自动配置了哪些 beans。在列表中,您也可以找到helloWorld bean。
  • 确认退出:不使用System.in.read();方法,应用打印 beans 名称,打印HelloWorld,然后退出。我们添加了这个调用,所以应用将等待开发人员按下一个键,然后退出。

这里的新奇之处在于@SpringBootApplication注释。这个注释是一个顶级注释,只设计用于类级别。这是一个方便的注释,相当于声明以下三个:

  • @Configuration:将该类标记为可以用@Bean声明 beans 的配置类。
  • @EnableAutoConfiguration:这是来自包org.springframework.boot.autoconfigure的一个特定的 Spring Boot 注释,它可以启用 Spring ApplicationContext,试图根据指定的依赖关系猜测和配置您可能需要的 beans。@EnableAutoConfiguration与 Spring 提供的启动器依赖项配合得很好,但它并不直接与它们绑定,因此可以使用启动器之外的其他依赖项。例如,如果类路径上有一个特定的嵌入式服务器,就会使用它,除非项目中有另一个EmbeddedServletContainerFactory配置。
  • 我们可以声明用原型注释标注的类,它们将成为某种类型的 beans。用于列出与@SpringBootApplication一起使用的要扫描的包的属性是basePackages。在 1.3.0 版本中,组件扫描增加了另一个属性:basePackageClasses。这个属性提供了一个类型安全的选择来代替basePackages来指定要扫描带注释组件的包。将扫描每个指定类别的包。

如果@SpringBootApplication注释没有定义组件扫描属性,它将只扫描用它注释的类所在的包。这就是为什么在这里给出的例子中,找到了helloWorld bean 定义,并创建了 bean。

以前的 Spring Boot 应用是一个简单的控制台应用,带有一个开发人员定义的 bean 和一个成熟的开箱即用的环境。但是 Spring Boot 也为 web 应用提供了启动依赖。要使用的依赖项是spring-boot-starter-web,在图 4-4 中你可以看到这个引导启动库的可传递依赖项。在boot-web项目中,HelloWorld类是一个 Spring 控制器,这是一个用于创建 Spring web bean 的特殊类型的类。这是一个典型的 Spring MVC 控制器类,你将在第十六章中了解到。

A315511_5_En_4_Fig4_HTML.jpg

图 4-4。

Gradle Projects view of the boot-web project

package com.apress.prospring5.ch4.ctrl;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController

public class HelloWorld {

        @RequestMapping("/")
         public String sayHi() {
                 return "Hello World!";
         }
}

用于声明 Spring web beans 的注释是对@Component的专门化:即@Controller注释。这些类型的类包含用@RequestMapping注释的方法,这些方法被映射到某个请求 URL。您在示例@RestController中看到的注释是出于实际原因而使用的。它是用于 REST 服务的@Controller注释。将helloWorld bean 公开为 REST 服务在这里很有用,因为您不必创建一个带有用户界面和其他 web 组件的成熟的 web 应用,这将污染本节的基本思想。这里介绍的所有 web 组件都包含在第十六章中。

接下来,我们使用一个简单的main()方法创建您的引导类,如下所示:

package com.apress.prospring5.ch4;

import com.apress.prospring5.ch4.ctrl.HelloWorld;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.Arrays;

@SpringBootApplication(scanBasePackageClasses = HelloWorld.class)
public class WebApplication {

        private static Logger logger =
            LoggerFactory.getLogger(WebApplication.class);

        public static void main(String args)  throws Exception {
                ConfigurableApplicationContext ctx =
                    SpringApplication.run(WebApplication.class, args);
                assert (ctx != null);
                logger.info("Application started...");

                System.in.read();
                ctx.close();
        }
}

因为HelloWorld控制器是在与WebApplication类不同的包中声明的,所以我们创造了一个机会来描述如何使用scanBasePackageClasses属性。

此时,您可能会问自己:web.xml配置文件和我必须为基本 web 应用创建的所有其他组件在哪里?您已经在前面的清单中定义了所有需要的东西!不信?编译项目并运行Application类,直到您看到表明应用已经启动的日志消息。如果您查看生成的日志文件,您会看到这么少的代码发生了很多事情。最值得注意的是,看起来 Tomcat 正在运行,并且已经为您定义了各种端点,比如健康检查、环境输出信息和指标。首先导航到http://localhost:8080,你会看到 Hello World 网页如预期显示。接下来看看一些预配置的端点(例如,http://localhost:8080/health,它返回应用状态的 JSON 表示)。更进一步,加载http://localhost:8080/metrics以更好地理解正在收集的各种度量,比如堆大小、垃圾收集等等。

仅仅从这个例子中你就可以看出,Spring Boot 从根本上简化了你创建任何类型的应用的方式。必须配置大量文件才能运行一个简单的 web 应用的日子已经一去不复返了,有了准备好为您的 web 应用服务的嵌入式 servlet 容器,一切都“正常工作”

虽然我们已经向您展示了一个简单的示例,但是请记住,Spring Boot 并不限制您使用它选择的内容;它只是采取“固执己见”的方法,为您选择默认值。如果您不想使用嵌入式 Tomcat,而是使用 Jetty,只需修改配置文件,从spring-boot-starter-web dependency中排除 Tomcat starter 模块。利用 Gradle Projects 视图是一种帮助您可视化项目中引入了哪些依赖项的方法。Spring Boot 还为其他类型的应用提供了许多其他 starter 依赖项,我们鼓励您通读文档以了解更多详细信息。

更多关于 Spring Boot 的信息,请查看其项目页面 http://projects.spring.io/spring-boot/

摘要

在本章中,您看到了大量 Spring 特有的特性,它们补充了核心 IoC 功能。您看到了如何挂钩到一个 bean 的生命周期,并让它知道 Spring 环境。我们引入了FactoryBean s 作为 IoC 的解决方案,支持更广泛的类集合。我们还展示了如何使用PropertyEditor来简化应用配置,并消除对人工String类型属性的需求。我们向您展示了使用 XML、注释和 Java 配置定义 beans 的多种方法。此外,我们还深入了解了由ApplicationContext提供的一些附加特性,包括 i18n、事件发布和资源访问。

我们还讨论了一些特性,比如使用 Java 类和新的 Groovy 语法代替 XML 配置、概要文件支持以及环境和属性源抽象层。最后,我们讨论了在 Spring 中使用 JSR-330 标准注释。

锦上添花的是如何使用 Spring Boot 来配置 beans 并尽快轻松启动您的应用。

到目前为止,我们已经介绍了 Spring 框架的主要概念及其作为阿迪容器的特性,以及核心 Spring 框架提供的其他服务。在下一章及以后,我们将讨论在特定领域使用 Spring,比如 AOP、数据访问、事务支持和 web 应用支持。

Footnotes 1

看看这段来自 JEE 官方 Javadoc 的片段: http://docs.oracle.com/javaee/7/api/javax/annotation/PostConstruct.html

  2

这些类不会在com.apress.prospring5.ch4包中再次创建,但是定义它们的项目被用作java-config项目的依赖项。

  3

您可以在 Maven 公共存储库中找到依赖特性,比如组 id 和最新版本。例如,这是java.inject : https://mvnrepository.com/artifact/javax.inject/javax.inject 的专用页面。