Spring动态切换数据源

需求背景:项目中需要根据用户身份访问不同数据源,涉及到动态切换数据源,先写个小demo

AbstractRoutingDataSource

经过网上搜索发现Spring中提供了一个叫AbstractRoutingDataSource的抽象类,该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上,先看下AbstractRoutingDataSource 源码

package org.springframework.jdbc.datasource.lookup;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.util.Assert;

/**
 * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
 * calls to one of various target DataSources based on a lookup key. The latter is usually
 * (but not necessarily) determined through some thread-bound transaction context.
 *
 * @author Juergen Hoeller
 * @since 2.0.1
 * @see #setTargetDataSources
 * @see #setDefaultTargetDataSource
 * @see #determineCurrentLookupKey()
 */
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    private Map targetDataSources;

    private Object defaultTargetDataSource;

    private boolean lenientFallback = true;

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    private Map resolvedDataSources;

    private DataSource resolvedDefaultDataSource;


    /**
     * 指定一个数据源Map对象,根据key来查找数据源
     * key可以是任意类型    
     * key将被resolveSpecifiedLookupKey 与 determineCurrentLookupKey方法使用
     */
    public void setTargetDataSources(Map targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    /**
     * 指定DataSource类型的默认数据源
     */
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    /**
     * Specify whether to apply a lenient fallback to the default DataSource
     * if no specific DataSource could be found for the current lookup key.
     * 

Default is "true", accepting lookup keys without a corresponding entry * in the target DataSource map - simply falling back to the default DataSource * in that case. *

Switch this flag to "false" if you would prefer the fallback to only apply * if the lookup key was {@code null}. Lookup keys without a DataSource * entry will then lead to an IllegalStateException. * @see #setTargetDataSources * @see #setDefaultTargetDataSource * @see #determineCurrentLookupKey() */ public void setLenientFallback(boolean lenientFallback) { this.lenientFallback = lenientFallback; } /** * Set the DataSourceLookup implementation to use for resolving data source * name Strings in the {@link #setTargetDataSources targetDataSources} map. *

Default is a {@link JndiDataSourceLookup}, allowing the JNDI names * of application server DataSources to be specified directly. */ public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); } @Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } this.resolvedDataSources = new HashMap(this.targetDataSources.size()); for (Map.Entry entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); } if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); } } /** * Resolve the given lookup key object, as specified in the * {@link #setTargetDataSources targetDataSources} map, into * the actual lookup key to be used for matching with the * {@link #determineCurrentLookupKey() current lookup key}. *

The default implementation simply returns the given key as-is. * @param lookupKey the lookup key object as specified by the user * @return the lookup key as needed for matching */ protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } /** * Resolve the specified data source object into a DataSource instance. *

The default implementation handles DataSource instances and data source * names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}). * @param dataSource the data source value object as specified in the * {@link #setTargetDataSources targetDataSources} map * @return the resolved DataSource (never {@code null}) * @throws IllegalArgumentException in case of an unsupported value type */ 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); } } @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } @Override @SuppressWarnings("unchecked") public T unwrap(Class iface) throws SQLException { if (iface.isInstance(this)) { return (T) this; } return determineTargetDataSource().unwrap(iface); } @Override public boolean isWrapperFor(Class iface) throws SQLException { return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); } /** * 检索当前目标数据源 根据determineCurrentLookupKey的key得到要使用的数据源 */ protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); 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 + "]"); } return dataSource; } /** * 确定要使用数据源的key */ protected abstract Object determineCurrentLookupKey(); }

上面这段源码的重点在于determineCurrentLookupKey方法,这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。

本次主要用到的setTargetDataSourcessetDefaultTargetDataSourcedetermineCurrentLookupKey这三个方法,下面来开始撸代码了

maven

<properties>
    <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    <spring.version>4.3.10.RELEASEspring.version>
    <mybatis.version>3.4.4mybatis.version>
    <jdk.version>1.8jdk.version>
    <aspectj.version>1.8.10aspectj.version>
properties>


