有需求就要想办法解决,最近参与的项目其涉及的三个数据表分别在三台不同的服务器上,这就有点突兀了,第一次遇到这种情况,可这难不倒笔者,资料一查,代码一打,回头看看源码,万事大吉

1. 预备知识

这里默认大家都会SSM框架了,使用时我们要往sqlSessionFactory里注入数据源。那么猜测:1、可以往sqlSessionFactory里注入多数据源来实现切换;2、将多个数据源封装成一个总源,再把这个总源注入到sqlSessionFactory里实现切换。答案是使用后者,即封装成总源的形式。Spring提供了动态切换数据源的功能,那么我们来看看其实现原理

2. 实现原理

笔者是根据源码讲解的,这些步骤讲完会贴出源码内容

一、

Spring提供了AbstractRoutingDataSource抽象类,其继承了AbstractDataSource。而AbstractDataSource又实现了DataSource。因此我们可以将AbstractRoutingDataSource的实现类注入到sqlSessionFactory中来实现切换数据源

二、

刚才我们将多个数据源封装成总源的想法在AbstractRoutingDataSource中有体现,其内部用一个Map集合封装多个数据源,即 private Map<Object, DataSource> resolvedDataSources; ,那么要使用时从该Map集合中获取即可

三、

AbstractRoutingDataSource中有个determineTargetDataSource()方法,其作用是决定使用哪个数据源。我们通过determineTargetDataSource()方法从Map集合中获取数据源,那么必须有个key值指定才行。所以determineTargetDataSource()方法内部通过调用determineCurrentLookupKey()方法来获取key值,Spring将determineCurrentLookupKey()方法抽象出来给用户实现,从而让用户决定使用哪个数据源

四、

既然知道我们需要重写determineCurrentLookupKey()方法,那么就开始把。实现时发现该方法没有参数,我们无法传参来决定返回的key值,又不能改动方法(因为是重写),所以方法内部调用我们自定义类的静态方法即可解决问题

public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSourceKey();
}
}

五、

自定义类,作用是让我们传入key值来决定使用哪个key

public class DynamicDataSourceHolder {

    // ThreadLocal没什么好说的,绑定当前线程
private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>(); public static String getDataSourceKey(){
return dataSourceKey.get();
} public static void setDataSourceKey(String key){
dataSourceKey.set(key);
} public static void clearDataSourceKey(){
dataSourceKey.remove();
}
}

六、

AbstractRoutingDataSource抽象类源码(不喜可跳)

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource; public AbstractRoutingDataSource() {
} public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
} public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
} public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
} public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
} public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
} }
} protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
} protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource)dataSource;
} else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String)dataSource);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
} public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
} public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
} public <T> T unwrap(Class<T> iface) throws SQLException {
return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
} public boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
} protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
} if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
} @Nullable
protected abstract Object determineCurrentLookupKey();
}

3. 配置

3.1 配置db.properties

这里配置两个数据库,一个评论库,一个用户库

# 问题库
howl.comments.driverClassName = com.mysql.jdbc.Driver
howl.comments.url = jdbc:mysql://127.0.0.1:3306/comment
howl.comments.username = root
howl.comments.password = # 用户库
howl.users.driverClassName = com.mysql.jdbc.Driver
howl.users.url = jdbc:mysql://127.0.0.1:3306/user
howl.users.username = root
howl.users.password =

3.2 配置applicationContext.xml

<!--  加载properties文件  -->
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder> <!-- 问题的数据源 -->
<bean id="commentsDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${howl.comments.driverClassName}"></property>
<property name="url" value="${howl.comments.url}"></property>
<property name="username" value="${howl.comments.username}"></property>
<property name="password" value="${howl.comments.password}"></property>
</bean> <!-- 用户的数据源 -->
<bean id="usersDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${howl.users.driverClassName}"></property>
<property name="url" value="${howl.users.url}"></property>
<property name="username" value="${howl.users.username}"></property>
<property name="password" value="${howl.users.password}"></property>
</bean> <!-- 通过setter方法,往DynamicDataSource的Map集合中注入数据 -->
<!-- 具体参数,看名字可以明白 -->
<bean id="dynamicDataSource" class="com.howl.util.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="cds" value-ref="commentsDataSource"/>
<entry key="uds" value-ref="usersDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="commentsDataSource"></property>
</bean> <!-- 将`总源`注入SqlSessionFactory工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="dataSource" ref="dynamicDataSource"></property>
</bean>

因为dynamicDataSource是继承AbstractRoutingDataSource,所以setter注入方法得去父类里面去找,开始笔者也是懵了一下

3.3 切换数据源

数据源是在Service层切换的

UserService

@Service
public class UserService { @Autowired
private UserDao userDao; public User selectUserById(int id) { // 表明使用usersDataSource库
DynamicDataSourceHolder.setDataSourceKey("uds");
return userDao.selectUserById(id);
}
}

CommentService

@Service
public class CommentService { @Autowired
CommentDao commentDao; public List<Comment> selectCommentById(int blogId) { // 表明使用评论库
DynamicDataSourceHolder.setDataSourceKey("cds");
return commentDao.selectCommentById(blogId, -1);
}
}

3.4 自动切换

手动切换容易忘记,我们学了AOP可以使用AOP来切换,这里使用注解实现

<!-- 开启AOP注解支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

切面类

