背景介绍

之前我们在一文带你掌握spring框架中import注解的作用中讲到了@import注解的作用,并且提到@import注解中可以放一个实现了org.springframework.context.annotation.ImportSelector接口的类用来实现条件配置。当时受限于篇幅,我没有讲到ImportSelector接口其实还有个子接口叫org.springframework.context.annotation.DeferredImportSelector。和原来的ImportSelector接口相比,它的特色在于可以实现延迟导入配置的功能,springboot的自动化配置就用到了这个接口,今天我们就来好好讲讲这个接口,这对大家理解springboot是如何实现自动化配置的会有很大的帮助。
在这里插入图片描述

DeferredImportSelector接口的作用

延迟导入配置类

前面我们讲过DeferredImportSelector接口最大的作用是延迟导入配置,这句话是什么意思呢?意思就是:当普通的配置类都解析完成后再去解析这个配置。关于它和ImportSelector接口之间的区别,我举一个例子大家就明白了:

下面是实现了ImportSelector接口的类:

public class ImmediateConfigImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{ImmediateConfig.class.getName()};
    }

}


public class ImmediateConfig {

    @Bean
    public Apple apple() {
        return new Apple();
    }

    public static class Apple {
    }
}

下面是实现了DeferredImportSelector接口的类:

public class DeferredConfigImportSelector implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{DeferredConfig.class.getName()} ;
    }
}

public class DeferredConfig {

    @Bean
    public Fish fish() {
        return new Fish();
    }

    public static class Fish {
    }
}

接下来我在一个主配置类中通过@import注解导入这两个类:

package com.lizemin.importcase.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author lzm
 * @date 2025/10/29 19:47
 * @description
 */
@Configuration
@Import({ImmediateConfigImportSelector.class, DeferredConfigImportSelector.class})
public class SecurityConfig {

    @Bean
    public Security security() {
        return new Security();
    }

    public static class Security {

    }
}

接下来我想问大家一个问题:spring框架创建这些bean的顺序是怎样的?是先创建Apple实例,还是先创建Fish的实例?请大家在此处认真思考两分钟。

验证方法很简单,我们在创建对象的地方打个断点,然后以调试模式重启项目,程序先经过哪个断点就表示哪个对象先创建。这里我直接公布答案,先创建Apple对象,然后创建Security对象,最后创建Fish对象,为什么会这样呢?

因为创建Fish对象的DeferredConfig配置类是通过DeferredImportSelector接口导入的,而这个接口的作用就是延迟导入配置类,凡是通过DeferredImportSelector接口导入的配置类都是到最后才被spring框架解析 ,也就是说凡是通过这种方式注册的bean都是放在最后面才创建。这就导致了DeferredConfig配置类最后被spring解析,里面的bean也是最后才创建。

配置类分组处理

DeferredImportSelector接口最主要的功能还是延迟导入配置类,除此之外,它还支持对DeferredImportSelector分组处理,将同一分组中的多个DeferredImportSelector导入的配置类收集起来进行批处理, 用于实现更复杂的功能,比如对某个模块的配置类进行排序。我们之前的案例中并没有为DeferredImportSelector指定分组,这时候它们会使用默认的分组。一般来说,我们也很少会用到分组功能,只有对于特别复杂的需求才会用DeferredImportSelector的分组。

下面我来通过一个案例讲解DeferredImportSelector接口的分组功能。

创建分组的实现

对DeferredImportSelector分组需要实现DeferredImportSelector.Group接口,下面是我的分组实现:

package com.lizemin.importcase.group;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.ArrayList;
import java.util.Comparator;

/**
 * @author lzm
 * @date 2025/10/30 18:56
 * @description 数据源分组,用来处理所有和数据库相关的配置类
 */
public class DataSourceGroup implements DeferredImportSelector.Group {

    private static final Logger log = LoggerFactory.getLogger(DataSourceGroup.class);
    
    private ArrayList<Entry> entries = new ArrayList<>();

    @Override
    public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
        String[] configClasses = selector.selectImports(metadata);
        for (String clazz : configClasses) {
            entries.add(new Entry(metadata, clazz));
        }
    }

    /**
     * 该方法会返回此分组下所有DeferredImportSelector最终要导入的配置类
     *
     * @return 该分组所有DeferredImportSelector最终要导入的配置类
     */
    @Override
    public Iterable<Entry> selectImports() {
        for (Entry entry : entries) {
            log.info("datasourceGroup中最终导入了配置类:{}", entry.getImportClassName());
        }
        // 这里还可以对entries进行排序,比如按照类名排序
        entries.sort(Comparator.comparing(Entry::getImportClassName));
        return entries;
    }

}

为DeferredImportSelector指定分组

下面我写了两个DeferredImportSelector实现类,将它们都划分到前面创建的DataSourceGroup分组。

