The principle of automatic registration of Mybatis mapper in SpringBoot environment

Previously, we talked about the process of springboot integrating mybatis. We only need a few simple configurations, and we can complete the operation of the database by operating our mapper mapper. I don’t know if you will have such doubts: the mapper I wrote is just a custom interface, why can I directly use it to add, delete, modify and check the database? Next, let's discuss the principle of this piece.

Since the principles related to mybatis involve many things, we will explain the internal knowledge points one by one. This article mainly explains the role of the mybatis starter, which is the registration process of the mapper instance.

1. The automatic configuration class of mybatis-spring-boot-starter

When we integrate mybatis with springboot, the following dependencies will be introduced in the pom file:

xml复制代码      <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

After the introduction, maven will automatically import the mybatis-spring-boot-autoconfigure package for us. Looking at the name, we can guess that this is an automatic configuration package, so we have to look at the META-INF/spring.factories file in the jar package, which has the following contents:

ini复制代码# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

The configuration class MybatisAutoConfiguration helps us with the following functions:

(1) Register SqlSessionFactory and SqlSessionTemplate to the spring container

(2) Register AutoConfiguredMapperScannerRegistrar to the spring container, which is needed later to complete the registration of the mapper marked with @Mapper annotation

For your convenience, here is the code of MybatisAutoConfiguration as follows:

kotlin复制代码package org.mybatis.spring.boot.autoconfigure;

import java.beans.PropertyDescriptor;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.sql.DataSource;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * {@link EnableAutoConfiguration Auto-Configuration} for Mybatis. Contributes a {@link SqlSessionFactory} and a
 * {@link SqlSessionTemplate}.
 *
 * If {@link org.mybatis.spring.annotation.MapperScan} is used, or a configuration file is specified as a property,
 * those will be considered, otherwise this auto-configuration will attempt to register mappers based on the interface
 * definitions in or under the root auto-configuration package.
 *
 * @author Eddú Meléndez
 * @author Josh Long
 * @author Kazuki Shimizu
 * @author Eduardo Macarrón
 */
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

  private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);

  private final MybatisProperties properties;

  private final Interceptor[] interceptors;

  private final TypeHandler[] typeHandlers;

  private final LanguageDriver[] languageDrivers;

  private final ResourceLoader resourceLoader;

  private final DatabaseIdProvider databaseIdProvider;

  private final List<ConfigurationCustomizer> configurationCustomizers;

  private final List<SqlSessionFactoryBeanCustomizer> sqlSessionFactoryBeanCustomizers;

  public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
      ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
      ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
      ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
      ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.typeHandlers = typeHandlersProvider.getIfAvailable();
    this.languageDrivers = languageDriversProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
    this.sqlSessionFactoryBeanCustomizers = sqlSessionFactoryBeanCustomizers.getIfAvailable();
  }

  @Override
  public void afterPropertiesSet() {
    checkConfigFileExists();
  }

  private 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)");
    }
  }

  @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()));
    }
    applyConfiguration(factory);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    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 (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);
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }
    Set<String> factoryPropertyNames = Stream
        .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
        .collect(Collectors.toSet());
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
      // Need to mybatis-spring 2.0.2+
      factory.setScriptingLanguageDrivers(this.languageDrivers);
      if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
        defaultLanguageDriver = this.languageDrivers[0].getClass();
      }
    }
    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      // Need to mybatis-spring 2.0.2+
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }
    applySqlSessionFactoryBeanCustomizers(factory);
    return factory.getObject();
  }

  private void applyConfiguration(SqlSessionFactoryBean factory) {
    Configuration configuration = this.properties.getConfiguration();
    if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
      configuration = new Configuration();
    }
    if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
      for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
        customizer.customize(configuration);
      }
    }
    factory.setConfiguration(configuration);
  }

  private void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) {
    if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) {
      for (SqlSessionFactoryBeanCustomizer customizer : this.sqlSessionFactoryBeanCustomizers) {
        customizer.customize(factory);
      }
    }
  }

  @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);
    }
  }

  /**
   * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
   * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
   * similar to using Spring Data JPA repositories.
   */
  public static class AutoConfiguredMapperScannerRegistrar
      implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;
    private Environment environment;

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

      if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
      }

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

      List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
      if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
      }

      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
      builder.addPropertyValue("processPropertyPlaceHolders", true);
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
      BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
      Set<String> propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName)
          .collect(Collectors.toSet());
      if (propertyNames.contains("lazyInitialization")) {
        // Need to mybatis-spring 2.0.2+
        builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
      }
      if (propertyNames.contains("defaultScope")) {
        // Need to mybatis-spring 2.0.6+
        builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
      }

      // for spring-native
      boolean injectSqlSession = environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class,
          Boolean.TRUE);
      if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
        ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory;
        Optional<String> sqlSessionTemplateBeanName = Optional
            .ofNullable(getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
        Optional<String> sqlSessionFactoryBeanName = Optional
            .ofNullable(getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
        if (sqlSessionTemplateBeanName.isPresent() || !sqlSessionFactoryBeanName.isPresent()) {
          builder.addPropertyValue("sqlSessionTemplateBeanName",
              sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
        } else {
          builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
        }
      }
      builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

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

    @Override
    public void setEnvironment(Environment environment) {
      this.environment = environment;
    }

    private String getBeanNameForType(Class<?> type, ListableBeanFactory factory) {
      String[] beanNames = factory.getBeanNamesForType(type);
      return beanNames.length > 0 ? beanNames[0] : null;
    }

  }

  /**
   * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
   * mappers based on the same component-scanning path as Spring Boot itself.
   */
  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }

  }

}

