spring如何掃描解析bean(注冊(cè)bean的多種方式)

注解bean

我們常用的向spring容器中添加bean的方式主要有三種

  • @Component注解
  • @Configuration 加 @bean
  • @Import

那么spring是如何解析這些注解的,本文具體研究這個(gè)問(wèn)題

parse

spring解析注解bean的代碼寫(xiě)在ConfigurationClassParser類的parse方法,參數(shù)就是SpringApplication.run時(shí)傳入的啟動(dòng)主類

spring啟動(dòng)時(shí)會(huì)解析我們的主配置類(就是帶@SpringBootApplication的啟動(dòng)類),解析的任務(wù)交給解析器ConfigurationClassParser,對(duì)應(yīng)的方法就是parse

ConfigurationClassParser中一個(gè)屬性configurationClasses,用來(lái)存放解析出來(lái)的結(jié)果

private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();

對(duì)應(yīng)的get方法

public Set<ConfigurationClass> getConfigurationClasses() {
    return this.configurationClasses.keySet();
}

processConfigurationClass

parse方法最終會(huì)走向processConfigurationClass方法

ConfigurationClassParser.parse

這個(gè)方法貼主要代碼

do {
    sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);

調(diào)用doProcessConfigurationClass方法,如果有返回值,遞歸調(diào)用doProcessConfigurationClass,看spring的注釋說(shuō)// Recursively process the configuration class and its superclass hierarchy.,也就是遞歸解析配置類和他的父類,所以這代碼的意思就是解析配置類,如果有父類再解析父類,如果父類有父類再一直解析下去。
所以重點(diǎn)就來(lái)到了doProcessConfigurationClass方法

doProcessConfigurationClass

doProcessConfigurationClass(spring源碼中的doXXX一般都很重要),接下來(lái)就分析這個(gè)代碼,貼完整代碼

protected final SourceClass doProcessConfigurationClass(
        ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
        throws IOException {
    if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
        // 1.內(nèi)部類  這一步看看有沒(méi)有內(nèi)部類,一般不咋用
        processMemberClasses(configClass, sourceClass, filter);
    }

    // 2.@PropertySource 這一步解析@PropertySource注解,更改配置文件位置時(shí)會(huì)使用,一般使用默認(rèn)位置,不咋更改
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
        if (this.environment instanceof ConfigurableEnvironment) {
            processPropertySource(propertySource);
        }
        else {
            logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }

    // 3.@ComponentScan 這一步就很重要了,解析@ComponentScan注解
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    if (!componentScans.isEmpty() &&
            !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
        for (AnnotationAttributes componentScan : componentScans) {
            // 開(kāi)始掃描,把@ComponentScan指定包下的@Component類全部掃描出來(lái)
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
            // 把所有掃描到的beanClass遞歸解析,所以我們也可以加多個(gè)@Configuration類
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) {
                    bdCand = holder.getBeanDefinition();
                }
                //判斷是不是ConfigurationClass 帶@Configuration注解和@Component注解都算
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    // 解析
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }

    // 4.@Import 解析@Import注解
    processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

    // 5.@ImportResource解析@ImportResource解析
    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    if (importResource != null) {
        String[] resources = importResource.getStringArray("locations");
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        for (String resource : resources) {
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }

    // 6.@Bean 解析帶有@Bean注解的方法,加入到configClass的beanMethods屬性中
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // 6.接口@Bean 解析實(shí)現(xiàn)的接口中帶有@Bean注解的默認(rèn)方法,加入到configClass的beanMethods屬性中
    processInterfaces(configClass, sourceClass);

    // 7.父類 如果有父類返回父類,以繼續(xù)解析
    if (sourceClass.getMetadata().hasSuperClass()) {
        String superclass = sourceClass.getMetadata().getSuperClassName();
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            this.knownSuperclasses.put(superclass, configClass);
            // Superclass found, return its annotation metadata and recurse
            return sourceClass.getSuperClass();
        }
    }

    // 沒(méi)有父類,解析結(jié)束
    return null;
}

整個(gè)流程總結(jié)如下

  • 解析內(nèi)部類
  • 解析@PropertySource注解
  • 解析@ComponentScan注解,掃描指定包下的所有@Component類,并遞歸解析
  • 解析@Import注解
  • 解析@ImportResource注解
  • 解析@Bean
  • 解析實(shí)現(xiàn)接口中的@Bean
  • 返回父類

