MyBatis在SpringBoot中集成以及执行逻辑

1,124 阅读6分钟

这篇文章将从源码层面,阐述以下问题:

  • MyBatis如何集成到SpringBoot中的
  • 为什么Mapper的方法不需要实现就能执行sql

MyBatis的执行流程

image-20210228225647918.png

先大致讲讲MyBatis的执行流程。

  1. 应用启动后,MyBatis读取配置文件,包括MyBatis的配置文件(比如数据源、连接池)、Mapper配置文件,它会把这些配置转换成单例模式的org.apache.ibatis.session.Configuration对象。其配置内容包括如下:
  • properties全局参数
  • settings设置
  • typeAliases别名
  • typeHandler类型处理器
  • ObjectFactory对象
  • plugin插件
  • environment环境
  • DatabaseIdProvider数据库标识
  • Mapper映射器
  1. 根据Configuration对象SqlSessionFactory
  2. SqlSessionFactory会创建SqlSession对象
  3. SqlSession通过代理生成Mapper接口代理对象
  4. Mapper接口代理对象调用方法,执行Sql语句

MyBatis的整个执行流程就是这5步,下面将来解析这5步的逻辑。

构建SqlSessionFactory

直接上源码

在mybatis-spring-boot-autoconfigure包中的MyBatisAutoConfiguration类中

    // 我将DataSource参数理解连接池对象,比如在我的项目中引入了Hikra,
    // 那么这个dataSource就是Hikara相关的代理类
    // 通过ide的debugger可以看到它的具体类是HikariDataSource$$EnhancerBySpringCGLIB$$4d16d247@8509,是通过CGLIB代理的
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        // 这个Vfs属性,个人理解就是SpringBoot提供的,能够用来帮助读取yaml配置文件中的属性
        factory.setVfs(SpringBootVFS.class);
        // 这个属性我没用过,估计是设置MyBatis的外部加载配置
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        // 这个方法就是设置已读取的MyBatis配置到factory对象
        this.applyConfiguration(factory);
        // 用得少,我觉得不用关注
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        // 设置MyBatis的插件
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        // 设置数据库的类别,比如MySql、oracle
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        
        // 用得少,我觉得不用关注
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        // 用得少,我觉得不用关注
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        // 用得少,我觉得不用关注
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        // 用得少,我觉得不用关注
        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        // 设置所有的mapper路径
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }
        // 用得少,我觉得不用关注
        Set<String> factoryPropertyNames = (Set)Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
            if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
                defaultLanguageDriver = this.languageDrivers[0].getClass();
            }
        }

        if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
            factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
        }
        // 构建SqlSessionFactory
        return factory.getObject();
    }

在sqlSessionFactory方法中,需要关注三点:

  • 设置MyBatis插件
  • 设置mapper路径
  • 构建SqlSessionFactory,也就是factory.getObject() 前两点非常简单,就是读取数据然后再设置,重点是第三点

下面看看getObject()方法

  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }
    return this.sqlSessionFactory;
  }

应用第一次启动的时候,sqlSessionFactory肯定是null,那么会进入afterPropertiesSet()方法

  public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
        "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }

然后就是buildSqlSessionFactory()方法 在这个方法里面,才是真正的加载这些内容来生成Configuration对象,然后创建SqlSessionFactory

  • properties全局参数
  • settings设置
  • typeAliases别名
  • typeHandler类型处理器
  • ObjectFactory对象
  • plugin插件
  • environment环境
  • DatabaseIdProvider数据库标识
  • Mapper映射器 下面是代码片段:
 protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      // 省略代码
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
    
      // 省略代码
      

    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    // 省略代码
    
    if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }

只展示出了两部分,一部分是配置插件(大部分情况就是保存自定义的插件信息),另一部分是设置mapper(就是mapper.xml中的select、update、resultMap等等配置内容)

