领先一步
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://localhost:${db.port.gold}/blog"/>
</bean>
<bean id="silverDataSource" parent="parentDataSource">
<property name="url" value="jdbc:hsqldb:hsql://localhost:${db.port.silver}/blog"/>
</bean>
<bean id="bronzeDataSource" parent="parentDataSource">
<property name="url" value="jdbc:hsqldb:hsql://localhost:${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。