<< 动态切换数据源(spring+hibernate) - liheping - 博客园 | 首页 | 同时入选IMDB和豆瓣电影100强的经典电影 >>

Dynamic DataSource Routing | SpringSource Team Blog

Anyways, here's the code for my 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);
            }
      });
   }
}

 

As you can see, the Catalog simply returns a list of Item objects. The Item just contains name and price properties:

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 + ")";
   }
 
}

 

Now, in order to demonstrate multiple DataSources, I created an enum for different Customer types (representing "levels" of membership I guess), and I created three different databases – so that each type of customer would get a distinct item list (I did mention that this would be a contrived example didn't I?). The important thing is that each of the databases are equivalent in terms of the schema. That way the Catalog's query will work against any of them – just returning different results. In this case, it's just the "item" table with 2 columns: name and price. And… here is the enum:

public enum CustomerType {
   BRONZE,
   SILVER,
   GOLD
}

 

It's time to create some bean definitions. Since I have 3 datasources where everything is the same except for the port number, I created a parent bean so that the shared properties can be inherited. Then, I added the 3 bean definitions to represent the per-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>

 

Notice that I added a PropertyPlaceholderConfigurer so that I could externalize the port numbers in a "db.properties" file, like so:

db.port.gold=9001
db.port.silver=9002
db.port.bronze=9003

 

Now things start to get interesting. I need to supply the "routing" DataSource to my Catalogso that it can dynamically get connections from the 3 different databases at runtime based on the current customer's type. As I mentioned, the AbstractRoutingDataSource can be rather simple to implement. Here is my implementation:

package blog.datasource;
 
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
public class CustomerRoutingDataSource extends AbstractRoutingDataSource {
 
   @Override
   protected Object determineCurrentLookupKey() {
      return CustomerContextHolder.getCustomerType();
   }
}

…and the CustomerContextHolder simply provides access to a thread-bound CustomerType. In reality, the 'context' would likely hold more information about the customer. Also note that if you are using Acegi, then you could retrieve some information from the userDetails. For this example, it's just the customer "type":

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();
   }
}

 

Finally, I just need to configure the catalog and routing DataSource beans. As you can see, the "real" DataSource references are provided in a Map. If you provide Strings, they can be resolved as JNDI names (or any custom resolution strategy can be provided – see the JavaDoc). Also, I've simply set the 'bronzeDataSource' as the default:

<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>

 

Of course I'd like to see this working, so I've created a simple test (extending one of Spring's integration test support classes). I added 3 items to the "gold" database, 2 items to the "silver" database, and only 1 item to the "bronze" database. This is the test:

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"};
   }
}

…and rather than simply taking a screenshot of the green bar, you'll notice I've provided some console output – the results!:

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)]

 

As you can see, the configuration is simple. Better still, the data-access code is not concerned with looking up different DataSources. For more information, consult the JavaDoc for AbstractRoutingDataSource.

阅读全文……

标签 : ,



发表评论 发送引用通报