前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot整合MyBatis原理

SpringBoot整合MyBatis原理

作者头像
张申傲
发布2020-09-03 15:59:00
7310
发布2020-09-03 15:59:00
举报
文章被收录于专栏:漫漫架构路漫漫架构路

SpringBoot整合MyBatis原理

一. 自动配置

SpringBoot提供了MyBatis的自动配置类MybatisAutoConfiguration,可以自动注册SqlSessionFactory、SqlSessionTemplate等组件,开发人员只需在配置文件中指定相关属性即可。

代码语言:javascript
复制
@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {

  private static Log log = LogFactory.getLog(MybatisAutoConfiguration.class);

  @Autowired
  private MybatisProperties properties;

  @Autowired(required = false)
  private Interceptor[] interceptors;

  @Autowired
  private ResourceLoader resourceLoader = new DefaultResourceLoader();

  @Autowired(required = false)
  private DatabaseIdProvider databaseIdProvider;

  //检查配置文件路径
  @PostConstruct
  public void checkConfigFileExists() {
    if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
      Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
      Assert.state(resource.exists(), "Cannot find config location: " + resource
                   + " (please add config file or check your Mybatis " + "configuration)");
    }
  }

  //注册SqlSessionFactory
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    factory.setConfiguration(properties.getConfiguration());
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }

    return factory.getObject();
  }

  //注册SqlSessionTemplate
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  //注册ClassPathMapperScanner,进行Mapper的自动扫描,也可以使用@MapperScan代替
  public static class AutoConfiguredMapperScannerRegistrar
    implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private BeanFactory beanFactory;

    private ResourceLoader resourceLoader;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      log.debug("Searching for mappers annotated with @Mapper'");

      ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

      try {
        if (this.resourceLoader != null) {
          scanner.setResourceLoader(this.resourceLoader);
        }

        List<String> pkgs = AutoConfigurationPackages.get(this.beanFactory);
        for (String pkg : pkgs) {
          log.debug("Using auto-configuration base package '" + pkg + "'");
        }

        scanner.setAnnotationClass(Mapper.class);
        scanner.registerFilters();
        scanner.doScan(StringUtils.toStringArray(pkgs));
      } catch (IllegalStateException ex) {
        log.debug("Could not determine auto-configuration " + "package, automatic mapper scanning disabled.");
      }
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
      this.beanFactory = beanFactory;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
      this.resourceLoader = resourceLoader;
    }
  }

  //如果程序中没有显式注入MapperFactoryBean,就打印日志
  @Configuration
  @Import({ AutoConfiguredMapperScannerRegistrar.class })
  @ConditionalOnMissingBean(MapperFactoryBean.class)
  public static class MapperScannerRegistrarNotFoundConfiguration {

    @PostConstruct
    public void afterPropertiesSet() {
      log.debug(String.format("No %s found.", MapperFactoryBean.class.getName()));
    }
  }

}

SpringBoot为我们自动注册了相应的组件:

  1. SqlSessionFactoryBean:用于构建MyBatis的SqlSessionFactory
  2. SqlSessionTemplate:MyBatis的代理类,将SqlSession与Spring的事务进行了整合
  3. ClassPathMapperScanner:提供MyBatis的Mapper的自动扫描

二. SqlSessionFactoryBean

SqlSessionFactoryBean是一个工厂Bean,其作用就是加载用户自定义的配置,然后使用MyBatis的API创建一个SqlSessionFactory,具体逻辑如下:

