springboot+mybatis-plus实现多数据源(从数据库加载多数据源)

springboot+mybatis-plus配置多数据源的方式网上有很多,但是都是把数据源配置在yml或者properties中,由于本人所在项目需要从数据库加载数据源,所以本文介绍本人实现的方法是从数据库加载数据源。
1.实现原理
如果数据源是配置文件配置的,在项目启动时就会自动加载所以所有数据源并且实例化成相应的bean。但是数据库配置时,需要先加载一个主数据源,读取数据库表,把表里面配置数据库源再加载为bean。
2.实现步骤
1.由于在MyBatisPlusConfig中配置的地方需要配置一个DataSource去查询数据加载多数据源。代码如下

@Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSourceSwitch) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();
        mybatisPlus.setDataSource(dataSourceSwitch);
        mybatisPlus.setVfs(SpringBootVFS.class);
        String configLocation = this.properties.getConfigLocation();
        if(StrUtil.isNotBlank(configLocation)) {
            mybatisPlus.setConfigLocation(this.resourceLoader.getResource(configLocation));
        }
        mybatisPlus.setConfiguration(properties.getConfiguration());
        mybatisPlus.setPlugins(this.interceptors);
        MybatisConfiguration mc = new MybatisConfiguration();
        mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        mc.setMapUnderscoreToCamelCase(true);// 数据库和java都是驼峰,就不需要
        mybatisPlus.setConfiguration(mc);
        if (this.databaseIdProvider != null) {
            mybatisPlus.setDatabaseIdProvider(this.databaseIdProvider);
        }
        mybatisPlus.setTypeAliasesPackage(typeAliasesPackage);
        mybatisPlus.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        mybatisPlus.setMapperLocations(this.properties.resolveMapperLocations());
        mybatisPlus.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        return mybatisPlus.getObject();

//        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
//        sessionFactory.setDataSource(dataSource);
//        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
//        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
//        return sessionFactory.getObject();
    }

2.mybatisPlus.setDataSource(dataSourceSwitch); 这句代码,但是如果在这里直接配置主数据源,那么后面在加载多个数据源之后的重新配置比较麻烦(其实自己不会实现)。所以在这里我直接配置一个动态数据源对象,这个对象继承了AbstractRoutingDataSource对象。这个对象的相关源码中有两个属性targetDataSources(目标数据源是一个map,也就是我们配置的多数据源)和defaultTargetDataSource(默认的数据源,如果从map获取为null则默认设置的数据源)。所以我们初始化的时候就需要配置这两个属性,默认数据源就是主数据源。但是我们初始化的时候数据源对象还没生成并没有读取数据库,那我们的数据源怎么加载了,这里我用到了一个全局的hashMap。提前设置到targetDataSources上,在之后读取的时候王这个hashMap里面添加即可。所以在初始化的我实现了一下BeanPostProcessor(spring提供的一个可以在bean实例化之前和之后调用的方法可以动态修改我们的bean),代码如下:

@Slf4j
@Configuration
public class DataSourceBeanPostProcessor implements BeanPostProcessor, BeanDefinitionRegistryPostProcessor {

    /**
     * 拦截dataSource初始化之后。将其装换为DataSourceSwitch,便于后续从数据库读取其他数据源注入
     * 这个地方需要在bean初始化之前执行.否则不会执行 {@link AbstractRoutingDataSource#afterPropertiesSet()}. 会提示数据源未初始化
     * @param bean:
     * @param beanName:
     * @date 2022/2/14 8:57
     * @return {@link Object}
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (Objects.equals(beanName, DataSourceEnum.DATA_SOURCE_PRIMARY.getName())) {
            log.info("replace dataSource");
            DataSourceSwitch dataSourceSwitch = new DataSourceSwitch();
            DataSourceContextHolder.DATA_SOURCE_MAP.put(DataSourceEnum.DATA_SOURCE_PRIMARY.getName(), bean);
            dataSourceSwitch.setTargetDataSources(DataSourceContextHolder.DATA_SOURCE_MAP);
            dataSourceSwitch.setDefaultTargetDataSource(bean);
            return dataSourceSwitch;
        }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        String[] beanDefinitionNames = registry.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            if (Objects.equals(beanDefinitionName, "sqlSessionTemplate")) {
                log.info("sqlSessionTemplate is primary");
                BeanDefinition sqlSessionTemplate = registry.getBeanDefinition("sqlSessionTemplate");
                sqlSessionTemplate.setPrimary(true);
                registry.registerBeanDefinition("sqlSessionTemplate", sqlSessionTemplate);
            }
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}


/**
 * @className DataSourceContextHolder
 * @date 2022/2/12 16:46
 **/
public class DataSourceContextHolder {

