在Mybatis-spring上基于注解的数据源实现方案

https://tech.youzan.com/zai-mybatis-springshang-ji-yu-zhu-jie-de-shu-ju-yuan-shi-xian-fang-an/

一、遇到的痛点

      最近在学习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); } } 
1234567891011121314151617181920212223242526272829303132333435

      这个方式,确实可用,不足在于,需要根据不同数据源建立不同的package,一旦数据源发生变更,需要更改所在的package。也看过了动态数据源,那也不是我想要的。

二、方案探索

      我在思考能不能基于注解来指定数据源呢? 
      然后开始写个注解DataSourceRoute。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceRoute {

    String name() default "master"; } 
123456

      之后,写了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); 
1234567891011121314151617

      然后,我通过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"); } } } 
1234567891011121314151617181920212223242526272829303132333435

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; } } 
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586

第二步、构造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; } } 
12345678910111213141516171819202122232425262728

第三步、构造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()); } } 
12345678910111213141516171819202122232425262728293031323334

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); } } 
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175

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()); } } } 
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879

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); } 
123456789

       修改Spring-boot启动的入口Application类,排除DataSourceAutoConfiguration的加载。

@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Bootstrap { public static void main(String[] args) { SpringApplication.run(Bootstrap.class,args); } } 
1234567

       至此,就可以启动测试了。 
       这个方案,只是做个引子,没有完全按照Spring的标准实现。Spring的标准要求,应该把DataSoure、SqlSessionFactoryBean、SqlSessionTemplate注入Spring工程池里,并给所有DAO类指定Bean的生命周期等。

转载于:https://www.cnblogs.com/davidwang456/articles/9238957.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值