2. Generate a BeanDefinition object for the mapper interface

The internal class MapperScannerRegistrarNotFoundConfiguration of MybatisAutoConfiguration registers AutoConfiguredMapperScannerRegistrar in the spring container through @Import, and the code is as follows:

less复制代码  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }
  }

AutoConfiguredMapperScannerRegistrar implements the ImportBeanDefinitionRegistrar interface. As I said in the previous article, all classes imported through @Import can be regarded as a configuration class. AutoConfiguredMapperScannerRegistrar only completes one thing, which is to register MapperScannerConfigurer in the spring container. Of course, the MapperScannerConfigurer in spring is just a BeanDefinition object at this time. So what is MapperScannerConfigurer used for?

2.1, MapperScannerConfigurer Mapper Scanner Configurator

MapperScannerConfigurer implements the BeanDefinitionRegistryPostProcessor interface. In its postProcessBeanDefinitionRegistry method, it manually creates a ClassPathMapperScanner object, and the ClassPathMapperScanner object completes the registration of the mapper. Here is a brief introduction to the BeanDefinitionRegistryPostProcessor interface. Its function is to provide an extension point that allows us to implement the interface, then override its postProcessBeanDefinitionRegistry method, and complete the registration of the BeanDefinition object in this method. Remember, it is the BeanDefinition object, and spring will complete the bean initialization work according to the BeanDefinition object during the bean initialization stage.

2.2, ClassPathMapperScanner class path mapper scanner

Its full path is org.mybatis.spring.mapper.ClassPathMapperScanner, which is specially used to scan the mapper interface marked with @Mapper annotation under the specified package directory under the class path, and then generate a BeanDefinition object for each of our mapper interfaces , stored in the spring container. The bean type of the BeanDefinition object corresponding to each mapper interface is MyFactoryBean type, and the code is as follows:

typescript复制代码private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {  
   。。。省略部分代码
   definition.setBeanClass(this.mapperFactoryBeanClass);
   definition.getPropertyValues().add("addToConfig", this.addToConfig);
   definition.setAttribute("factoryBeanObjectType", beanClassName);
   。。。省略部分代码
}

Among them, mapperFactoryBeanClass is a member variable of ClassPathMapperScanner, and its definition is as follows:

php复制代码private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;

Subsequent spring will call the getObject method of MyFactoryBean to generate our mapper interface instance during the bean initialization stage.

3. The process of initializing the bean phase

After the previous steps, there is already a complete list of BeanDefinition objects in the spring container, and then spring will continue to execute, and generate actual bean objects based on these BeanDefinition objects.

3.1. Generate the bean instance object of SqlSessionFactory

The bean definition information of SqlSessionFactoryBean is stored in the spring container. When spring initializes the bean, it will call its getObject method according to the bean definition information to generate a real sqlSessionFactory object. During this process, the following operations will be involved:

(1) Generate the global Configuration object of mybatis, which can be parsed according to the xml configuration file of mybatis

(2) If there are custom mybatis plugin interceptor plugins, add them to the configured interceptor list.

kotlin复制代码if (!ObjectUtils.isEmpty(this.plugins)) {
            Stream.of(this.plugins).forEach((plugin) -> {
                targetConfiguration.addInterceptor(plugin);
                LOGGER.debug(() -> {
                    return "Registered plugin: '" + plugin + "'";
                });
            });
        }

(3) If there are custom type handlers typeHandlers, add them to the configured type handler registry object

kotlin复制代码if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            Stream.of(this.typeHandlers).forEach((typeHandler) -> {
                targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
                LOGGER.debug(() -> {
                    return "Registered type handler: '" + typeHandler + "'";
                });
            });
        }

