• springboot-mybatis多数据源以及踩坑之旅


    首先,springboot项目结构如下

    springboot配置文件内容如下

    动态数据源的配置类如下(必须保证能被ComponentScan扫描到):

     1 package com.letzgo.config;
     2 
     3 import com.alibaba.druid.pool.DruidDataSource;
     4 import org.apache.ibatis.session.SqlSessionFactory;
     5 import org.mybatis.spring.SqlSessionFactoryBean;
     6 import org.mybatis.spring.SqlSessionTemplate;
     7 import org.mybatis.spring.annotation.MapperScan;
     8 import org.springframework.beans.factory.annotation.Qualifier;
     9 import org.springframework.boot.context.properties.ConfigurationProperties;
    10 import org.springframework.context.annotation.Bean;
    11 import org.springframework.context.annotation.Configuration;
    12 import org.springframework.context.annotation.Primary;
    13 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    14 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    15 
    16 import javax.sql.DataSource;
    17 
    18 /**
    19  * @author allen
    20  * @date 2019-01-10 15:08
    21  */
    22 public class DynamicDatasourceConfig {
    23 
    24     @Configuration
    25     @MapperScan(basePackages = "com.letzgo.dao.master")
    26     public static class Master {
    27         @Primary
    28         @Bean("masterDataSource")
    29         @Qualifier("masterDataSource")
    30         @ConfigurationProperties(prefix = "spring.datasource.master")
    31         public DataSource dataSource() {
    32             return new DruidDataSource();
    33         }
    34 
    35         @Primary
    36         @Bean("masterSqlSessionFactory")
    37         @Qualifier("masterSqlSessionFactory")
    38         public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
    39             SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    40             factoryBean.setDataSource(dataSource);
    41             factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/master/*.xml"));
    42             return factoryBean.getObject();
    43         }
    44 
    45         @Primary
    46         @Bean("masterTransactionManager")
    47         @Qualifier("masterTransactionManager")
    48         public DataSourceTransactionManager transactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
    49             return new DataSourceTransactionManager(dataSource);
    50         }
    51 
    52         @Primary
    53         @Bean("masterSqlSessionTemplate")
    54         @Qualifier("masterSqlSessionTemplate")
    55         public SqlSessionTemplate sqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    56             return new SqlSessionTemplate(sqlSessionFactory);
    57         }
    58 
    59     }
    60 
    61     @Configuration
    62     @MapperScan(basePackages = "com.letzgo.dao.slave")
    63     public static class Slave {
    64         @Bean("slaveDataSource")
    65         @Qualifier("slaveDataSource")
    66         @ConfigurationProperties(prefix = "spring.datasource.slave")
    67         public DataSource dataSource() {
    68             return new DruidDataSource();
    69         }
    70 
    71         @Bean("slaveSqlSessionFactory")
    72         @Qualifier("slaveSqlSessionFactory")
    73         public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
    74             SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    75             factoryBean.setDataSource(dataSource);
    76             factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/slave/*.xml"));
    77             return factoryBean.getObject();
    78         }
    79 
    80         @Bean("slaveTransactionManager")
    81         @Qualifier("slaveTransactionManager")
    82         public DataSourceTransactionManager transactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
    83             return new DataSourceTransactionManager(dataSource);
    84         }
    85 
    86         @Bean("slaveSqlSessionTemplate")
    87         @Qualifier("slaveSqlSessionTemplate")
    88         public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
    89             return new SqlSessionTemplate(sqlSessionFactory);
    90         }
    91     }
    92 
    93 }

    完成基本配置之后,分别在master和slave中写一个数据库访问操作,再开放两个简单的接口,分别触发master和slave的数据看访问操作。

    至此没项目基本结构搭建已完成,启动项目,进行测试。

    我们会发现这样master的数据库访问是能正常访问的,但是slave的数据库操作是不行的,报错信息如下:

    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):***

    对于这样错误,起初企图通过百度解决,大部分都是说xml文件的命名空间和dao接口全名不对应或者说是接口方法和xml中的方法不对应等等解决方法,

    本人检查了自己的代码多遍重启多遍均无法解决,并不是说这些方法不对,但是本案例的问题却不是这些问题导致的。最后无奈,只能硬着头皮去看源码,最后发现了问题所在。

    debug源码调试到最后,发现不论是执行mater还是slave的数据库操作,使用了相同的SqlSession,同一个!!!这个肯定是有问题的。

    继续看源码进行查,看SqlSession的注入过程。

    我们知道mybatis只要写接口不用写实现类(应该是3.0之后的版本),实际上是使用了代理,每个dao接口,在spring容器中其实是对应一个MapperFactoryBean(不懂FactoryBean的可以去多看看spring的一些核心接口,要想看懂spring源码必须要知道的)。

    当从容器中获取bean的时候,MapperFactoryBean的getObject方法就会根据SqlSession实例生产一个MapperProxy对象的代理类。

    问题的关键就在于MapperFactoryBean,他继承了SqlSessionDaoSupport类,他有一个属性,就是SqlSession,而且刚才所说的创建代理类所依赖的SqlSession实例就是这个。那我们看这个SqlSession实例是什么时候注入的就可以了,就能找到为什么注入了同一个对象了。

    找spring注入的地方,spring注入的方式个人目前知道的有注解处理器如@Autowired的注解处理器AutowiredAnnotationBeanPostProcessor等类似的BeanPostProcessor接口的实现类,还有一种就是在BeanDefinition中定义器属性的注入方式,在bean的定义阶段就决定了的,前者如果不知道的可以看看,在此不做赘述,后者的处理过程源码如下(只截取核心部分,感兴趣的可以自己看一下处理过程,调用链比较深,贴代码会比较多,看着眼花缭乱):

    debug到dao接口类的的BeanDefinition(上文已说过其实是MapperFactoryBean),发现他的autowiremode是2,参照源码

    即可发现为按照类型自动装配

    最关键的来了:

    debug的时候发现,master的dao接口执行到this.autowireByType(beanName, mbd, bw, newPvs)方法中,给MapperFactoryBean中SqlSession属性注入的实例是masterSqlSessionTemplate对象,

    slave的dao接口执行该方法时注入的也是masterSqlSessionTemplate对象,按类型注入,spring容器中找到一个即注入(此时slaveSqlSessionTemplate也在容器中,为什么按类型注入找到了masterSqlSessionTemplate却没报错,应该是@Primary的作用

    至此,问题产生的原因已基本找到,那该如何解决呢?BeanDefinition为什么会定义成autowiremode=2呢,只能找@MapperScan看了,看这个注解的处理源码,最后找到ClassPathMapperScanner以下方法:

     1 private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
     2         Iterator var3 = beanDefinitions.iterator();
     3 
     4         while(var3.hasNext()) {
     5             BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
     6             GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
     7             if (this.logger.isDebugEnabled()) {
     8                 this.logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface");
     9             }
    10 
    11             definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
    12             definition.setBeanClass(this.mapperFactoryBean.getClass());
    13             definition.getPropertyValues().add("addToConfig", this.addToConfig);
    14             boolean explicitFactoryUsed = false;
    15             if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
    16                 definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
    17                 explicitFactoryUsed = true;
    18             } else if (this.sqlSessionFactory != null) {
    19                 definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
    20                 explicitFactoryUsed = true;
    21             }
    22 
    23             if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
    24                 if (explicitFactoryUsed) {
    25                     this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
    26                 }
    27 
    28                 definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
    29                 explicitFactoryUsed = true;
    30             } else if (this.sqlSessionTemplate != null) {
    31                 if (explicitFactoryUsed) {
    32                     this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
    33                 }
    34 
    35                 definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
    36                 explicitFactoryUsed = true;
    37             }
    38 
    39             if (!explicitFactoryUsed) {
    40                 if (this.logger.isDebugEnabled()) {
    41                     this.logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
    42                 }
    43 
    44                 definition.setAutowireMode(2);
    45             }
    46         }
    47 
    48     }

    44行是关键,但是有个条件,这个条件成立的原因就是@MapperScan注解没有指定过sqlSessionTemplateRef或者sqlSessionFactoryRef,正因为没有指定特定的sqlSessionTemplate或者sqlSessionFactory,mybatis默认采用按类型自动装配的方式进行注入。

    至此,问题解决方案已出:

    代码中的两个@MapperScan用法分别改为:

    1 @MapperScan(basePackages = "com.letzgo.dao.master", sqlSessionFactoryRef = "masterSqlSessionFactory", sqlSessionTemplateRef = "masterSqlSessionTemplate")
    2  
    3 @MapperScan(basePackages = "com.letzgo.dao.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory", sqlSessionTemplateRef = "slaveSqlSessionTemplate")

    重启进行测试,问题解决。

    PS:

    还是对各种注解使用方法不了解(或者说对框架的源码不了解),导致搞了这么久的问题,还好最后查到了,记录于此,给自己加深印象,也希望解决方案能帮到部分同行。以后还是要多看源码,哈哈哈。

  • 相关阅读:
    Redis分布式锁服务(转)
    redis分布式锁(转)
    MySQL+InnoDB semi-consitent read原理及实现分析(转)
    MySQL加锁处理分析(转)
    实战经验丨PHP反序列化漏洞总结
    脚本语言丨Batch入门教程第四章:调用与传参
    福利狂欢已开启,请做好准备!
    脚本语言丨Batch入门教程第三章:逻辑判断
    WinRAR存在严重的安全漏洞影响5亿用户
    Batch入门教程丨第二章:认识变量相关概念
  • 原文地址:https://www.cnblogs.com/xiao-tao/p/10268016.html
Copyright © 2020-2023  润新知