刘耀文

刘耀文

java开发者
github

Mybatis Auto-Configuration Principle

Automatic configuration mainly involves several aspects

  • Registration of mapper proxies
  • Registration of SQL statements

View Automatic Configuration Class#

Path: D:\maven-repository\com\baomidou\mybatis-plus-boot-starter\3.4.1\mybatis-plus-boot-starter-3.4.1.jar!\META-INF\spring.factories

Code Explanation#

@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})  
@ConditionalOnSingleCandidate(DataSource.class)  
@EnableConfigurationProperties(MybatisPlusProperties.class)  
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})  
public class MybatisPlusAutoConfiguration implements InitializingBean {  
    // Other fields omitted...
    
    // Check if the configuration file exists
    private void checkConfigFileExists() {  
        // Execute when the configuration file location needs to be checked and is not empty
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {  
            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());  
            // Ensure the resource exists
            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 {  
        // Create MybatisSqlSessionFactoryBean instance and set the data source
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();  
        factory.setDataSource(dataSource);  
        // Omitted configuration and setting other properties...
        
        // Return SqlSessionFactory instance
        return factory.getObject();  
    }  

    // Check beans in the Spring container and consume them
    private <T> void getBeanThen(Class<T> clazz, Consumer<T> consumer) {  
        if (this.applicationContext.getBeanNamesForType(clazz, false, false).length > 0) {  
            consumer.accept(this.applicationContext.getBean(clazz));  
        }  
    }  

    // Apply Mybatis configuration
    private void applyConfiguration(MybatisSqlSessionFactoryBean factory) {  
        MybatisConfiguration configuration = this.properties.getConfiguration();  
        // Omitted other configurations...
        factory.setConfiguration(configuration);  
    }  

    @Bean  
    @ConditionalOnMissingBean    
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {  
        // Return SqlSessionTemplate instance based on properties
        ExecutorType executorType = this.properties.getExecutorType();  
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);  
    }  

    // Omitted other inner classes and methods...
}

This class does several things

  • Registers SqlSessionFactory
    This class mainly provides a session factory for SqlSessionTemplate, which is further encapsulated by mybatisplus into SqlSessionTemplate for integration with Spring transactions.
  • Registers SqlSessionTemplate
  • Registers AutoConfiguredMapperScannerRegistrar

Let's see how the mapper is registered in the Spring container#

public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {  
  
    private BeanFactory beanFactory;  
  
    @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);  
        Stream.of(beanWrapper.getPropertyDescriptors())  
            // Need to mybatis-spring 2.0.2+  
            .filter(x -> x.getName().equals("lazyInitialization")).findAny()  
            .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));  
        registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());  
    }  
  
    @Override  
    public void setBeanFactory(BeanFactory beanFactory) {  
        this.beanFactory = beanFactory;  
    }  
}

As we can see, this class registers a BeanDefinition called MapperScannerConfigurer. Let's take a closer look.

public class MapperScannerConfigurer  
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {  
  
@Override  
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {  
    if (this.processPropertyPlaceHolders) {  
      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(lazyInitialization)) {  
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));  
    }  
    scanner.registerFilters();  
    scanner.scan(  
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));  
  }  

We can see that it further uses ClassPathMapperScanner to scan all classes with this.annotationClass.
And when configuring MapperScannerConfigurer, it scans for this annotation.

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);  
builder.addPropertyValue("processPropertyPlaceHolders", true);  
builder.addPropertyValue("annotationClass", Mapper.class);   // Here
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));

The ClassPathMapperScanner overrides the doScan method.

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

Let's take a look at processBeanDefinitions(beanDefinitions).

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {  
  GenericBeanDefinition definition;  
  for (BeanDefinitionHolder holder : beanDefinitions) {  
    definition = (GenericBeanDefinition) holder.getBeanDefinition();  
    String beanClassName = definition.getBeanClassName();  
    LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName  
        + "' mapperInterface");  
  
    // the mapper interface is the original class of the bean  
    // but, the actual class of the bean is MapperFactoryBean    
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59  
    definition.setBeanClass(this.mapperFactoryBeanClass);  
  
    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) {  
      LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");  
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);  
    }  
    definition.setLazyInit(lazyInitialization);  
  }  
}

We can see that the scanned mappers are wrapped as follows:

    definition.setBeanClass(this.mapperFactoryBeanClass);  

This class will eventually register a mapper proxy.

@Override  
protected void checkDaoConfig() {  
  super.checkDaoConfig();  
  
  notNull(this.mapperInterface, "Property 'mapperInterface' is required");  
  
  Configuration configuration = getSqlSession().getConfiguration();  
  if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {  
    try {  
      configuration.addMapper(this.mapperInterface);  
    } catch (Exception e) {  
      logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);  
      throw new IllegalArgumentException(e);  
    } finally {  
      ErrorContext.instance().reset();  
    }  
  }  
}  
  
/**  
 * {@inheritDoc}  
 */@Override  
public T getObject() throws Exception {  
  return getSqlSession().getMapper(this.mapperInterface);  
}

