@Import注解:導入配置類的四種方式&源碼解析

微信搜索:碼農(nóng)StayUp
主頁地址:https://gozhuyinglong.github.io
源碼分享:https://github.com/gozhuyinglong/blog-demos

平時喜歡看源碼的小伙伴,應該知道Spring中大量使用了@Import注解重抖。該注解是Spring用來導入配置類的捡絮,等價于Spring XML中的<import/>元素贝淤。

本文將對該注解進行介紹蓬蝶,并通過實例演示它導入配置類的四種方式,最后對該注解進行源碼解析融涣。

話不多說柬讨,走起~

簡介

@Import注解的全類名是org.springframework.context.annotation.Import。其只有一個默認的value屬性伪货,該屬性類型為Class<?>[]们衙,表示可以傳入一個或多個Class對象。

通過注釋可以看出碱呼,該注解有如下作用:

  • 可以導入一個或多個組件類(通常是@Configuration配置類)
  • 該注解的功能與Spring XML中的<import/>元素相同蒙挑。可以導入@Configuration配置類愚臀、ImportSelectImportBeanDefinitionRegistrar的實現(xiàn)類忆蚀。從4.2版本開始,還可以引用常規(guī)組件類(普通類)姑裂,該功能類似于AnnotationConfigApplicationContext.register方法馋袜。
  • 該注解可以在類中聲明,也可以在元注解中聲明舶斧。
  • 如果需要導入XML或其他非@Configuration定義的資源欣鳖,可以使用@ImportResource注釋。

導入配置類的四種方式

源碼注釋寫得很清楚捧毛,該注解有四種導入方式:

  1. 普通類
  2. @Configuration配置類
  3. ImportSelector的實現(xiàn)類
  4. ImportBeanDefinitionRegistrar的實現(xiàn)類

下面我們逐個來介紹~

準備工作

創(chuàng)建四個配置類:ConfigA观堂、ConfigB、ConfigC呀忧、ConfigD师痕。其中ConfigB中增加@Configuration注解,表示為配置類而账,其余三個均為普通類胰坟。

ConfigA:

public class ConfigA {

    public void print() {
        System.out.println("輸出:ConfigA.class");
    }
}

ConfigB:

@Configuration
public class ConfigB {

    public void print() {
        System.out.println("輸出:ConfigB.class");
    }

}

ConfigC:

public class ConfigC {

    public void print() {
        System.out.println("輸出:ConfigC.class");
    }

}

ConfigD:

public class ConfigD {
    
    public void print() {
        System.out.println("輸出:ConfigD.class");
    }

}

再創(chuàng)建一個主配置類Config,并試圖通過@Resource注解將上面四個配置類進行注入泞辐。當然笔横,這樣是不成功的竞滓,還需要將它們進行導入。

@Configuration
public class Config {

    @Resource
    ConfigA configA;

    @Resource
    ConfigB configB;

    @Resource
    ConfigC configC;

    @Resource
    ConfigD configD;


    public void print() {
        configA.print();
        configB.print();
        configC.print();
        configD.print();
    }
}

方式一:導入普通類

導入普通類非常簡單吹缔,只需在@Import傳入類的Class對象即可商佑。

@Configuration
@Import(ConfigA.class)
public class Config {
   ...
}

方式二:導入@Configuration配置類

導入配置類與導入普通類一樣,在@Import注解中傳入目標類的Class對象厢塘。

@Configuration
@Import({ConfigA.class,
        ConfigB.class})
public class Config {
    ...
}

方式三:導入ImportSelector的實現(xiàn)類

ImportSelector接口的全類名為org.springframework.context.annotationImportSelector茶没。其主要作用的是收集需要導入的配置類,并根據(jù)條件來確定哪些配置類需要被導入晚碾。

該接口的實現(xiàn)類同時還可以實現(xiàn)以下任意一個Aware接口抓半,它們各自的方法將在selectImport之前被調(diào)用:

另外,該接口實現(xiàn)類可以提供一個或多個具有以下形參類型的構造函數(shù):

如果你想要推遲導入配置類格嘁,直到處理完所有的@Configuration笛求。那么你可以使用DeferredImportSelector

下面我們創(chuàng)建一個實現(xiàn)該接口的類 MyImportSelector。

看下面示例:

selectImports方法中糕簿,入?yún)?code>AnnotationMetadata為主配置類 Config 的注解元數(shù)據(jù)探入。
返回值為目標配置類 ConfigC 的全類名,這里是一個數(shù)組懂诗,表示可以導入多個配置類新症。

