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

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. Servlet&JSP复习笔记 04

    1.状态管理 因为HTTP协议是无状态协议,但很多时候需要将客户端和服务端的多次请求当做一个来对待.将多次交互中设计的数据进行保存. 状态:数据 管理:对数据的维护 2.Cookie 客户端向服务器发 ...

  2. Web Service概述 及 应用案例

    Web Service的定义  W3C组织对其的定义如下,它是一个软件系统,为了支持跨网络的机器间相互操作交互而设计.Web Service服务通常被定义为一组模块化的API,它们可以通过网络进行调用 ...

  3. Java面试题2-附答案

    JVM的内存结构 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.Java虚拟机栈: 线程私有:每个方法在执行的时候会创建一个栈帧,存储了局部变量表, ...

  4. IntelliJ IDEA项目断开版本管理解决方案

    今天使用idea时打开项目突然发现项目不受svn管理(项目目录依然受svn管理,只是idea脱管了),如遇到可用以下方法: 图片示例: 1. 2. 希望能帮到你

  5. COMET探索系列一【COMET实践笔记】

    这几天在给公司的一个点对点聊天系统升级,之前只是使用简单的ajax轮询方式实现,每5秒钟取一次数据,延时太长,用户体验不是很好,因此打算采用服务器推送技术,故此整理了以下文档,将自己找到的一些资料及心 ...

  6. [LC] 102. Binary Tree Level Order Traversal

    Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, ...

  7. 调参、最优化、ml算法(未完成)

    最优化方法 调参方法 ml算法 梯度下降gd grid search lr 梯度上升 随机梯度下降 pca 随机梯度下降sgd  贝叶斯调参 lda 牛顿算法   knn 拟牛顿算法   kmeans ...

  8. mycat(读写分离、负载均衡、主从切换)

    博主本人平和谦逊,热爱学习,读者阅读过程中发现错误的地方,请帮忙指出,感激不尽 1.环境准备 1.1新增两台虚拟机 mycat01:192.168.247.81 mycat02:192.168.247 ...

  9. python之循删list

    先来看下循环遍历删除list元素的一段代码: L=[1,3,1,4,3,6,5] # 0 1 2 3 4 5 6(下标) for i in L: if i%2!=0:#%表示除商取余数,除以2余数为0 ...

  10. python3之urllib基础

    urllib简单应用html=urllib.request.urlopen(域名/网址).read().decode('utf-8')----->--->urlopen-->获取源码 ...