mybatis集成springboot的多数据源最新实现

一、背景

mybatis自身没有做多数据源(包括读写分离)的支持,网上很多现有博文都是C&V产物,实现较老且缺乏对mybatis-spring的良好支持,故简单写过一个springboot的demo,仅依赖配置即可完成对多数据源的支持。

二、使用

1、项目中增加对应的maven依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>      
</dependency>
<!-- 非必需,方便使用 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

2、将以下两个个文件复制到项目中:

  • 自定义配置映射类
import lombok.Data;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.springframework.beans.factory.support.BeanNameGenerator;

import java.io.Serializable;

/**
 * mybatis复合数据源自动配置属性
 * @author zxf
 * @date 2020-08-16 20:26:13
 */
@Data
class MultiMybatisProperties implements Serializable {
    /**
     * dataSource连接池类型
     */
    private Class<? extends javax.sql.DataSource> pool;
    /**
     * 连接池驱动类型
     */
    private Class<? extends java.sql.Driver> driver;
    /**
     * 连接完整url
     */
    private String url;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 扫描的包路径
     */
    private String mapperLocations;
    /**
     * 扫描的mapper接口包集合
     * @see MapperScan#basePackages()
     */
    private String[] basePackages;
    /**
     * 组件命名器
     * @see MapperScan#nameGenerator()
     */
    private Class<BeanNameGenerator> namingClass;
    /**
     * dao映射代理类工厂类型
     * @see MapperScan#factoryBean()
     */
    private Class<? extends MapperFactoryBean<?>> factoryBean;
    /**
     * 是否懒加载
     * @see MapperScan#lazyInitialization()
     */
    private boolean lazyInitialization;
}
  • 实际逻辑实现类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.Assert;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * mybatis多数据源扫描注册器
 * @author zxf
 * @date 2020-08-16 20:09:23
 */