(4) According to the mybatis.mapper-locations attribute value we configured in the application.yml file, use XMLMapperBuilder to parse the mybatis mapping file in xml format under the corresponding path, and find the corresponding class according to the value of the namespace namespace in the mapping file (or interface) class object, and add it to the mapper registry mapperRegistry of the configuration. And for each sql statement in the mapping file, it will be parsed into a MappedStatement object, and this MappedStatement object will be added to the mappedStatements (Map type) object of the configuration. The key of the mappedStatements is the id of each MappedStatement object, and the id The value of is the value of namespace in the mapping file + "." + the id of the sql statement. Let's take the findbyId mapping statement in the orderMapper.xml file as an example. The MappedStatement object id generated after it is parsed is com.xk.mybatis.springboot.mapper.OrderMapper.findById

csharp复制代码<mapper namespace="com.xk.mybatis.springboot.mapper.OrderMapper">

    <select id="findById" parameterType="Long" resultType="com.xk.mybatis.springboot.entity.Order">
        select * from t_order where id=#{id};
    </select>

</mapper>

The core code is as follows:

kotlin复制代码targetConfiguration.setEnvironment(new Environment(this.environment, (TransactionFactory)(this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory), this.dataSource));
        if (this.mapperLocations != null) {
            if (this.mapperLocations.length == 0) {
                LOGGER.warn(() -> {
                    return "Property 'mapperLocations' was specified but matching resources are not found.";
                });
            } else {
                Resource[] var3 = this.mapperLocations;
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    Resource mapperLocation = var3[var5];
                    if (mapperLocation != null) {
                        try {
                            //解析映射文件
                            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                            xmlMapperBuilder.parse();
                        } catch (Exception var19) {
                            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", var19);
                        } finally {
                            ErrorContext.instance().reset();
                        }

                        LOGGER.debug(() -> {
                            return "Parsed mapper file: '" + mapperLocation + "'";
                        });
                    }
                }
            }
        }

Among them, the implementation of xmlMapperBuilder.parse() is as follows. For details, you can check it yourself:

scss复制代码public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //根据映射文件中的命名空间namespace的值,找到对应的类(或接口)的class对象,并将其加入到configuration的映射器注册中心mapperRegistry
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    //将映射文件中的每一个sql语句都解析成一个MappedStatement对象
    parsePendingStatements();
  }

3.2. Generate the bean instance object of SqlSessionTemplate

SqlSessionTemplate implements the SqlSession interface, maintains a sqlSessionProxy object internally, and sqlSessionProxy itself is a SqlSession proxy object created by jdk dynamic proxy. The construction method of SqlSessionTemplate is as follows:

ini复制代码public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
        Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        Assert.notNull(executorType, "Property 'executorType' is required");
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
    }

The implementation of the SqlSessionInterceptor class is as follows:

kotlin复制代码private class SqlSessionInterceptor implements InvocationHandler {
        private SqlSessionInterceptor() {
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //通过sqlSessionFactory生成sqlSession对象
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

            Object unwrapped;
            try {
                //通过反射执行目标方法
                Object result = method.invoke(sqlSession, args);
                if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                unwrapped = result;
            } catch (Throwable var11) {
                unwrapped = ExceptionUtil.unwrapThrowable(var11);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw (Throwable)unwrapped;
            } finally {
                if (sqlSession != null) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
            return unwrapped;
        }
    }

When we call the select method of sqlSessionTemplate in the code, the following method will be executed:

typescript复制代码public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
 }

All operations on sqlSessionTemplate will be completed by its internal sqlSessionProxy proxy object, which will further execute the invoke method of the above SqlSessionInterceptor class. Inside invoke, a new sqlsession object is created through the sqlSessionFactory object, and then the new sqlSession object completes the operation on the database.

3.3. Generate the bean instance object corresponding to the mapper interface

As we said earlier, the bean type specified by the bean definition information about the mapper interface stored in the spring container is the MyFactoryBean type. MyFactoryBean is a factory bean. When spring initializes the bean according to the BeanDefinition object of the mapper interface, it will call MyFactoryBean The getObject method generates the real mapper interface instance. The getObject method is implemented as follows:

kotlin复制代码public T getObject() throws Exception {
    return this.getSqlSession().getMapper(this.mapperInterface);
}

Among them, getSqlSession() returns the sqlSessionTemplate object generated in Section 3.2.

3.3.1, enter the getMapper method of SqlSessionTemplate,

typescript复制代码public <T> T getMapper(Class<T> type) {
    return this.getConfiguration().getMapper(type, this);
}

