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我们声明的数据源对象,并且设置对象相关属性targetDataSources和defaultTargetDataSource。全局的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异常,因为在实例化主数据源的时候,加载的多数据源还没来得及实例化完毕