动态数据源路由

工程 | Mark Fisher | 2007年1月23日 | ...

Spring 2.0.1 引入了一个AbstractRoutingDataSource。我相信它值得关注,因为(根据客户的频繁提问)我有一种预感,有很多针对此问题的“自制”解决方案在四处流传。再加上它实现起来非常简单,但却很容易被忽略,现在我有几个理由来整理一下我的团队博客的角落。

总体思路是,路由DataSource充当中间体——而“真实”的 DataSource 可以根据查找键在运行时动态确定。一个潜在的用例是确保标准 JTA 不支持的事务特定隔离级别。为此,Spring 提供了一个实现:IsolationLevelDataSourceRouter。请参阅其 JavaDoc 以获取详细说明,包括配置示例。

另一个有趣的用例是根据当前用户上下文的一些属性确定 DataSource。下面是一个相当人为的示例来演示此想法。

首先,我创建了一个扩展 Spring 2.0 的SimpleJdbcDaoSupportCatalog。该基类只需要任何javax.sql.DataSource实现的实例,然后它会为你创建一个SimpleJdbcTemplate。由于它扩展了JdbcDaoSupport,因此JdbcTemplate也可使用。但是,“简单”版本提供了许多不错的 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仅包含名称和价格属性

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,我为不同的客户类型(代表“会员级别”)创建了一个枚举,并且我创建了三个不同的数据库——以便每种类型的客户获得不同的项目列表(我确实说过这将是一个人为的示例,对吧?)。重要的是,每个数据库在模式方面都是等价的。这样,Catalog 的查询就可以针对其中的任何一个工作——只是返回不同的结果。在这种情况下,它只是具有 2 列的“item”表:名称和价格。还有……这是枚举

public enum CustomerType {
   BRONZE, 
   SILVER, 
   GOLD
}

现在是时候创建一些 bean 定义了。由于我有 3 个数据源,其中除了端口号外,所有内容都相同,因此我创建了一个父 bean,以便可以继承共享属性。然后,我添加了 3 个 bean 定义来表示每个 CustomerType 的 DataSource

<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://127.0.0.1:${db.port.gold}/blog"/>
</bean>

<bean id="silverDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://127.0.0.1:${db.port.silver}/blog"/>
</bean>

<bean id="bronzeDataSource" parent="parentDataSource">
   <property name="url" value="jdbc:hsqldb:hsql://127.0.0.1:${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的访问。实际上,“上下文”可能会包含有关客户的更多信息。另请注意,如果你正在使用 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 bean。如你所见,“真实”的 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)]

如你所见,配置很简单。更好的是,数据访问代码无需关心查找不同的 DataSource。有关更多信息,请参阅AbstractRoutingDataSource的 JavaDoc。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速你的进步。

了解更多

获得支持

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

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部