刘耀文

刘耀文

java开发者
github

Mybatis 自動配置原理

自動配置無非涉及幾個方面

  • mapper 的代理註冊
  • sql 語句的註冊

查看自動配置類#

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

代碼解釋#

@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})  
@ConditionalOnSingleCandidate(DataSource.class)  
@EnableConfigurationProperties(MybatisPlusProperties.class)  
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})  
public class MybatisPlusAutoConfiguration implements InitializingBean {  
    // 省略其他字段...
    
    // 檢查配置文件是否存在
    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 {  
        // 創建 MybatisSqlSessionFactoryBean 實例,設置數據源
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();  
        factory.setDataSource(dataSource);  
        // 省略配置和設置其他屬性...
        
        // 返回 SqlSessionFactory 實例
        return factory.getObject();  
    }  

    // 檢查 Spring 容器中的 bean,並進行消費
    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));  
        }  
    }  

    // 應用 Mybatis 配置
    private void applyConfiguration(MybatisSqlSessionFactoryBean factory) {  
        MybatisConfiguration configuration = this.properties.getConfiguration();  
        // 省略其他配置...
        factory.setConfiguration(configuration);  
    }  

    @Bean  
    @ConditionalOnMissingBean    
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {  
        // 根據屬性返回 SqlSessionTemplate 實例
        ExecutorType executorType = this.properties.getExecutorType();  
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);  
    }  

    // 省略其他內部類和方法...
}

這個類做了幾件事

  • 註冊 SqlSessionFactory
    這個類主要是為 SqlSessionTemplate 提供會話獲取工廠 當然了 mybatisplus 進一步封裝為 SqlSessionTemplate 為了和 spring 的事務結合
  • 註冊 SqlSessionTemplate
  • 註冊 AutoConfiguredMapperScannerRegistrar

我們看看 mapper 是怎麼註冊到 spring 容器裡面的#

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

可以看到這個類註冊了一個 BeanDefinition 叫 MapperScannerConfigurer,再進去看看

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

可以看到 裡面進一步使用 ClassPathMapperScanner 來掃描所有的 this.annotationClass 類
而上面配置 MapperScannerConfigurer 的時候是掃描這個註解

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

其中 ClassPathMapperScanner 重寫了 doScan 方法

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

點進去 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);  
  }  
}

可以看到將掃描的 mapper 包裝為

    definition.setBeanClass(this.mapperFactoryBeanClass);  

這個類最後會註冊一個 mapper 代理

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

我們進一步進去 configuration.addMapper (this.mapperInterface); 看看怎麼註冊的

    public <T> void addMapper(Class<T> type) {  
        if (type.isInterface()) {  
            if (hasMapper(type)) {  
                // TODO 如果之前注入 直接返回  
                return;  
                // TODO 這裡就不拋異常了  
//                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);  
                }  
            }  
        }  
    }

這裡進一步的將 new MapperProxyFactory<>(type) 註冊到 knownMappers 中,我們點進去看看

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

可以看到 最後是封裝為一個 MybatisMapperProxy 對象

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

好了 mapper 怎麼註冊到 spring 的流程通了

sql 語句什麼時候註冊進去的呢?#

我們注意到 註冊 mapper 的時候下面還有兩行語句

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

這裡就是註冊 sql 語句的地方,我們點進去看看

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 注入 CURD 動態 SQL , 放在在最後, because 可能會有人會用註解重寫sql  
        try {  
            // https://github.com/baomidou/mybatis-plus/issues/3038  
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {  
                parserInjector();  
            }  
        } catch (IncompleteElementException e) {  
            configuration.addIncompleteMethod(new InjectorResolver(this));  
        }  
    }  
    parsePendingMethods();  
}

其中有一句loadXmlResource();
我們點進去看看

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

可以看到這裡就是加載 xml 的地方String xmlResource = type.getName().replace('.', '/') + ".xml"; 這一句就是為什麼 xml 要和 mapper 放在一個文件夾的關鍵了,直接是吧後綴替換為 xml 再去讀取

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

這裡就是加載一些配置和一些 sql 語句了,接著就是註冊 mybatisplus 自帶的增刪改查了

// TODO 注入 CURD 動態 SQL , 放在在最後, because 可能會有人會用註解重寫sql  
try {  
    // https://github.com/baomidou/mybatis-plus/issues/3038  
    if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {  
        parserInjector();  
    }  
} catch (IncompleteElementException e) {  
    configuration.addIncompleteMethod(new InjectorResolver(this));  
}

此文由 Mix Space 同步更新至 xLog
原始鏈接為 https://me.liuyaowen.cn/posts/codenotes/2024112


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。