title: 【転載】SpringBoot スキャン注釈分析と同名の衝突解決
date: 2021-08-24 15:02:22
comment: false
toc: true
category:
- Java
tags: - 転載
 - Java
 - SpringBoot
 - 注釈
 - 分析
 - シナリオ
 - 衝突
 - 解決
 - スキャン
 - プロジェクト
 - モード
 - pom
 - 起動
 - 機能
 
この記事は転載です:SpringBoot パッケージスキャンの多モジュール多パッケージ名スキャンと同名スキャンの衝突解決|8 月更新チャレンジ
私たちが SpringBoot プロジェクトを開発する際、SpringBoot プロジェクトを作成すると、起動クラスから直接起動し、web プロジェクトを実行することができ、とても便利で簡単です。
以前の Spring+SpringMvc では、web プロジェクトを起動するためにさまざまなパッケージスキャンや tomcat の設定が必要でした。
私はアプリケーションを parent+common+component+app というモードに分けました。
- parent は単純な pom ファイルで、プロジェクトのいくつかの共通依存関係を保存します。
 - common は起動クラスのない 
SpringBootプロジェクトで、プロジェクトのコア共通コードを保存します。 - component はさまざまなコンポーネント機能サービスモジュールで、使用する際に直接参照してプラグイン方式で実現します。
 - app は実際のアプリケーションプロジェクトで、SpringBoot の起動クラスを含み、さまざまな実際の機能を提供します。
 


その中で kmall-admin と kmall-api は実際のアプリケーションプロジェクトで、SpringBoot の起動クラスを含みます。
しかし、プロジェクトを起動する際に、いくつかのモジュールが正常に注入されていないことに気付きました。設定クラスも機能していませんでした。つまり、SpringBoot はこれらのファイルをスキャンできていませんでした。
シナリオ分析#
SpringBoot のデフォルトスキャンメカニズム#
SpringBoot のデフォルトのパッケージスキャンメカニズムは、起動クラスが存在するパッケージから始まり、現在のパッケージおよびそのサブパッケージ内のすべてのファイルをスキャンします。
最初は私の起動クラスのパッケージ名が cn.soboys.kmall.admin.WebApplication であり、他のプロジェクトファイルのパッケージ名はすべて cn.soboys.kmall.*.XxxClass であったため、他のモジュールが参照されると、以下のファイルがスキャンされて注入されませんでした。
SpringBoot 起動クラスのスキャン注釈#
SpringBoot の起動クラスでは、スキャンパッケージパスを設定する方法が 3 つあります。最近、あるアプリケーションで 3 つの注釈がすべて使用されているのを見ました。コードは以下の通りです:
@SpringBootApplication(scanBasePackages ={"a","b"})  
@ComponentScan(basePackages = {"a","b","c"})  
@MapperScan({"XXX"})  
public class XXApplication extends SpringBootServletInitializer {  
}  
では、疑問が生じます:SpringBoot では、これら 3 つの注釈の有効優先度はどうなっているのか、第一と第二の違いは何か?
SpringBootApplication 注釈#
これは SpringBoot の注釈で、本質的には 3 つの Spring 注釈の和であり、ソースコードを見るとわかります。
//  
// IntelliJ IDEAによって.classファイルから再構築されたソースコード  
// (Fernflowerデコンパイラによって提供)  
//  
package org.springframework.boot.autoconfigure;  
import java.lang.annotation.Documented;  
import java.lang.annotation.ElementType;  
import java.lang.annotation.Inherited;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
import org.springframework.beans.factory.support.BeanNameGenerator;  
import org.springframework.boot.SpringBootConfiguration;  
import org.springframework.boot.context.TypeExcludeFilter;  
import org.springframework.context.annotation.ComponentScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.context.annotation.FilterType;  
import org.springframework.context.annotation.ComponentScan.Filter;  
import org.springframework.core.annotation.AliasFor;  
@Target({ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Inherited  
@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan(  
    excludeFilters = {@Filter(  
    type = FilterType.CUSTOM,  
    classes = {TypeExcludeFilter.class}  
), @Filter(  
    type = FilterType.CUSTOM,  
    classes = {AutoConfigurationExcludeFilter.class}  
)}  
)  
public @interface SpringBootApplication {  
    @AliasFor(  
        annotation = EnableAutoConfiguration.class  
    )  
    Class<?>[] exclude() default {};  
    @AliasFor(  
        annotation = EnableAutoConfiguration.class  
    )  
    String[] excludeName() default {};  
    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "basePackages"  
    )  
    String[] scanBasePackages() default {};  
    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "basePackageClasses"  
    )  
    Class<?>[] scanBasePackageClasses() default {};  
    @AliasFor(  
        annotation = ComponentScan.class,  
        attribute = "nameGenerator"  
    )  
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;  
    @AliasFor(  
        annotation = Configuration.class  
    )  
    boolean proxyBeanMethods() default true;  
}  
- @SpringBootConfiguration
 - @EnableAutoConfiguration
 - @ComponentScan
 
