一、遇到的痛点

最近在学习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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @date 17/9/20 下午2:31
* @desc
*/
@Data
public class SqlSessionTemplateBuilder { /**
* SqlSessionFactory构建实体
*/
SqlSessionFactoryBuilder ssfb = new SqlSessionFactoryBuilder(); /**
*
* @author: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @date 17/9/20 下午3:29
* @desc
* 1、扫描指定package路径下的类文件列表
*/
public class ClassScanner { /**
* 扫描的包路径
*/
String scanpPackage ; /**
* @author: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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: lvguoyong@youzan.com 无影
* @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上基于注解的数据源实现方案的更多相关文章

  1. Spring boot 基于注解方式配置datasource

    Spring boot 基于注解方式配置datasource 编辑 ​ Xml配置 我们先来回顾下,使用xml配置数据源. 步骤: 先加载数据库相关配置文件; 配置数据源; 配置sqlSessionF ...

  2. Spring:基于注解的Spring MVC

    什么是Spring MVC Spring MVC框架是一个MVC框架,通过实现Model-View-Controller模式来很好地将数据.业务与展现进行分离.从这样一个角度来说,Spring MVC ...

  3. SpringMVC4 + Spring + MyBatis3 基于注解的最简配置

    本文使用最新版本(4.1.5)的springmvc+spring+mybatis,采用最间的配置方式来进行搭建. 1. web.xml 我们知道springmvc是基于Servlet: Dispatc ...

  4. 【Spring】基于注解的实现SpringMVC+MySQL

    目录结构: // contents structure [-] SprinigMVC是什么 SpringMVC工作原理 @Controller和@RequestMapping注解 @Controlle ...

  5. springAOP实现基于注解的数据源动态切换

    需求 代码实现读写数据库分离 武器 spring3.0以上版本 实现思路 1.继承org.springframework.jdbc.datasource.lookup.AbstractRoutingD ...

  6. spring mvc 基于注解的使用总结

    本文转自http://blog.csdn.net/lufeng20/article/details/7598801 概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Sprin ...

  7. Spring IoC — 基于注解的配置

    基于XML的配置,Bean定义信息和Bean实现类本身是分离的,而采用基于注解的配置方式时,Bean定义信息即通过在Bean实现类上标注注解实现. @Component:对类进行标注,Spring容器 ...

  8. spring中基于注解使用AOP

    本文内容:spring中如何使用注解实现面向切面编程,以及如何使用自定义注解. 一个场景 比如用户登录,每个请求发起之前都会判断用户是否登录,如果每个请求都去判断一次,那就重复地做了很多事情,只要是有 ...

  9. SPRINGAOP实现基于注解的数据源动态切换(转)

    需求 代码实现读写数据库分离 武器 spring3.0以上版本 实现思路 1.继承org.springframework.jdbc.datasource.lookup.AbstractRoutingD ...

随机推荐

  1. 如何oracle调试存储过程

    1.打开PL/SQL Developer 如果在机器上安装了PL/SQL Developer的话,打开PL/SQL Developer界面 输入用户名,密码和host名字,这个跟在程序中web.con ...

  2. Mac OS X下让ruby支持tcl/tk

    我记得在老早在OS X10.8下使用ruby1.9.x的时候只要到下载安装ActiveTcl8.5,没怎么配置就运行tk好好的.但是近日想重新执行下tk代码,发现在require 'tk'的时候就报错 ...

  3. 别跟我谈EF抵抗并发,敢问你到底会不会用EntityFramework

    前言 一直以来写的博文都是比较温婉型的博文,今天这篇博文算是一篇批判性博文,有问题欢迎探讨,如标题,你到底会不会用EntityFramework啊. 你到底会不会用EntityFramework啊 面 ...

  4. Struts,Spring,Hibernate三大框架的

    1.Hibernate工作原理及为什么要用? 原理: 1.读取并解析配置文件 2.读取并解析映射信息,创建SessionFactory 3.打开Session 4.创建事务Transation 5.持 ...

  5. 视频压缩:I帧、P帧、B帧

    /*************************************************************************************************** ...

  6. Wex5执行Class[search.login__do] Method[login]失败

    ====================开发工具版本:WeX5_V3.3======================== 报错背景:大二的时候用这个工具开发了一款APP,备份了项目数据库的SQL文件+ ...

  7. linux配置https站点

    配置https站点呢,那就需要https证书,证书从何而来,花钱买?no,no,no,阿里有免费的,只是比较难发现,下面就图文解说一下怎么买免费的阿里https证书 首先阿里云,登录,购买链接———— ...

  8. LocalDB + IIS

    Win7 + IIS7 1. 安装 (1)LocalDB SQL Express 2012 选中:ENU\x64\SqlLocalDB.MSI (2).net4.5 .net4.5 然后,再配置IIS ...

  9. ImportError: cannot import name webdriver

    遇到问题: 学习selenium过程中为了方便自己知道学习的脚本的存放路径,以selenium命名 起初.py文件都在selenium文件夹下面,使用 from selenium import web ...

  10. 安装SQL Server DQS 和 MDS

    tep1:   安装特性时选择Data Quality Services 和 Master Data Services Step2:  安装完成之后, 打开 SQL Server 2017 Data ...