JdbcTemplateConfigSelector:

package com.lizemin.importcase.group;

import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author lzm
 * @date 2025/10/30 19:00
 * @description
 */
public class JdbcTemplateConfigSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{JdbcTemplateConfig.class.getName()};
    }

    /**
     * 标记该selector的分组,该接口不是必须要实现的,如果
     * <p>
     * 如果不实现该接口,默认是null,该selector都会被放到一个默认的分组中
     *
     * @return 分组class
     */
    @Override
    public Class<? extends Group> getImportGroup() {
        return DataSourceGroup.class;
    }
}

MySqlConfigSelector:

package com.lizemin.importcase.group;

import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author lzm
 * @date 2025/10/30 19:11
 * @description 需要注意的是:该类不需要加@configuration,否则会影响DeferredImportSelector的正常功能
 */
public class MySqlConfigSelector implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{MySqlConfig.class.getName()};
    }

    @Override
    public Class<? extends Group> getImportGroup() {
        return DataSourceGroup.class;
    }
}

在上面两个DeferredImportSelector实现类中,我们通过getImportGroup方法指定了它们的分组是DataSourceGroup。

接下来我们通过一个主配置类导入这两个DeferredImportSelector实现类:

package com.lizemin.importcase.group;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author lzm
 * @date 2025/10/30 19:18
 * @description
 */
@Configuration
@Import({JdbcTemplateConfigSelector.class, MySqlConfigSelector.class})
public class MainConfig {
}

重启项目,可以看到如下日志:
在这里插入图片描述
这说明我们的DataSourceGroup分组中已经成功获取了它分组下所有DeferredImportSelector导入的配置类信息。到这里,DeferredImportSelector的分组功能我们也讲完了。

有些小伙伴可能会好奇DeferredImportSelector接口的延迟导入和配置类分组是如何实现的,下面我将带大家通过分析源码来了解springboot是怎么做到这一切的。

DeferredImportSelector接口延迟导入的原理

DeferredImportSelector接口之所以能够实现延迟导入,答案在这个方法中:
org.springframework.context.annotation.ConfigurationClassParser#parse。

public void parse(Set<BeanDefinitionHolder> configCandidates) {
  // 首先在这个for循环中处理所有普通的配置类,也就是加了@Configuration,或者@Component的类
	for (BeanDefinitionHolder holder : configCandidates) {
		BeanDefinition bd = holder.getBeanDefinition();
		try {
			ConfigurationClass configClass;
			if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) {
			  // 解析配置类
				configClass = parse(annotatedBeanDef, holder.getBeanName());
			}
			else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) {
				configClass = parse(abstractBeanDef.getBeanClass(), holder.getBeanName());
			}
			else {
				configClass = parse(bd.getBeanClassName(), holder.getBeanName());
			}
		}
	}
    // 开始处理延迟导入的配置类
	this.deferredImportSelectorHandler.process();
}

在这段代码中,首先在for循环中处理所有的普通配置类,也就是加了@Configuration,或者@Component的类。for循环结束后,普通配置类的解析也就完成了,接下来才处理DeferredImportSelector接口导入的配置类。

最后我们看下解析完成后的类是怎样的:
在这里插入图片描述
通过idea的值表达式查看解析后的配置类的顺序,可以看到我们写的DeferredConfig配置类放在了最后,这就是延迟导入的原理。
在这里插入图片描述
接下来我们看下springboot框架是如何处理DeferredImportSelector接口的,我们回去看下this.deferredImportSelectorHandler.process();这行代码的具体实现:

void process() {
	List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
	this.deferredImportSelectors = null;
	try {
		if (deferredImports != null) {
			DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
			deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
			// 重点在下面两行代码,将deferredImports转成DeferredImportSelectorGroupingHandler对象
			deferredImports.forEach(handler::register);
			// 通过DeferredImportSelectorGroupingHandler对象来处理DeferredImportSelector导入的配置类
			handler.processGroupImports();
		}
	}
	finally {
		this.deferredImportSelectors = new ArrayList<>();
	}
}

在上面的代码中,重点在这两行代码:

// 类型转换
deferredImports.forEach(handler::register);

// 处理导入的配置类
handler.processGroupImports();
  1. 第一行代码的作用是:将deferredImports转成DeferredImportSelectorGroupingHandler对象,类似于我们平时开发项目的toVO,toPO的方法,其实就是一个类型转换,没啥好说的。
  2. 第二行代码的作用是:用前面转换得到的对象来处理导入的配置类

接下来我们具体看看handler::register的实现:

