SSM动态切换数据源
有需求就要想办法解决,最近参与的项目其涉及的三个数据表分别在三台不同的服务器上,这就有点突兀了,第一次遇到这种情况,可这难不倒笔者,资料一查,代码一打,回头看看源码,万事大吉
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. 总结
- 以前笔者认为Service层多了impl包和接口是多余的,现在要用到AOP的时候后悔莫及,所以默认结构如此肯定有道理的
- 出bug的时候,才知道分步测试哪里出问题了,如果TDD推动那么能快速定位报错地方,日志也很重要
参考
https://www.jianshu.com/p/d97cd60e404f
SSM动态切换数据源的更多相关文章
- ssm 动态切换数据源
1,添加数据库配置 jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver #jdbc.url=jdbc:sqlserver://192.16 ...
- Spring动态切换数据源及事务
前段时间花了几天来解决公司框架ssm上事务问题.如果不动态切换数据源话,直接使用spring的事务配置,是完全没有问题的.由于框架用于各个项目的快速搭建,少去配置各个数据源配置xml文件等.采用了动态 ...
- Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法
一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...
- hibernate动态切换数据源
起因: 公司的当前产品,主要是两个项目集成的,一个是java项目,还有一个是php项目,两个项目用的是不同的数据源,但都是mysql数据库,因为java这边的开发工作已经基本完成了,而php那边任务还 ...
- Spring + Mybatis 项目实现动态切换数据源
项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法. ...
- 在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法
相关项目地址:https://github.com/helloworlde/SpringBoot-DynamicDataSource 1. org.apache.ibatis.binding.Bind ...
- Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源方法
一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...
- Spring学习总结(16)——Spring AOP实现执行数据库操作前根据业务来动态切换数据源
深刻讨论为什么要读写分离? 为了服务器承载更多的用户?提升了网站的响应速度?分摊数据库服务器的压力?就是为了双机热备又不想浪费备份服务器?上面这些回答,我认为都不是错误的,但也都不是完全正确的.「读写 ...
- Spring+Mybatis动态切换数据源
功能需求是公司要做一个大的运营平台: 1.运营平台有自身的数据库,维护用户.角色.菜单.部分以及权限等基本功能. 2.运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A.服务B的数据库 ...
随机推荐
- Servlet&JSP复习笔记 04
1.状态管理 因为HTTP协议是无状态协议,但很多时候需要将客户端和服务端的多次请求当做一个来对待.将多次交互中设计的数据进行保存. 状态:数据 管理:对数据的维护 2.Cookie 客户端向服务器发 ...
- Web Service概述 及 应用案例
Web Service的定义 W3C组织对其的定义如下,它是一个软件系统,为了支持跨网络的机器间相互操作交互而设计.Web Service服务通常被定义为一组模块化的API,它们可以通过网络进行调用 ...
- Java面试题2-附答案
JVM的内存结构 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.Java虚拟机栈: 线程私有:每个方法在执行的时候会创建一个栈帧,存储了局部变量表, ...
- IntelliJ IDEA项目断开版本管理解决方案
今天使用idea时打开项目突然发现项目不受svn管理(项目目录依然受svn管理,只是idea脱管了),如遇到可用以下方法: 图片示例: 1. 2. 希望能帮到你
- COMET探索系列一【COMET实践笔记】
这几天在给公司的一个点对点聊天系统升级,之前只是使用简单的ajax轮询方式实现,每5秒钟取一次数据,延时太长,用户体验不是很好,因此打算采用服务器推送技术,故此整理了以下文档,将自己找到的一些资料及心 ...
- [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, ...
- 调参、最优化、ml算法(未完成)
最优化方法 调参方法 ml算法 梯度下降gd grid search lr 梯度上升 随机梯度下降 pca 随机梯度下降sgd 贝叶斯调参 lda 牛顿算法 knn 拟牛顿算法 kmeans ...
- mycat(读写分离、负载均衡、主从切换)
博主本人平和谦逊,热爱学习,读者阅读过程中发现错误的地方,请帮忙指出,感激不尽 1.环境准备 1.1新增两台虚拟机 mycat01:192.168.247.81 mycat02:192.168.247 ...
- 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 ...
- python3之urllib基础
urllib简单应用html=urllib.request.urlopen(域名/网址).read().decode('utf-8')----->--->urlopen-->获取源码 ...