public class MyImportSelector implements ImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"io.github.gozhuyinglong.importanalysis.config.ConfigC"};
    }
}

在配置類 Config 中導入 MyImportSelector 類。

@Configuration
@Import({ConfigA.class,
        ConfigB.class,
        MyImportSelector.class})
public class Config {
    ...
}

方式四:導入ImportBeanDefinitionRegistrar的實現(xiàn)類

該接口的目的是有選擇性的進行注冊Bean响禽,注冊時可以指定Bean名稱,并且可以定義bean的級別荚醒。其他功能與ImportSelector類似芋类,這里就不再贅述。

下面來看示例:

創(chuàng)建一個實現(xiàn) ImportBeanDefinitionRegistrar 接口的類 MyImportBeanDefinitionRegistrar界阁,并在 registerBeanDefinitions方法中注冊 configD 類侯繁。
入?yún)?AnnotationMetadata為主配置類 Config 的注解元數(shù)據(jù);BeanDefinitionRegistry參數(shù)可以注冊Bean的定義信息泡躯。

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registry.registerBeanDefinition("configD", new RootBeanDefinition(ConfigD.class));
    }
}

在配置類 Config 中導入 MyImportBeanDefinitionRegistrar 類贮竟。

@Configuration
@Import({ConfigA.class,
        ConfigB.class,
        MyImportSelector.class,
        MyImportBeanDefinitionRegistrar.class})
public class Config {
    ...
}

測試結果

創(chuàng)建一個測試類 ImportDemo,看上面四個配置類是否被注入较剃。

public class ImportDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
        Config config = ctx.getBean(Config.class);
        config.print();
    }
}

輸出結果:

輸出:ConfigA.class
輸出:ConfigB.class
輸出:ConfigC.class
輸出:ConfigD.class

通過輸出結果可以看出咕别,這四個配置類被導入到主配置類中,并成功注入写穴。

源碼解析

ConfigurationClassParser類為Spring的工具類惰拱,主要用于分析配置類,并產(chǎn)生一組ConfigurationClass對象(因為一個配置類中可能會通過@Import注解來導入其它配置類)啊送。也就是說偿短,其會遞歸的處理所有配置類欣孤。

doProcessConfigurationClass

其中的doProcessConfigurationClass方法是處理所有配置類的過程,其按下面步驟來處理:

  1. @Component注解
  2. @PropertySource注解
  3. @ComponentScan注解
  4. @Import注解
  5. @ImportResource注解
  6. @Bean注解
  7. 配置類的接口上的默認方法
  8. 配置類的超類
@Nullable
protected final SourceClass doProcessConfigurationClass(
    ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
    throws IOException {

    if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
        // 1.首先會遞歸的處理所有成員類昔逗,即@Component注解
        processMemberClasses(configClass, sourceClass, filter);
    }

    // 2.處理所有@PropertySource注解
    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注解
    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) {
            // 配置類的注解為@ComponentScan-> 立即執(zhí)行掃描
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
            // 檢查掃描過的BeanDefinition集合降传,看看是否有其他配置類,如果需要勾怒,遞歸解析
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) {
                    bdCand = holder.getBeanDefinition();
                }
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }

    // 4.處理所有@Import注解
    processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

    // 5.處理所有@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注解的方法
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // 7.處理配置類的接口上的默認方法
    processInterfaces(configClass, sourceClass);

    // 8.處理配置類的超類(如果有的話)
    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();
        }
    }

    // 處理完成
    return null;
}

processImports

processImports方法為處理@Import注解導入的配置類婆排,是我們本篇的主題。

該方法會循環(huán)處理每一個由@Import導入的類:

  1. ImportSelector類的處理
  2. ImportBeanDefinitionRegistrar類的處理
  3. 其它類統(tǒng)一按照@Configuration類來處理控硼,所以加不加@Configuration注解都能被導入
