SSM框架之多数据源配置

时间:2024-01-22 11:34:26

多数据源的应用场景:主要是数据库拆分后,怎样让多个数据库结合起来来达到业务需求。

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框架的搭建。有哪位朋友不会搭建框架,可以参考我的这个。希望能对你们有什么帮助。