3、实现抽象类AbstractRoutingDataSource定义自己的动态数据源DataSource类
@Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { //所有的请求都会走此处,所以没有切换的时候,不要输出日志吧 String dataSourceId = DynamicDataSourceContextHolder.getDataSourceId(); if (dataSourceId != null) { //有指定切换数据源切换的时候,才给输出日志 并且也只给输出成debug级别的 否则日志太多了 log.debug("线程[{}],此时切换到的数据源为:{}", Thread.currentThread().getId(), dataSourceId); } return dataSourceId; } }
就这样三步,我们带有动态切换能力的数据源类DynamicDataSource
就完成了。
接下来就可以这么直接使用在项目里了:
Java配置文件JdbcConfig.java
如下:
@EnableTransactionManagement @Configuration @PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8") public class JdbcConfig implements TransactionManagementConfigurer { @Value("${datasource.username}") private String userName; @Value("${datasource.password}") private String password; @Value("${datasource.url}") private String url; // 从库配置 @Value("${datasource.slave.username}") private String slaveUserName; @Value("${datasource.slave.password}") private String slavePassword; @Value("${datasource.slave.url}") private String slaveUrl; =====配置好两个数据源: @Bean public DataSource masterDataSource() { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUser(userName); dataSource.setPassword(password); dataSource.setURL(url); return dataSource; } @Bean public DataSource slaveDataSource() { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUser(slaveUserName); dataSource.setPassword(slavePassword); dataSource.setURL(slaveUrl); return dataSource; } // 定义动态数据源 @Primary @Bean public DataSource dataSource() { DynamicDataSource dataSource = new DynamicDataSource(); // 初始化值必须设置进去 且给一个默认值 dataSource.setTargetDataSources(new HashMap<Object, Object>() {{ put(DynamicDataSourceId.MASTER, masterDataSource()); put(DynamicDataSourceId.SLAVE1, slaveDataSource()); //顺手注册上去,方便后续的判断 DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.MASTER); DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.SLAVE1); }}); dataSource.setDefaultTargetDataSource(masterDataSource()); return dataSource; } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource()); dataSourceTransactionManager.setEnforceReadOnly(true); // 让事务管理器进行只读事务层面上的优化 建议开启 return dataSourceTransactionManager; } // 指定注解使用的事务管理器 @Override public PlatformTransactionManager annotationDrivenTransactionManager() { return transactionManager(); } }
可以和上面配置对比,这里并不需要什么都配置两份了,而是都只需要配置一份即可,其余的交给动态去切换吧。
单元测试如下:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {JdbcConfig.class}) public class TestSpringBean { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private DataSource dataSource; @Test public void test1() throws SQLException { System.out.println(jdbcTemplate.getDataSource() == dataSource); //true System.out.println(DataSourceUtils.getConnection(jdbcTemplate.getDataSource())); //com.mysql.jdbc.JDBC4Connection@17503f6b DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.SLAVE1); System.out.println(jdbcTemplate.getDataSource() == dataSource); //true System.out.println(DataSourceUtils.getConnection(jdbcTemplate.getDataSource())); //com.mysql.jdbc.JDBC4Connection@20bd8be5 // 完成操作后 最好把数据源再set回去 否则可能会对该线程后续再使用JdbcTemplate的时候造成影响 //DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.MASTER); } }
从上结果,此处有几个细节需要注意:
- 数据源DataSource永远没变(用的是我们配置的DynamicDataSource)
- DynamicDataSourceContextHolder.setDataSourceId(DynamicDataSourceId.SLAVE1);后JdbcTemplate绑定的数据源肯定是不会变的。只是内部去获取链接的时候,从所属数据源变化了
- 用完之后记得还原现场、清理线程(切换数据源源用完之后记得切回来)
附:第一次链接所属的数据源截图:
切换后为:
理解了原理之后。其实我们的masterDataSource
以及slaveDataSource
是完全没有比较放到Spring容器内的,减轻Spring容器容器的负担嘛。使用下面配置效果一样(这样做更能体现对多数据源的理解,逼格更高~~~):
@EnableTransactionManagement @Configuration @PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8") public class JdbcConfig implements TransactionManagementConfigurer { ... // 定义动态数据源 @Bean public DataSource dataSource() { DynamicDataSource dataSource = new DynamicDataSource(); final DataSource masterDataSource = masterDataSource(); final DataSource slaveDataSource = slaveDataSource(); // 初始化值必须设置进去 且给一个默认值 dataSource.setTargetDataSources(new HashMap<Object, Object>() {{ put(DynamicDataSourceId.MASTER, masterDataSource); put(DynamicDataSourceId.SLAVE1, slaveDataSource); //顺手注册上去,方便后续的判断 DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.MASTER); DynamicDataSourceId.DATA_SOURCE_IDS.add(DynamicDataSourceId.SLAVE1); }}); dataSource.setDefaultTargetDataSource(masterDataSource); return dataSource; } private DataSource masterDataSource() { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUser(userName); dataSource.setPassword(password); dataSource.setURL(url); return dataSource; } private DataSource slaveDataSource() { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUser(slaveUserName); dataSource.setPassword(slavePassword); dataSource.setURL(slaveUrl); return dataSource; } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } ... }
上面讲述了Spring动态切换数据源的核心原理逻辑以及使用方式,但是我们可以看到它使用上还是有一点点代码侵入性的。其实绝大多数情况下我们都希望切换数据源在方法级别即可,并不需要这么细粒度的控制。因此下面继续介绍更加优雅的操作方式(自定义注解+AOP)
使用AOP+自定义注解方式优雅的实现数据源动态切换
为了实现更优雅的动态数据源的切换,我们可以使用Spring AOP+自定义注解的方式实现对方法级别的数据源切换。
因为注解最低只能定义在方法上(而非代码块上),所以此种方式最细粒度为方法级别,99.99%情况下都够用了
自定义切换数据源的注解:(此处我定义的注解表示:可以用在方法上和类上)
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DynamicDataSourceSwitch { String dataSourceId() default DynamicDataSourceId.MASTER; }
书写切面:
@Slf4j @Order(1) @Aspect @Component // 切面必须交给容器管理 public class DynamicDataSourceHandlerAspect { @Pointcut("@annotation(com.fsx.dynamic.DynamicDataSourceSwitch)") public void pointcut() { } @Before("pointcut()") public void doBefore(JoinPoint joinPoint) { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); DynamicDataSourceSwitch annotationClass = method.getAnnotation(DynamicDataSourceSwitch.class);//获取方法上的注解 if (annotationClass == null) { annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicDataSourceSwitch.class);//获取类上面的注解 if (annotationClass == null) return; } //获取注解上的数据源的值的信息(这里最好还判断一下dataSourceId是否是有效的 若是无效的就用warn提醒 此处我就不处理了) String dataSourceId = annotationClass.dataSourceId(); // 此处:切换数据源~~~ DynamicDataSourceContextHolder.setDataSourceId(dataSourceId); log.info("AOP动态切换数据源,className" + joinPoint.getTarget().getClass().getName() + "methodName" + method.getName() + ";dataSourceId:" + dataSourceId == "" ? "默认数据源" : dataSourceId); } // 清理掉当前设置的数据源,让默认的数据源不受影响 @After("pointcut()") public void after(JoinPoint point) { DynamicDataSourceContextHolder.clearDataSourceId(); } }
这样我们只需要在方法上标注一个注解,指定要切换到的数据源key就搞定了,非常的优雅,没有代码侵入性。
@Transactional @DynamicDataSourceSwitch @Override public Object hello(Integer id) { ... }
在实际开发中我也是推荐使用此方式,若存在极其特殊的场景,你也可以结合编程的方式进行更细粒度的控制。
请确保标注此注解的Bean是交给Spring容器管理的~
另外一个常识:你的@Aspect切面只能切入同容器内的Bean,而不能切入子容器内的Bean。(这点在常规SpringMVC开发中可能存在Controller标注注解无效的情况吗,但在SpringBoot开发中无此顾虑,因为SpringBoot面向开发者只定义了一个容器)
总结
本文介绍了多种实现同一个工程内对多个数据源管理,但很显然,它的最佳实践是有一个:
Spring在2.0.1引入了AbstractRoutingDataSource,它并不是在1.0里几有的抽象,可见它也是时代的产物。
该类充当了DataSource的路由中介。 能有在运行时, 根据某种key值来动态切换到真正的DataSource上, 同时对于不支持事务隔离级别的JTA事务来说, Spring还提供了另外一个类IsolationLevelDataSourceRouter来处理这个问题。(具体在JTA事务里再会详解)
另外,上面讲述的这些API都在spring-jdbc.jar里。
最后也留一个小悬念:多数据源切换是成功了,但牵涉到事务呢?单数据源事务是ok的,但如果多数据源需要同时使用一个事务呢?