@Slf4j
class MultiMybatisScannerRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private Environment env;

    /** 作为模板的默认配置key **/
    private static final String DEFAULT_KEY = "default";

    private static final Pattern ID_MATCH = Pattern.compile("^[\\w]+$");

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        log.info("Ready to load properties of multi mybatis configs from environment.");
        Map<String, MultiMybatisProperties> sources = processConfigs();
        registerComponents(sources, registry);
        log.info("Finish to count loading properties of multi mybatis configs from environment:" + sources.size());
    }

    /**
     * 向spring容器中注册对应的相关mybatis组件
     * 其中MapperScannerConfigurer 参考MapperScannerRegistrar的registerBeanDefinitions的实现
     * @see org.mybatis.spring.annotation.MapperScannerRegistrar
     * @param sources 有效的配置集合
     * @param registry bean注冊器
     */
    private void registerComponents(Map<String, MultiMybatisProperties> sources, BeanDefinitionRegistry registry) {
        sources.forEach((key, value) -> {
            Class<? extends BeanNameGenerator> clazz =
                null == value.getNamingClass() ? DefaultBeanNameGenerator.class : value.getNamingClass();
            BeanNameGenerator generator = BeanUtils.instantiateClass(clazz);

            //DataSource
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(value.getPool());
            //builder.addPropertyValue("processPropertyPlaceHolders", true);
            builder.addPropertyValue("jdbcUrl", value.getUrl());
            builder.addPropertyValue("username", value.getUsername());
            builder.addPropertyValue("password", value.getPassword());
            builder.addPropertyValue("driverClassName", value.getDriver().getCanonicalName());
            AbstractBeanDefinition definition = builder.getBeanDefinition();
            String dataSourceName = generator.generateBeanName(definition, registry);
            registry.registerBeanDefinition(dataSourceName, builder.getBeanDefinition());

            //sqlSessionFactory
            builder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionFactoryBean.class);
            builder.addPropertyReference("dataSource", dataSourceName);
            try {
                builder.addPropertyValue("mapperLocations", new PathMatchingResourcePatternResolver().
                    getResources(value.getMapperLocations()));
            } catch (IOException e) {
                e.printStackTrace();
            }
            String SqlSessionFactoryName = generator.generateBeanName(definition, registry);
            registry.registerBeanDefinition(SqlSessionFactoryName, builder.getBeanDefinition());

            //DataSourceTransactionManager
            builder = BeanDefinitionBuilder.genericBeanDefinition(DataSourceTransactionManager.class);
            builder.addPropertyReference("dataSource", dataSourceName);
            String transactionManagerName = generator.generateBeanName(definition, registry);
            registry.registerBeanDefinition(transactionManagerName, builder.getBeanDefinition());

            //SqlSessionTemplate
            builder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionTemplate.class);
            //builder.addPropertyValue("sqlSessionFactory", SqlSessionFactoryName);
            builder.addConstructorArgReference(SqlSessionFactoryName);
            String sqlSessionTemplateName = generator.generateBeanName(definition, registry);
            registry.registerBeanDefinition(sqlSessionTemplateName, builder.getBeanDefinition());

            //MapperScannerConfigurer
            builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
            builder.addPropertyValue("processPropertyPlaceHolders", true);
            if (!BeanNameGenerator.class.equals(clazz)) {
                builder.addPropertyValue("nameGenerator", generator);
            }
            if (null != value.getFactoryBean()) {
                builder.addPropertyValue("mapperFactoryBeanClass", value.getFactoryBean().getCanonicalName());
            }
            builder.addPropertyValue("sqlSessionTemplateBeanName", sqlSessionTemplateName);
            List<String> basePackages = Arrays.stream(value.getBasePackages()).
                filter(org.springframework.util.StringUtils::hasText).collect(Collectors.toList());
            builder.addPropertyValue("basePackage",
                org.springframework.util.StringUtils.collectionToCommaDelimitedString(basePackages));
            builder.addPropertyValue("lazyInitialization", String.valueOf(value.isLazyInitialization()));
            registry
                .registerBeanDefinition(generator.generateBeanName(definition, registry), builder.getBeanDefinition());
        });
    }

    /**
     * 获取有效的配置集合
     * @return java.util.List<com.resintec.lilliput.system.config.data.MultiMybatisProperties>
     */
    private Map<String, MultiMybatisProperties> processConfigs(){
        //首先确保在配置文件中能解析到对应的配置
        Bindable<Map<String, MultiMybatisProperties>>bindable = Bindable.mapOf(String.class, MultiMybatisProperties.class);
        BindResult<Map<String, MultiMybatisProperties>>bind = Binder.get(env).bind("mybatis.multi", bindable);
        Assert.isTrue(null != bind && bind.isBound(),
            "Failed to load multi mybatis dataSource due to no config found in th environment.");

        //“default”关键字作为id将标识当前配置为模板属性配置,不作为独立的真实配置来处理
        Map<String, MultiMybatisProperties> sources, finalSources = sources = bind.get();
        sources.entrySet().stream().filter(en -> DEFAULT_KEY.equals(en.getKey())).findFirst().ifPresent(temp -> finalSources.values().forEach(s -> {
            s.setUrl(StringUtils.isBlank(s.getUrl()) ? temp.getValue().getUrl() : s.getUrl());
            s.setUsername(StringUtils.isBlank(s.getUsername()) ? temp.getValue().getUsername() : s.getUsername());
            s.setPassword(StringUtils.isBlank(s.getPassword()) ? temp.getValue().getPassword() : s.getPassword());
            s.setBasePackages(null == s.getBasePackages() ? temp.getValue().getBasePackages() : s.getBasePackages());
            s.setMapperLocations(StringUtils.isBlank(s.getMapperLocations()) ? temp.getValue().getMapperLocations() : s.getMapperLocations());
            s.setPool(null == s.getPool() ? temp.getValue().getPool() : s.getPool());
            s.setDriver(null == s.getDriver() ? temp.getValue().getDriver() : s.getDriver());
            s.setNamingClass(null == s.getNamingClass() ? temp.getValue().getNamingClass() : s.getNamingClass());
            s.setFactoryBean(null == s.getFactoryBean() ? temp.getValue().getFactoryBean() : s.getFactoryBean());
        }));
        sources = finalSources.entrySet().stream().filter(en -> !DEFAULT_KEY.equals(en.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        //基本校验
        sources.forEach((key, value) -> Assert.isTrue(
            ID_MATCH.matcher(key).matches()
                && StringUtils.isNotBlank(value.getUrl())
                && StringUtils.isNotBlank(value.getUsername())
                && StringUtils.isNotBlank(value.getPassword()) && null != value.getBasePackages()
                && value.getBasePackages().length != 0
                && StringUtils.isNotBlank(value.getMapperLocations())
                && null != value.getPool()
                && null != value.getDriver()
            //&& null != s.getNamingClass()
            //&& null != s.getFactoryBean()
            , "Failed to load multi mybatis dataSource due to the illegal config:" + value));
        Assert.isTrue(!sources.isEmpty(),
            "Failed to load multi mybatis dataSource due to no available config found in th environment.");
        log.info("Find multi mybatis dataSource config from environment:{}.", sources.keySet());

        return sources;
    }

    @Override
    public void setEnvironment(Environment environment) {
        env = environment;
    }
}

3、在项目的 application.yml或者其他可纳入spring管理的配置中增加以下配置内容(以yml文件为例):

mybatis:
    multi:
        db1: #某个数据源的唯一标识;不要重复;“default”将会作为关键字标识此数据源为模板配置(不作为独立的数据源,仅供其他数据源参考的默认设置)
            pool: com.zaxxer.hikari.HikariDataSource #使用的连接池类型
            driver: com.mysql.jdbc.Driver #使用的数据库连接驱动
            username: root #使用的数据库用户名
            password: root #使用的数据库密码
            url: jdbc:mysql://127.0.0.1:3306/test1 #使用的数据库url
            lazy-initialization: true #是否懒加载
        db2:
            pool: com.zaxxer.hikari.HikariDataSource
            driver: com.mysql.jdbc.Driver
            username: root
            password: root
            url: jdbc:mysql://127.0.0.1:3306/test2
            lazy-initialization: true
        db3:
            pool: com.zaxxer.hikari.HikariDataSource
            driver: com.mysql.jdbc.Driver
            username: root
            password: root
            url: jdbc:mysql://127.0.0.1:3306/test3
            lazy-initialization: true
        db4:
            pool: com.zaxxer.hikari.HikariDataSource
            driver: com.mysql.jdbc.Driver
            username: root
            password: root
            url: jdbc:mysql://127.0.0.1:3306/test4
            lazy-initialization: true

三、扩展

1、该demo只是基本示范,在配置映射的类MultiMybatisProperties中只有几个最基本的属性,但是实际项目中dataSource、数据库连接池、mybatis-spring可能会需要更多的配置属性。只要在MultiMybatisProperties中增加对应属性并在MultiMybatisScannerRegistrar类的registerComponents中增加对该属性的解析即可。

2、关于读写分离,建议将读与写相关的mapper与dao放在不同的包下当作两个不同的数据源来处理即可。如果实在不想分包,建议从自己继承org.mybatis.spring.mapper.MapperFactoryBean入手,手动管理org.apache.ibatis.binding.MapperRegistry,在业务代码中通过aop或者ThreadLocal等手段灵活切换需要使用的org.apache.ibatis.binding.MapperMethod,需要处理的内容就比较多了就懒得写。

3、此demo中的属性映射对象MultiMybatisProperties由实现类从environment中读取,先于spring容器初始化完成,故无法使用@ConfigurationProperties来管理,这导致了该对象在yml等配置文件中缺乏对应的属性提示。用户可以将实现类MultiMybatisScannerRegistrar中的属性绑定关系由现在的map改为list,然后在/resources/META-INF/additional-spring-configuration-metadata.json(该文件对map支持不友好)中增加提示即可。

4、本示例支持本地事务回滚,多数据源的事务请自己通过集成seata等现有的成熟解决方案处理,此demo不涉及。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值