注意xmlMapperBuilder.parse();这个方法的调用,里面有一步configurationElement(parser.evalNode("/mapper"));这一步会解析mapper对应的xml配置,将每一个 SELECT、UPDATE、DELETE操作转换成对应的MappedStatement,而MappedStatement这个对象会在执行Mapper方法的时候用到。

配置插件的方法层层往里跟进,其最后的结果就是下面这样

  public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

InterceptorChain对象实例会作为Configuration对象属性,然后plugin就通过addInterceptor添加

最后就是调用this.sqlSessionFactoryBuilder.build(targetConfiguration)生成SqlSessionFactory

构建SqlSession

在SpringBoot中,SqlSession通过SqlSessionTepmlate管理(方便实现事务)。所以前面初始化完SqlSessionFactoryBuild的下一步就是初始化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);
    }
  }

这里根据ExecutorType执行不同的构造方法。我一般都没有指定ExecutorType,所以初始化的时候使用系统提供的默认值。 跟着构造方法一直跟进,最后的构造方法内容如下

  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;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());
  }
  • sqlSessionFactory: 就是上文初始化的SqlSessionFactory
  • exceptionTranslator: 官方解释是说将MyBatis抛出的异常转换成运行时异常,该属性可以为null
  • sqlSessionProxy: SqlSession的代理对象。有两个作用:1、spring的事务处理 2、捕获MyBatis的异常,转换成运行时异常

Mapper 接口的代理和注入

在使用MyBatis的时候,我们会添加@MapperScan或者@Mapper注解。这两个注解的作用就是将Mapper注入到Spring的Bean容器中。因为所有的Mapper都是接口,所以实际注入容器之前,mybatis-spring会把这些的真实类设置为MapperFactoryBean。后面在@Service层的类中自动注入mapper就会调用MapperFactoryBean的getObject(),获得Mapper的代理对象类。

比如注入UserMappper,会调用下面这个方法

  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

Mapper的执行流程

下面通过代码来探寻Mapper的执行流程,这里在控制器中注入Mapper,然后调用Mapper的接口

// TestController.java
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private AppServiceMapper appServiceMapper;

    @PostMapping
    public void test() {
        appServiceMapper.selectById(1L);
    }
}
// AppServiceMapper.xmlselectById
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="AppServiceMapper">
    <select id="selectById" resultType="io.choerodon.devops.infra.dto.AppServiceDTO">
        SELECT * FROM devops_app_service das where das.id=#{id}
    </select>
</mapper>

image.png 上面的截图可以看到,appServiceMapper是由MapperProxy类进行代理的。 image.png 从截图的断点开始,通过step into开始debug 现在进入MapperProxy的invoke方法,最终执行到第85行 image.png cachedInvoker方法会根据调用的Mapper方法找到对应的PlainMethodInvoker对象,其包含Mapper对应的xml配置内容,比如这里的method是selectById,那么返回的PlainMethodInvoker里面包含内容如下: image.png 然后调用PlainMethodInvoker的invoke方法,其实也就是调用MapperMethod的execute方法。 image.png 下面看看MapperMethod的execute方法内容

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

整体结构就是switch/case,可以发现里面有四个case:INSERT、UPDATE、DELETE、SELECT,而刚才调用的方法是SELECT,所以会进入SELECT,最后执行到这里

image.png 接下来就执行sqlSession的selectOne。这里的sqlSession本质是前面生成的sqlSessionTemplate,所以执行的是sqlSessionTemplate的selectOne方法。

image.png 上面的sqlSessionProxy对象是由JDK代理生成的

image.png 这里可以看到声明逻辑,所以下一步是执行SqlSessionInterceptor的invoke方法

最后执行到这里

image.png 先获取真正的SqlSession,即MyBatis的DefaultSqlSession,然后调用DefaultSqlSession的selectOne方法

image.png 往里跟进,到达这一层

image.png

接下来进入executor.query方法

image.png 到达这里,开始执行MyBatis的所有拦截器逻辑

image.png

所有拦截器执行完毕后,开始执行真正的query方法

image.png

然后查出所有结果并返回