在Mybatis-spring上基於註解的數據源實現方案
一、遇到的痛點
最近在學習Spring-boot過程中,涉及到操作數據庫。按照DOC引入mybatis-spring-boot-starter,然後按照套路配置application.properties、碼Mapper、dataobject、xxx-mapper.xml的代碼就OK了。這個時候,采用DataSourceAutoConfiguration默認方式實現的,這時單數據源可用了。這種方式,網上有很Blog。
但是,我是測試開發工程師,自動化工程經常要連N個數據源。對於多數據源,網上提供了重寫DataSourceAutoConfiguration的方式。代碼如下:
@Configuration @MapperScan(basePackages = "com.youzan.springboot.dal.master", sqlSessionTemplateRef = "masterSST") public class MasterSouceConfig { private String localMapper = "classpath:mapper/*.xml"; @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource") @Primary public DataSource buildDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "masterSSF") @Primary public SqlSessionFactory buildSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean; bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(localMapper)); return bean.getObject(); } @Bean(name = "masterTM") @Primary public DataSourceTransactionManager buildTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "masterSST") @Primary public SqlSessionTemplate buildSqlSessionTemplate(@Qualifier("masterSSF") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
這個方式,確實可用,不足在於,需要根據不同數據源建立不同的package,一旦數據源發生變更,需要更改所在的package。也看過了動態數據源,那也不是我想要的。
二、方案探索
我在思考能不能基於註解來指定數據源呢?
然後開始寫個註解DataSourceRoute。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceRoute {
String name() default "master";
}
之後,寫了AOP處理器來檢測這個註解,一直無法正確切入。那我在想是不是可以通過重寫mybatis啟動掃描方式實現多數據源呢?然後,閱讀了下mybatis-spring的源碼。org.mybatis.spring.mapper.ClassPathMapperScanner.processBeanDefinitions發現,啟動時,mybatis生成了MapperFactoryBean對象。
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name ‘"
+ holder.getBeanName() + "‘ and ‘"
+ definition.getBeanClassName()
+ "‘ mapperInterface");
}
definition.getConstructorArgumentValues()
.addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues()
.add("addToConfig",this.addToConfig);
然後,我通過Debug看下生成的對象,驗證對代碼的理解。那就朝著創建MapperFactoryBean去就好了。
三、具體方案實現
3.1 知識儲備
請通過網絡等途徑了解下BeanDefinition、BeanDefinitionRegistryPostProcessor、ApplicationContextAware、BeanFactoryPostProcessor、InitializingBean、MapperFactoryBean、MapperProxyFactory、ClassPathMapperScanner、GenericBeanDefinition。前面這些,在你閱讀mybatis源碼時會看到,請先了解。
3.2 實現內容
- 實現多數據源的加載
- Mapper對象掃描加載
- 生成MapperFactoryBean對象與裝配
下面直接上代碼。
3.2.1 讀取配置文件公共類
@Data
public class Config {
// dao的package,現在只支持一個包名
private String daoPath;
// *-mapper.xml的目錄信息
private String mapperPath;
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:56
* @modify history:
*
* @desc:
* 1、讀取數據庫、DAO初始化需要的一些配置信息
*
*/
public Config() {
InputStream in =
this.getClass().getClassLoader()
.getResourceAsStream("application-db.properties");
if (in != null) {
Properties properties = new Properties();
try {
properties.load(in);
} catch (IOException e) {
throw new BeanInitializationException("加載屬性配置文件過程失敗。", e);
}
daoPath = properties.getProperty("mybatis.dao-path");
mapperPath = properties.getProperty("mybatis.mapper-locations");
}
}
}
3.2.2 實現多數據源的加載
第一步、構造多數據源的DataSource
/**
* youzan.com Inc.
* Copyright (c) 2012-2017 All Rights Reserved.
*
* @author: [email protected] 無影
* @date 17/9/20 下午1:20
* @desc
*/
@Data
public class DataSourceBuilder {
/**
* 存儲實例化後的多數據元對象
*/
private Map<String, DataSource> dataSourceMap = new HashMap<>();
/**
* 存儲數據庫別名,在DAO類中,只能使用這些別名
*/
private List<String> dataSourceAlias = new ArrayList<>();
/**
*
* 存儲數據源配置信息,按照數據源分組
*/
private Map<String, Map<String, String>> dataSourceProperties = new HashMap<>();
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午2:10
* @modify history:
*
* @desc:
* 1、讀取系統classpath環境下,application-db.properties文件的數據庫配置
* 2、將數據庫配置按照數據源進行分組
* 3、實例化javax.sql.DataSource對象
*
* @return DataSourceBuilder
*
*/
public DataSourceBuilder builder() {
InputStream in = this.getClass().getClassLoader().
getResourceAsStream("application-db.properties");
if (in != null) {
Properties properties = new Properties();
try {
properties.load(in);
} catch (IOException e) {
throw new BeanInitializationException("read property file error!", e);
}
//結束數據庫配置信息
Iterator<String> propertyKeys = properties.stringPropertyNames().iterator();
while (propertyKeys.hasNext()) {
String key = propertyKeys.next();
String value = properties.getProperty(key);
String[] keys = key.split("[.]");
if (dataSourceProperties.containsKey(keys[0])) {
dataSourceProperties.get(keys[0]).put(key, value);
} else {
Map<String, String> innerMap = new HashMap<>();
innerMap.put(key, value);
dataSourceProperties.put(keys[0], innerMap);
dataSourceAlias.add(keys[0]);
}
}
/**
* 生成數據源
*/
Iterator<String> DSNames = dataSourceProperties.keySet().iterator();
while (DSNames.hasNext()) {
String dsName = DSNames.next();
Map<String, String> dsconfig = dataSourceProperties.get(dsName);
DataSource dataSource = org.springframework.boot.autoconfigure.jdbc
.DataSourceBuilder.create()
.type(MysqlDataSource.class).
.driverClassName(dsconfig.get(dsName + ".datasource.driver-class-name")
.url(dsconfig.get(dsName + ".datasource.url"))
.username(dsconfig.get(dsName + ".datasource.username"))
.password(dsconfig.get(dsName + ".datasource.password")).build();
dataSourceMap.put(dsName, dataSource);
}
}
return this;
}
}
第二步、構造SqlSessionFactoryBean對象
@Data
public class SqlSessionFactoryBuilder {
/**
* 數據庫與實體對象間映射文件目錄
*/
private String localMapper = "classpath:mapper/*.xml";
/**
* @author: [email protected] 無影
* @date: 17/9/20 下午2:28
* @modify history:
* @desc:
* 1、創建一個SqlSessionFactoryBean實例對象
*
* @param dbAlias
* @param dataSource
* @return
*/
public SqlSessionFactoryBean builder(String dbAlias, DataSource dataSource)throws Exception{
SqlSessionFactoryBean bean;
bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(localMapper));
bean.afterPropertiesSet();
return bean;
}
}
第三步、構造SqlSessionFactoryBean對象
/**
* youzan.com Inc.
* Copyright (c) 2012-2017 All Rights Reserved.
*
* @author: [email protected] 無影
* @date 17/9/20 下午2:31
* @desc
*/
@Data
public class SqlSessionTemplateBuilder {
/**
* SqlSessionFactory構建實體
*/
SqlSessionFactoryBuilder ssfb = new SqlSessionFactoryBuilder();
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午2:31
* @modify history:
*
* @desc:
* 1、創建一個SqlSessionFactoryBean實例對象
*
* @param dbAlias
* @param dataSource
* @return
*/
public SqlSessionTemplate builder(String dbAlias, DataSource dataSource)throws Exception{
SqlSessionFactoryBean bean = ssfb.builder(dbAlias,dataSource);
return new SqlSessionTemplate(bean.getObject());
}
}
3.2.3 Mapper對象掃描加載
/**
*
* youzan.com Inc.
* Copyright (c) 2012-2017 All Rights Reserved.
*
* @author: [email protected] 無影
* @date 17/9/20 下午3:29
* @desc
* 1、掃描指定package路徑下的類文件列表
*/
public class ClassScanner {
/**
* 掃描的包路徑
*/
String scanpPackage ;
/**
* @author: [email protected] 無影
* @date: 17/9/20 下午6:49
* @modify history:
*
* @desc:
* 1、掃描指定package下的所有*DAO文件,並轉換成Class<?>
*
* @return Map<String, Class<?>>
* key:為DAO的alais,例如 AppInfoDao,key則為appInfoDao。
* value: Class類型的類信息,非實例化的
*
* @throws Exception
*/
public Map<String, Class<?>> scan() throws Exception{
Config config = new Config();
scanpPackage = config.getDaoPath();
Map<String,Class<?>> classMap = new HashMap<>();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String packagePath = scanpPackage.replace(".", "/");
URL url = loader.getResource(packagePath);
List<String> fileNames = null;
if (url != null) {
String type = url.getProtocol();
if ("file".equals(type)) {
fileNames = getClassNameByFile(url.getPath(), null, true);
}
}
for (String classPath : fileNames) {
classMap.putAll(this.getClassByPath(classPath));
}
return classMap;
}
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:51
* @modify history:
*
* @desc:
* 1、讀取package下的所有類文件
*
* @param filePath
* @param className
* @param childPackage
* @return
*/
private static List<String> getClassNameByFile(String filePath, List<String> className, boolean childPackage) {
List<String> myClassName = new ArrayList<String>();
File file = new File(filePath);
File[] childFiles = file.listFiles();
for (File childFile : childFiles) {
if (childFile.isDirectory()) {
if (childPackage) {
myClassName.addAll(getClassNameByFile(childFile.getPath(), myClassName, childPackage));
}
} else {
String childFilePath = childFile.getPath();
if (childFilePath.endsWith(".class")) {
childFilePath = childFilePath.substring(childFilePath.indexOf("\\classes") + 9,
childFilePath.lastIndexOf("."));
childFilePath = childFilePath.replace("\\", ".");
myClassName.add(childFilePath);
}
}
}
return myClassName;
}
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:52
* @modify history:
*
* @desc:
* 1、將DAO的標準文件,轉成 DAO Class
*
* @param classPath
* @return
* @throws Exception
*/
public Map<String, Class<?>> getClassByPath(String classPath)
throws Exception{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Map<String, Class<?>> classMap = new HashMap<>();
classMap.put(this.getClassAlias(classPath),loader.loadClass(this.getFullClassName(classPath)));
return classMap;
}
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:53
* @modify history:
*
* @desc:
* 1、將DAO的標準文件,轉成java標準的類名稱
*
* @param classPath
* @return
* @throws Exception
*/
private String getFullClassName(String classPath)
throws Exception{
int comIndex = classPath.indexOf("com");
classPath = classPath.substring(comIndex);
classPath = classPath.replaceAll("\\/", ".");
return classPath;
}
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:54
* @modify history:
*
* @desc:
* 1、根據類地址,獲取類的Alais,即根據名稱,按照駝峰規則,生成可作為變量的名稱
*
* @param classPath
* @return
* @throws Exception
*/
private String getClassAlias(String classPath)
throws Exception{
String split = "\\/";
String[] classTmp = classPath.split(split);
String className = classTmp[classTmp.length-1];
return this.toLowerFisrtChar(className);
}
/**
*
* @author: [email protected] 無影
* @date: 17/9/20 下午6:55
* @modify history:
*
* @desc:
* 1、將字符串的第一個字母轉小寫
*
* @param className
* @return
*/
private String toLowerFisrtChar(String className){
String fisrtChar = className.substring(0,1);
fisrtChar = fisrtChar.toLowerCase();
return fisrtChar+className.substring(1);
}
}
3.2.4 生成MapperFactoryBean對象與裝配
前面獲取了所有DAO類的Map集合,同時實現了多數據源的加載。這裏通過org.mybatis.spring.mapper.MapperFactoryBean把DAO、數據源模板進行綁定,並註入到Spring Bean工程池了。
@Component
public class MapperScanner implements BeanFactoryPostProcessor, InitializingBean {
/**
* SqlSessionTemplate集合,按照數據庫Alias分組
*/
Map<String, SqlSessionTemplate> sstMap = new HashMap<>();
@Override
public void afterPropertiesSet() throws Exception {
}
public void buildSqlSessionTemplate(Map<String, DataSource> dataSourceMap) throws Exception {
Iterator<String> dataSourceIter = dataSourceMap.keySet().iterator();
while (dataSourceIter.hasNext()) {
String dbAlias = dataSourceIter.next();
DataSource db = dataSourceMap.get(dbAlias);
SqlSessionTemplateBuilder sstb = new SqlSessionTemplateBuilder();
sstMap.put(dbAlias, sstb.builder(dbAlias, db));
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
/**
* 加載所有到DAO類
*/
ClassScanner classScanner = new ClassScanner();
Map<String, Class<?>> daoClasses = new HashMap<>();
try {
daoClasses = classScanner.scan();
} catch (Exception e) {
throw new BeanInstantiationException(this.getClass(), e.getMessage());
}
/**
* 加載多數據源
*/
DataSourceBuilder dsBuiler = new DataSourceBuilder();
Map<String, DataSource> dataSourceMap = dsBuiler.builder().getDataSourceMap();
try {
this.buildSqlSessionTemplate(dataSourceMap);
} catch (Exception e) {
throw new BeanInstantiationException(this.getClass(), e.getMessage());
}
/**
* 生命可執行數據庫DAO代理對象
*/
try {
Iterator<String> classIter = daoClasses.keySet().iterator();
while (classIter.hasNext()) {
String classAlias = classIter.next();
Class<?> classBean = daoClasses.get(classAlias);
/**
* 獲取該類上的數據源註解
*/
DataSourceRoute annotation = classBean.getAnnotation(DataSourceRoute.class);
//實例化MapperFactory
MapperFactoryBean bean = new MapperFactoryBean();
// 給MapperFactory指定其應該使用的數據庫模
String dbAlias = annotation.name();
bean.setSqlSessionTemplate(sstMap.get(dbAlias));
// 指定DAO
bean.setMapperInterface(classBean);
// 刷新
bean.afterPropertiesSet();
// 寫入Spring Bean工廠裏
beanFactory.registerSingleton(classAlias, bean.getObject());
}
} catch (Exception e) {
throw new BeanInstantiationException(this.getClass(), e.getMessage());
}
}
}
3.2.5 應用
這時,我們就可以修改DAO的實現。指定的數據源名稱為配置文件裏數據庫配置信息的第一段名稱,例如:「master.datasource.url=jdbc:mysql://127.0.0.1:3006/testdb」,這時名稱就是master。同時去掉了Spring-boot指導方案中的@Mapper註解。
@DataSourceRoute(name="master")
public interface AppInfoDAO {
int delete(Integer id);
int insert(AppInfoDO appInfoDO);
int insertSelective(AppInfoDO appInfoDO);
AppInfoDO select(Integer id);
int updateByPrimaryKeySelective(AppInfoDO appInfoDO);
int update(AppInfoDO appInfoDO);
}
修改Spring-boot啟動的入口Application類,排除DataSourceAutoConfiguration的加載。
@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Bootstrap {
public static void main(String[] args) {
SpringApplication.run(Bootstrap.class,args);
}
}
至此,就可以啟動測試了。
這個方案,只是做個引子,沒有完全按照Spring的標準實現。Spring的標準要求,應該把DataSoure、SqlSessionFactoryBean、SqlSessionTemplate註入Spring工程池裏,並給所有DAO類指定Bean的生命周期等。
在Mybatis-spring上基於註解的數據源實現方案