说明:现在的场景是,采用MySQL Replication的方式在两台不同服务器部署并配置主从(Master-Slave)复制;
并需要程序上的数据操作方法访问不同的数据库,比如,update*方法访问主数据库服务器,query*方法访问从数据库服务器,从而减轻读写操作数据库的压力。即把“增删改”和“查”分开访问两台服务器,当然两台服务器的数据库同步事先已经配置好。
然而程序是早已完成的使用Spring JdbcTemplate的架构,如何在不修改任何源代码的情况下达到此功能呢?
分析:
1.目前有两个数据源需要配置到Spring框架中,如何统一管理这两个数据源?
JdbcTemplate有很多数据库操作方法,关键的可以分为以下几类(使用简明通配符):execute(args..)、update(args..)、batchUpdate(args..)、query*(args..)
2.如何根据这些方法名来使用不同的数据源呢?
3.多数据源的事务管理(暂未处理)
实现:
Spring配置文件applicationContext.xml(包含相关bean类的代码)
1.数据源配置(省略了更为详细的连接参数设置):
01 |
< bean id = "masterDataSource"
|
02 |
class = "org.apache.commons.dbcp.BasicDataSource"
|
03 |
destroy-method = "close" >
|
04 |
< property name = "driverClassName"
|
05 |
value = "${jdbc.driverClassName}" />
|
06 |
< property name = "url" value = "${jdbc.url}" />
|
07 |
< property name = "username" value = "${jdbc.username}" />
|
08 |
< property name = "password" value = "${jdbc.password}" />
|
09 |
< property name = "poolPreparedStatements" value = "true" />
|
10 |
< property name = "defaultAutoCommit" value = "true" />
|
12 |
< bean id = "slaveDataSource"
|
13 |
class = "org.apache.commons.dbcp.BasicDataSource"
|
14 |
destroy-method = "close" >
|
15 |
< property name = "driverClassName"
|
16 |
value = "${jdbc.driverClassName}" />
|
17 |
< property name = "url" value = "${jdbc.url2}" />
|
18 |
< property name = "username" value = "${jdbc.username}" />
|
19 |
< property name = "password" value = "${jdbc.password}" />
|
20 |
< property name = "poolPreparedStatements" value = "true" />
|
21 |
< property name = "defaultAutoCommit" value = "true" />
|
24 |
class = "test.my.serivce.ds.DynamicDataSource" >
|
25 |
< property name = "targetDataSources" >
|
27 |
< entry key = "master" value-ref = "masterDataSource" />
|
28 |
< entry key = "slave" value-ref = "slaveDataSource" />
|
31 |
< property name = "defaultTargetDataSource" ref = "masterDataSource" />
|
首先定义两个数据源(连接地址及用户名等数据存放在properties属性文件中),Spring可以设置多个数据源,究其根本也不过是一个普通bean罢了。
关键是ID为“dataSource”的这个bean的设置,它是这个类“test.my.serivce.ds.DynamicDataSource”的一个实例:
1 |
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
3 |
public class DynamicDataSource extends AbstractRoutingDataSource {
|
5 |
protected Object determineCurrentLookupKey() {
|
6 |
return CustomerContextHolder.getCustomerType();
|
DynamicDataSource类继承了Spring的抽象类AbstractRoutingDataSource,而AbstractRoutingDataSource本身实现了javax.sql.DataSource接口(由其父类抽象类AbstractDataSource实现),因此其实际上也是一个标准数据源的实现类。该类是Spring专为多数据源管理而增加的一个接口层,参见Spring-api-doc可知:
Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
它根据一个数据源唯一标识key来寻找已经配置好的数据源队列,它通常是与当前线程绑定在一起的。
查看其源码,知道它还实现了Spring的初始化方法类InitializingBean,这个类只有一个方法:afterPropertiesSet(),由Spring在初始化bean完成之后调用:
01 |
public void afterPropertiesSet() {
|
02 |
if ( this .targetDataSources == null ) {
|
03 |
throw new IllegalArgumentException( "targetDataSources is required" );
|
05 |
this .resolvedDataSources = new HashMap( this .targetDataSources.size());
|
06 |
for (Iterator it = this .targetDataSources.entrySet().iterator(); it.hasNext(); ) {
|
07 |
Map.Entry entry = (Map.Entry)it.next();
|
08 |
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
|
09 |
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
|
10 |
this .resolvedDataSources.put(lookupKey, dataSource);
|
12 |
if ( this .defaultTargetDataSource != null )
|
13 |
this .resolvedDefaultDataSource = resolveSpecifiedDataSource( this .defaultTargetDataSource);
|
查看其具体实现可知,Spring将所有已经配置好的数据源存放到一个名为targetDataSources的hashMap对象中(targetDataSources属性必须设置,否则异常;defaultTargetDataSource属性可以不必设置)。只是把数据源统一存到一个map中并不能做什么,关键是它还重写了javax.sql.DataSource的getConnection()方法,该方法无论你在何时使用数据库操作相关的方法时都会使用到,即使ibatis、hibernate、JPA等进行多层封装的框架底层还是使用最普通的JDBC来实现。
01 |
public Connection getConnection() throws SQLException {
|
02 |
return determineTargetDataSource().getConnection();
|
04 |
protected DataSource determineTargetDataSource() {
|
05 |
Object lookupKey = determineCurrentLookupKey();
|
06 |
DataSource dataSource = (DataSource) this .resolvedDataSources.get(lookupKey);
|
07 |
if (dataSource == null )
|
08 |
dataSource = this .resolvedDefaultDataSource;
|
09 |
if (dataSource == null )
|
10 |
throw new IllegalStateException( "Cannot determine target DataSource for lookup key [" + lookupKey + "]" );
|
13 |
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
|
16 |
protected abstract Object determineCurrentLookupKey();
|
省略部分校验代码,这里有一个必须的关键方法:determineCurrentLookupKey,也是一个抽象的有你自己实现的方法,从这个方法返回实际要使用的数据源的key(也即在前面配置的数据源bean的ID)。从Spring-api-doc中可以看到详细说明:
Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context. Allows for arbitrary keys. The returned key needs to match the stored lookup key type.
它允许任意类型的key,但必须是跟保存到数据源hashMap中的key类型一致。我们可以在Spring配置文件中指定该类型,网上看到有仁兄使用枚举类型的,是一个有不错约束性的主意。
我们的“test.my.serivce.ds.DynamicDataSource”实现了这个方法,可见具体的数据源key是从CustomerContextHolder类中获得的,并且也是使用key与当前线程绑定的方式:
01 |
public class CustomerContextHolder {
|
02 |
private static final ThreadLocal contextHolder = new ThreadLocal();
|
03 |
public static void setCustomerType(String customerType) {
|
04 |
contextHolder.set(customerType);
|
06 |
public static String getCustomerType() {
|
07 |
return (String) contextHolder.get();
|
09 |
public static void clearCustomerType() {
|
10 |
contextHolder.remove();
|
我们也可以使用全局变量的方式来存储这个key。参见java.lang.ThreadLocal:http://t.cn/zWyu2X0
有一位评论者 一针见血的指出问题来:
Why is userThreadLocal declared public? AFAIK, ThreadLocal instances are typically private static fields. Also, ThreadLocal is a generic type, it is ThreadLocal<T>. An important benefit of ThreadLocal worth mentioning (from 1.4 JVMs forward), is as an alternative to synchronization, to improve scalability in transaction-intensive environments. Classes encapsulated in ThreadLocal are automatically thread-safe in a pretty simple way, since it's clear that anything stored in ThreadLocal is not shared between threads.
ThreadLocal是线程安全的,并且不能在多线程之间共享。根据这个原理,我写了下面的小例子以便进一步理解:
02 |
private static ThreadLocal tl = new ThreadLocal();
|
03 |
public static void main(String[] args) {
|
05 |
System.out.println(tl.get());
|
06 |
new Thread( new Runnable() {
|
08 |
System.out.println(tl.get());
|
做到这里,我们已经解决了第一个问题,但似乎还没有进入正题,如何根据JdbcTemplate方法名动态设置数据源呢?
2.Spring AOP切入JdbcTemplate方法配置:
01 |
< bean id = "ba" class = "test.my.serivce.ds.BeforeAdvice" />
|
02 |
< aop:config proxy-target-class = "true" >
|
04 |
< aop:pointcut id = "update"
|
05 |
expression = "execution(* org.springframework.jdbc.core.JdbcTemplate.update*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))" />
|
06 |
< aop:before method = "setMasterDataSource"
|
07 |
pointcut-ref = "update" />
|
10 |
< aop:before method = "setSlaveDataSource"
|
11 |
pointcut = "execution(* org.springframework.jdbc.core.JdbcTemplate.query*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.execute(..))" />
|
可以看到我已经使用<aop:aspect>将JdbcTemplate的4类方法进行拦截,并使用前置通知的方式(<aop:before>)在执行这些方法之前调用其他方法,具体的AOP表达式语言的含义我就不细说了。
根据最开始的说明,分别对update操作和select操作进行拦截并调用不同的方法,这个方法到底是什么呢?
其实就是给ThreadLocal设置数据源的名字(key),以便DynamicDataSource知道到底是使用哪一个数据源。
前置方法就是调用“test.my.serivce.ds.BeforeAdvice”类的某个set方法:
1 |
public class BeforeAdvice {
|
2 |
public void setMasterDataSource() {
|
3 |
CustomerContextHolder.setCustomerType( "master" );
|
5 |
public void setSlaveDataSource() {
|
6 |
CustomerContextHolder.setCustomerType( "slave" );
|
当前线程就会保存下设置进去的key名称并随时可以调用。
最后再配置一个JdbcTemplate bean即可。
1 |
< bean id = "jdbcTemplate"
|
2 |
class = "org.springframework.jdbc.core.JdbcTemplate" >
|
3 |
< property name = "dataSource" ref = "dataSource" />
|
附注:
1.在解决过程中遇到的一个问题(参考:http://t.cn/zWyu79h ):
Spring异常:no matching editors or conversion strategy found
引用:Spring注入的是接口,关联的是实现类。 这里注入了实现类,所以报异常了。
2.本文主要参考的文章有:
该文还包含事务管理的配置:http://t.cn/zWyuPsJ
该文与多数据源的设置对我有一定的启发(此外还包含测试用例):http://t.cn/zWyuwV5
之前做过ibatis采用ehCache和osCache做缓存的配置,这篇有点类似:http://t.cn/zWyuwBN
多数据源的一些实际场景分析,理论重于实际:http://t.cn/zWyuPz6
此外,javaeye(现为iteye)的一些文章也是有参考价值的:http://t.cn/zWyuASq
EOF.最初的设想到这里变成了现实。本文讲述了“Spring AOP根据JdbcTemplate方法名动态设置数据源”的整个实现过程和一些浅显的分析。
使用这样配置后在实际使用中发现仍然有问题。比如,调用jdbcTemplate的update方法后立即调用query方法查询该条记录,或者使用以下方法:
this.jdbcTemplate.update(new PreparedStatementCreator(), keyHolder)
因为数据库复制有同步间隙,这个时间晚于程序的调用,就会出现查询不到数据的情况,实际上是数据还未同步到从服务器。期待更好的解决方案!
相关推荐
总结来说,"springAop多数据源"项目涉及到Spring框架的多数据源配置、JdbcTemplate的使用、面向切面编程的应用,以及使用JUnit进行测试。理解并掌握这些技术对于构建灵活、可扩展的Java应用程序至关重要。在实践中,...
3. **测试友好**:支持模拟数据源,便于单元测试。 4. **适用场景**:适用于简单到中等复杂的数据库操作,如CRUD(创建、读取、更新、删除)操作,但不适合高度复杂的SQL逻辑。 总的来说,Spring JdbcTemplate是...
在Spring框架中,动态数据源实现是一个重要的特性,它允许应用程序根据特定的条件或用户需求在运行时切换数据源。这种灵活性对于多租户系统、数据隔离或者在不同环境(如开发、测试、生产)之间切换数据库配置尤其...
JdbcTemplate对象的创建可以通过默认构造函数或指定数据源的方式完成。通常推荐使用依赖注入的方式注入数据源,这可以在Spring配置文件中实现。 ##### 在配置文件中配置JdbcTemplate 配置文件示例: ```xml ...
- **配置JdbcTemplate**:在Spring配置文件中声明一个JdbcTemplate实例,并注入数据源。 - **执行SQL查询**:使用`query(String sql, RowMapper<T> rowMapper)`方法执行SQL查询,`RowMapper`接口用于将结果集中的...
接下来,JdbcTemplate的使用意味着你需要配置数据源和JdbcTemplate实例。数据源是连接数据库的工厂,通常可以使用连接池如Apache Commons DBCP或C3P0来实现。在XML配置中,你会看到如何定义数据源,然后创建一个bean...
- **配置**:Spring Boot通过`spring.datasource`配置项来设置数据源,支持多种类型的数据源,如HikariCP、Tomcat JDBC等。 - **连接池**:使用连接池如HikariCP可以有效地管理数据库连接,提高应用性能。 - **...
这两个配置文件需要正确地集成,确保Action类能够被Spring容器管理,并且JdbcTemplate的相关配置(如数据源、事务管理器)也应完整无误。 在实际应用中,这样的组合提供了灵活的架构和高效的数据库访问。Struts2...
- 使用Spring AOP来实现数据源的动态切换。创建一个切面类,如`DataSourceAspect`,其中定义一个环绕通知方法`@Around("execution(* com.example..service.*.*(..))")`,该方法将在所有Service层的方法执行前后被...
开发者可能需要配置Spring的数据源(DataSource)来连接Oracle数据库,并使用JdbcTemplate进行数据操作。 另外,项目还引入了Redis,一个内存中的数据结构存储系统,常被用作数据库、缓存和消息中间件。在Web应用中...
Spring则是一个全面的后端框架,提供依赖注入、AOP(面向切面编程)、事务管理等功能;Spring JDBC Template是Spring提供的一个简化数据库操作的工具,它封装了JDBC的繁琐部分,让开发者可以更专注于业务逻辑。 在...
在提供的压缩包中,`securitydb_2.sql`很可能包含了预设的数据库结构和初始数据,这些数据可能是为了设置Spring Security的安全配置,如用户角色、权限等。这通常涉及创建用户表、角色表以及它们之间的关联。执行这...
动态数据源路由的关键在于一个名为`AbstractRoutingDataSource`的类,它可以根据当前的上下文环境选择合适的数据源。例如: ```java @Configuration public class DynamicDataSourceConfig extends ...
2. 创建JdbcTemplate实例:使用数据源创建JdbcTemplate实例,作为与数据库交互的主要对象。 3. 编写SQL:定义SQL语句,可以是静态的字符串或者可参数化的PreparedStatement。 4. 使用JdbcTemplate方法:调用...
例如,可以创建一个`@SwitchDataSource`注解,并在方法上使用,根据注解的参数来决定使用哪个数据源。 **5. 测试与验证** 确保所有配置正确后,编写单元测试或集成测试以验证多数据源的配置是否正常工作。使用`@...
- 一个Spring配置文件,定义了bean、AOP切面、数据源和事务管理器。 - 使用了@Autowired注解自动装配Bean的Java类,展示了依赖注入。 - 针对AOP的切面类,定义了通知(advice)并指定了切入点(pointcut)。 - 使用...
### Spring Boot 动态数据源实现详解 #### 概述 Spring Boot 动态数据源是一种常见的企业级应用需求,特别是在需要支持多个不同数据库的情况下。本文档将详细介绍如何在Spring Boot项目中实现动态数据源的配置与...
也可以创建一个路由数据源(如`AbstractRoutingDataSource`),它可以根据特定的规则(如事务上下文、方法参数等)动态决定使用哪个实际的数据源。 4. **JdbcTemplate与NamedParameterJdbcTemplate**:Spring提供的...
// 在这里,jdbcTemplate会根据MyRoutingDataSource的设置自动选择合适的数据源 public void performOperation(String operation) { // ... } } ``` 四、注意事项 4. 配置事务管理: 当涉及到事务管理时,确保...
在多数据源环境下,我们需要根据数据源选择合适的JdbcTemplate实例。 4. **DAO层设计**:为了确保不同的数据源操作不会混淆,通常会为每个数据源创建独立的DAO层。这些DAO接口或抽象类应明确标记,以便服务层可以...