Green Beans: Putting the Spring in Your Step (and Application)

工程 | Josh Long | 2010年11月9日 | ...

Spring 框架在 2003 年成为事实上的标准,并从那时起一直帮助人们用更整洁的代码构建更大、更好的应用程序。在这篇博客文章中,我们将讨论使用 Spring 组件模型配置应用程序的可用选项。我们将从最简单的形式开始,构建一个简单的应用程序,然后对其进行重构,以利用 Spring 框架中的许多简化特性,正是这些特性使其成为当今应用程序的事实标准,并将继续如此。

现代企业 Java 应用程序有许多协作对象,它们协同工作以实现一个目标,通常是具有内在商业价值的目标。即使在简单的情况下,这个对象图也很深。例如,一个简单的服务(也许是一个支持客户数据的服务?)想要与数据库通信。这样的服务需要一个数据源,并且可选地需要一些便捷库来简化数据库访问,无论是通过 JDBC、JPA、JDO、NoSQL 选项等。在这样的对象图中,很容易在使用时即时创建对象。在这种系统中,对象构造或获取的知识散布在使用它们的 Bean 中。如果(正如数据库 javax.sql.DataSource 的情况)多个地方需要同一个对象,那么最好在一个地方设置所有对象,然后共享新创建的实例。这样做的好处是,将容易出错和易变的配置信息集中在一个地方,方便修改(例如,开发和生产环境的数据库凭据不同时)。

这是人们使用 Spring 的主要原因之一——因为 Spring 使人们能够集中描述这些协作对象。从 Spring 的早期版本开始,就有一个 XML 文件用于描述对象图。早期(大约 2003 年)这个文件使用 DTD,但现在使用 XML Schema。这里是使用 Spring 的 XML 格式描述我们的示例服务。随着我们继续,我们将移除越来越多的 XML 配置。每个 bean 元素描述一个将要创建并给定一个 id 的对象。每个 property 元素描述对象上的一个 setter 方法以及应该赋予它的值。这些 setter 方法由 Spring 应用程序容器为您调用。


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/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">

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="ds">
        <property name="driverClassName" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:~/cs"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
      <constructor-arg ref="ds"/>
    </bean>

    <bean class="org.springframework.samples.DatabaseCustomerService" id="databaseCustomerService">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>

    <bean class="org.springframework.samples.CustomerClient" id="client">
        <property name="customerService" ref="databaseCustomerService"/>
    </bean>

</beans>

很简单,对吧?好处是我们可以针对接口编写代码,而无需了解具体的实现。在上面的示例中,我们实例化了一个 DriverManagerDataSource,并使用 ref 属性将其传递给 JdbcTemplate 实例的构造函数。ref 告诉 Spring 框架您希望传递对在同一容器中配置的另一个 Bean 的引用。类似地,在我们的示例中,我们传递对 CustomerClient 实例的引用,但在消费的 Java 代码中,我们针对 CustomerService 接口编写代码,而不是特定的类型 DatabaseCustomerService

这种简单的设置为我们提供了很多间接性和灵活性。现在对象创建和构造已移至 Spring 配置中,我们可以在 Spring 配置中隐藏真正复杂的设置,而让我们的代码对此一无所知。隐藏复杂构造逻辑的一种常见方法是通过工厂模式。如果您正在构造许多需要协同工作的对象,或者如果您想在创建对象时考虑许多不同的因素,工厂模式特别有用。本质上,您正在提供一种比任何单个类的构造函数自然能做到的更强大的方式来描述对象的创建。Spring 明确支持这种模式。如果配置的 Bean 实现了 org.springframework.beans.factory.FactoryBean 接口,则将调用该接口上的 getObject() 方法,其结果就是 Spring 上下文中可用的对象。Spring 框架本身广泛使用这种做法,以可重用的方式提供方便的方式来构造复杂的对象图。

在我们的示例中,我们使用了一个名为 H2 的嵌入式数据库,它是一个强大的内存 Java 数据库。通常,嵌入式数据库用于开发环境。一种常见的做法是在开发中使用嵌入式数据库来快速测试和重置数据集。通常,这还涉及从 SQL 脚本加载数据以引导嵌入式数据库。Spring 框架明确支持配置嵌入式数据源,然后针对 javax.sql.DataSource 评估脚本。

Spring 3.1 中的环境特定 Bean - 在 Spring 框架的下一次迭代中,我们将引入对*环境特定* Bean 的支持。环境特定 Bean 提供了一种更直接的方式来根据某些环境开关是否为真来“开启”Bean。这在一些定义明确的场景中非常有用,例如 Bean 配置在不同环境中差异很大。这里使用嵌入式数据库进行测试,而在生产中使用另一个 javax.sql.DataSource 的示例就是这种情况。当然,使用 Spring 3.0 或更早版本,还有其他方法可以实现同样的灵活性,包括 FactoryBeanPropertyPlaceHolderConfigurer

回到我们之前的示例,我们可能会像这样声明我们的 javax.sql.DataSource,名称为 ds

    <bean id="ds" class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean">
        <property name="databaseType" value="H2"/>
        <property name="databasePopulator">
            <bean class="org.springframework.jdbc.datasource.init.ResourceDatabasePopulator">
                <property name="scripts" value="setup.sql"/>
            </bean>
        </property>
    </bean> 