<dependencies>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-coreartifactId>
        <version>${spring.version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-context-supportartifactId>
        <version>${spring.version}version>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webartifactId>
        <version>${spring.version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webmvcartifactId>
        <version>${spring.version}version>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-aopartifactId>
        <version>${spring.version}version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-txartifactId>
        <version>${spring.version}version>
    dependency>
    <dependency>
        <groupId>org.aspectjgroupId>
        <artifactId>aspectjrtartifactId>
        <version>${aspectj.version}version>
    dependency>
    <dependency>
        <groupId>org.aspectjgroupId>
        <artifactId>aspectjweaverartifactId>
        <version>${aspectj.version}version>
        <scope>runtimescope>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-jdbcartifactId>
        <version>${spring.version}version>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-testartifactId>
        <version>${spring.version}version>
    dependency>
    
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>4.12version>
        <scope>testscope>
    dependency>
    
    <dependency>
        <groupId>org.mybatisgroupId>
        <artifactId>mybatisartifactId>
        <version>${mybatis.version}version>
    dependency>
    <dependency>
        <groupId>org.mybatisgroupId>
        <artifactId>mybatis-springartifactId>
        <version>1.3.1version>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>5.1.40version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.0.25version>
    dependency>
    
    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
        <version>3.1.0version>
        <scope>providedscope>
    dependency>
    
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.7version>
    dependency>
    <dependency>
        <groupId>commons-iogroupId>
        <artifactId>commons-ioartifactId>
        <version>2.4version>
    dependency>
    <dependency>
        <groupId>commons-fileuploadgroupId>
        <artifactId>commons-fileuploadartifactId>
        <version>1.3.1version>
    dependency>
    <dependency>
        <groupId>commons-collectionsgroupId>
        <artifactId>commons-collectionsartifactId>
        <version>3.2.2version>
    dependency>
    <dependency>
        <groupId>commons-beanutilsgroupId>
        <artifactId>commons-beanutilsartifactId>
        <version>1.9.3version>
    dependency>
    
    <dependency>
        <groupId>log4jgroupId>
        <artifactId>log4jartifactId>
        <version>1.2.17version>
    dependency>
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
        <version>1.7.21version>
    dependency>
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>jcl-over-slf4jartifactId>
        <version>1.7.21version>
    dependency>
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.1.2version>
    dependency>
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-coreartifactId>
        <version>1.1.2version>
    dependency>
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.16.10version>
        <scope>providedscope>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.24version>
    dependency>
    
    <dependency>
        <groupId>joda-timegroupId>
        <artifactId>joda-timeartifactId>
        <version>2.9.9version>
    dependency>
    
    <dependency>
        <groupId>com.squareup.okhttp3groupId>
        <artifactId>okhttpartifactId>
        <version>3.6.0version>
    dependency>
dependencies>

创建DynamicDataSource继承AbstractRoutingDataSource

package com.os.common.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 获取数据源(依赖于spring)
 *
 * @author Peng
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceUtils.getDataSource();
    }
}

创建DataSourceUtils

保证线程与线程之间互不影响

package com.os.common.config;

/**
 * 线程使用数据源工具类
 * 保证线程使用数据源互不影响
 *
 * @author Peng
 */
public class DataSourceUtils {
    /**
     * 线程本地环境
     */
    private static final ThreadLocal DATA_SOURCES = new ThreadLocal<>();

    /**
     * 设置数据源
     */
    public static void setDataSource(String customerType) {
        DATA_SOURCES.set(customerType);
    }

    /**
     * 获取数据源
     */
    public static String getDataSource() {
        return DATA_SOURCES.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        DATA_SOURCES.remove();
    }

}

Spring配置

package com.os.common.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * spring 配置
 *
 * @author Peng
 */
