[Source code analysis] SpringBoot integrates source code analysis of MybatisPlus framework

MybatisPlusAutoConfiguration

Introduce mybatis-plus-boot-starter, there is spring.factories under the package, the content is as follows:

# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

MybatisPlusAutoConfigurationThis class injects components such as SqlSessionFactory and SqlSessionTemplate.

SqlSessionFactory

MybatisPlusAutoConfiguration#sqlSessionFactory, create SqlSessionFactory.

 @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {<!-- -->
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
//...
        return factory. getObject();
    }

MybatisSqlSessionFactoryBean#getObject

 @Override
    public SqlSessionFactory getObject() throws Exception {<!-- -->
        if (this.sqlSessionFactory == null) {<!-- -->
            afterPropertiesSet();
        }

        return this.sqlSessionFactory;
    }

MybatisSqlSessionFactoryBean#afterPropertiesSet

public void afterPropertiesSet() throws Exception {<!-- -->
    notNull(dataSource, "Property 'dataSource' is required");
    state((configuration == null & amp; & amp; configLocation == null) || !(configuration != null & amp; & amp; configLocation != null),
        "Property 'configuration' and 'configLocation' can not be specified with together");
    //TODO clean up resources, it is recommended not to keep this thing
    SqlRunner. DEFAULT. close();
    this.sqlSessionFactory = buildSqlSessionFactory();
}

MybatisSqlSessionFactoryBean#buildSqlSessionFactory, use MybatisXMLConfigBuilder to parse xml and build Configuration information.

@MapperScan

Add @MapperScan(basePackages = {"com.charles.mapper"}) to the startup class, and all classes that inherit BaseMapper under the directory will be scanned.

@Retention(RetentionPolicy.RUNTIME)
@Target({<!-- -->ElementType. TYPE})
@Documented
@Import({<!-- -->MapperScannerRegistrar.class})
@Repeatable(MapperScans. class)
public @interface MapperScan {<!-- -->
}

MapperScannerRegistrar

MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, and injects MapperScannerConfigurer into the Spring container in the rewriting method MapperScannerRegistrar#registerBeanDefinitions()

 public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {<!-- -->
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if (mapperScanAttrs != null) {<!-- -->
            this.registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
        }

    }

    void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {<!-- -->
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        builder.addPropertyValue("processPropertyPlaceHolders", true);
//...
        builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
        builder.setRole(2);
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }

MapperScannerConfigurer

MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, rewrite method. Using ClassPathMapperScanner

 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {<!-- -->
        if (this. processPropertyPlaceHolders) {<!-- -->
            this. processPropertyPlaceHolders();
        }

        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
        if (StringUtils. hasText(this. lazyInitialization)) {<!-- -->
            scanner.setLazyInitialization(Boolean.valueOf(this.lazyInitialization));
        }

        if (StringUtils. hasText(this. defaultScope)) {<!-- -->
            scanner.setDefaultScope(this.defaultScope);
        }

        scanner. registerFilters();
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\\
"));
    }