Let's take a closer look at how configuration.addMapper(this.mapperInterface); registers it.

    public <T> void addMapper(Class<T> type) {  
        if (type.isInterface()) {  
            if (hasMapper(type)) {  
                // TODO If previously injected, return directly  
                return;  
                // TODO No exception thrown here  
//                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");  
            }  
            boolean loadCompleted = false;  
            try {              
                knownMappers.put(type, new MybatisMapperProxyFactory<>(type));  
			    MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);  
                parser.parse();  
                loadCompleted = true;  
            } finally {  
                if (!loadCompleted) {  
                    knownMappers.remove(type);  
                }  
            }  
        }  
    }

Here, a new MapperProxyFactory<>(type) is registered into knownMappers. Let's take a look inside.

public class MybatisMapperProxyFactory<T> {  
      
    @Getter  
    private final Class<T> mapperInterface;  
    @Getter  
    private final Map<Method, MybatisMapperProxy.MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();  
    public MybatisMapperProxyFactory(Class<T> mapperInterface) {  
        this.mapperInterface = mapperInterface;  
    }  
  
    @SuppressWarnings("unchecked")  
    protected T newInstance(MybatisMapperProxy<T> mapperProxy) {  
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);  
    }  
  
    public T newInstance(SqlSession sqlSession) {  
        final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);  
        return newInstance(mapperProxy);  
    }  
}

We can see that it is ultimately encapsulated as a MybatisMapperProxy object.

public class MybatisMapperProxyFactory<T> {  
      
    @Getter  
    private final Class<T> mapperInterface;  
    @Getter  
    private final Map<Method, MybatisMapperProxy.MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();  
    public MybatisMapperProxyFactory(Class<T> mapperInterface) {  
        this.mapperInterface = mapperInterface;  
    }  
  
    @SuppressWarnings("unchecked")  
    protected T newInstance(MybatisMapperProxy<T> mapperProxy) {  
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);  
    }  
  
    public T newInstance(SqlSession sqlSession) {  
        final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);  
        return newInstance(mapperProxy);  
    }  
}

Now the process of how mappers are registered into Spring is clear.

When are SQL statements registered?#

We notice that when registering mappers, there are two lines of code below.

	MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);  
	parser.parse();  

This is where SQL statements are registered. Let's take a closer look.

public void parse() {  
    String resource = type.toString();  
    if (!configuration.isResourceLoaded(resource)) {  
        loadXmlResource();  
        configuration.addLoadedResource(resource);  
        String mapperName = type.getName();  
        assistant.setCurrentNamespace(mapperName);  
        parseCache();  
        parseCacheRef();  
        InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);  
        for (Method method : type.getMethods()) {  
            if (!canHaveStatement(method)) {  
                continue;  
            }  
            if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()  
                && method.getAnnotation(ResultMap.class) == null) {  
                parseResultMap(method);  
            }  
            try {  
                parseStatement(method);  
                InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);  
                SqlParserHelper.initSqlParserInfoCache(mapperName, method);  
            } catch (IncompleteElementException e) {  

            }  
        }  
        // TODO Inject CURD dynamic SQL, placed at the end, because someone may override SQL with annotations  
        try {  
            // https://github.com/baomidou/mybatis-plus/issues/3038  
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {  
                parserInjector();  
            }  
        } catch (IncompleteElementException e) {  
            configuration.addIncompleteMethod(new InjectorResolver(this));  
        }  
    }  
    parsePendingMethods();  
}

Among them, the line loadXmlResource(); is crucial.

Let's take a closer look.

private void loadXmlResource() {  
    // Spring may not know the real resource name so we check a flag  
    // to prevent loading again a resource twice    
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace    
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {  
        String xmlResource = type.getName().replace('.', '/') + ".xml";  
        // #1347  
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);  
        if (inputStream == null) {  
            // Search XML mapper that is not in the module but in the classpath.  
            try {  
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);  
            } catch (IOException e2) {  
                // ignore, resource is not required  
            }  
        }  
        if (inputStream != null) {  
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());  
            xmlParser.parse();  
        }  
    }  
}

Here, the line String xmlResource = type.getName().replace('.', '/') + ".xml"; explains why the XML must be placed in the same folder as the mapper, as it directly replaces the suffix with XML to read.

public void parse() {  
  if (!configuration.isResourceLoaded(resource)) {  
    configurationElement(parser.evalNode("/mapper"));  
    configuration.addLoadedResource(resource);  
    bindMapperForNamespace();  
  }  
  
  parsePendingResultMaps();  
  parsePendingCacheRefs();  
  parsePendingStatements();  
}

This is where some configurations and SQL statements are loaded, followed by the registration of MybatisPlus's built-in CRUD operations.

// TODO Inject CURD dynamic SQL, placed at the end, because someone may override SQL with annotations  
try {  
    // https://github.com/baomidou/mybatis-plus/issues/3038  
    if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {  
        parserInjector();  
    }  
} catch (IncompleteElementException e) {  
    configuration.addIncompleteMethod(new InjectorResolver(this));  
}

This article is synchronized and updated to xLog by Mix Space
The original link is https://me.liuyaowen.cn/posts/codenotes/2024112


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.