void register(DeferredImportSelectorHolder deferredImport) {
	Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
	DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
		  // 如果分组为空,则使用DeferredImportSelector实现类对象作为key
			(group != null ? group : deferredImport),
			key -> new DeferredImportSelectorGrouping(createGroup(group))); // 这里的createGroup(group)比较重要,我们重点看下,了解它是如何构建group的
	grouping.add(deferredImport);
	this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
			deferredImport.getConfigurationClass());
}

上面代码中的createGroup(group)比较重要,我们重点看下:

private Group createGroup(@Nullable Class<? extends Group> type) {
  // 当type为空,也就是分组为空时,使用默认的分组对象
	Class<? extends Group> effectiveType = (type != null ? type : DefaultDeferredImportSelectorGroup.class);
	return ParserStrategyUtils.instantiateClass(effectiveType, Group.class,
			ConfigurationClassParser.this.environment,
			ConfigurationClassParser.this.resourceLoader,
			ConfigurationClassParser.this.registry);
}

重点是默认分组对象的代码,也就是Class<? extends Group> effectiveType = (type != null ? type : DefaultDeferredImportSelectorGroup.class); ,这行代码中包含了分组为空时的处理,具体逻辑是当分组为空时,创建默认的分组对象,也就是DefaultDeferredImportSelectorGroup。我们看下这个类的实现:

private static class DefaultDeferredImportSelectorGroup implements Group {

	private final List<Entry> imports = new ArrayList<>();

	@Override
	public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
	// 这里是在调用父接口的selectImports方法,相当于做了一次类型转换,类似于适配器模式。
		for (String importClassName : selector.selectImports(metadata)) {
			this.imports.add(new Entry(metadata, importClassName));
		}
	}

	@Override
	public Iterable<Entry> selectImports() {
		return this.imports;
	}
}

接下来我们看下具体的处理方法:

void processGroupImports() {
	for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
		Predicate<String> filter = grouping.getCandidateFilter();
		// 调用group接口的两个方法获取到最终的配置类信息
		grouping.getImports().forEach(entry -> {
			ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
			// 这里开始解析DeferredImportSelector.Group的selectImports方法返回的配置类信息
			processImports(configurationClass, asSourceClass(configurationClass, filter),
					Collections.singleton(asSourceClass(entry.getImportClassName(), filter)),
					filter, false);
	}
}

上面的代码中,重点代码如下:

  1. grouping.getImports(): 调用group接口的两个方法获取到最终的配置类信息
  2. processImports(configurationClass, asSourceClass(configurationClass, filter),: 解析前面得到的配置类,然后将配置类的BeanDefinition注册到spring容器中。

我们重点看下 grouping.getImports()的具体实现,可以看到跟上面spring的使用方式完全一样。

Iterable<Group.Entry> getImports() {
	for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
	    //先调用process方法,解析出配置类信息
		this.group.process(deferredImport.getConfigurationClass().getMetadata(),
				deferredImport.getImportSelector());
	}
	// 再调用selectImports方法捣鼓一番后返回
	return this.group.selectImports();
}

这个方法做了两件事:

  1. 先调用process方法,解析出配置类信息
  2. 再调用selectImports方法捣鼓一番后返回

我们看下这个DeferredImportSelector.Group接口的定义:

interface Group {

	/**
	 * @metadata: @import所在类的注解元信息
	 * 
	 * @DeferredImportSelector : 该接口的实现类对象
	 */
	void process(AnnotationMetadata metadata, DeferredImportSelector selector);

	/**
	 * 返回这个分组中应该被导入的配置类信息
	 */
	Iterable<Entry> selectImports();

}

所以使用DeferredImportSelector是可以使用分组的,通过分组可以实现更加负责的功能,比如springboot的自动化配置需要对配置类的解析顺序进行排序,就需要用到这里的分组功能。

我们前面为了让大家快速理解DeferredImportSelector接口延迟导入配置类的作用,就没搞那么复杂,在没有指定分组的时候,springboot会自动为我们创建一个默认的分组对象,前面我们也在源码分析中讲过,默认的分组实现类是org.springframework.context.annotation.ConfigurationClassParser.DefaultDeferredImportSelectorGroup

到这里,关于DeferredImportSelector接口的作用我们就讲完了。大家有兴趣的可以按照这个思路去分析springboot的自动化配置是如何实现的,比如springboot是如何对自动化配置类进行排序,这个就当是留给大家的家庭作业吧,哈哈哈。

本文完整示例代码的地址

本文完整示例代码的地址
在这里插入图片描述

最后的总结

这篇文章主要带大家学习了spring框架中DeferredImportSelector接口的作用,它最主要的作用还是延迟导入配置类 ,另外就是它还支持分组,实现更加复杂的配置类导入功能,关于分组不是必须的,对于简单的延迟导入配置不需要使用它的分组功能。

最后我们还通过源码分析来帮助大家理解spring框架是如何解析这个接口,从而实现延迟导入配置类的功能的。

觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