/**
 * 處理配置類上的@Import注解引入的類
 *
 * @param configClass 配置類泽论,這里是Config類
 * @param currentSourceClass 當前資源類
 * @param importCandidates 該配置類中的@Import注解導入的候選類列表
 * @param exclusionFilter 排除過濾器
 * @param checkForCircularImports 是否循環(huán)檢查導入
 */
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
                            Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
                            boolean checkForCircularImports) {
    // 如果該@Import注解導入的列表為空,直接返回
    if (importCandidates.isEmpty()) {
        return;
    }
    // 循環(huán)檢查導入
    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
        this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    }
    else {
        this.importStack.push(configClass);
        try {
            // 循環(huán)處理每一個由@Import導入的類
            for (SourceClass candidate : importCandidates) {
                if (candidate.isAssignable(ImportSelector.class)) {
                    // 1. ImportSelector類的處理
                    Class<?> candidateClass = candidate.loadClass();
                    ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
                                                                                   this.environment, this.resourceLoader, this.registry);
                    Predicate<String> selectorFilter = selector.getExclusionFilter();
                    if (selectorFilter != null) {
                        exclusionFilter = exclusionFilter.or(selectorFilter);
                    }
                    if (selector instanceof DeferredImportSelector) {
                        // 1.1 若是DeferredImportSelector接口的實現(xiàn)卡乾,則延時處理
                        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                    }
                    else {
                        // 1.2 在這里調(diào)用我們的ImportSelector實現(xiàn)類的selectImports方法
                        String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                        Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                        // 1.3 遞歸處理每一個selectImports方法返回的配置類
                        processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
                    }
                }
                else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                     // 2. ImportBeanDefinitionRegistrar類的處理
                    Class<?> candidateClass = candidate.loadClass();
                    ImportBeanDefinitionRegistrar registrar =
                        ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                                                             this.environment, this.resourceLoader, this.registry);
                    configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                }
                else {
                    // 3. 其它類統(tǒng)一按照@Configuration類來處理翼悴,所以加不加@Configuration注解都能被導入
                    this.importStack.registerImport(
                        currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                    processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
                }
            }
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                "Failed to process import candidates for configuration class [" +
                configClass.getMetadata().getClassName() + "]", ex);
        }
        finally {
            this.importStack.pop();
        }
    }
}

總結

通過上面源碼的解析可以看出,@Import注解主要作用是導入外部類的幔妨,并且普通類也會按照@Configuration類來處理鹦赎。這大大方便了我們將自己的組件類注入到容器中了(無需修改自己的組件類)。

源碼分享

完整代碼請訪問我的Github误堡,若對你有幫助古话,歡迎給個?,感謝~~??????

https://github.com/gozhuyinglong/blog-demos/tree/main/spring-source-analysis/src/main/java/io/github/gozhuyinglong/importanalysis

推薦閱讀

關于作者

項目 內(nèi)容
公眾號 碼農(nóng)StayUp(ID:AcmenStayUp)
主頁 https://gozhuyinglong.github.io
CSDN https://blog.csdn.net/gozhuyinglong
掘進 https://juejin.cn/user/1239904849494856
Github https://github.com/gozhuyinglong
Gitee https://gitee.com/gozhuyinglong
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末陪踩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悉抵,更是在濱河造成了極大的恐慌肩狂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姥饰,死亡現(xiàn)場離奇詭異傻谁,居然都是意外死亡,警方通過查閱死者的電腦和手機列粪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門审磁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岂座,你說我怎么就攤上這事态蒂。” “怎么了掺逼?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵吃媒,是天一觀的道長。 經(jīng)常有香客問我,道長赘那,這世上最難降的妖魔是什么刑桑? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮募舟,結果婚禮上祠斧,老公的妹妹穿的比我還像新娘。我一直安慰自己拱礁,他們只是感情好琢锋,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呢灶,像睡著了一般吴超。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸯乃,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天鲸阻,我揣著相機與錄音,去河邊找鬼缨睡。 笑死鸟悴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的奖年。 我是一名探鬼主播细诸,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陋守!你這毒婦竟也來了震贵?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤水评,失蹤者是張志新(化名)和其女友劉穎屏歹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體之碗,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年季希,在試婚紗的時候發(fā)現(xiàn)自己被綠了褪那。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡式塌,死狀恐怖博敬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情峰尝,我是刑警寧澤偏窝,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響祭往,放射性物質(zhì)發(fā)生泄漏伦意。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一硼补、第九天 我趴在偏房一處隱蔽的房頂上張望驮肉。 院中可真熱鬧,春花似錦已骇、人聲如沸离钝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卵渴。三九已至,卻和暖如春鲤竹,著一層夾襖步出監(jiān)牢的瞬間浪读,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工宛裕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瑟啃,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓揩尸,卻偏偏與公主長得像蛹屿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子岩榆,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容