代码语言:javascript
复制
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

  Configuration configuration;

  XMLConfigBuilder xmlConfigBuilder = null;
  if (this.configuration != null) {
    configuration = this.configuration;
    if (configuration.getVariables() == null) {
      configuration.setVariables(this.configurationProperties);
    } else if (this.configurationProperties != null) {
      configuration.getVariables().putAll(this.configurationProperties);
    }
  } else if (this.configLocation != null) {
    xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
    configuration = xmlConfigBuilder.getConfiguration();
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Property `configuration` or 'configLocation' not specified, using default MyBatis Configuration");
    }
    configuration = new Configuration();
    configuration.setVariables(this.configurationProperties);
  }

  if (this.objectFactory != null) {
    configuration.setObjectFactory(this.objectFactory);
  }

  if (this.objectWrapperFactory != null) {
    configuration.setObjectWrapperFactory(this.objectWrapperFactory);
  }

  if (this.vfs != null) {
    configuration.setVfsImpl(this.vfs);
  }

  if (hasLength(this.typeAliasesPackage)) {
    String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
                                                           ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    for (String packageToScan : typeAliasPackageArray) {
      configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                                                           typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
      }
    }
  }

  if (!isEmpty(this.typeAliases)) {
    for (Class<?> typeAlias : this.typeAliases) {
      configuration.getTypeAliasRegistry().registerAlias(typeAlias);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registered type alias: '" + typeAlias + "'");
      }
    }
  }

  if (!isEmpty(this.plugins)) {
    for (Interceptor plugin : this.plugins) {
      configuration.addInterceptor(plugin);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registered plugin: '" + plugin + "'");
      }
    }
  }

  if (hasLength(this.typeHandlersPackage)) {
    String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
                                                              ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    for (String packageToScan : typeHandlersPackageArray) {
      configuration.getTypeHandlerRegistry().register(packageToScan);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
      }
    }
  }

  if (!isEmpty(this.typeHandlers)) {
    for (TypeHandler<?> typeHandler : this.typeHandlers) {
      configuration.getTypeHandlerRegistry().register(typeHandler);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registered type handler: '" + typeHandler + "'");
      }
    }
  }

  if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
    try {
      configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
    } catch (SQLException e) {
      throw new NestedIOException("Failed getting a databaseId", e);
    }
  }

  if (this.cache != null) {
    configuration.addCache(this.cache);
  }

  if (xmlConfigBuilder != null) {
    try {
      xmlConfigBuilder.parse();

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
      }
    } catch (Exception ex) {
      throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  if (this.transactionFactory == null) {
    this.transactionFactory = new SpringManagedTransactionFactory();
  }

  configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

  if (!isEmpty(this.mapperLocations)) {
    for (Resource mapperLocation : this.mapperLocations) {
      if (mapperLocation == null) {
        continue;
      }

      try {
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                                                                 configuration, mapperLocation.toString(), configuration.getSqlFragments());
        xmlMapperBuilder.parse();
      } catch (Exception e) {
        throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
      } finally {
        ErrorContext.instance().reset();
      }

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
      }
    }
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
    }
  }

  return this.sqlSessionFactoryBuilder.build(configuration);
}

三. SqlSessionTemplate

SqlSessionTemplate是Spring提供的一个对MyBatis的SqlSession的一个增强类,它的作用就是将SqlSession与当前的事务所绑定,而且是线程安全的,一个SqlSessionTemplate可以被多个dao所共享。

SqlSessionTemplate基于动态代理模式,内部委托了一个SqlSession对象,并且在其基础上进行了增强。代码如下:

代码语言:javascript
复制
//传入的SqlSessionFactory
private final SqlSessionFactory sqlSessionFactory;

//Executor类型
private final ExecutorType executorType;

//SqlSession的动态代理
private final SqlSession sqlSessionProxy;

private final PersistenceExceptionTranslator exceptionTranslator;


public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                          PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  
  //创建SqlSession的动态代理
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());
}

在创建SqlSessionTemplate传入SqlSessionFactory,而通过SqlSessionFactory创建的SqlSession其实是一个动态代理类,其增强的逻辑在SqlSessionInterceptor中定义,如下:

