@Import注解:导入配置类的四种方式&源码解析

微信搜索:码农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的实现类。从4.2版本开始,还可以引用常规组件类(普通类),该功能类似于AnnotationConfigApplicationContext.register方法。
  • 该注解可以在类中声明,也可以在元注解中声明。
  • 如果需要导入XML或其他非@Configuration定义的资源,可以使用@ImportResource注释。

导入配置类的四种方式

源码注释写得很清楚,该注解有四种导入方式:

  1. 普通类
  2. @Configuration配置类
  3. ImportSelector的实现类
  4. ImportBeanDefinitionRegistrar的实现类

下面我们逐个来介绍~

准备工作

创建四个配置类: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");
    }

}

再创建一个主配置类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的实现类

ImportSelector接口的全类名为org.springframework.context.annotationImportSelector。其主要作用的是收集需要导入的配置类,并根据条件来确定哪些配置类需要被导入。

该接口的实现类同时还可以实现以下任意一个Aware接口,它们各自的方法将在selectImport之前被调用:

另外,该接口实现类可以提供一个或多个具有以下形参类型的构造函数:

如果你想要推迟导入配置类,直到处理完所有的@Configuration。那么你可以使用DeferredImportSelector

下面我们创建一个实现该接口的类 MyImportSelector。

看下面示例:

selectImports方法中,入参AnnotationMetadata为主配置类 Config 的注解元数据。
返回值为目标配置类 ConfigC 的全类名,这里是一个数组,表示可以导入多个配置类。

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的实现类

该接口的目的是有选择性的进行注册Bean,注册时可以指定Bean名称,并且可以定义bean的级别。其他功能与ImportSelector类似,这里就不再赘述。

下面来看示例:

创建一个实现 ImportBeanDefinitionRegistrar 接口的类 MyImportBeanDefinitionRegistrar,并在 registerBeanDefinitions方法中注册 configD 类。
入参 AnnotationMetadata为主配置类 Config 的注解元数据;BeanDefinitionRegistry参数可以注册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 {
    ...
}

测试结果

创建一个测试类 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的工具类,主要用于分析配置类,并产生一组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-> 立即执行扫描
            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注解导入的配置类,是我们本篇的主题。

该方法会循环处理每一个由@Import导入的类:

  1. ImportSelector类的处理
  2. ImportBeanDefinitionRegistrar类的处理
  3. 其它类统一按照@Configuration类来处理,所以加不加@Configuration注解都能被导入
/**
 * 处理配置类上的@Import注解引入的类
 *
 * @param configClass 配置类,这里是Config类
 * @param currentSourceClass 当前资源类
 * @param importCandidates 该配置类中的@Import注解导入的候选类列表
 * @param exclusionFilter 排除过滤器
 * @param checkForCircularImports 是否循环检查导入
 */
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
                            Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
                            boolean checkForCircularImports) {
    // 如果该@Import注解导入的列表为空,直接返回
    if (importCandidates.isEmpty()) {
        return;
    }
    // 循环检查导入
    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
        this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    }
    else {
        this.importStack.push(configClass);
        try {
            // 循环处理每一个由@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接口的实现,则延时处理
                        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                    }
                    else {
                        // 1.2 在这里调用我们的ImportSelector实现类的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. 其它类统一按照@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

推荐阅读

关于作者

项目 内容
公众号 码农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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容