これはデフォルトで起動クラスが存在するパッケージおよびそのすべてのサブパッケージをスキャンしますが、サードパーティの jar パッケージの他のディレクトリは含まれません。scanBasePackages 属性を使用してスキャンパッケージパスを再設定できます。
ComponentScan 注釈#
これは Spring フレームワークの注釈で、コンポーネントスキャンパスを指定するために使用されます。この注釈を使用する場合、その値はプロジェクト全体でスキャンする必要があるすべてのパスを含む必要があります。なぜなら、これは SpringBootApplication のデフォルトスキャンパスを上書きし、無効にするからです。
無効になる現象は 2 つあります:
- ComponentScan が 1 つの値だけを含み、それがデフォルトの起動クラスのディレクトリである場合、SpringBootApplication は有効になり、ComponentScan 注釈は無効になり、エラーが発生します。
 - ComponentScan が複数の具体的なサブディレクトリを指定する場合、この時 SpringBootApplication は無効になり、Spring は ComponentScan で指定されたディレクトリ内の注釈のみをスキャンします。もしちょうどディレクトリ外に Controller クラスがある場合、残念ながら、これらのコントローラーにはアクセスできません。
 
MapperScan 注釈#
- ここでは 
@Mapper注釈が関係しています。 
直接 Mapperクラス の上に @Mapper 注釈を追加する必要があり、この方法では各 mapperクラス にこの注釈を追加する必要があり、少し面倒です。
@MapperScanを使用することで、スキャンする Mapper クラスのパッケージパスを指定できます。
これは MyBatis の注釈で、指定されたディレクトリ内のすべての Mapper クラスを MyBatis の BaseMapper クラスにラップし、対応する XxxMapper プロキシインターフェース実装クラスを生成して Spring コンテナに注入します。追加の注釈なしで注入を完了できます。
// @MapperScan注釈を使用して複数のパッケージを指定  
@SpringBootApplication  
@MapperScan({"com.kfit.demo","com.kfit.user"})  
public class App {  
    public static void main(String[] args) {  
       SpringApplication.run(App.class, args);  
    }  
}  
もし mapper クラスが SpringBoot 主プログラムがスキャンできるパッケージまたはサブパッケージの下にない場合、次のように設定できます:
@SpringBootApplication  
@MapperScan({"com.kfit.*.mapper","org.kfit.*.mapper"})  
public class App {  
    public static void main(String[] args) {  
       SpringApplication.run(App.class, args);  
    }  
}  
シナリオ解決#
したがって、上記のすべての注釈を分析した結果、2 つの解決策があります:
- 
@SpringBootApplicationを使用してscanBasePackages属性を指定し、スキャンパスを再設定できます。@SpringBootApplication(scanBasePackages = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class) @MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper", "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class) public class WebApplication { private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(WebApplication.class, args); //displayAllBeans(); } /** * すべてのロードされたbeanを表示 */ public static void displayAllBeans() { String[] allBeanNames = applicationContext.getBeanDefinitionNames(); for (String beanName : allBeanNames) { System.out.println(beanName); } } } - 
@ComponentScanを使用してbasePackages属性を指定し、コンポーネントスキャンパスを指定できます。@ComponentScan(basePackages = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class) @MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper", "cn.soboys.kmall.security.mapper","cn.soboys.kmall.monitor.mapper"},nameGenerator = UniqueNameGenerator.class) public class WebApplication { private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(WebApplication.class, args); //displayAllBeans(); } /** * すべてのロードされたbeanを表示 */ public static void displayAllBeans() { String[] allBeanNames = applicationContext.getBeanDefinitionNames(); for (String beanName : allBeanNames) { System.out.println(beanName); } } } 
もちろん、スキャン中に属性
nameGeneratorが指定されているのは、複数のモジュール、多くのパッケージ名の下で同名のクラス、スキャン注入の衝突問題を解決するためです。
Spring は 2 つの beanName 生成戦略を提供しており、注釈に基づく SpringBoot のデフォルトは AnnotationBeanNameGenerator を使用しています。これは、現在のクラス名(完全修飾クラス名ではない)を beanName として取得する戦略です。したがって、異なるパッケージ構造で同じクラス名が存在する場合、衝突が発生することは明らかです。
解決策:自分でクラスを作成し、
org.springframework.beans.factory.support.BeanNameGeneratorインターフェースを実装して、beanName 生成戦略を再定義し、AnnotationBeanNameGenerator を継承し、generateBeanName をオーバーライドできます。
同様に、MyBatis の異なるパッケージ内の同名 mapper bean 名の重複問題を解決します。
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {  
    @Override  
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {  
        // 完全修飾クラス名  
        String beanName = definition.getBeanClassName();  
        return beanName;  
    }  
}  
public class UniqueNameGenerator extends AnnotationBeanNameGenerator {  
    @Override  
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {  
        // 値が設定されている場合はそれを使用し、設定されていない場合は完全クラス名を使用  
        if (definition instanceof AnnotatedBeanDefinition) {  
            String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);  
            if (StringUtils.hasText(beanName)) {  
                // 明示的なbean名が見つかりました。  
                return beanName;  
            }else{  
                // 完全修飾クラス名  
                beanName = definition.getBeanClassName();  
                return beanName;  
            }  
        }  
        // デフォルトのクラス名を使用  
        return buildDefaultBeanName(definition, registry);  
    }  
}  
これは完全修飾クラス名、つまり パッケージ名+クラス名 です。
package com;  
import org.springframework.beans.factory.config.BeanDefinition;  
import org.springframework.context.annotation.AnnotationBeanNameGenerator;  
import org.springframework.stereotype.Component;  
import org.springframework.util.Assert;  
@Component("myNameGenerator")  
public class MyNameGenerator extends AnnotationBeanNameGenerator {  
    @Override  
    protected String buildDefaultBeanName(BeanDefinition definition) {  
        String beanClassName = definition.getBeanClassName();  
        Assert.state(beanClassName != null, "No bean class name set");  
        // クラスの全パスを分割  
        String[] packages = beanClassName.split("\\.");  
        StringBuilder beanName = new StringBuilder();  
        // クラスのパッケージ名の最初の文字を小文字にして、クラス名を最後のbean名として追加  
        for (int i = 0; i < packages.length - 1; i++) {  
            beanName.append(packages[i].toLowerCase().charAt(0));  
        }  
        beanName.append(packages[packages.length - 1]);  
        return beanName.toString();  
    }  
}  
これはクラスの パッケージ名の最初の文字を小文字 にして クラス名 を最後の bean 名として追加します。
- 衝突するクラス名のスキャン名を個別に指定して解決することもできます。
 
同名のクラスに @Service 注釈または @controller 注釈をスキャンする際に、値を指定します。
- @Primary 注釈
 
この注釈は、複数の bean が注入条件を満たす場合に、この注釈が付けられたインスタンスが選択されるためのものです。