这个 FactoryBean 负责读取任何 SQL 脚本(我这里只指定了一个名为 setup.sql 的文件,但您可以指定任意多个,用逗号分隔)并将它们加载到数据库中,然后以一种简单方便的方式返回一个 javax.sql.DataSource 实例。

我们已经看到了 FactoryBean 的强大功能。一个精心设计的 FactoryBean 将公开在设置对象时最可能派上用场的选项,并且在运行时,它还会提供关于各种选项无效配置的反馈。然而,通过 XML Schema 提供的验证,XML 可以在设计时提供更多指导。这就是为什么 Spring 框架长期以来一直支持使用基于 XML Schema 的命名空间,这些命名空间可以实现更多的反馈和简化。让我们回顾一下嵌入式数据库的示例。Spring 框架提供了一个用于配置嵌入式数据源的命名空间。要在 Spring 中使用命名空间,您只需限定命名空间并添加对 schemaLocation 元素的引用。之前的 Spring 配置文件,如果修改为支持 JDBC XML 命名空间,看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
  <!-- ...  same as before ... -->
</beans>

这个声明将使命名空间中符合条件的元素在任何现代 IDE 的 XML 自动完成功能中得到提示。安装了它之后,我们现在可以将之前的声明替换为更简洁的形式。

   <jdbc:embedded-database id="ds" type="H2">
        <jdbc:script location="classpath:setup.sql"/>
   </jdbc:embedded-database>

这在功能上等同于前面的示例:它创建一个嵌入式数据库,并在启动嵌入式数据库时评估脚本的内容。它创建一个类型为 javax.sql.DataSource 的对象,就像之前一样。我们巧妙地将嵌入式数据库简化到了其本质。看起来我们的工作已经完成,我们可以继续了,对吗?嗯,并非如此。我们还可以做更多的事情来进一步消除配置。这里显示的一些代码是我们自己编写的自定义代码。如果我们愿意对代码进行注解,那么我们可以让 Spring 为我们自动处理,而不是必须明确地写出来。为此,我们需要将 context 命名空间添加到我们的文件中,并启用组件扫描。组件扫描会扫描带有特定注解的 Bean 并自动注册它们。类似地,在类本身上发现的注解也会被处理。这是修改后的 XML 文件,其中包含了适当的 XML 命名空间。


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:context="http://www.springframework.org/schema/context"
 	  xsi:schemaLocation="... http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springframework.samples"/>
    <!-- ...  same as before ... -->
</beans>

我们在 org.springframework.samples 包中用 Spring 框架的 @Component 注解标记的所有 Bean 都将被 Spring 上下文拾取并注册为 Bean,就像我们用 XML 中的 bean 元素配置它们一样。我们已经在 DatabaseCustomerServiceCustomerClient 类上用 @Component 进行了注解,这使得我们可以从 XML 配置中移除这些 Bean 对应的 bean 元素。组件扫描非常方便,因为 Spring 完成了大部分繁重的工作,尽管它会分散配置。

我们知道这个 bean 依赖于一个 JdbcTemplateJdbcTemplate 已经在上下文中配置好了。由于只配置了一个实例,我们可以简单地在类上的 setter 方法上添加 @Autowired 注解,这告诉 Spring 按类型解析依赖并注入它。如果上下文中配置了多个实例,这种情况下就会抛出错误。

依赖解析的注解 - 除了 @Autowired 之外,还有很多方法可以告诉 Spring 要注入哪个 Bean。您可以使用 JSR 330 支持的注解,例如 @javax.inject.Inject,或者 JSR 250 的 @javax.annotation.Resource,或者 Spring 的 @Value 注解。
package org.springframework.samples;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;

@Component
public class DatabaseCustomerService implements CustomerService {
    private JdbcTemplate jdbcTemplate;
    private RowMapper<Customer> customerRowMapper = new CustomerRowMapper();

    public Customer getCustomerById(long id) {
        return jdbcTemplate.queryForObject(
            "select * from CUSTOMERS where ID = ?", this.customerRowMapper, id);
    }

    @Autowired
    public void setJdbcTemplate( JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    class CustomerRowMapper implements RowMapper<Customer> {
        public Customer mapRow(ResultSet resultSet, int i)  throws SQLException {
            String fn = resultSet.getString("FIRST_NAME");
            String ln = resultSet.getString("LAST_NAME");
            String email = resultSet.getString("EMAIL");
            long id = resultSet.getInt("ID");
            return new Customer(id, fn, ln, email);
        }
    }
}

最后一个类是使用 CustomerService 实例的客户端。我们像之前一样使用 @Component 注解注册它。它需要引用 CustomerService 实例,就像 DatabaseCustomerService 实例需要引用 JdbcTemplate 一样。所以,我们使用了我们的老朋友 @Autowired

package org.springframework.samples;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class CustomerClient {
	private CustomerService customerService ;

	@Autowired
	public void setCustomerService(CustomerService customerService) {
		this.customerService = customerService;
	}

	public void printCustomerInformation ( long customerId ) {
		Customer customer = this.customerService.getCustomerById( customerId );
		System.out.println( customer ) ;
	}
}

经过一番努力,我们修改后的 XML 文件变得更精简了

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springframework.samples"/>