@Configuration
@ComponentScan(basePackages = {"com.os"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
@EnableTransactionManagement
public class RootConfig {
    /**
     * 这里写死的数据库url 实际项目应配置在配置文件中
     */
    private static final String DB_URL1 = "jdbc:mysql://localhost:3306/demo1?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useSSL=true";
    private static final String DB_URL2 = "jdbc:mysql://localhost:3306/demo2?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useSSL=true";

    /**
     * 配置数据源1
     */
    @Bean(name = "db1", initMethod = "init", destroyMethod = "close")
    public DataSource dataSource1() throws SQLException {
        return this.createDataSource(DB_URL1, "root", "");
    }

    /**
     * 配置数据源2
     */
    @Bean(name = "db2", initMethod = "init", destroyMethod = "close")
    public DataSource dataSource2() throws SQLException {
        return this.createDataSource(DB_URL2, "root", "");
    }

    /**
     * 创建DataSource
     *
     * @param url      数据库地址
     * @param username 数据库用户名
     * @param password 数据库密码
     */
    private DruidDataSource createDataSource(String url, String username, String password) throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        druidDataSource.setUrl(url);// 数据库地址
        druidDataSource.setUsername(username);// 用户名
        druidDataSource.setPassword(password);// 密码
        // 初始化大小,最小,最大
        druidDataSource.setInitialSize(5);
        druidDataSource.setMinIdle(3);
        druidDataSource.setMaxActive(20);
        druidDataSource.setMaxWait(60000);//配置获取连接等待超时的时间
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        druidDataSource.setMinEvictableIdleTimeMillis(300000);//配置一个连接在池中最小生存的时间,单位是毫秒
        druidDataSource.setValidationQuery("SELECT 'x'");
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        //打开PSCache,并且指定每个连接上PSCache的大小
        druidDataSource.setPoolPreparedStatements(true);//   在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。 5.5及以上版本有PSCache,建议开启。
        druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
        druidDataSource.setFilters("stat,wall,log4j");//配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        //通过connectProperties属性来打开mergeSql功能;慢SQL记录
        Properties properties = new Properties();
        properties.put("slowSqlMillis", "5000");
        druidDataSource.setConnectProperties(properties);
        druidDataSource.setUseGlobalDataSourceStat(true);//合并多个DruidDataSource的监控数据
        return druidDataSource;
    }

    /**
     * 创建动态DataSource
     *
     * @param dataSource1 数据源1
     * @param dataSource2 数据源2
     */
    @Bean
    public DynamicDataSource source(@Qualifier("db1") DataSource dataSource1, @Qualifier("db2") DataSource dataSource2) throws SQLException {
        DynamicDataSource source = new DynamicDataSource();
        Map map = new HashMap<>();
        map.put("db1", dataSource1);
        map.put("db2", dataSource2);
        source.setTargetDataSources(map);
        source.setDefaultTargetDataSource(dataSource1);
        return source;
    }

    /**
     * sqlSessionFactory
     *
     * @param dataSource 数据源
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactoryBean(DynamicDataSource dataSource) throws SQLException, IOException {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setMapperLocations(resolver.getResources("classpath:/mapper/**/*Mapper.xml"));
        return factory;
    }

    /**
     * dao映射配置
     */
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        configurer.setBasePackage("com.**.dao");
        return configurer;
    }

    /**
     * 事务配置
     *
     * @param dataSource 数据源
     */
    @Bean
    public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) throws SQLException {
        return new DataSourceTransactionManager(dataSource);
    }
}

至此就算是配置成功了,下面开始测试

测试Controller

@RequestMapping("/")
@ResponseBody
public Map<String, Object> list(@RequestParam(name = "type", defaultValue = "0") int type) {
    Map<String, Object> json = new HashMap<>(4);
    // 根据type参数切换 如果type==2就查询demo2数据库
    if (type == 2) {
        DataSourceUtils.setDataSource("db2");
    }
    // 访问db 代码就不贴了
    List<User> userList = userService.listUser();
    json.put("list", userList);
    System.out.println(Thread.currentThread().getName());
    DataSourceUtils.clearDataSource();
    return json;
}

访问127.0.0.1

{"list":[{"createTime":"2017-09-18 19:58:44","id":1,"password":"123456","username":"demo1数据库"}]}

访问127.0.0.1?type=2

{"list":[{"createTime":"2017-09-18 19:58:44","id":1,"password":"123456","username":"demo2数据库"}]}

ok,总结完毕

你可能感兴趣的:(Spring)