    /**数据源名称**/
    private static final ThreadLocal<String> databaseHolder = new ThreadLocal<>();

    public static final Map<Object, Object> DATA_SOURCE_MAP = new ConcurrentHashMap<>(8);

    public static void setDatabaseHolder(String dataSourceName) {
        databaseHolder.set(dataSourceName);
    }

    /**
     * 取得当前数据源
     *
     * @return
     */
    public static String getDatabaseHolder() {
        return databaseHolder.get();
    }

    /**
     * 清除上下文数据
     */
    public static void clear() {
        databaseHolder.remove();
    }
}

3.postProcessBeforeInitialization (bean初始化之前)。该方法中,我们拦截到dataSource这个对象,beanName就是dataSource。然后将dataSource对象转换为dataSourceSwitch我们声明的数据源对象,并且设置对象相关属性targetDataSourcesdefaultTargetDataSource。全局的hashMap就是DataSourceContextHolder 类中的 DATA_SOURCE_MAP。这里说一下targetDataSources这个map获取数据源的方法。在DataSourceSwitch继承AbstractRoutingDataSource 后,需要重写一个方法determineCurrentLookupKey方法,这个方法返回一个对象就是targetDataSources这个map中的key。所以这个key我配置到了ThreadLocal中databaseHolder

@Slf4j
public class DataSourceSwitch extends AbstractRoutingDataSource {

    /**
     * 根据{@link AbstractRoutingDataSource#targetDataSources} 获取当前数据源。如果为null,则获取默认 {@link AbstractRoutingDataSource#defaultTargetDataSource}
     * @param
     * @author xiatie
     * @date 2022/2/14 9:40
     * @return {@link Object}
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceName = DataSourceContextHolder.getDatabaseHolder();
        log.info("----------------get dataSource {}----------------", dataSourceName);
        return dataSourceName;
    }
}

4.经过上面的,主数据源就已经配置完成了,并且提供的一个可以随时添加数据源的map。然后在自己写的DataSourceConfig中查询数据源注入到容器中并且添加到全局数据源map里面

@Slf4j
@Configuration
public class DataSourceConfig implements InitializingBean {

    @Resource
    private DataSourceMapper dataSourceMapper;

    @Autowired
    private SpringUtil springUtil;

    @Override
    public void afterPropertiesSet() {
        try {
            // 1.查询所有数据源
            QueryWrapper<DataSourceEntity> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("status", Const.VALID);
            List<DataSourceEntity> list = dataSourceMapper.selectList(queryWrapper);
            // 2.为每个数据源生成bean
            createBeanByDataSource(list);
        }catch (Exception e) {
            log.info("数据源加载失败{}", e.getMessage(), e);
        }
    }


    /**
     * 为每个数据源生成bean
     * 初始化配置文件主数据源 ---> 通过主数据源查询获取表中配置的数据源 ---> 将所有数据源配置成bean --->再将所有数据源从新注册到容器中
     * @param list:
     * @author xiatie
     * @date 2022/2/12 9:09
     */
    private void createBeanByDataSource(List<DataSourceEntity> list) throws Exception {
        if (CollectionUtil.isEmpty(list)) {
            log.info("数据源为空");
            return;
        }
        for (DataSourceEntity dataSourceEntity : list) {
            log.info("----------------start register dataSource{}----------------", dataSourceEntity.getDataSourceName());
            DruidDataSource dataSource = new DruidDataSource();

            dataSource.setDbType(dataSourceEntity.getType());
            dataSource.setUrl(dataSourceEntity.getUrl());
            dataSource.setUsername(dataSourceEntity.getUserName());
            dataSource.setPassword(dataSourceEntity.getPassWord());
            dataSource.setDriverClassName(dataSourceEntity.getDriverClassName());
            dataSource.setInitialSize(dataSourceEntity.getPoolInitialSize());
            dataSource.setMinIdle(dataSourceEntity.getPoolMinIdle());
            dataSource.setMaxActive(dataSourceEntity.getPoolMaxActive());
            dataSource.setMaxWait(dataSourceEntity.getPoolMaxWait());
            dataSource.setTimeBetweenEvictionRunsMillis(dataSourceEntity.getPoolTimeBetweenEvictionRunsMillis());
            dataSource.setMinEvictableIdleTimeMillis(dataSourceEntity.getPoolMinEvictableIdleTimeMillis());
            dataSource.setValidationQuery(dataSourceEntity.getPoolValidationQuery());

            springUtil.registerBean(dataSourceEntity.getDataSourceName(), dataSource);
            // 获取bean看是否注册成功
            Object registerBean = springUtil.getBean(dataSourceEntity.getDataSourceName());
            if (Objects.isNull(registerBean)) {
                log.info("{}数据源注册失败", dataSourceEntity.getDataSourceName());
                continue;
            }
            DataSourceContextHolder.DATA_SOURCE_MAP.put(dataSourceEntity.getDataSourceName(), registerBean);

            // 1.为每个数据源注册SqlSessionFactory
            SqlSessionFactory sqlSessionFactory = createSqlSessionFactoryByDataSource(((DruidDataSource) registerBean), dataSourceEntity.getDataSourceName());
            // 2.注册事务管理器
            createDataSourceTransactionManager(((DruidDataSource) registerBean), dataSourceEntity.getDataSourceName());
            // 3.注册sqlSessionTemplate
            createSqlSessionTemplate(sqlSessionFactory, dataSourceEntity.getDataSourceName());
            // 4.这个时候动态数据源中DataSourceSwitch已经存在相应数据,需要从新加载到DataSourceSwitch父类的resolvedDataSources属性中
            ((DataSourceSwitch) springUtil.getBean(DataSourceEnum.DATA_SOURCE_PRIMARY.getName())).afterPropertiesSet();
        }
    }