代码语言:javascript
复制
private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //获取当前事务下绑定的SqlSession,由于Spring通过ThreadLocal将线程与事务绑定,所以也可以认为获取的是当前线程绑定的SqlSession
    SqlSession sqlSession = getSqlSession(
      SqlSessionTemplate.this.sqlSessionFactory,
      SqlSessionTemplate.this.executorType,
      SqlSessionTemplate.this.exceptionTranslator);
    try {
      //执行SqlSession的方法
      Object result = method.invoke(sqlSession, args);
      
      //如果当前在非事务环境下允许,则强制commit一下,因为有些数据库要求在close()方法前要先调用commit()或rollback()
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      throw unwrapped;
    } finally {
      //关闭sqlSession
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

这样一来,Spring就将事务与SqlSession整合到了一起。

四. Mapper自动扫描

通过@MapperScan注解,可以指定Mapper自动扫描的包路径。自动扫描的处理是通过ClassPathMapperScanner实现的,其doScan()方法如下:

代码语言:javascript
复制
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
  Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

  if (beanDefinitions.isEmpty()) {
    logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
  } else {
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}

ClassPathMapperScanner会将所有的Mapper扫描进来,并且将每个Mapper包装成一个类型为MapperFactoryBean的BeanDefinition,注册到IoC容器中。 MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,是一个BeanFactoryPostProcessor,它的功能就是在容器启动阶段动态向容器中注册BeanDefinition。经过MapperScannerConfigurer处理后,所有Mapper接口的BeanDefinition就以MapperFactoryBean的形式注册到Spring IoC容器中了。 代码可见ClassPathMapperScanner#processBeanDefinitions():

代码语言:javascript
复制
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");
      }

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      //这里实际注册的BeanDefinition是Mapper接口所对应的MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }

      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

五. MapperFactoryBean

MapperFactoryBean也是一个工厂Bean,可以通过它创建Mapper实例:

代码语言:javascript
复制
@Override
public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

这样一来,所有程序中定义的Mapper就都以Bean的形式加载到Spring的IoC容器中了。

六. Spring对MyBatis生命周期的修改

  1. SqlSession 在MyBatis原生API中,SqlSession的生命周期是方法级别的,在每个方法中创建一个新的SqlSession实例,用完了之后就销毁。而Spring对MyBatis原生的SqlSession进行了事务的增强,通过一个单例的SqlSessionTemplate,保证每次获取的SqlSession其实都是与当前线程绑定的。
  2. Mapper MyBatis原生API中,通过SqlSession#getMapper()获取到的Mapper,是一个MapperProxy动态代理类,它的生命周期与SqlSession一致,都是方法级别的。但是Spring通过MapperFactoryBean创建的Mapper是单例的,也就是将其声明周期提升到了全局级别。

七. MyBatis一级缓存失效问题

我们知道MyBatis有两级缓存,其中一级缓存是默认开启的。但是在Spring整合了MyBatis后,却经常出现一级缓存失效的问题,其原因是在SqlSessionTemplate对SqlSession进行了代理后,在非事务环境下,每次执行完SqlSession的操作后都会进行一次commit(),且最后后关闭SqlSession,因此会清空一级缓存(强调下仅在非事务环境下,在事务中调用不会出现这个问题),源码如下:

代码语言:javascript
复制
private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          //在非事务环境下执行,会强制commit()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
      	//关闭SqlSession
      	//在非事务环境下,就是直接调用SqlSession.close()方法
      	//在事务环境下,并不会关闭SqlSession,而仅是对当前事务的引用计数-1
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

八. SpringBoot整合MyBatis简单演示

  1. 引入MyBatis的Starter依赖
代码语言:javascript
复制
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.1.1</version>
</dependency>
  1. 在application.properties中增加MyBatis相关配置
代码语言:javascript
复制
#数据源配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=xxx
spring.datasource.password=xxx

#MyBatis配置
mybatis.type-aliases-package=xxx
mybatis.mapper-locations=classpath:mappers/*.xml
  1. 编写Mapper和对应的映射文件(或注解)
  2. 开启Mapper自动扫描,并指定包路径
代码语言:javascript
复制
@MapperScan("xxx.mapper")
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-05-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SpringBoot整合MyBatis原理
    • 一. 自动配置
      • 二. SqlSessionFactoryBean
        • 三. SqlSessionTemplate
          • 四. Mapper自动扫描
            • 五. MapperFactoryBean
              • 六. Spring对MyBatis生命周期的修改
                • 七. MyBatis一级缓存失效问题
                  • 八. SpringBoot整合MyBatis简单演示
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档