ClassPathMapperScanner#doScan, scan under the specified package, and the type of registered Bean is MapperFactoryBean.

 public Set<BeanDefinitionHolder> doScan(String... basePackages) {<!-- -->
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions. isEmpty()) {<!-- -->
            LOGGER. warn(() -> {<!-- -->
                return "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.";
            });
        } else {<!-- -->
            this.processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {<!-- -->
        BeanDefinitionRegistry registry = this. getRegistry();
        Iterator var4 = beanDefinitions. iterator();

        while(var4.hasNext()) {<!-- -->
            BeanDefinitionHolder holder = (BeanDefinitionHolder)var4.next();
            AbstractBeanDefinition definition = (AbstractBeanDefinition)holder.getBeanDefinition();
            //...
            try {<!-- -->
                definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
            } catch (ClassNotFoundException var10) {<!-- -->
            }

            definition.setBeanClass(this.mapperFactoryBeanClass);
            //...

            definition.setLazyInit(this.lazyInitialization);
            if (!scopedProxy) {<!-- -->
                if ("singleton".equals(definition.getScope()) & amp; & amp; this.defaultScope != null) {<!-- -->
                    definition.setScope(this.defaultScope);
                }

                if (!definition.isSingleton()) {<!-- -->
                    BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
                    if (registry. containsBeanDefinition(proxyHolder. getBeanName())) {<!-- -->
                        registry. removeBeanDefinition(proxyHolder. getBeanName());
                    }

                    registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
                }
            }
        }

    }

ClassPathMapperScanner#isCandidateComponent, specify the candidate class. needs to be an interface

 protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {<!-- -->
        return beanDefinition.getMetadata().isInterface() & amp; & amp; beanDefinition.getMetadata().isIndependent();
    }

MapperFactoryBean

MapperFactoryBean#getObject, the object of this class in the Spring container.

 public T getObject() throws Exception {<!-- -->
        return this.getSqlSession().getMapper(this.mapperInterface);
    }

MybatisMapperRegistry#getMapper (mybatis: MapperRegistry#getMapper)

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {<!-- -->
        MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {<!-- -->
            mapperProxyFactory = (MybatisMapperProxyFactory)this.knownMappers.entrySet().stream().filter((t) -> {<!-- -->
                return ((Class)t. getKey()). getName(). equals(type. getName());
            }).findFirst().map(Entry::getValue).orElseThrow(() -> {<!-- -->
                return new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry.");
            });
        }

        try {<!-- -->
            return mapperProxyFactory. newInstance(sqlSession);
        } catch (Exception var5) {<!-- -->
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }

MybatisMapperProxyFactory#newInstance(), create a proxy class.

 protected T newInstance(MybatisMapperProxy<T> mapperProxy) {<!-- -->
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{<!-- -->this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {<!-- -->
        MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }

Execute Mapper

MybatisMapperProxy.PlainMethodInvoker#invoke will be executed with the encapsulated MybatisMapperMethod

 public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {<!-- -->
            return this.mapperMethod.execute(sqlSession, args);
        }

MybatisMapperMethod#execute, the core processing class

 public Object execute(SqlSession sqlSession, Object[] args) {<!-- -->
        Object result;
        Object param;
        switch(this.command.getType()) {<!-- -->
        case INSERT:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
            break;
        case UPDATE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
            break;
        case DELETE:
            param = this.method.convertArgsToSqlCommandParam(args);
            result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
            break;
        case SELECT:
            if (this.method.returnsVoid() & amp; & amp; this.method.hasResultHandler()) {<!-- -->
                this. executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (this. method. returnsMany()) {<!-- -->
                result = this. executeForMany(sqlSession, args);
            } else if (this. method. returnsMap()) {<!-- -->
                result = this. executeForMap(sqlSession, args);
            } else if (this. method. returnsCursor()) {<!-- -->
                result = this. executeForCursor(sqlSession, args);
            } else if (IPage. class. isAssignableFrom(this. method. getReturnType())) {<!-- -->
                result = this. executeForIPage(sqlSession, args);
            } else {<!-- -->
                param = this.method.convertArgsToSqlCommandParam(args);
                result = sqlSession. selectOne(this. command. getName(), param);
                if (this.method.returnsOptional() & amp; & amp; (result == null || !this.method.getReturnType().equals(result.getClass()))) {<!-- -->
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            result = sqlSession. flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + this.command.getName());
        }

        if (result == null & amp; & amp; this.method.getReturnType().isPrimitive() & amp; & amp; !this.method.returnsVoid()) {<!-- -->
            throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ") .");
        } else {<!-- -->
            return result;
        }
    }

Mybatis cache

DefaultSqlSession#selectList(), this method is used to query the collection

 private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {<!-- -->
    try {<!-- -->
      MappedStatement ms = configuration. getMappedStatement(statement);
      return executor. query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {<!-- -->
      throw ExceptionFactory. wrapException("Error querying database. Cause: " + e, e);
    } finally {<!-- -->
      ErrorContext.instance().reset();
    }
  }

BaseExecutor#query(), the specific query method. A cache key will be generated based on information such as parameters to determine whether it exists in the cache. This cache is the first level cache.

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {<!-- -->
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {<!-- -->
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {<!-- -->
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 & amp; & amp; ms.isFlushCacheRequired()) {<!-- -->
      clearLocalCache();
    }
    List<E> list;
    try {<!-- -->
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache. getObject(key) : null;
      if (list != null) {<!-- -->
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {<!-- -->
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {<!-- -->
      queryStack--;
    }
    if (queryStack == 0) {<!-- -->
      for (DeferredLoad deferredLoad : deferredLoads) {<!-- -->
        deferredLoad.load();
      }
      // issue #601
      deferredLoads. clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {<!-- -->
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

CachingExecutor#query() uses the second-level cache.

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {<!-- -->
    Cache cache = ms. getCache();
    if (cache != null) {<!-- -->
      flushCacheIfRequired(ms);
      if (ms.isUseCache() & amp; & amp; resultHandler == null) {<!-- -->
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm. getObject(cache, key);
        if (list == null) {<!-- -->
          list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issues #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

SimpleExecutor#doQuery, the default BaseExecutor implementation of Mybatis is SimpleExecutor, and finally calls the SimpleExecutor#doQuery method.

 @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {<!-- -->
    Statement stmt = null;
    try {<!-- -->
      Configuration configuration = ms. getConfiguration();
      StatementHandler handler = configuration. newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler. query(stmt, resultHandler);
    } finally {<!-- -->
      closeStatement(stmt);
    }
  }