@Component
@Aspect
public class DataSourceAspect { @Pointcut("execution(* com.howl.service.impl.*(..))")
private void pt1() {
} @Around("pt1()")
public Object around(ProceedingJoinPoint pjp) { Object rtValue = null;
try {
String name = pjp.getTarget().getClass().getName();
if (name.equals("com.howl.service.UserService")) {
DynamicDataSourceHolder.setDataSourceKey("uds");
}
if (name.equals("com.howl.service.CommentService")){
DynamicDataSourceHolder.setDataSourceKey("cds");
}
// 调用业务层方法
rtValue = pjp.proceed(); System.out.println("后置通知");
} catch (Throwable t) {
System.out.println("异常通知");
t.printStackTrace();
} finally {
System.out.println("最终通知");
}
return rtValue;
}
}

使用环绕通知实现切入com.howl.service.impl里的所有方法,在遇到UserService、CommentService时,前置通知动态切换对应的数据源

4. 总结

  1. 以前笔者认为Service层多了impl包和接口是多余的,现在要用到AOP的时候后悔莫及,所以默认结构如此肯定有道理的
  2. 出bug的时候,才知道分步测试哪里出问题了,如果TDD推动那么能快速定位报错地方,日志也很重要

参考

https://www.jianshu.com/p/d97cd60e404f

SSM动态切换数据源的更多相关文章

  1. ssm 动态切换数据源

    1,添加数据库配置 jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver #jdbc.url=jdbc:sqlserver://192.16 ...

  2. Spring动态切换数据源及事务

    前段时间花了几天来解决公司框架ssm上事务问题.如果不动态切换数据源话,直接使用spring的事务配置,是完全没有问题的.由于框架用于各个项目的快速搭建,少去配置各个数据源配置xml文件等.采用了动态 ...

  3. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  4. hibernate动态切换数据源

    起因: 公司的当前产品,主要是两个项目集成的,一个是java项目,还有一个是php项目,两个项目用的是不同的数据源,但都是mysql数据库,因为java这边的开发工作已经基本完成了,而php那边任务还 ...

  5. Spring + Mybatis 项目实现动态切换数据源

    项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法. ...

  6. 在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法

    相关项目地址:https://github.com/helloworlde/SpringBoot-DynamicDataSource 1. org.apache.ibatis.binding.Bind ...

  7. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  8. Spring学习总结(16)——Spring AOP实现执行数据库操作前根据业务来动态切换数据源

    深刻讨论为什么要读写分离? 为了服务器承载更多的用户?提升了网站的响应速度?分摊数据库服务器的压力?就是为了双机热备又不想浪费备份服务器?上面这些回答,我认为都不是错误的,但也都不是完全正确的.「读写 ...

  9. Spring+Mybatis动态切换数据源

    功能需求是公司要做一个大的运营平台: 1.运营平台有自身的数据库,维护用户.角色.菜单.部分以及权限等基本功能. 2.运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A.服务B的数据库 ...

随机推荐

  1. ORs-4-Enhanced Role of OR Gene Loss (Pseudogenization) in Birds

    Enhanced Role of OR Gene Loss (Pseudogenization) in Birds 1.因为文献已经证明(a)基因缺失和得到对于进化有影响,(b)大的基因家族对进化影响 ...

  2. java高并发之线程池

    Java高并发之线程池详解   线程池优势 在业务场景中, 如果一个对象创建销毁开销比较大, 那么此时建议池化对象进行管理. 例如线程, jdbc连接等等, 在高并发场景中, 如果可以复用之前销毁的对 ...

  3. 1、简述在java网络编程中,服务端程序与客户端程序的具体开发步骤?

    网络编程分为UDP通信和TCP通信 UDP协议: 发送端:1.创建DatagramSocket对象.2.创建DatagramPacket对象,并封装数据.3.发送数据.4.释放 资源. 接收端:1.创 ...

  4. android愤怒小鸟游戏、自定义View、掌上餐厅App、OpenGL自定义气泡、抖音电影滤镜效果等源码

    Android精选源码 精练的范围选择器,范围和单位可以自定义 自定义View做的小鸟游戏 android popwindow选择商品规格颜色尺寸效果源码 实现Android带有锯齿背景的优惠样式源码 ...

  5. F5 BIG-IPLTM串联组网连接模式

  6. Linux挂载存储(供应商给的资料)

    infofile iqn.1994-05.com.redhat:16a2b8b7d8 infodb iqn.1994-05.com.redhat:8518efa2fe72 在iscsi server上 ...

  7. python-django框架-电商项目-首页开发_20191122

    python-django框架-电商项目-首页开发 业务背景: 用户浏览网站一定是先到首页, 没有登陆的话首页内容完全一样,而且是不经常变化的, 一段时间内,有100用户访问,就要有几个用户就要查询多 ...

  8. SQL中的一些关键字用法

    1.where 条件筛选结果 select * from `表名` where `列名`='value' 上诉语句的意思是在某表中查询某列名等于某特定值得所有列 2.Like 模糊查询 select ...

  9. 有用户及目录判断的删除文件内容的Shell脚本

    [root@localhost Qingchu]# cat Qingchu_version2.sh #!/bin/bash #描述: # 清除脚本! #作者:孤舟点点 #版本:2.0 #创建时间:-- ...

  10. AI在自动化测试领域的应用

    阿里QA导读:最近一两年随着深入学习技术浪潮的诞生,智能化测试迎来了新的发展,而AI也会引领下一代测试的新航向.Testin云测CTO陈冠诚先生的分享让我们看到AI在移动自动化测试领域里面的创新机会点 ...