接下來(lái)一個(gè)個(gè)看

解析內(nèi)部類

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
    // 1.內(nèi)部類  這一步看看有沒(méi)有內(nèi)部類,一般不咋用
    processMemberClasses(configClass, sourceClass, filter);
}

也就是說(shuō)如果一個(gè)類有@Component注解,會(huì)解析他的內(nèi)部類如果也有@Component會(huì)注冊(cè)成bean,寫(xiě)個(gè)代碼測(cè)試一下

@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        Nested nested = context.getBean(Nested.class);
        System.out.println(nested); // com.pqsir.parser.ClassParserApplication$Nested@4e7912d8
    }

    @Component
    class Nested {

    }
}

所以內(nèi)部類也可以注冊(cè)到bean容器

解析@PropertySource注解

這個(gè)真沒(méi)用過(guò),我覺(jué)得配置文件放在規(guī)定的地就不錯(cuò),以后也好找,這個(gè)就不研究了

解析@ComponentScan

這個(gè)都懂,就是掃描的包路徑,值的注意的是掃描到的class都會(huì)遞歸調(diào)用parser
這一步的代碼細(xì)分析下

  • 掃描包下的類
Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

這個(gè)componentScanParser內(nèi)部有個(gè)scaner(掃描器),掃描@Component注解的類(包括子注解@Configuration@Service等)

  • 循環(huán)判斷掃描到的類是否是ConfigurationClass,如果是則遞歸解析
// 如果是ConfigurationClass
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    // 解析
    parse(bdCand.getBeanClassName(), holder.getBeanName());
}

這一步ConfigurationClass不單單是指帶@Configuration注解的類,帶@Component注解的也算ConfigurationClass,spring內(nèi)部有個(gè)集合,只要是這個(gè)集合里的注解,都算ConfigurationClass

// ConfigurationClassUtils
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
    candidateIndicators.add(Component.class.getName());
    candidateIndicators.add(ComponentScan.class.getName());
    candidateIndicators.add(Import.class.getName());
    candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
    // 省略
    // 是否有候選注解
    for (String indicator : candidateIndicators) {
        if (metadata.isAnnotated(indicator)) {
            return true;
        }
    }
    // 省略
}

總結(jié)【1】帶@Configuration或@Component類都是ConfigurationClass

最后做個(gè)小測(cè)試--主類制定了@ComponentScan包下又一個(gè)帶@ComponentScan的類指向另一個(gè)包,那么這兩個(gè)包下的bean都會(huì)被注入,測(cè)試一下,我們建兩個(gè)包parserparser2
parser2下一個(gè)普通bean:BeanOut

package com.pqsir.parser2;
@Component
public class BeanOut {
    @Override
    public String toString() {
        return "BeanOut";
    }
}

parser下ClassParserApplication主類掃描parser和 OtherConfiguration:在parser包下定義掃描parser2

package com.pqsir.parser
@Configuration
@ComponentScan("com.pqsir.parser") //掃描parser
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}
@Configuration
@ComponentScan("com.pqsir.parser2")
public class OtherConfiguration {
}

最終正常輸出"BeanOut"

解析@Import注解

這個(gè)也比較好理解,就是如果某個(gè)bean帶@Import注解,就把@Import注解指定的類也注冊(cè)成bean
測(cè)試一下,我們把上一步BeanOut的@Component注解去掉,刪除OtherConfiguration

@Configuration
@ComponentScan("com.pqsir.parser")
@Import({BeanOut.class})
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}