Among them, getConfiguration() is the mybatis global configuration object generated when creating the SqlSessionFactory object in Section 3.1.

3.3.2, enter the getMapper method of the Configuration class

typescript复制代码public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

3.3.3, enter the getMapper method of MapperRegistry

typescript复制代码public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
     // 返回mapper接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

The MapperProxyFactory object will be obtained above, and the MapperProxyFactory creates a proxy object of the mapper interface (mapper interface) by calling its own newInstance method.

3.3.4, enter the newInstance method of MapperProxyFactory

The code looks like this:

typescript复制代码  // 第二步
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  //第一步
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
     // 调用第二步
    return newInstance(mapperProxy);
  }

At this point, the instance object (proxy object) of our custom mapper interface is successfully created through JDK dynamic proxy, and then spring will inject it into the container for us to use directly at the application system level, such as Call OrderMapper directly in our OrderService. One thing to say, in fact, the real type of the instance object of the mapper interface is org.apache.ibatis.binding.MapperProxy. After the dynamic proxy step, we can understand that the proxy object has also implemented the mapper interface, such as Implemented our OrderMapper interface, so spring can help us inject the proxy object into our OrderService.

4. Execute the mapper interface method

In this part, we take calling the orderMapper.findById() method in orderService as an example to explain the execution process of the mapper interface method.

We mentioned in section 3.3.4 above : the real type of the mapper instance object in the spring container is org.apache.ibatis.binding.MapperProxy, so what is MapperProxy?

4.1、MapperProxy

MapperProxy implements the InvocationHandler interface. The newInstance method in section 3.3.4 above creates a MapperProxy object, which is to create a proxy object for the mapper interface through the JDK's own Proxy class (this is the implementation of jdk's dynamic proxy). When the method of the proxied mapper interface is called, the invoke method inside MapperProxy will be executed. The internal implementation of invoke is as follows:

java复制代码  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
       //调用MapperMethodInvoker的invoke方法,内部再调用MapperMethod,最终会执行我们的sql语句
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

 //获取MapperMethod方法的调用器,通过调用器完成对底层MappedStatement的调用
  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

4.2、PlainMethodInvoker

Because we generally do not write default methods in the mapper interface, the cachedInvoker(method) method in the invoke method of MapperProxy above generally returns a PlainMethodInvoker object, and passes a MapperMethod object to PlainMethodInvoker.

sql复制代码return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));

The invoke method of PlainMethodInvoker will call the MapperMethod.execute method, and the invoke implementation is as follows:

typescript复制代码@Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

4.3、MapperMethod

MapperMethod is a wrapper for the internal method of the mapper interface we are currently calling. The PlainMethodInvoker above creates a MapperMethod object by calling the new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()) constructor. Let's look at its construction method implementation:

arduino复制代码public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  this.command = new SqlCommand(config, mapperInterface, method);
  this.method = new MethodSignature(config, mapperInterface, method);
}

The parameter mapperInterface of the constructor is the name of our mapper interface. We observe that a SqlCommand object is created in the constructor. Let's look at the implementation of SqlCommand:

ini复制代码public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);
        }
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }
    ...省略部分代码
}

SqlCommand is an internal class of MapperMethod, which is used to maintain the sql operation type of the method in the mapper interface. It will obtain the MappedStatement object from the configuration by obtaining the fully qualified name of the mapper interface + "." + interface method name, and Assign the name of the MappedStatement object to the name attribute of the SqlCommand object, and assign the sql operation type (select, update, insert or delete) of the MappedStatement object to the type attribute of the SqlCommand object. Why mention these two attributes? Because it will be used below.

The execute method of MapperMethod is implemented as follows:

ini复制代码  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;
  }

In execute, we saw a familiar figure: sqlSession, which is the sqlSessionTemplate object we mentioned earlier. When we call the orderMapper.findById method, we will execute the following:

ini复制代码         Object param = method.convertArgsToSqlCommandParam(args);
         result = sqlSession.selectOne(command.getName(), param);

As mentioned earlier, the value of command.getName() is the id value of the MappedStatement object. In more detail, it is: the fully qualified name of the mapper interface + "." + interface method name, and the process of executing sqlSession.selectOne is equivalent to Operate sqlSessionTemplate directly.

5. Summary

This article mainly talks about the process of automatic registration of the mapper, and some processes involved in calling the mapper interface method. We also mentioned a very important concept: MappedStatement. Each sql mapping statement in the mapping file corresponds to a MappedStatement object, and through the method name of the mapper interface, we can find the only MappedStatement object, and then execute the final sql mapping statement.

Guess you like

Origin blog.csdn.net/m0_71777195/article/details/130722103