    <jdbc:embedded-database id="ds" type="H2">
        <jdbc:script location="classpath:setup.sql"/>
    </jdbc:embedded-database>

    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <constructor-arg ref="ds"/>
    </bean>

</beans>

我们使用了命名空间支持来简化配置嵌入式 javax.sql.DataSource 实例。我们使用了组件扫描来让 Spring 自动为我们注册 DatabaseCustomerServiceCustomerClient bean。事情看起来进展顺利,但我们仍然使用 XML 来描述 JdbcTemplate 的配置,这在 Java 中可以更方便地描述。在这种情况下,XML 并不比普通 Java 更具说服力;它只提供了对等的功能。如果我们可以也对 JdbcTemplate 实例使用组件扫描就好了。然而,组件扫描只适用于带有 @Component 注解的 bean。由于我们不能将 @Component 注解添加到第三方类,因为我们可能没有它们的源代码,所以组件扫描不适用于 JdbcTemplate 实例。

Spring 提供了 Java 配置支持,让您直接使用 Java 来描述和配置 bean。Java 配置选项提供了两全其美的优势:它允许您配置任何类,甚至是您没有源代码的类(与 XML 配置选项一样),而且它仍然以 Java 为中心,因此受益于 Java 语言的所有类型安全特性(以及 Java IDE 中可用的重构工具)。

Java 配置会处理在上下文中注册的 bean,并查找带有 @Bean 注解的方法并调用它们。方法调用的结果将注册到应用程序上下文中作为 bean,就像您使用 XML 配置对象一样。bean 的类型是返回对象的类型,id 取自方法名。由于配置是由方法中的 Java 代码提供的,您可以进行任何设置,就像 FactoryBean 允许您做的那样。人们经常选择 Java 配置选项,因为它允许您将 bean 配置保存在一两个知名、中心的类中。XML 配置和 Java 配置都提供了一种集中描述应用程序的方式。

配置类是 Spring Bean,就像其他任何 Bean 一样。适用于普通 Spring Bean 的所有规则都适用于配置 Bean,除了带有 @Bean 注解的方法。Spring 将通过组件扫描拾取您的配置类。如果您想在配置类中使用其他 Bean(例如,我们之前使用 XML 命名空间配置的 javax.sql.DataSource 实例),那么您可以使用所有常规可用选项来获取它们,包括 @Autowired。让我们看看我们的示例的配置类。


package org.springframework.samples;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class CustomerConfiguration {

	@Autowired private DataSource dataSource;

	@Bean
	public JdbcTemplate jdbcTemplate() {
		return new JdbcTemplate(this.dataSource);
	}
}

这个类使用 @Autowired 获取嵌入式数据源的引用。这与之前示例中的用法类似,不同之处在于这里我们在私有字段变量上使用了注解,而不是在 setter 方法上。Spring 框架可以在类的构造函数、字段变量或 setter 方法上使用注解。最后,我们有一个带有 @Bean 注解的方法。这个方法提供了 JdbcTemplate 实例的定义,这意味着我们可以从文件中删除 XML 配置。我们在这里没有这样做,但您可以在一个类中定义多个 @Bean 定义方法,它们可以通过简单地相互调用来相互引用。如果一个带有 @Bean 注解的方法调用另一个方法,返回值将是新创建的对象,或者(如果 Bean 已经创建)是已注册到上下文中的 Bean。在上面的类中,我们还在类上添加了 @Configuration 注解。这个注解告诉 Spring 将此类视为一种特殊的组件类型,专门用于配置。本质上,这个 Bean 享有与 Spring 上下文中注册的任何 Bean 相同的服务,*并且*它还应用了额外的服务以启用 Java 配置。要使用 Java 配置,请确保您的类路径中包含 CGLIB 库。

最终修订后的 XML 文件如下所示


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springframework.samples"/>

    <jdbc:embedded-database id="ds" type="H2">
        <jdbc:script location="classpath:setup.sql"/>
    </jdbc:embedded-database>   

</beans>

在这篇文章中,我们追溯了您也可以遵循的步骤,通过 Spring 的依赖注入能力实现更清晰、更友好的代码库。尽管我们讨论了比现有任何其他技术都提供更多灵活性和更多功能的奇妙技术,但重要的是要记住,这种令人难以置信的支持至少已经存在了 4 年。其中大部分已经存在更长时间。人们一直将这些组件作为他们应用程序的基石。依赖注入和控制反转仅仅是个开始。Spring 框架为大量不断增长的使用场景提供了众多简化库,这些库都建立在这里建立的组件模型之上。

当您在 Spring 之上构建应用程序时,您将自己与应用程序部署到的 Web 服务器、应用程序服务器和云环境的锁定隔离开来,同时最大化您对底层平台的投资回报。

订阅 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

取得领先

VMware 提供培训和认证,助您加速进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

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

查看全部