結(jié)果也可以正常輸出
這個(gè)@Import的最大好處可以把一些第三方的類給注入到bean容器,因?yàn)榈谌降念愐话阋哺牟涣丝偛荒苋ゼ?code>@Component注解吧,而且如果你是一個(gè)第三方開(kāi)發(fā)者,肯定不希望一直的工具依賴spring(萬(wàn)一哪天沒(méi)人用了你的工具也廢了),所以通過(guò)@Import把你的工具引入spring是一個(gè)完美的解決方案。
還有個(gè)比較大的好處是可以做封裝,@Import注解可以被繼承,比如我們寫(xiě)個(gè)自定義注解繼承了@Import,并指定import的類,就可以把這個(gè)類注冊(cè)到bean容器中,甚至可以讓這個(gè)類繼承一些后置處理器來(lái)給bean容器做調(diào)整,比如AOP的@EnableAspectJAutoProxy就是用到這一點(diǎn),還有mybaits的也是用@MapperScan也是使用Import的方式完成一些mapper bean的生成工作
上例是@Configuration+@Import,用@Component+@Import也ok,因?yàn)椤?】

解析@ImportResource注解

主要為了兼容之前的xml寫(xiě)法

解析@Bean

@Bean注解一般經(jīng)常使用,使用工廠方法創(chuàng)建一個(gè)bean,一般就是@Configuration+@Bean,由于上述原因【1】,所以@Component+@Bean也可以。

解析實(shí)現(xiàn)接口中的@Bean

這是對(duì)@Bean注解的一個(gè)擴(kuò)展,解析實(shí)現(xiàn)的接口中帶有@Bean注解的默認(rèn)方法,寫(xiě)個(gè)例子測(cè)試一下

  • 接口(包含默認(rèn)方法帶@Bean注解,本身不帶任何注解)
public interface IBeanA {
    @Bean
    default BeanOut beanOut() {
        return new BeanOut();
    }
}
  • 實(shí)現(xiàn)(帶@Component注解)
@Component
//@Import({BeanOut.class})
public class BeanA implements IBeanA {

}
  • 測(cè)試類
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}

也會(huì)正常輸出,這個(gè)真沒(méi)想到什么使用場(chǎng)景,遇到再說(shuō)吧

返回父類

最后一步如果返回父類繼續(xù)遞歸解析,測(cè)試一下

  • 父類(沒(méi)有@Component注解)
public class BeanFather {
    @Override
    public String toString() {
        return "BeanFather";
    }
}
  • 子類(有@Component注解)
@Component
public class BeanSon extends BeanFather{
}
  • 測(cè)試類(嘗試獲取父類bean)
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanFather bean = context.getBean(BeanFather.class);
        System.out.println(bean); // BeanFather 
    }
}

正??色@取

最后

完成這一系列的解析掃描再解析過(guò)程,就可以通過(guò)getConfigurationClasses拿到所有掃描并解析到的類。
spring拿到這些類之后再通過(guò)一個(gè)readerConfigurationClasse轉(zhuǎn)換為bean定義,注冊(cè)到beanFactory,所以parser+reader,就完成了這些bean的掃描&解析&注冊(cè)工作,代碼在ConfigurationClassPostProcessor中。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    // 創(chuàng)建一個(gè)解析器
    ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    // 開(kāi)始解析,這個(gè)candidates就是我們傳入的MainApplication
    parser.parse(candidates);
    // 獲取解析到的類
    Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
    // 初始化一個(gè)reader
    this.reader = new ConfigurationClassBeanDefinitionReader(
                        registry, this.sourceExtractor, this.resourceLoader, this.environment,
                        this.importBeanNameGenerator, parser.getImportRegistry());
    // 把上面解析到的類轉(zhuǎn)化為bean定義并注冊(cè)到bean定義注冊(cè)器(registry)
    this.reader.loadBeanDefinitions(configClasses);
}

擴(kuò)展

@Configuration和@Component的區(qū)別

上文很多@Configuration的注解都可以用@Component代替,甚至主類使用@Component來(lái)替換@Configuration也能正常跑,那問(wèn)題來(lái)了,他倆就沒(méi)有區(qū)別嗎,那要@Configuration有啥用。
卻別主要兩方面
一.首先,@Configuration和@Service,@Controller注解很像,都繼承@Component注解,沒(méi)有實(shí)際的什么作用只是告訴別人這個(gè)類是個(gè)配置類型的bean
二.其次,也是實(shí)際功能上的區(qū)別,使用@Configuration類+@Bean,sping會(huì)生成一個(gè)cglib動(dòng)態(tài)代理,這個(gè)代理的工能就是第一次調(diào)用@Bean的方法會(huì)直接執(zhí)行并返回結(jié)果,同時(shí)存儲(chǔ)結(jié)果,下一次調(diào)用同樣方法直接返回結(jié)果,這樣可以保證單例,不管調(diào)用多少次@Bean的方法最終得到的結(jié)果是同一個(gè)對(duì)象,而使用@Component則不會(huì)

