多数据源的应用场景:主要是数据库拆分后,怎样让多个数据库结合起来来达到业务需求。
SSM框架(Spring+SpringMVC+MyBatis(MyBatis-Plus))是目前最常用的,此次仍然是maven工程。
关于这个多数据源例子,我已经上传到我的github上,地址为:https://github.com/youcong1996/study_simple_demo.git
不过需要注意的是,分支为demo1,不是主分支,如图所示:
如果下面的示例,你们看不懂或者不能理解,可以git clone我的地址
在编程的世界里,简洁即完美。
如何实现多数据源?
一句话,三个类加xml配置即可达到这个目的。
一、编写三个类
AbstractDynamicDataSource.java
package com.blog.datasource; import java.util.Map; import javax.sql.DataSource; import org.apache.commons.collections.MapUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态数据源父类 * @create ll * @update * @updateDate */ public abstract class AbstractDynamicDataSource<T extends DataSource> extends AbstractRoutingDataSource implements ApplicationContextAware { /** 日志 */ protected Logger logger = LoggerFactory.getLogger(getClass()); /** 默认的数据源KEY */ protected static final String DEFAULT_DATASOURCE_KEY = "defaultDataSource"; /** 数据源KEY-VALUE键值对 */ public Map<Object, Object> targetDataSources; /** spring容器上下文 */ private static ApplicationContext ctx; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ctx = applicationContext; } public static ApplicationContext getApplicationContext() { return ctx; } public static Object getBean(String name) { return ctx.getBean(name); } /** * @param targetDataSources the targetDataSources to set */ public void setTargetDataSources(Map<Object, Object> targetDataSources) { this.targetDataSources = targetDataSources; super.setTargetDataSources(targetDataSources); // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的 super.afterPropertiesSet(); } /** * 创建数据源 * @param driverClassName 数据库驱动名称 * @param url 连接地址 * @param username 用户名 * @param password 密码 * @return 数据源{@link T} * @Author : ll. create at 2017年3月27日 下午2:44:34 */ public abstract T createDataSource(String driverClassName, String url, String username, String password); /** * 设置系统当前使用的数据源 * <p>数据源为空或者为0时,自动切换至默认数据源,即在配置文件中定义的默认数据源 * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey() */ @Override protected Object determineCurrentLookupKey() { logger.info("【设置系统当前使用的数据源】"); Map<String, Object> configMap = DBContextHolder.getDBType(); logger.info("【当前数据源配置为:{}】", configMap); if (MapUtils.isEmpty(configMap)) { // 使用默认数据源 return DEFAULT_DATASOURCE_KEY; } // 判断数据源是否需要初始化 this.verifyAndInitDataSource(); logger.info("【切换至数据源:{}】", configMap); return configMap.get(DBContextHolder.DATASOURCE_KEY); } /** * 判断数据源是否需要初始化 * @Author : ll. create at 2017年3月27日 下午3:57:43 */ private void verifyAndInitDataSource() { Map<String, Object> configMap = DBContextHolder.getDBType(); Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY)); if (obj != null) { return; } logger.info("【初始化数据源】"); T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER) .toString(), configMap.get(DBContextHolder.DATASOURCE_URL).toString(), configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(), configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString()); this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(), datasource); } /** * 往数据源key-value键值对集合添加新的数据源 * @param key 新的数据源键 * @param dataSource 新的数据源 */ private void addTargetDataSource(String key, T dataSource) { this.targetDataSources.put(key, dataSource); super.setTargetDataSources(this.targetDataSources); // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的 super.afterPropertiesSet(); } }
DBContextHolder.java
package com.blog.datasource; import java.util.HashMap; import java.util.Map; public class DBContextHolder { /** 数据源的KEY */ public static final String DATASOURCE_KEY = "DATASOURCE_KEY"; /** 数据源的URL */ public static final String DATASOURCE_URL = "DATASOURCE_URL"; /** 数据源的驱动 */ public static final String DATASOURCE_DRIVER = "DATASOURCE_DRIVER"; /** 数据源的用户名 */ public static final String DATASOURCE_USERNAME = "DATASOURCE_USERNAME"; /** 数据源的密码 */ public static final String DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD"; private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>(); public static void setDBType(Map<String, Object> dataSourceConfigMap) { contextHolder.set(dataSourceConfigMap); } public static Map<String, Object> getDBType() { Map<String, Object> dataSourceConfigMap = contextHolder.get(); if (dataSourceConfigMap == null) { dataSourceConfigMap = new HashMap<String, Object>(); } return dataSourceConfigMap; } public static void clearDBType() { contextHolder.remove(); } }
DruidDynamicDataSource.java
package com.blog.datasource; import java.sql.SQLException; import java.util.List; import org.apache.commons.lang3.StringUtils; import com.alibaba.druid.filter.Filter; import com.alibaba.druid.pool.DruidDataSource; /** * Druid数据源 * @update * @updateDate */ public class DruidDynamicDataSource extends AbstractDynamicDataSource<DruidDataSource> { private boolean testWhileIdle = true; private boolean testOnBorrow = false; private boolean testOnReturn = false; // 是否打开连接泄露自动检测 private boolean removeAbandoned = false; // 连接长时间没有使用,被认为发生泄露时长 private long removeAbandonedTimeoutMillis = 300 * 1000; // 发生泄露时是否需要输出 log,建议在开启连接泄露检测时开启,方便排错 private boolean logAbandoned = false; // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,使用oracle时可以设定此值。 // private int maxPoolPreparedStatementPerConnectionSize = -1; // 配置监控统计拦截的filters private String filters; // 监控统计:"stat" 防SQL注入:"wall" 组合使用: "stat,wall" private List<Filter> filterList; /* * 创建数据源 * @see com.cdelabcare.pubservice.datasource.IDynamicDataSource#createDataSource(java.lang.String, java.lang.String, java.lang.String, java.lang.String) */ @Override public DruidDataSource createDataSource(String driverClassName, String url, String username, String password) { DruidDataSource parent = (DruidDataSource) super.getApplicationContext().getBean( DEFAULT_DATASOURCE_KEY); DruidDataSource ds = new DruidDataSource(); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); ds.setDriverClassName(driverClassName); ds.setInitialSize(parent.getInitialSize()); ds.setMinIdle(parent.getMinIdle()); ds.setMaxActive(parent.getMaxActive()); ds.setMaxWait(parent.getMaxWait()); ds.setTimeBetweenConnectErrorMillis(parent.getTimeBetweenConnectErrorMillis()); ds.setTimeBetweenEvictionRunsMillis(parent.getTimeBetweenEvictionRunsMillis()); ds.setMinEvictableIdleTimeMillis(parent.getMinEvictableIdleTimeMillis()); ds.setValidationQuery(parent.getValidationQuery()); ds.setTestWhileIdle(testWhileIdle); ds.setTestOnBorrow(testOnBorrow); ds.setTestOnReturn(testOnReturn); ds.setRemoveAbandoned(removeAbandoned); ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis); ds.setLogAbandoned(logAbandoned); // 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,参照druid的源码 ds.setMaxPoolPreparedStatementPerConnectionSize(parent .getMaxPoolPreparedStatementPerConnectionSize()); if (StringUtils.isNotBlank(filters)) try { ds.setFilters(filters); } catch (SQLException e) { throw new RuntimeException(e); } addFilterList(ds); return ds; } private void addFilterList(DruidDataSource ds) { if (filterList != null) { List<Filter> targetList = ds.getProxyFilters(); for (Filter add : filterList) { boolean found = false; for (Filter target : targetList) { if (add.getClass().equals(target.getClass())) { found = true; break; } } if (!found) targetList.add(add); } } } }
二、修改配置文件
主要是修改spring-mybatis.xml
<!-- 配置数据源 --> <bean name="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc_url}"/> <property name="username" value="${jdbc_username}"/> <property name="password" value="${jdbc_password}"/> <!-- 初始化连接大小 --> <property name="initialSize" value="0"/> <!-- 连接池最大使用连接数量 --> <property name="maxActive" value="20"/> <!-- 连接池最大空闲 --> <property name="maxIdle" value="20"/> <!-- 连接池最小空闲 --> <property name="minIdle" value="0"/> <!-- 获取连接最大等待时间 --> <property name="maxWait" value="60000"/> <property name="validationQuery" value="${validationQuery}"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <property name="testWhileIdle" value="true"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="25200000"/> <!-- 打开removeAbandoned功能 --> <property name="removeAbandoned" value="true"/> <!-- 1800秒,也就是30分钟 --> <property name="removeAbandonedTimeout" value="1800"/> <!-- 关闭abanded连接时输出错误日志 --> <property name="logAbandoned" value="true"/> <!-- 监控数据库 --> <property name="filters" value="mergeStat"/> </bean> <bean id="druidDynamicDataSource" class="com.blog.datasource.DruidDynamicDataSource"> <property name="defaultTargetDataSource" ref="defaultDataSource" /> <property name="targetDataSources"> <map> <entry key="defaultDataSource" value-ref="defaultDataSource"/> <!-- 这里还可以加多个dataSource --> </map> </property> </bean> <!-- Spring整合Mybatis,更多查看文档:http://mp.baomidou.com --> <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean"> <property name="dataSource" ref="druidDynamicDataSource" /> <!-- 自动扫描Mapping.xml文件 --> <property name="mapperLocations" value="classpath:mybatis/system/*.xml"/> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> <property name="typeAliasesPackage" value="com.blog.entity"/> <property name="plugins"> <array> <!-- 分页插件配置 --> <bean id="paginationInterceptor" class="com.baomidou.mybatisplus.plugins.PaginationInterceptor"> </bean> </array> </property> <!-- 全局配置注入 --> <property name="globalConfig" ref="globalConfig" /> </bean> <!-- 配置事务管理 --> <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="druidDynamicDataSource"/> </bean>
三、单元测试
import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.blog.datasource.DBContextHolder; import com.blog.entity.User; import com.blog.mapper.PostDao; import com.blog.service.UserService; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring/spring.xml") public class BlogTest { @Autowired private UserService ud; @Test public void testName() throws Exception { Map<String, Object> map = new HashMap<String, Object>(); map.put(DBContextHolder.DATASOURCE_KEY, "localhost"); map.put(DBContextHolder.DATASOURCE_DRIVER, "com.mysql.jdbc.Driver"); map.put(DBContextHolder.DATASOURCE_URL, "jdbc:mysql://127.0.0.1:3306/blog_test?useUnicode=true&characterEncoding=UTF-8"); map.put(DBContextHolder.DATASOURCE_USERNAME, "root"); map.put(DBContextHolder.DATASOURCE_PASSWORD, "1234"); DBContextHolder.setDBType(map); List<User> list = ud.selectList(null); for (User user : list) { System.out.println(user); } } }
测试后,控制台如图:
小结:
其实配置多数据源有很多方式,有aop,也有配置多个bean的方式,当然了,只要能达到目的就是王道,当然了,我也强调一点,不是实现完就不管了,背后的为什么比只要实现就好更重要。
其实,有一点我想说的是,有些时候遇到难题,最好的方式是迎面而上解决这个问题,而不是逃避或者独自焦躁。同时直面问题,也是解决焦躁的最好方式。这个我已经深有体会了。
另外补充到,上传至github上的多数据源示例同时也是ssm框架的搭建。有哪位朋友不会搭建框架,可以参考我的这个。希望能对你们有什么帮助。