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 社群的所有近期活動。

檢視全部