原文出处:http://blog.springsource.com/2007/01/23/dynamic-datasource-routing/
Spring 2.0.1 introduced an AbstractRoutingDataSource. I believe that it deserves attention, since (based on frequent questions from clients) I have a hunch that there are quite a few 'home-grown' solutions to this problem floating around. That combined with the fact that it is trivial to implement yet easy to overlook, and now I have several reasons to dust off my corner of the Interface21 team blog.
The general idea is that a routing DataSource acts as an intermediary – while the 'real' DataSource can be determined dynamically at runtime based upon a lookup key. One potential use-case is for ensuring transaction-specific isolation levels which are not supported by standard JTA. For that, Spring provides an implementation: IsolationLevelDataSourceRouter. Consult its JavaDoc for a detailed description including configuration examples. Another interesting use-case is determination of the DataSource based on some attribute of the current user's context. What follows is a rather contrived example to demonstrate this idea.
First, I created a Catalog that extends Spring 2.0's SimpleJdbcDaoSupport. That base class only requires an instance of any implementation of javax.sql.DataSource, and then it creates a SimpleJdbcTemplate for you. Since it extends JdbcDaoSupport, the JdbcTemplate is also available. However, the "simple" version provides many nice Java 5 conveniences. You can read more detail about that in this blog by Ben Hale.
Anyways, here's the code for my Catalog:
01 |
package blog.datasource; |
03 |
import java.sql.ResultSet; |
04 |
import java.sql.SQLException; |
05 |
import java.util.List; |
07 |
import org.springframework.jdbc.core.simple.ParameterizedRowMapper; |
08 |
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport; |
10 |
public class Catalog extends SimpleJdbcDaoSupport { |
12 |
public List<Item> getItems() { |
13 |
String query = "select name, price from item" ; |
14 |
return getSimpleJdbcTemplate().query(query, new ParameterizedRowMapper<Item>() { |
15 |
public Item mapRow(ResultSet rs, int row) throws SQLException { |
16 |
String name = rs.getString( 1 ); |
17 |
double price = rs.getDouble( 2 ); |
18 |
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:
01 |
package blog.datasource; |
08 |
public Item(String name, double price) { |
13 |
public String getName() { |
17 |
public double getPrice() { |
21 |
public String toString() { |
22 |
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:
1 |
public enum CustomerType { |
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:
01 |
< bean id = "parentDataSource" |
02 |
class = "org.springframework.jdbc.datasource.DriverManagerDataSource" |
04 |
< property name = "driverClassName" value = "org.hsqldb.jdbcDriver" /> |
05 |
< property name = "username" value = "sa" /> |
08 |
< bean id = "goldDataSource" parent = "parentDataSource" > |
12 |
< bean id = "silverDataSource" parent = "parentDataSource" > |
13 |
< property name = "url" value = "jdbc:hsqldb:hsql://localhost:${db.port.silver}/blog" /> |
16 |
< bean id = "bronzeDataSource" parent = "parentDataSource" > |
17 |
< property name = "url" value = "jdbc:hsqldb:hsql://localhost:${db.port.bronze}/blog" /> |
20 |
< bean class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" > |
21 |
< property name = "location" value = "classpath:/blog/datasource/db.properties" /> |
Notice that I added a PropertyPlaceholderConfigurer so that I could externalize the port numbers in a "db.properties" file, like so:
Now things start to get interesting. I need to supply the "routing" DataSource to my Catalog so 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:
01 |
package blog.datasource; |
03 |
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; |
05 |
public class CustomerRoutingDataSource extends AbstractRoutingDataSource { |
08 |
protected Object determineCurrentLookupKey() { |
09 |
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":
01 |
public class CustomerContextHolder { |
03 |
private static final ThreadLocal<CustomerType> contextHolder = |
04 |
new ThreadLocal<CustomerType>(); |
06 |
public static void setCustomerType(CustomerType customerType) { |
07 |
Assert.notNull(customerType, "customerType cannot be null" ); |
08 |
contextHolder.set(customerType); |
11 |
public static CustomerType getCustomerType() { |
12 |
return (CustomerType) contextHolder.get(); |
15 |
public static void clearCustomerType() { |
16 |
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:
01 |
< bean id = "catalog" class = "blog.datasource.Catalog" > |
02 |
< property name = "dataSource" ref = "dataSource" /> |
05 |
< bean id = "dataSource" class = "blog.datasource.CustomerRoutingDataSource" > |
06 |
< property name = "targetDataSources" > |
07 |
< map key-type = "blog.datasource.CustomerType" > |
08 |
< entry key = "GOLD" value-ref = "goldDataSource" /> |
09 |
< entry key = "SILVER" value-ref = "silverDataSource" /> |
12 |
< property name = "defaultTargetDataSource" ref = "bronzeDataSource" /> |
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 { |
03 |
private Catalog catalog; |
05 |
public void setCatalog(Catalog catalog) { |
06 |
this .catalog = catalog; |
09 |
public void testDataSourceRouting() { |
10 |
CustomerContextHolder.setCustomerType(CustomerType.GOLD); |
11 |
List<Item> goldItems = catalog.getItems(); |
12 |
assertEquals( 3 , goldItems.size()); |
13 |
System.out.println( "gold items: " + goldItems); |
15 |
CustomerContextHolder.setCustomerType(CustomerType.SILVER); |
16 |
List<Item> silverItems = catalog.getItems(); |
17 |
assertEquals( 2 , silverItems.size()); |
18 |
System.out.println( "silver items: " + silverItems); |
20 |
CustomerContextHolder.clearCustomerType(); |
21 |
List<Item> bronzeItems = catalog.getItems(); |
22 |
assertEquals( 1 , bronzeItems.size()); |
23 |
System.out.println( "bronze items: " + bronzeItems); |
26 |
protected String[] getConfigLocations() { |
27 |
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!:
1 |
gold items: [gold item # 1 ( 250.0 ), gold item # 2 ( 325.45 ), gold item # 3 ( 55.6 )] |
2 |
silver items: [silver item # 1 ( 25.0 ), silver item # 2 ( 15.3 )] |
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.