原文:http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
Spring 2.0.1 引入了一个 AbstractRoutingDataSource 。因为(基于频繁的客户反映)我觉察到有许多'土鳖级'的解决方案开始在周围出现,我相信这值得关注。结合现在有许多并不容易忽略掉琐碎的实现的现实还有,现在我有了几个理由去重开我的 Interface21 team blog。
主要的想法是一个支持路由功能的数据源连接是一个介质,真正的数据源连接可以在运行时基于一个key被动态的选择。一个潜在的用例是在JPA不支持的情况下如何确保事务的独立性。基于这个理由,spring提供了一个实现:IsolationLevelDataSourceRouter。通过查阅javadoc可以找到他的详细描述包括配置的例子。另一个有趣的用例是基于当前用于用户上下文的一些属性决定数据源。等等会用一个捏造的例子来证明这个想法。
首先,我创建一个Catalog(库)继承Spring的SimpleJdbcDaoSupport。那个基本类只需要一个实现javax.sql.DataSource接口的实例,还有jdbcTemplate。然而,这个“简单”的版本提供了很多牛逼的java 5 的特性。你可以多读一些Ben Hale的博客this blog来了解细节。
言归正传,这是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 getItems() {
String query = "select name, price from item";
return getSimpleJdbcTemplate().query(query,
new ParameterizedRowMapper() {
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 object的列表。item只包含了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 + ")";
}
}
现在,为了要模拟多个datasource。我创建了一个不同的客户类型的枚举类(我认为是代表不同的客户级别),并且我创建三个不同的数据库,这样每一个类型的客户可以获得不一样的item list(虽然听起来很荒谬,但是我的确提到这是一个捏造的例子,不是么?)。重要的是每一个数据库等同于scheme(catalog)。在这种情况下,item表只有两个字段 name 和 price。还有,这是那个枚举类:
public enum CustomerType {
BRONZE,
SILVER,
GOLD
}
是时候建立一些bean了。因为我有3个数据源他们除了端口以外所有东西都是一样的,我创建了一个父类bean这样有一些属性可以通过继承来获取。然后我每个客户级别的数据源建立了三个bean
注意这边我创建了一个 PropertyPlaceholderConfigurer 这样我可以把端口配置在 db.properties 文件里面,像这样:javadb.port.gold=9001db.port.silver=9002db.port.bronze=9003
现在事情变得有趣了。我需要为我的数据源配置上支持路由的数据源 ,这样它可以在运行时基于当前用户的类型动态的获取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 简单的提供一个线程绑定的客户关系的入口。事实上上下文很可能含有比客户信息更多的东西。同时注意到如果我们使用 Acegi,那么你可以获取一些userDetails的信息。在这个例子里,这些信息只有 type(类型)
public class CustomerContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal();
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 bean。正如你所见,真正的数据源引用是由一个Map提供过的。如果你提供一个String 作为key可以(从Map中)转换为为一个JNDI名称(或者其他的可用的客户端解决策略-详见javaDoc),并且我们简单的将bronzeDataSource设置为默认的数据源连接。
<bean id="customerRoutingDataSource" class="blog.datasource.CustomerRoutingDataSource" >
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="glodDataSource" key="glodDataSource"></entry>
<entry value-ref="silverDataSource" key="silverDataSource"></entry>
<entry value-ref="bronzeDataSource" key="bronzeDataSource"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="bronzeDataSource" >
</property>
</bean>
当然,我希望看到这些正常工作,所以我创建了一个简单的test(继承了Spring的支持集成测试的class)我增加了3个item到gold(金牌)数据库,2个item到silver(银牌)数据库,bronze(铜牌)数据库只有1个item。这是测试代码:
public class CatalogTests extends AbstractDependencyInjectionSpringContextTests {
private Catalog catalog;
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
public void testDataSourceRouting() {
CustomerContextHolder.setCustomerType(CustomerType.GOLD);
List goldItems = catalog.getItems();
assertEquals(3, goldItems.size());
System.out.println("gold items: " + goldItems);
CustomerContextHolder.setCustomerType(CustomerType.SILVER);
List silverItems = catalog.getItems();
assertEquals(2, silverItems.size());
System.out.println("silver items: " + silverItems);
CustomerContextHolder.clearCustomerType();
List bronzeItems = catalog.getItems();
assertEquals(1, bronzeItems.size());
System.out.println("bronze items: " + bronzeItems);
}
protected String[] getConfigLocations() {
return new String[] { "/blog/datasource/beans.xml" };
}
}
比起简单的绿条(他指的是junit)截图你要注意到我还提供了一些 控制台 输出:
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)]
正如你所见,配置是简单的。更好的是,操作数据的代码看起来并不是在操作不同的数据库。要获取更多的信息,请查看 AbstractRoutingDataSource的javadoc