This article will explain the following issues from the source code level:

  • How does MyBatis integrate into SpringBoot
  • Why Mapper methods do not need to be implemented to execute SQL

The execution process of MyBatis

Firstly, we will talk about the execution process of MyBatis.

  1. Application starts, MyBatis reading Configuration files, including MyBatis Configuration files (such as data source, the connection pool), Mapper Configuration file, it will convert these Configuration to the singleton pattern org. The apache. Ibatis. Session. The Configuration object. The configuration includes the following:
  • Properties Global parameters
  • Settings set
  • TypeAliases alias
  • TypeHandler Type of processor
  • The ObjectFactory object
  • The plugin plug-in
  • The environment conditions
  • DatabaseIdProvider Identifies the database
  • Mapper Mapper
  1. According to the Configuration object SqlSessionFactory
  2. SqlSessionFactory creates the SqlSession object
  3. SqlSession generates Mapper interface proxy objects by proxy
  4. The Mapper interface proxy object invokes methods to execute Sql statements

The whole execution process of MyBatis is these 5 steps, and the logic of these 5 steps will be analyzed in the future.

Build a SqlSessionFactory

Direct source code

In the MyBatisAutoConfiguration class in the Mybatis – spring-boot-Autoconfigure package

    // I interpret the DataSource argument as a connection pool object, such as Hikra in my project,
    // The dataSource is the hikara-related proxy class
    // The ide debugger can see that its concrete class is HikariDataSource$$EnhancerBySpringCGLIB$$4d16d247@8509, which is via the CGLIB proxy
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        // This Vfs property, as I understand it, is provided by SpringBoot and can be used to help read properties in yamL configuration files
        factory.setVfs(SpringBootVFS.class);
        // Set MyBatis external load configuration
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        // This method sets the read MyBatis configuration to the Factory object
        this.applyConfiguration(factory);
        // I don't need to pay attention to it
        if (this.properties.getConfigurationProperties() ! =null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        // Set MyBatis plugin
        if(! ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        // Set the category of the database, such as MySql and Oracle
        if (this.databaseIdProvider ! =null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        
        // I don't need to pay attention to it
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        // I don't need to pay attention to it
        if (this.properties.getTypeAliasesSuperType() ! =null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        // I don't need to pay attention to it
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        // I don't need to pay attention to it
        if(! ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        // Set all mapper paths
        if(! ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }
        // I don't need to pay attention to it
        Set<String> factoryPropertyNames = (Set)Stream.of((newBeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collecto rs.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);
        }
        / / build SqlSessionFactory
        return factory.getObject();
    }
Copy the code

In the sqlSessionFactory method, there are three things to focus on:

  • Set up MyBatis plug-in
  • Set the Mapper path
  • Build SqlSessionFactory, factory.getobject ()

The first two points are very simple, just read the data and set it up, but the third point is important

Now look at the getObject() method

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

The sqlSessionFactory must be null when the application is first started, then the afterPropertiesSet() method is entered

  public void afterPropertiesSet(a) 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();
  }
Copy the code

Then there’s the buildSqlSessionFactory() method where you actually load this stuff to generate the Configuration object, and then create the SqlSessionFactory

  • Properties Global parameters
  • Settings set
  • TypeAliases alias
  • TypeHandler Type of processor
  • The ObjectFactory object
  • The plugin plug-in
  • The environment conditions
  • DatabaseIdProvider Identifies the database
  • Mapper Mapper

Here is the code snippet:

 protected SqlSessionFactory buildSqlSessionFactory(a) throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration ! =null) {
      // omit the code
    } else if (this.configLocation ! =null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null.this.configurationProperties);
      targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
    
      // omit the code
      

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

    // omit the code
    
    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);
  }
Copy the code

Only two parts are shown, one is to configure the plug-in (in most cases, save the custom plug-in information), and the other is to set the mapper(select, update, resultMap, etc.).

Note xmlMapperBuilder. The parse (); ConfigurationElement (Parser. EvalNode (“/mapper”)); This step parses the CORRESPONDING XML configuration of the Mapper and transforms each

The SELECT, UPDATE, and DELETE operations are converted to the corresponding MappedStatement, which is used when the Mapper method is executed.

The method of configuring the plug-in cascaded down, and the result was something like this

  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(a) {
    returnCollections.unmodifiableList(interceptors); }}Copy the code

The Instance of the InterceptorChain object is provided as an attribute of the Configuration object, which is then added by addInterceptor

Finally is to call this. The sqlSessionFactoryBuilder is. Build generate SqlSessionFactory (targetConfiguration)

Build the SqlSession

In SpringBoot, SqlSession is managed through SqlSessionTepmlate (facilitating transactions). So the next step after initializing the SqlSessionFactoryBuild is to initialize the SqlSessionTemplate

Look at the code

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if(executorType ! =null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return newSqlSessionTemplate(sqlSessionFactory); }}Copy the code

Different constructs are performed depending on the ExecutorType. I generally don’t specify ExecutorType, so I use the default values provided by the system for initialization. Follow the constructor all the way through, and the final constructor will look like this

  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());
  }
Copy the code
  • SqlSessionFactory: the same sqlSessionFactory initialized above
  • ExceptionTranslator: To convert an exception thrown by MyBatis to a runtime exception, this attribute can be null
  • SqlSessionProxy: proxy object of SqlSession. There are two functions: 1, Spring transaction processing 2, catch MyBatis exceptions, into runtime exceptions

Proxy and injection of Mapper interfaces

When using MyBatis, we will add @mapperscan or @mapper annotation. The purpose of these two annotations is to inject Mapper into Spring’s Bean container. Since all Mappers are interfaces, mybatis- Spring sets their real classes to MapPerFactoryBeans before actually injecting them into the container. Then automatically injecting mapper into the @Service layer class calls getObject() of MapperFactoryBean to get the proxy object class of Mapper.

For example, if you inject UserMappper, the following method is called

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

Mapper execution process

The following code explores the execution flow of Mapper by injecting Mapper into the controller and then calling Mapper’s interface

// TestController.java @RestController @RequestMapping("/test") public class TestController { @Autowired private AppServiceMapper appServiceMapper; @PostMapping public void test() { appServiceMapper.selectById(1L); } } // AppServiceMapper.xmlselectById <? The 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>Copy the code

As you can see in the screenshot above, appServiceMapper is propped by the MapperProxy class.Starting from the breakpoint in the screenshot, debug with step into now enters the Invoke method of MapperProxy and finally executes to line 85The cachedInvoker method finds the corresponding PlainMethodInvoker object based on the called Mapper method, which contains the XML configuration for the Mapper, for example, selectById, The returned PlainMethodInvoker contains the following contents:It then calls the invoke method of PlainMethodInvoker, which is essentially the execute method of MapperMethod.Let’s look at the contents of the execute method of MapperMethod

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;
  }
Copy the code

INSERT, UPDATE, DELETE, SELECT (SELECT); SELECT (SELECT)

SelectOne of sqlSession is then executed. The sqlSession here is essentially the sqlSessionTemplate generated earlier, so the selectOne method of the sqlSessionTemplate is executed.

The sqlSessionProxy object above is generated by the JDK proxy

You can see the declaration logic here, so the next step is to execute the invoke method of the SqlSessionInterceptor

And I’m going to end up here

Select selectOne from DefaultSqlSession (MyBatis

Follow it in. Get to this level

Next, enter the executor.query method

When you get here, start executing all of MyBatis’ interceptor logic

After all interceptors are executed, the actual Query method is executed

Then find all the results and return them