场景:项目需要集成多个第三方数据源,数据源配置从主数据库读取,并动态加载,能够支持在springboot+mybatis框架中编写查询mapper和xml,最终实现不同调用不同mapper,查询不同数据源的数据。
前言:查询网上资料多数据源实现都是@Configuration、@MapperScan等注解方式,增加数据源就需要增加配置,不满足要求。因此考虑在springboot加载完毕后,通过代码方式,将多个数据源根据库中配置进行实例化,并将其注入到spring IOC中,在对相关mapper进项手动扫描初始化。此方式即可实现需求,并且支持多数据源热加载。
1.定义多数据源配置参数实体
public class AppDatasource {
/**
* 主键
*/
private Long id;
/**
* 数据库类型(01 mysql ,02 oracle , 03 postgre)
*/
private String dbType;
private String url;
private String username;
private String password;
private String driverClassName;
/**
* 数据源dao所在包目录
*/
private String basePackage;
/**
* mapper所在目录
*/
private String mapperLocations;
/**
* 状态 0 正常 1停用 2 删除
*/
private Integer status;
}
2.定义ApplicationUtil ,用于处理spring容器、Bean注册和查询
@Component
@Lazy(false)
public class ApplicationUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
//通过name获取 Bean.
public static Object getBean(String name) {
return getContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getContext().getBean(clazz);
}
/**
* 同步方法注册bean到ApplicationContext中
*
* @param beanName bean名
* @param clazz 类对象
* @param original bean的属性值
*/
public static synchronized void setBean(String beanName, Class<?> clazz, Map<String,Object> original,Object... constructorArg) {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory();
if(beanFactory.containsBean(beanName)){
return;
}
//BeanDefinition beanDefinition = new RootBeanDefinition(clazz);
GenericBeanDefinition definition = new GenericBeanDefinition();
//类class
definition.setBeanClass(clazz);
if (original!=null){
//属性赋值
definition.setPropertyValues(new MutablePropertyValues(original));
}
if (constructorArg!=null){
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(0,constructorArg);
definition.setConstructorArgumentValues(constructorArgumentValues);
}
//注册到spring上下文
beanFactory.registerBeanDefinition(beanName, definition);
}
public static synchronized void setBean(String beanName, Class<?> clazz, Map<String,Object> original){
setBean(beanName,clazz,original,null);
}
/**
* 同步方法注册bean到ApplicationContext中
*
* @param beanName bean名
* @param clazz 类对象
*/
public static synchronized void setBean(String beanName, Class<?> clazz) {
setBean(beanName,clazz,null);
}
}
3.利用@PostConstruct注解实现启动时执行
@Configuration
public class DataSourceConfig {
@Autowired
ApplicationUtil applicationUtil;
@PostConstruct
public void initDataSource() throws Exception {
// 查询多数据源配置数据
List<SysOpenapiAppDatasource> appDataSources = new ArrayList<>();
// 省略appDataSources 赋值逻辑
for (SysOpenapiAppDatasource appDataSource: appDataSources){
doInit(appDataSource);
}
}
public void doInit(SysOpenapiAppDatasource appDataSource){
// 初始化数据源...代码见后续步骤
}
}
4.创建DataSource,实例化 sqlSessionFactory和sqlSessionTemplate
DruidDataSource _dataSource = new DruidDataSource();
_dataSource.setUrl(appDataSource.getUrl());
_dataSource.setUsername(appDataSource.getUsername());
_dataSource.setPassword(DBPasswordUtil.decrypt(appDataSource.getPassword()));
_dataSource.setDriverClassName(appDataSource.getDriverClassName());
Long appId = appDataSource.getId();
Map<String,Object> original = new HashMap<>();
original.put("dataSource",_dataSource);
original.put("vfs",SpringBootVFS.class);
original.put("mapperLocations",new PathMatchingResourcePatternResolver().getResources(appDataSource.getMapperLocations()));
ApplicationUtil.setBean("sessionFactory"+appId,MybatisSqlSessionFactoryBean.class,original);
DefaultSqlSessionFactory sessionFactoryBean = (DefaultSqlSessionFactory) ApplicationUtil.getBean("sessionFactory" +appId);
ApplicationUtil.setBean("sqlSessionTemplate"+appId,
SqlSessionTemplate.class,null,sessionFactoryBean);
Map<String,Object> scannerOriginal = new HashMap<>();
original.put("sqlSessionFactory",sessionFactoryBean);
//scannerOriginal.put("sqlSessionTemplate",ApplicationUtil.getBean("sqlSessionTemplate"+appId));
scannerOriginal.put("basePackage",appDataSource.getBasePackage());
scannerOriginal.put("sqlSessionTemplateBeanName","sqlSessionTemplate"+appId);
ApplicationUtil.setBean("mapperScanner"+appId,MapperScannerConfigurer.class,scannerOriginal);
5.调用 MapperScannerConfigurer根据配置加载mapper
MapperScannerConfigurer mapperScannerConfigurer = (MapperScannerConfigurer)ApplicationUtil.getBean("mapperScanner"+appId);
mapperScannerConfigurer.postProcessBeanDefinitionRegistry((DefaultListableBeanFactory)ApplicationUtil.getContext().getAutowireCapableBeanFactory());
到这里核心代码就完成了,接下来,可根据自己mapper和xml配置的路径编写相关代码即可。
原理说明
为什么最后要调用 postProcessBeanDefinitionRegistry方法?
这个方法是MapperScannerConfigurer中用于加载mapper,和@MapperScan的内部处理机制一样。它实现了BeanDefinitionRegistryPostProcessor接口,spring会在Bean初始化完毕调用到这里,用于实例化mapper。而我们在这里必须要手动再次调用一下,才能确保我们的mapper被实例化,否则我们在调用mapper时会找不到的。