    /**
     * 注册sqlSessionFactory
     * @param druidDataSource:
     * @param dataSourceName:
     * @author xiatie
     * @date 2022/2/12 10:02
     */
    private SqlSessionFactory createSqlSessionFactoryByDataSource(DruidDataSource druidDataSource, String dataSourceName){
        try {
            MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource((druidDataSource));
            MybatisConfiguration configuration = new MybatisConfiguration();
            configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
            configuration.setJdbcTypeForNull(JdbcType.NULL);
            sqlSessionFactoryBean.setConfiguration(configuration);

            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().
                    getResources("classpath*:com/sevalo/data/statistics/**/*.xml"));
            sqlSessionFactoryBean.setPlugins(new MybatisPlusInterceptor());
            sqlSessionFactoryBean.setGlobalConfig(new GlobalConfig().setBanner(false));

            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();

            springUtil.registerBean(Const.SQL_SESSION_FACTORY + dataSourceName, sqlSessionFactory);
            // 获取是否注册成功
            Object bean = springUtil.getBean(Const.SQL_SESSION_FACTORY + dataSourceName);
            if (Objects.isNull(bean)) {
                log.error("sqlSessionFactory注册失败");
                return null;
            }
            return (SqlSessionFactory) bean;
        }catch (Exception e) {
            log.error("sqlSessionFactory注册失败{}", e.getMessage(), e);
        }
        return null;
    }

    /**
     * 注册事务管理器
     * @param druidDataSource:
     * @param dataSourceName:
     * @author xiatie
     * @date 2022/2/12 10:05
     */
    private void createDataSourceTransactionManager(DruidDataSource druidDataSource, String dataSourceName){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(druidDataSource);

        springUtil.registerBean(Const.TRANSACTION_MANAGER + dataSourceName, transactionManager);
    }

    /**
     * 注册sqlSessionTemplate
     * @param sqlSessionFactory:
     * @param dataSourceName:
     * @author xiatie
     * @date 2022/2/12 10:17
     */
    private void createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, String dataSourceName){
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);

        springUtil.registerBean(Const.SQL_SESSION_TEMPLATE + dataSourceName, sqlSessionTemplate);
    }
}

5.由于AbstractRoutingDataSource 真正保存数据源使用的是resolvedDataSources 这个map。所以需要重新执行一遍加载方法。这个地方获取的bean就是刚才拦截的dataSource,也就是自定义的DataSourceSwitch对象

((DataSourceSwitch) springUtil.getBean(DataSourceEnum.DATA_SOURCE_PRIMARY.getName())).afterPropertiesSet();

6.具体启动执行的时候注入其他的mapper的时候会报找到多个sqlSessionTemplate实例,spring不知道使用哪一个,我们需要指定主sqlSessionTemplate。所以上面代码从除了实现BeanPostProcessor,还实现了一个BeanDefinitionRegistryPostProcessor。这个和BeanPostProcessor类似,只不过这个在beanDefinition(描述spring中bean对象的一个对象)注册前后执行的。

@Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        String[] beanDefinitionNames = registry.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            if (Objects.equals(beanDefinitionName, "sqlSessionTemplate")) {
                log.info("sqlSessionTemplate is primary");
                BeanDefinition sqlSessionTemplate = registry.getBeanDefinition("sqlSessionTemplate");
                sqlSessionTemplate.setPrimary(true);
                registry.registerBeanDefinition("sqlSessionTemplate", sqlSessionTemplate);
            }
        }
    }

设置一个主要的sqlsessionTemplate.为什么在dataSourceMapper中不会报多个sqlsessionTemplate异常,因为在实例化主数据源的时候,加载的多数据源还没来得及实例化完毕

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

猜你喜欢

转载自blog.csdn.net/web18224617243/article/details/126114331