領先一步
VMware 提供培訓和認證,以加速您的進步。
瞭解更多Spring 2.0.1 引入了一個 AbstractRoutingDataSource
。我認為它值得關注,因為(基於來自客戶的常見問題)我預感有很多“自制”的解決方案在這個問題上徘徊。再加上它實現起來很簡單但又容易被忽視,現在我有幾個理由來擦拭一下我在團隊部落格上的角落。
總體思路是,路由 DataSource
充當中間媒介 - 而“真實”的 DataSource 可以在執行時根據查詢鍵動態確定。 一個潛在的用例是確保標準 JTA 不支援的事務特定隔離級別。 為此,Spring 提供了一個實現:IsolationLevelDataSourceRouter
。 有關詳細說明(包括配置示例),請查閱其 JavaDoc。
另一個有趣的用例是基於當前使用者上下文的某些屬性來確定 DataSource。 下面是一個相當牽強的例子來演示這個想法。
首先,我建立了一個擴充套件 Spring 2.0 的 SimpleJdbcDaoSupport
的 Catalog
。 該基類只需要任何 javax.sql.DataSource
實現的例項,然後它會為您建立一個 SimpleJdbcTemplate
。 由於它擴充套件了 JdbcDaoSupport
,因此 JdbcTemplate
也可用。 但是,“simple”版本提供了許多不錯的 Java 5 便利。 您可以在 Ben Hale 的這篇部落格中閱讀更多詳細資訊。
無論如何,這是我的 Catalog
的程式碼
package blog.datasource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;
public class Catalog extends SimpleJdbcDaoSupport {
public List<Item> getItems() {
String query = "select name, price from item";
return getSimpleJdbcTemplate().query(query, new ParameterizedRowMapper<Item>() {
public Item mapRow(ResultSet rs, int row) throws SQLException {
String name = rs.getString(1);
double price = rs.getDouble(2);
return new Item(name, price);
}
});
}
}
正如你所看到的,Catalog
只是返回一個 item
物件列表。 Item
僅包含 name 和 price 屬性
package blog.datasource;
public class Item {
private String name;
private double price;
public Item(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public String toString() {
return name + " (" + price + ")";
}
}
現在,為了演示多個 DataSources,我為不同的客戶型別建立了一個列舉(代表我猜想的會員“級別”),並且我建立了三個不同的資料庫 - 這樣每種型別的客戶都會獲得不同的專案列表(我確實提到過這會是一個牽強的例子,不是嗎?)。 重要的一點是,每個資料庫在模式方面都是等效的。 這樣 Catalog 的查詢就可以針對它們中的任何一個工作 - 只是返回不同的結果。 在這種情況下,它只是具有 2 列的“item”表:name 和 price。 並且... 這是列舉
public enum CustomerType {
BRONZE,
SILVER,
GOLD
}
現在是建立一些 bean 定義的時候了。 由於我有 3 個 datasources,其中除了埠號之外所有內容都相同,所以我建立了一個父 bean,以便可以繼承共享屬性。 然後,我添加了 3 個 bean 定義來表示每個 CustomerType 的 DataSources
<bean id="parentDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
abstract="true">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="username" value="sa"/>
</bean>
<bean id="goldDataSource" parent="parentDataSource">
<property name="url" value="jdbc:hsqldb:hsql://:${db.port.gold}/blog"/>
</bean>
<bean id="silverDataSource" parent="parentDataSource">
<property name="url" value="jdbc:hsqldb:hsql://:${db.port.silver}/blog"/>
</bean>
<bean id="bronzeDataSource" parent="parentDataSource">
<property name="url" value="jdbc:hsqldb:hsql://:${db.port.bronze}/blog"/>
</bean>
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:/blog/datasource/db.properties"/>
</bean>
請注意,我添加了一個 PropertyPlaceholderConfigurer
,以便我可以將埠號外部化到“db.properties”檔案中,如下所示
db.port.gold=9001 db.port.silver=9002 db.port.bronze=9003
現在事情開始變得有趣了。 我需要將“路由”DataSource 提供給我的 Catalog
,以便它可以根據當前客戶的型別在執行時動態地從 3 個不同的資料庫獲取連線。 正如我提到的,AbstractRoutingDataSource
實現起來可能相當簡單。 這是我的實現
package blog.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class CustomerRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}
...並且 CustomerContextHolder
只是提供對執行緒繫結的 CustomerType
的訪問。 實際上,“context”可能會儲存有關客戶的更多資訊。 另請注意,如果您使用的是 Spring Security,則可以從 userDetails 檢索一些資訊。 對於此示例,它只是客戶“型別”
public class CustomerContextHolder {
private static final ThreadLocal<CustomerType> contextHolder =
new ThreadLocal<CustomerType>();
public static void setCustomerType(CustomerType customerType) {
Assert.notNull(customerType, "customerType cannot be null");
contextHolder.set(customerType);
}
public static CustomerType getCustomerType() {
return (CustomerType) contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
}
最後,我只需要配置 catalog 和路由 DataSource beans。 正如你所看到的,“真實”的 DataSource 引用在一個 Map 中提供。 如果您提供字串,則可以將它們解析為 JNDI 名稱(或者可以提供任何自定義解析策略 - 請參閱 JavaDoc)。 此外,我只是將“bronzeDataSource”設定為預設值
<bean id="catalog" class="blog.datasource.Catalog">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="blog.datasource.CustomerRoutingDataSource">
<property name="targetDataSources">
<map key-type="blog.datasource.CustomerType">
<entry key="GOLD" value-ref="goldDataSource"/>
<entry key="SILVER" value-ref="silverDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="bronzeDataSource"/>
</bean>
當然,我想看到它工作,所以我建立了一個簡單的測試(擴充套件了 Spring 的一個整合測試支援類)。 我向“gold”資料庫添加了 3 個專案,向“silver”資料庫添加了 2 個專案,並且只向“bronze”資料庫添加了 1 個專案。 這是測試
public class CatalogTests extends AbstractDependencyInjectionSpringContextTests {
private Catalog catalog;
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
public void testDataSourceRouting() {
CustomerContextHolder.setCustomerType(CustomerType.GOLD);
List<Item> goldItems = catalog.getItems();
assertEquals(3, goldItems.size());
System.out.println("gold items: " + goldItems);
CustomerContextHolder.setCustomerType(CustomerType.SILVER);
List<Item> silverItems = catalog.getItems();
assertEquals(2, silverItems.size());
System.out.println("silver items: " + silverItems);
CustomerContextHolder.clearCustomerType();
List<Item> bronzeItems = catalog.getItems();
assertEquals(1, bronzeItems.size());
System.out.println("bronze items: " + bronzeItems);
}
protected String[] getConfigLocations() {
return new String[] {"/blog/datasource/beans.xml"};
}
}
...與其僅僅擷取綠色條的螢幕截圖,不如說您會注意到我提供了一些控制檯輸出 - 結果!
gold items: [gold item #1 (250.0), gold item #2 (325.45), gold item #3 (55.6)] silver items: [silver item #1 (25.0), silver item #2 (15.3)] bronze items: [bronze item #1 (23.75)]
正如你所看到的,配置很簡單。 更好的是,資料訪問程式碼不關心查詢不同的 DataSources。 有關更多資訊,請查閱 AbstractRoutingDataSource
的 JavaDoc。