做個(gè)測(cè)試

通過(guò)@Bean注冊(cè)三個(gè)bean A B C,其中BC依賴注入A,先使用@Configuration

@Configuration
public class BeanConfiguration {
    @Bean
    public A a() {
        return new A();
    }
    @Bean
    public B b() {
        B b = new B();
        b.a = a();
        return b;
    }
    @Bean
    public C c() {
        C c = new C();
        c.a = a();
        return c;
    }
    static class A {
    }
    static class B {
        public A a;
    }
    static class C {
        public A a;
    }
}

試一下

ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanConfiguration.B b = context.getBean(BeanConfiguration.B.class);
BeanConfiguration.C c = context.getBean(BeanConfiguration.C.class);
System.out.println(b.a.equals(c.a));

輸出結(jié)果是true
如果把@Configuration改成@Component,輸出結(jié)果就變成false了,這個(gè)代理的代碼ConfigurationClassPostProcessor.enhanceConfigurationClasses中,有興趣的自己研究吧
其實(shí)這種寫(xiě)法本來(lái)就不太好,還是覺(jué)得用依賴注入更好,如下

@Configuration
public class BeanConfiguration {
    @Bean
    public A a() {
        return new A();
    }
    @Bean
    public B b(A a) {
        B b = new B();
        b.a = a;
        return b;
    }
    @Bean
    public C c(A a) {
        C c = new C();
        c.a = a;
        return c;
    }
    static class A {
    }
    static class B {
        public A a;
    }
    static class C {
        public A a;
    }
}

這種寫(xiě)法就算改成@Component也沒(méi)問(wèn)題

@Import+ImportBeanDefinitionRegistrar

上面說(shuō)了@Import可以導(dǎo)入bean,一個(gè)或多個(gè)bean,但如果比如把一個(gè)包下的所有類都注入到bean,它就不能實(shí)現(xiàn)了,除非你一個(gè)一個(gè)寫(xiě),但是這樣新增一個(gè)就得寫(xiě)一個(gè)。
這種需求其實(shí)很常見(jiàn),比如mybaits的@MapperScan,他需要你指定一個(gè)mapper的位置,然后把mapper全部注入到bean容器,不管你加多少mapper都會(huì)注入進(jìn)去。
spring批量注冊(cè)bean是有個(gè)后置處理器支持的,就是BeanDefinitionRegistryPostProcessor,如果你某個(gè)bean實(shí)現(xiàn)了BeanDefinitionRegistryPostProcessor,就會(huì)拿到bean定義注冊(cè)器BeanDefinitionRegistry,然后愛(ài)怎么注冊(cè)bean定義、注冊(cè)多少隨你。
所以我最開(kāi)始遇到這種需求解決思路是@Import+BeanDefinitionRegistryPostProcessor,雖然可行,但獲取不到注解的屬性,比如@MapperScan的value

所以spring要引入ImportBeanDefinitionRegistrar這個(gè)接口,其中重要方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

通過(guò)實(shí)現(xiàn)這個(gè)方法也能拿到BeanDefinitionRegistry(ConfigurationClassPostProcessor本身繼承BeanDefinitionRegistryPostProcessor,所以可以拿到),然后可以按照自己的意思注冊(cè)bean定義,更重要的:通過(guò)第一個(gè)參數(shù)importingClassMetadata可以獲取使用@Import注解的類的元數(shù)據(jù),就可以獲取到外層注解的屬性值
那么問(wèn)題來(lái)了,什么時(shí)候執(zhí)行吶。
剛才"最后"章節(jié)的代碼有一句this.reader.loadBeanDefinitions(configClasses);,就是在這個(gè)時(shí)候繼承這個(gè)接口的類(被@Import引入)執(zhí)行registerBeanDefinitions方法

registerBeanDefinitions

可以自己去找代碼,整個(gè)過(guò)程再ConfigurationClassPostProcessor執(zhí)行的生命周期執(zhí)行完畢

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容