实现mybatis未知个数数据源动态切换

    公司在做多租户的方案时,决定根据不同租户创建多个数据库。所以,请求调mybatis时,需要根据请求的某个参数去走不同的库,需要实现动态数据源切换。实现的时候遇到不少坑,今天看了一下mybatis源码,在这记录一下。

    关键类:AbstractRoutingDataSource

    这个类是spring-jdbc专门用来实现动态数据源切换的类。在项目启动的时候,我们可以把所有数据源根据key-value的形式放到这个类的targetDataSources里去。在调用数据库时,可以用determineCurrentLookupKey来决定当前调用(线程)使用哪个数据源。是的,这里可以用ThreadLocal来实现key的管理。

    下面详细介绍:

  1.     依赖:
            
                mysql
                mysql-connector-java
            
            
                org.springframework.boot
                spring-boot-starter-data-jpa
            
    
            
                com.alibaba
                druid
                1.0.31
            
    
            
                org.springframework
                spring-jdbc
                5.1.5.RELEASE
            
    
            
                org.mybatis
                mybatis-spring
                1.3.1
            
    
            
                org.mybatis
                mybatis
                3.4.4
            
    

     

      2..yml文件中定义数据源,我的方案是多数据源的name之间逗号隔开

mutiljdbc:
  driverClassName: com.mysql.jdbc.Driver
 url:jdbc:mysql://xxx.xx.xxx:3306/aaaa,jdbc:mysql://xxx.xx.xxx:3306/bbbb
  username: aaa
  password: aaa

    这个依据个人想实现的方案而定。

    3.动态解析上面的参数,动态生成DruidDataSource类型的springbean扔到spring容器里。这一步是为了给AbstractRoutingDataSource提供数据源做准备。

    用一个配置类实现InitializingBean,让这个类在初始化的时候做这个事情;

    简单说一下InitializingBean这个接口,它是spring提供的,可以在bean初始化的时候做一些自定义的事情,注意这个阶段spring容器未初始化完毕。代码如下:

  /**
     * 本bean初始化时,根据数据源个数,动态生成数据源bean
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        String urls = environment.getProperty("mutiljdbc.url");
        if (urls != null) {
            String[] urlArray = urls.split(",");
            for (String url : urlArray) {
                urlList.add(url);
            }
            for (String url : urlList) {
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
                beanDefinitionBuilder.addPropertyValue("driverClassName", environment.getProperty("mutiljdbc.driverClassName"));
                beanDefinitionBuilder.addPropertyValue("username", environment.getProperty("mutiljdbc.username"));
                beanDefinitionBuilder.addPropertyValue("password", environment.getProperty("mutiljdbc.password"));
                beanDefinitionBuilder.addPropertyValue("url", url);
                BeanDefinition dataBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
                BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
                //不知道怎么给bean取名字,直接用url作为bean的名字,去掉冒号和斜扛
                url = url.replace("/", "").replace(":", "");
                beanFactory.registerBeanDefinition(url, dataBeanDefinition);
            }
        }
    }

    其中urlList是个静态的全局list,专门放url。

    4.自己实现一个类,继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法。

/**
 * @author honc.z
 * @date 2019/4/3
 *
 * 动态数据源(需要继承AbstractRoutingDataSource)
 * 读写分离核心
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseType();
    }
}

    其中DatabaseContextHolder就是一个控制key的ThreadLocal.

/**
 * @author honc.z
 * @date 2019/4/3
 * 

* 线程安全的datebase容器 * threadlocal实现 */ public class DatabaseContextHolder { private static final ThreadLocal contextHolder = new ThreadLocal<>(); public static void setDatabaseType(String type) { contextHolder.set(type); } public static String getDatabaseType() { return contextHolder.get(); } public static void remove(){ contextHolder.remove(); } }

    5.将DynamicDataSource作为springbean配置一下,把数据源放进去。

    

  /**
     * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        Map targetDataSources = new HashMap<>();
        String defaultUrl = null;
        //将所有数据源注册到动态数据源map里,key为去掉冒号和斜扛的url,value为对应的datasource所形成的bean
        for (String url : urlList) {
            url = url.replace("/","").replace(":","");
            targetDataSources.put(url, applicationContext.getBean(url));
            defaultUrl = url;
        }

        DynamicDataSource dataSource = new DynamicDataSource();
        // 该方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默认的datasource设置为最后一个数据源
        dataSource.setDefaultTargetDataSource(applicationContext.getBean(defaultUrl));

        return dataSource;
    }

    这样,DynamicDataSource就会有一个默认的datasource(如果在请求过程中不使用determineCurrentLookupKey方法,就会走这个默认数据源),以及两个在yml中配置的数据源。

    6.最后,把这个DynamicDataSource给Mybatis启动核心SqlSessionFactory。

 /**
     * 根据数据源创建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        // 指定数据源(这个必须有,否则报错)
        fb.setDataSource(this.dataSource());
        // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
        // fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));
        // 指定基包
        fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(environment.getProperty("mybatis.mapper-locations")));

        //把可能定义的拦截器扔进去
        try {
            String[] interceptors = applicationContext.getBeanNamesForType(Interceptor.class);
            if (interceptors != null){
                Interceptor[] interceptorsArray = new Interceptor[interceptors.length];
                for (int i = 0;i

    这里特别注意:如果有自定义的拦截器,必须要在这里手动加进去。因为没有引入mybatis-spring-boot-starter这个依赖(不能引,因为这个依赖会自动去找spring.datasource参数,而我们必须手动配置,找不到它会报错),这个依赖在定义启动类时,会去扫描所有的Interceptor(源码我就不贴了,在MybatisAutoConfiguration这个类里)。

    到这里基本就结束了,在调用的时候,更具某个参数去调用DatabaseContextHolder.setDatabaseType(key);就可以实现指定数据源。这个地方的key,我是根据来数据源url去匹配请求中的某个参数来设定,这个方法不太好,限制比较大。我的解决方案是提供一个接口让开发去设置key,然后让开发根据参数来匹配。

    另外,还有一个必须要注意的地方,springboot启动类注解必须加上@SpringBootApplication(exclude={DataSourceAutoConfiguration.class}),这个究竟是为什么暂时没时间看。

    下面是源码地址,https://github.com/skesunny/sample-all,在sample-datasource这个module下面。

 

 

 

 

你可能感兴趣的:(数据库)