刘耀文

刘耀文

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

このクラスは MapperScannerConfigurer という BeanDefinition を登録します。さらに見てみましょう。

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");  
  
    // mapperインターフェースはbeanの元のクラスですが、beanの実際のクラスは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 を登録する際に、以下の 2 行の文があります。

	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を注入、最後に配置、なぜなら注釈で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は実際のリソース名を知らないかもしれないので、フラグを確認して  
    // リソースを二重に読み込むのを防ぎます  
    // このフラグはXMLMapperBuilder#bindMapperForNamespaceで設定されます  
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {  
        String xmlResource = type.getName().replace('.', '/') + ".xml";  
        // #1347  
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);  
        if (inputStream == null) {  
            // モジュール内ではなく、クラスパスにあるXMLマッパーを検索します。  
            try {  
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);  
            } catch (IOException e2) {  
                // 無視、リソースは必須ではありません  
            }  
        }  
        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 が提供する CRUD を登録します。

// TODO CURD動的SQLを注入、最後に配置、なぜなら注釈で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


読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。