Spring Boot 4.1.1 深度源码解析-第 13 章 · Bean 定义加载:`BeanDefinitionLoader`、组件扫描与 Bean 命名

16 阅读21分钟

Spring Boot 4.1.1 深度源码解析-第 13 章 · Bean 定义加载:BeanDefinitionLoader、组件扫描与 Bean 命名

本卷目标:把 Spring Boot 4.1.1 的 IoC 与自动配置引擎拆开讲透。 前三卷(01–12 章)解决了**"配置从哪来、怎么变成对象";从本章起解决"容器里的 Bean 定义从哪来、谁决定它们生效"**。


0. 本章要回答的五个问题

前三卷花了大量篇幅讲 Environment、ConfigData、Binder、@ConfigurationProperties。读者此时应该已经能把 application.yaml 里的一行配置追踪到 Java 字段上。但有一个环节始终没有被正面拆开:

容器里那几百上千个 BeanDefinition 究竟是谁、在什么时候、以什么顺序放进去的?

本章把这个问题拆成五个子问题,逐个给出源码级答案:

#问题关键答案(先剧透)
1run() 里 Bean 定义在哪个时间点被加载?prepareContext() 的末尾、contextLoaded() 之前;AOT 模式下整段被跳过
2"源"(source)到底是什么类型?有几种?四种运行时类型 + 一条字符串降级链;Class / Resource / Package / CharSequence
3BeanDefinitionLoader 里的 ClassPathBeanDefinitionScanner 和 @ComponentScan 是同一个吗?不是。这是全章最重要的一个结论
4Bean 名字是怎么来的?四层优先级如何?注解 value → nameGenerator → SpringApplication.setBeanNameGenerator → 容器单例
5扫描为什么不会把自动配置类重复注册?AutoConfigurationExcludeFilter + ImportCandidates,两个不同的"名单"比对

读完本章,读者应该能回答一个面试高频却几乎没人答对的问题:

"@SpringBootApplication 标注的类,为什么既是配置类又是扫描起点?它自己被注册了几次?"

(答案在本章 §14 与 §20.7。)


1. 全景:Bean 定义的五条来源通道

先把视野拉高。一个 Spring Boot 应用启动完成后,DefaultListableBeanFactory.beanDefinitionMap 里通常有 200–800 个 BeanDefinition(取决于引入的 starter 数量)。它们来自五条互不相同的通道:

┌────────────────────────────────────────────────────────────────────────┐
│                     BeanDefinition 的五条来源通道                        │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  ① primarySources          SpringApplication.run(App.class, args)     │
│     └─ Class 对象            → BeanDefinitionLoader → annotatedReader  │
│                                                                        │
│  ② spring.main.sources     Set<String>,绑定自 Environment             │
│     └─ 字符串                → BeanDefinitionLoader → 四步降级探测      │
│                                                                        │
│  ③ @ComponentScan          由主类的 @SpringBootApplication 触发         │
│     └─ 包路径                → ConfigurationClassParser 的 scanner     │
│                              (注意:不是 BeanDefinitionLoader 那个!) │
│                                                                        │
│  ④ @Import / @EnableXxx    由配置类上的注解触发                         │
│     └─ ImportSelector / ImportBeanDefinitionRegistrar                  │
│                              → 自动配置就走这条(第 14 章)             │
│                                                                        │
│  ⑤ 编程式注册               BeanFactoryPostProcessor /                  │
│                              BeanDefinitionRegistryPostProcessor        │
│                              → MyBatis、Spring Data、Feign 等           │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

本章覆盖 ①②③ 与 ⑤ 的一半(⑤ 中 Boot 自带的两个 BeanFactoryPostProcessor)。④ 是第 14 章(自动配置引擎)的主战场。

1.1 为什么这个顺序重要

通道 ①②③ 都在 refresh() 之前发生,即它们注册的是"原始 Bean 定义";而 ④ 中的自动配置类是在 ConfigurationClassPostProcessor(一个 BeanDefinitionRegistryPostProcessor)里被解析的,它们注册的是"派生 Bean 定义"。

这个区分在排查问题时极其有用:

  • 如果你 --debug 看到的 ConditionEvaluationReport 里没有某个类 → 它走的是 ④,被条件注解挡了(第 14 章)
  • 如果你 getBeanDefinitionNames() 里连自动配置类本身都没有 → 看 ④ 的 import 列表(第 14 章)
  • 如果你的 @Component 类压根没进容器 → 看 ③(本章 §12)
  • 如果你 new SpringApplication(App.class) 里 App.class 自己没注册上 → 看 ①(本章 §5)

1.2 一个很容易被忽略的时间点

通道 ①②③ 的注册发生在 prepareContext() 里,而此时 refresh() 还没调用。这意味着:

  • @ConditionalOnXxx 此时尚未求值(它们在 ConfigurationClassPostProcessor 里才被处理)
  • BeanFactoryPostProcessor 尚未执行
  • 所以 BeanDefinitionLoader 往里塞的定义都是"无条件生效"的

唯一的例外是 AnnotatedBeanDefinitionReader 内部持有的 ConditionEvaluator——它会在 register() 时求值 @Conditional。这一点在 §5.5 展开。


2. run() 时间线上的确切位置

先把 SpringApplication.run() 与 Bean 加载相关的部分精确摘出来(源码行号来自本仓库 SpringApplication.java):

// SpringApplication.java: 316-330(run 方法主体,节选)
public ConfigurableApplicationContext run(String... args) {
    Startup startup = Startup.create();
    if (this.properties.isRegisterShutdownHook()) {
        SpringApplicationShutdownHook.registerShutdownHook(...);
    }
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    this.applicationStartup = startup.getApplicationStartup(this.applicationStartup);
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass, startup);
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        startup.started(startup.getTimeTakenToStarted(), this.properties.getLogStartupInfo());
        listeners.started(context, startup.getTimeTakenToStarted());
        callRunners(context, applicationArguments);
    }
    ...
    listeners.ready(context, startup.getTimeTakenToReady());
    ...
}

prepareContext 是本章的主场(源码行 380–419):

private void prepareContext(DefaultBootstrapContext bootstrapContext,
        ConfigurableApplicationContext context, ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments,
        @Nullable Banner printedBanner) {
    context.setEnvironment(environment);                                    // ①
    postProcessApplicationContext(context);                                 // ②
    addAotGeneratedInitializerIfNecessary(this.initializers);               // ③
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); // ④
    if (beanFactory instanceof AbstractAutowireCapableBeanFactory acbf) {
        acbf.setAllowCircularReferences(this.properties.isAllowCircularReferences());
        if (beanFactory instanceof DefaultListableBeanFactory dlbf) {
            dlbf.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
        }
    }
    applyInitializers(context);                                             // ⑤
    listeners.contextPrepared(context);                                     // ⑥
    bootstrapContext.close(context);                                        // ⑦
    if (this.properties.isLogStartupInfo()) {
        logStartupInfo(context);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);  // ⑧
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);               // ⑨
    }
    if (this.properties.isLazyInitialization()) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());  // ⑩
    }
    if (this.properties.isKeepAlive()) {
        context.addApplicationListener(new KeepAlive());                                 // ⑪
    }
    context.addBeanFactoryPostProcessor(
            new PropertySourceOrderingBeanFactoryPostProcessor(context));                // ⑫
    if (!AotDetector.useGeneratedArtifacts()) {                                          // ⑬
        // Load the sources
        Set<Object> sources = getAllSources();
        Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
        load(context, sources.toArray(new Object[0]));                                   // ⑭
    }
    listeners.contextLoaded(context);                                                    // ⑮
}

十五个步骤里与本章直接相关的是 ②⑦⑧⑨⑩⑫⑬⑭,其余各步的意义:

步动作由哪一章负责
①Environment 装配到 context第 06 章
②注册 BeanNameGenerator 单例、ResourceLoader、ConversionService本章 §13.5
③AOT 时插入 __ApplicationContextInitializer第 27 章 / 本章 §17
④循环引用、定义覆盖开关本章 §15.1
⑤ApplicationContextInitializer 执行第 12 章
⑥ApplicationContextInitializedEvent第 12 章
⑦Bootstrap 上下文关闭第 12 章
⑧springApplicationArguments 单例本章 §15.2
⑨springBootBanner 单例本章 §15.3
⑩懒初始化后处理器本章 §16
⑪KeepAlive 监听器(CRaC)第 12 章
⑫defaultProperties 移到末尾本章 §15.5
⑬AOT 判定本章 §17
⑭Bean 定义加载(本章核心)本章 §3–§11
⑮ApplicationPreparedEvent第 12 章

2.1 为什么 load() 排在 applyInitializers() 之后

这是个容易被误解的顺序。直觉上"先装 Bean 定义,再让 Initializer 改",但 Boot 是反的:

applyInitializers(context);   // ⑤ 先
...
load(context, sources);       // ⑭ 后

原因是:ApplicationContextInitializer 的契约是"在 refresh 之前对 context 做任何定制",它的很多实现会直接操作 BeanDefinitionRegistry(比如注册额外的 BeanFactoryPostProcessor、加 PropertySource、甚至注册 Bean 定义)。如果 load() 先跑,Initializer 就无法"在源被加载之前"插入自己的逻辑,比如替换 BeanNameGenerator 或加一个 TypeExcludeFilter。

对使用者的影响:你在 ApplicationContextInitializer 里注册的 Bean 定义,会排在 primary source 之前(因为 beanDefinitionNames 是注册顺序的数组)。这会影响 @Autowired 按类型注入时的候选排序。


3. getAllSources():两个集合的合并

load() 的输入来自 getAllSources()(源码行 1187–1196):

public Set<Object> getAllSources() {
    Set<Object> allSources = new LinkedHashSet<>();
    if (!CollectionUtils.isEmpty(this.primarySources)) {
        allSources.addAll(this.primarySources);
    }
    if (!CollectionUtils.isEmpty(this.properties.getSources())) {
        allSources.addAll(this.properties.getSources());
    }
    return Collections.unmodifiableSet(allSources);
}

两个来源:

3.1 primarySources:LinkedHashSet<Class<?>>

// 字段(行 211)
private final Set<Class<?>> primarySources;

// 构造(行 277)
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  • 元素类型是 Class<?>,所以走 BeanDefinitionLoader.load(Class) 分支(§5)
  • final 字段,但集合本身可变 → addPrimarySources() 可以在 run() 之前追加
  • LinkedHashSet 保证声明顺序(这一点在 §20.2 会用到)

3.2 properties.getSources():Set<String>

// ApplicationProperties.java
private Set<String> sources = new LinkedHashSet<>();

void setSources(Set<String> sources) {
    this.sources = new LinkedHashSet<>(sources);   // 防御性拷贝
}
  • 元素类型是 String,走 BeanDefinitionLoader.load(CharSequence) 分支(§8)

  • 来自 spring.main.sources 属性绑定(Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties)),行 552)

  • 元数据描述(本仓库 additional-spring-configuration-metadata.json:609):

    spring.main.sources
      type: java.util.Set<java.lang.String>
      sourceType: org.springframework.boot.SpringApplication
      description: Sources (class names, package names, or XML resource locations)
                   to include in the ApplicationContext.
    

    注意类级 Javadoc 写的是 "a CSV list"——Binder 对 Set<String> 确实支持逗号分隔(第 08 章讲过的 aggregate binding),所以 spring.main.sources=A,B 与 YAML 列表等价。

  • setSources 做了 new LinkedHashSet<>(...) 拷贝 → 从 getSources() 拿到的可变 Set 上直接 add 是无效的(它返回的是内部引用,但 setSources 会替换掉)。这是一个小陷阱:

    app.getSources().add("com.example.Foo");   // ❌ 无效?
    

    实际有效——因为 getSources() 返回的就是内部引用(return this.sources;)。但 setSources 会替换而不是合并,所以:

    app.setSources(Set.of("A"));
    app.getSources().add("B");        // ✅ 此时 effective,内部集合变成 {A, B}
    app.setSources(Set.of("C"));      // 现在变成 {C},B 丢了
    

    推荐做法:要么只用 getSources().add(),要么只用 setSources(),别混用。

3.3 合并顺序:primarySources 在前

LinkedHashSet 的迭代顺序 = 插入顺序,所以:

[App.class (primary), ...其他 primary, "com.example.Extra" (字符串源), ...]

这个顺序就是 Bean 定义的注册顺序(除了 springApplicationArguments 等预置单例)。它会间接影响:

  1. @Autowired 按类型注入多个候选时的顺序(虽然有 @Order/@Primary 会覆盖)
  2. getBeanNamesForType() 的返回顺序
  3. 同名覆盖时谁保留(allowBeanDefinitionOverriding=false 时后者抛异常)

3.4 Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined")

这个断言在什么情况下会触发?

Set<Object> sources = getAllSources();
Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");

必须同时满足:

  • primarySources 为空(即 new SpringApplication() 无参构造 + 没调 addPrimarySources())
  • spring.main.sources 为空

典型触发场景:用 SpringApplicationBuilder 但忘了指定源,或者写了 new SpringApplication().run(args)。

注意 SpringApplication() 无参构造是存在的(本仓库行 250 附近有 public SpringApplication(Class<?>... primarySources),可变参数 → 零参合法)。所以这个断言是唯一的保护。

3.5 mainApplicationClass 是怎么推断出来的

SpringApplication 构造里有一行:

this.mainApplicationClass = deduceMainApplicationClass();   // 行 283

实现(行 286–296):

private @Nullable Class<?> deduceMainApplicationClass() {
    return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
        .walk(this::findMainClass)
        .orElse(null);
}

private Optional<Class<?>> findMainClass(Stream<StackFrame> stack) {
    return stack.filter((frame) -> Objects.equals(frame.getMethodName(), "main"))
        .findFirst()
        .map(StackWalker.StackFrame::getDeclaringClass);
}

关键细节:

  1. StackWalker 是 JDK 9+ 的 API,比老的 new RuntimeException().getStackTrace() 快很多(惰性遍历 + 不填充完整栈)
  2. Option.RETAIN_CLASS_REFERENCE 是必须的——默认 StackWalker 不保留 Class 对象,只有类名字符串
  3. .filter(methodName == "main").findFirst() → 从栈顶往下找第一个名为 main 的方法,它的声明类就是主类

这个方法名匹配的粗糙性会带来两个后果:

  • 如果你在 main 方法里调用了一个也叫 main 的辅助方法,且它声明在别的类 → 推断出的主类会错
  • 如果你的 main 方法在一个抽象基类里,子类调用 → 推断出的是基类

mainApplicationClass 用在哪?

  • getApplicationLog()(日志类别)
  • AOT 的 {主类名}__ApplicationContextInitializer(§17.2)
  • ApplicationInfoPropertySource(第 06 章)
  • SpringApplicationRunListeners.starting(bootstrapContext, this.mainApplicationClass, startup)

4. BeanDefinitionLoader:一个门面,四个 reader

4.1 先注意一个"奇怪"的可见性组合

// BeanDefinitionLoader.java: 62
class BeanDefinitionLoader {          // ← 包级私有!
    ...
}

// SpringApplication.java: 747
protected BeanDefinitionLoader createBeanDefinitionLoader(
        BeanDefinitionRegistry registry, Object[] sources) {   // ← protected + 包级私有返回类型
    return new BeanDefinitionLoader(registry, sources);
}

protected 方法返回包级私有类型——这是一个真实存在的设计瑕疵(自 Boot 1.0 延续至今)。后果:

  • org.springframework.boot 包外的子类无法覆写 createBeanDefinitionLoader() 并引用其返回类型(编译期"类型不可访问")
  • 包内子类(比如测试里的)可以

Boot 团队保留这个设计的原因大概率是:BeanDefinitionLoader 的 API 尚未稳定,不想暴露成公共契约。这本身就是一个信号——不要在你的代码里依赖 BeanDefinitionLoader。

顺带一提,BeanDefinitionLoader 里还嵌了一个 protected interface GroovyBeanDefinitionSource,在一个包级私有类里嵌套 protected 接口——同样是不可访问的。这是历史遗留。

4.2 四个 reader 字段

private final Object[] sources;                              // 待加载的源
private final AnnotatedBeanDefinitionReader annotatedReader;  // 注解类
private final AbstractBeanDefinitionReader xmlReader;         // XML
private final @Nullable BeanDefinitionReader groovyReader;    // Groovy(可能为 null)
private final ClassPathBeanDefinitionScanner scanner;         // 包扫描
private @Nullable ResourceLoader resourceLoader;

构造(行 84–93):

BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
    Assert.notNull(registry, "'registry' must not be null");
    Assert.notEmpty(sources, "'sources' must not be empty");
    this.sources = sources;
    this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
    this.xmlReader = new XmlBeanDefinitionReader(registry);
    this.groovyReader = isGroovyPresent() ? new GroovyBeanDefinitionReader(registry) : null;
    this.scanner = new ClassPathBeanDefinitionScanner(registry);
    this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}

逐行解读:

  1. Assert.notEmpty(sources, ...) —— 这里又一道空检查(上一道在 prepareContext 的 Assert.state)。双重保护是因为 BeanDefinitionLoader 是包级私有类,可能被别的调用路径使用。

  2. new AnnotatedBeanDefinitionReader(registry) —— 这一行有副作用,见 §5.5。

  3. new XmlBeanDefinitionReader(registry) —— 注意类型是 AbstractBeanDefinitionReader(Framework 里 XmlBeanDefinitionReader extends AbstractBeanDefinitionReader)。声明成抽象父类是为了后面 setBeanNameGenerator / setResourceLoader / setEnvironment 能统一调用。

  4. isGroovyPresent():

    private boolean isGroovyPresent() {
        return ClassUtils.isPresent("groovy.lang.MetaClass", null);
    }
    

    Boot 4 仍然支持 Groovy(本仓库 core/spring-boot/build.gradle:40 有 optional("org.apache.groovy:groovy"))。所以 groovyReader 在引入 Groovy 时非 null。

  5. this.scanner.addExcludeFilter(new ClassExcludeFilter(sources)) —— 这是全文第一个关键设计,见 §9。

4.3 三个 setter 的"不对称"

void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
    this.annotatedReader.setBeanNameGenerator(beanNameGenerator);   // ✅
    this.scanner.setBeanNameGenerator(beanNameGenerator);           // ✅
    this.xmlReader.setBeanNameGenerator(beanNameGenerator);         // ✅
}

void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
    this.scanner.setResourceLoader(resourceLoader);                 // ✅
    this.xmlReader.setResourceLoader(resourceLoader);               // ✅
    // annotatedReader:❌ 没有
}

void setEnvironment(ConfigurableEnvironment environment) {
    this.annotatedReader.setEnvironment(environment);               // ✅
    this.scanner.setEnvironment(environment);                       // ✅
    this.xmlReader.setEnvironment(environment);                     // ✅
}

为什么 setResourceLoader 不设 annotatedReader?

因为 AnnotatedBeanDefinitionReader 不需要 ResourceLoader——它只接收 Class<?> 对象,从不解析资源位置。而 XmlBeanDefinitionReader(要加载 XML 文件)和 ClassPathBeanDefinitionScanner(要扫 **/*.class)都必须有 ResourceLoader。

为什么 setBeanNameGenerator 要设 xmlReader?

XML 里的 <bean> 可以不写 id,此时需要一个生成器来补名字。XmlBeanDefinitionReader 从 AbstractBeanDefinitionReader 继承了 beanNameGenerator 字段。

4.4 SpringApplication.load() 的三个条件设置

// SpringApplication.java: 683-698
protected void load(ApplicationContext context, Object[] sources) {
    if (logger.isDebugEnabled()) {
        logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
    }
    BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
    if (this.beanNameGenerator != null) {
        loader.setBeanNameGenerator(this.beanNameGenerator);
    }
    if (this.resourceLoader != null) {
        loader.setResourceLoader(this.resourceLoader);
    }
    if (this.environment != null) {
        loader.setEnvironment(this.environment);
    }
    loader.load();
}

这里藏着本章第二个反直觉结论(§8.1 详述):this.environment 在正常流程下是 null,所以 loader.setEnvironment() 根本不会被调用。

证据:全仓库搜索 this.environment 的赋值点,只有第 1133 行的 setEnvironment() 方法:

public void setEnvironment(@Nullable ConfigurableEnvironment environment) {
    this.isCustomEnvironment = true;      // ← 注意这个副作用
    this.environment = environment;
}

而 run() 从不调用 setEnvironment()。所以:

  • 正常流程:this.environment == null → loader.setEnvironment() 跳过 → BeanDefinitionLoader.scanner 使用自己新建的 StandardEnvironment
  • 自定义流程(用户显式 app.setEnvironment(env)):isCustomEnvironment = true,并且会跳过 EnvironmentConverter.convertEnvironmentIfNecessary(行 362–365)

4.5 getBeanDefinitionRegistry():两种形态的降级

private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {
    if (context instanceof BeanDefinitionRegistry registry) {
        return registry;                                              // ① 直接就是
    }
    if (context instanceof AbstractApplicationContext abstractApplicationContext) {
        return (BeanDefinitionRegistry) abstractApplicationContext.getBeanFactory();  // ② 拿 BeanFactory
    }
    throw new IllegalStateException("Could not locate BeanDefinitionRegistry");
}
  • 路径 ①:GenericApplicationContext(含 AnnotationConfigApplicationContext、ServletWebServerApplicationContext 的父类)本身实现 BeanDefinitionRegistry,走这条
  • 路径 ②:某些古老的/自定义 context 不实现该接口,但其 BeanFactory(DefaultListableBeanFactory)实现
  • 路径 ③:抛异常 —— 注意这是一个强转,如果 BeanFactory 也不是 BeanDefinitionRegistry 会 ClassCastException 而不是 IllegalStateException

5. Class 源:load(Class<?>) 与 isEligible 三重排除

5.1 分派入口

private void load(Object source) {
    Assert.notNull(source, "'source' must not be null");
    if (source instanceof Class<?> type) {
        load(type);
        return;
    }
    if (source instanceof Resource resource) {
        load(resource);
        return;
    }
    if (source instanceof Package pack) {
        load(pack);
        return;
    }
    if (source instanceof CharSequence sequence) {
        load(sequence);
        return;
    }
    throw new IllegalArgumentException("Invalid source type " + source.getClass());
}

用的是 instanceof 模式匹配,不是 getClass() 精确匹配。这意味着:

  • String → 同时是 CharSequence,走字符串分支 ✅
  • StringBuilder → 也是 CharSequence,同样合法 ✅
  • Class 的子类型(比如 Class<?> 的匿名子类,虽然罕见)→ 走 Class 分支

顺序很重要:Class 第一,CharSequence 最后。因为 Class 和 CharSequence 是互斥的(没有类同时是两者),所以顺序在这里无实际影响;但 Resource 与 CharSequence 也互斥。真正需要小心的是自定义 source 类型:File 既不是 Resource 也不是 CharSequence → 直接 IllegalArgumentException。

5.2 load(Class<?>) 的两段逻辑

private void load(Class<?> source) {
    if (this.groovyReader != null && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
        // Any GroovyLoaders added in beans{} DSL can contribute beans here
        GroovyBeanDefinitionSource loader =
                BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
        ((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
    }
    if (isEligible(source)) {
        this.annotatedReader.register(source);
    }
}

注意 Groovy 段与 isEligible 段不是 if/else——两者都可能执行。Groovy DSL 类自己也可以同时被注册成 Bean。

5.3 isEligible:三重排除

private boolean isEligible(Class<?> type) {
    return !(type.isAnonymousClass() || isGroovyClosure(type) || hasNoConstructors(type));
}

private boolean isGroovyClosure(Class<?> type) {
    return GROOVY_CLOSURE_PATTERN.matcher(type.getName()).matches();
}

private boolean hasNoConstructors(Class<?> type) {
    Constructor<?>[] constructors = type.getDeclaredConstructors();
    return ObjectUtils.isEmpty(constructors);
}

其中:

private static final Pattern GROOVY_CLOSURE_PATTERN = Pattern.compile(".*\\$_.*closure.*");

逐条解释:

排除 1:匿名类 type.isAnonymousClass()

匿名类(如 new Runnable() {...})的类名是 com.example.Foo$1。注册匿名类毫无意义——它无法被再次实例化(没有可引用的类型名),且通常是闭包/回调。

排除 2:Groovy closure .*\$_.*closure.*

Groovy 编译闭包时生成的类名形如 com.example.MyScript$_run_closure1。正则要求:

  • .* 任意前缀
  • \$_ 字面量 $_
  • .*closure.* 后面含 "closure"

注意用的是 matches() 而不是 find()——必须整串匹配。所以 com.example.Foo$_Bar 不含 "closure",不匹配(正确)。

为什么单独排除它,匿名类排除已经覆盖了? Groovy closure 类是具名的内部类,不是匿名类(isAnonymousClass() 返回 false)。所以必须单独处理。

排除 3:无构造器 hasNoConstructors
Constructor<?>[] constructors = type.getDeclaredConstructors();
return ObjectUtils.isEmpty(constructors);   // 等价于 constructors.length == 0

哪些类型没有构造器?

  1. 接口 —— interface 的 getDeclaredConstructors() 返回长度 0 的数组(接口没有构造器)
  2. 数组类型 —— String[].class.getDeclaredConstructors() 也是空
  3. 基本类型 —— int.class.getDeclaredConstructors() 空
  4. void —— 空

最关键的是接口:如果你把接口当作 primary source 传进来(比如 SpringApplication.run(MyService.class, args) 里 MyService 是接口),它会被静默跳过(既不注册也不报错)。这是一个静默失败的坑。

小心:getDeclaredConstructors() 只返回显式声明的构造器。一个没有任何显式构造器的普通类会有一个编译器生成的默认构造器,所以 getDeclaredConstructors() 返回长度 1 的数组 → 不会被排除。✅ 逻辑正确。

另一个边界:ObjectUtils.isEmpty(Object[]) 对 null 也返回 true。但 getDeclaredConstructors() 永不返回 null,所以这只是防御性编码。

5.4 Groovy 通道的细节

if (this.groovyReader != null && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
    GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
    ((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
}

GroovyBeanDefinitionSource 是:

@FunctionalInterface
protected interface GroovyBeanDefinitionSource {
    Closure<?> getBeans();
}
  • @FunctionalInterface → Groovy 的闭包可以自动适配
  • BeanUtils.instantiateClass(source, ...) → 要求该类有一个可访问的无参构造器
  • 整个条件是 groovyReader != null &&,即只有 Groovy 在 classpath 上才检查

一个巧妙之处:isAssignableFrom 检查的是"这个类是否实现了 GroovyBeanDefinitionSource 接口"。Groovy 编译器对于实现了单方法接口的闭包类,会自动生成实现类——这是 Groovy 的 SAM 转换。所以 .groovy 脚本编译出来的 DSL 类能被识别。

5.5 annotatedReader.register() 的隐藏副作用

this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);

这一行看起来只是构造一个对象,但在 Spring Framework 里,AnnotatedBeanDefinitionReader 的构造器会调用:

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

这会把整套注解处理基础设施注册进容器(Spring Framework 7.0.9 行为,本仓库不含其源码,基于接口契约与长期稳定的行为描述):

后处理器 Bean 名类型作用
org.springframework.context.annotation.internalConfigurationAnnotationProcessorConfigurationClassPostProcessor解析 @Configuration/@ComponentScan/@Import/@Bean
org.springframework.context.annotation.internalAutowiredAnnotationProcessorAutowiredAnnotationBeanPostProcessor@Autowired/@Value/@Inject
org.springframework.context.annotation.internalCommonAnnotationProcessorCommonAnnotationBeanPostProcessor@PostConstruct/@PreDestroy/@Resource
org.springframework.context.annotation.internalPersistenceAnnotationProcessorPersistenceAnnotationBeanPostProcessorJPA 注解(若 classpath 有)
org.springframework.context.event.internalEventListenerProcessorEventListenerMethodProcessor@EventListener
org.springframework.context.event.internalEventListenerFactoryDefaultEventListenerFactory@EventListener 适配器

也就是说:new SpringApplication(App.class).run() 里,BeanDefinitionLoader 的构造函数顺手把 Spring 的注解体系全装上了。这不是 Boot 做的,是 Framework 的 AnnotatedBeanDefinitionReader 做的。

这解释了一个常见疑惑:为什么 AnnotationConfigApplicationContext 不需要手动注册 ConfigurationClassPostProcessor?因为构造器里的 AnnotatedBeanDefinitionReader 已经注册了。

同时它也解释了 AOT 模式的差异(§17):AOT 模式下 BeanDefinitionLoader 根本不创建,但这些 processor 仍然需要注册——由 AOT 生成的 __ApplicationContextInitializer 负责。

5.6 register() 时 @Conditional 会被求值

AnnotatedBeanDefinitionReader.doRegisterBean() 里有一步:

if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
    return;
}

conditionEvaluator 是 AnnotatedBeanDefinitionReader 自己的字段(new ConditionEvaluator(registry, environment, null))。

这意味着:primary source 上的 @ConditionalXxx 会在 prepareContext 阶段被求值——比自动配置的条件求值早得多(后者在 refresh() 的 invokeBeanFactoryPostProcessors 阶段)。

关键差异:

primary source 上的 @Conditional自动配置类上的 @Conditional
求值时机prepareContext()(refresh 前)refresh() → ConfigurationClassPostProcessor
求值者AnnotatedBeanDefinitionReader.conditionEvaluatorConfigurationClassParser 的 ConditionEvaluator
可见的 Bean 定义只有已注册的(很少)几乎所有(含其他自动配置)
典型用途几乎没人用自动配置的主力

实践含义:在 primary source 上写 @ConditionalOnMissingBean 是危险的——此时容器里几乎什么都没有,条件几乎必然通过,然后当真正的 Bean 后面注册进来时,你就有两个实例了。


6. Resource 源:.groovy 与 XML 的二分

private void load(Resource source) {
    String filename = source.getFilename();
    Assert.state(filename != null, "Source has no filename");
    if (filename.endsWith(".groovy")) {
        if (this.groovyReader == null) {
            throw new BeanDefinitionStoreException(
                    "Cannot load Groovy beans without Groovy on classpath");
        }
        this.groovyReader.loadBeanDefinitions(source);
    }
    else {
        this.xmlReader.loadBeanDefinitions(source);
    }
}

三个细节:

  1. 判定依据是文件名后缀,不是内容。所以 foo.groovy.txt 会被当成 XML(不以 .groovy 结尾)。
  2. Assert.state(filename != null, "Source has no filename") —— 某些 Resource 实现(如 ByteArrayResource、InputStreamResource)的 getFilename() 返回 null。如果你自定义 Resource 作为 source 且不实现 getFilename(),直接 IllegalStateException。
  3. Groovy 缺失时的异常是 BeanDefinitionStoreException(Spring 的标准异常),而不是 IllegalStateException——因为这是"Bean 定义加载失败",语义更准确。

Resource 作为 source 从哪来?

只有一条路径:load(CharSequence) → loadAsResources() → findResources() 返回 Resource[]。用户直接传 Resource 对象到 SpringApplication 也是可以的(getAllSources() 返回 Set<Object>),但 SpringApplication.setSources() 只接受 Set<String>,所以通过配置属性只能给字符串。


7. Package 源:一行代码的扫描

private void load(Package source) {
    this.scanner.scan(source.getName());
}

看似简单,但这里有几个值得深挖的点。

7.1 Package 对象作为 source 的来源

java.lang.Package 的实例只有一个来源:findPackage(CharSequence)(§8.4)。用户无法直接传——getAllSources() 是 Set<Object>,理论上可以 app.getAllSources() 拿到……但它是 unmodifiableSet,且 primarySources 是 Set<Class<?>>。所以实际只能从字符串源降级而来。

7.2 scanner.scan() vs @ComponentScan

这是本章的核心洞察,先在这里埋一个标记,§12 完整展开:

this.scanner = new ClassPathBeanDefinitionScanner(registry);   // ← Scanner A

这个 scanner 只在两处被使用:

  • load(Package) → scanner.scan(pkg)
  • load(CharSequence) → scanner.getEnvironment()(仅用于占位符解析)

而 @SpringBootApplication 上的 @ComponentScan 是由 ConfigurationClassPostProcessor 在 refresh() 期间用另一个新建的 ClassPathBeanDefinitionScanner 处理的。

所以:

  • Scanner A 的 addExcludeFilter(ClassExcludeFilter) 对 @ComponentScan 无效
  • Scanner A 的 setBeanNameGenerator() 对 @ComponentScan 无效(但 SpringApplication.setBeanNameGenerator() 还通过 CONFIGURATION_BEAN_NAME_GENERATOR 单例影响了它,见 §13.5)

7.3 scan() 的返回值被忽略

ClassPathBeanDefinitionScanner.scan() 返回 int(注册的 Bean 定义数量)。BeanDefinitionLoader 丢弃了这个返回值。所以即使一个 Bean 都没扫到,也不会报错——静默成功。

这与 Boot 一贯的"静默优于报错"风格一致,但也意味着"包名写错"不会有任何提示。


8. CharSequence 源:四步降级探测(本章最绕的一段)

这是 BeanDefinitionLoader 里逻辑最复杂的分支,也是最容易出诡异行为的地方。

private void load(CharSequence source) {
    String resolvedSource = this.scanner.getEnvironment().resolvePlaceholders(source.toString());
    // Attempt as a Class
    try {
        load(ClassUtils.forName(resolvedSource, null));
        return;
    }
    catch (IllegalArgumentException | ClassNotFoundException ex) {
        // swallow exception and continue
    }
    // Attempt as Resources
    if (loadAsResources(resolvedSource)) {
        return;
    }
    // Attempt as package
    Package packageResource = findPackage(resolvedSource);
    if (packageResource != null) {
        load(packageResource);
        return;
    }
    throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'");
}

四步降级:

输入字符串 "X"
   ↓
① 占位符解析(scanner.getEnvironment())
   ↓
② 尝试当类名:ClassUtils.forName(X, null)
   ├─ 成功 → load(Class) → 结束
   └─ 失败(IAE / CNFE)→ 吞异常,继续
   ↓
③ 尝试当资源位置:findResources(X) → 逐个 isLoadCandidate → load(Resource)
   ├─ 至少一个候选 → 结束
   └─ 没找到 → 继续
   ↓
④ 尝试当包名:findPackage(X)
   ├─ 找到 → load(Package) → 结束
   └─ 没找到 → 继续
   ↓
⑤ throw new IllegalArgumentException("Invalid source 'X'")

8.1 【反直觉】占位符用的是"另一个 Environment"

String resolvedSource = this.scanner.getEnvironment().resolvePlaceholders(source.toString());

this.scanner 是 BeanDefinitionLoader 自己 new 的 ClassPathBeanDefinitionScanner。如 §4.4 所述,正常流程下 loader.setEnvironment() 不会被调用(SpringApplication.this.environment == null)。

那么 scanner.getEnvironment() 是什么?

ClassPathScanningCandidateComponentProvider 有一个字段(Spring Framework 行为):

private Environment environment = new StandardEnvironment();

一个全新的 StandardEnvironment,只有两个 PropertySource:

  • systemProperties
  • systemEnvironment

因此:

spring:
  main:
    sources: ${my.extra.source}     # ❌ 解析不到!application.yaml 不在这个 Environment 里

但这样可以:

java -Dmy.extra.source=com.example.Foo -jar app.jar
# 或
export MY_EXTRA_SOURCE=com.example.Foo

这是一个真实存在的行为差异,且 Boot 文档没有明确说明。

验证方法(本章 §21.2 给出完整步骤):在 application.yaml 里写 my.extra.source=com.example.Foo,同时 spring.main.sources=${my.extra.source},会发现 ${my.extra.source} 原样保留,然后 ClassUtils.forName("${my.extra.source}") 失败,接着尝试资源、包,最后抛 IllegalArgumentException: Invalid source '${my.extra.source}'。

诚实标注:我在此给出的是基于源码调用链的推演。scanner.getEnvironment() 返回默认 StandardEnvironment 这一点依赖 Spring Framework 7.0.9 的字段初始化行为,本仓库不含 Framework 源码,我无法逐行核实。但 SpringApplication.this.environment == null(正常流程)这一点是本仓库源码可确证的(全文件只有 setEnvironment() 一处赋值)。两者叠加,结论成立的概率极高。§21.2 给了自测方法。

8.2 尝试类名:ClassUtils.forName(resolvedSource, null)

load(ClassUtils.forName(resolvedSource, null));

第二个参数是 null → ClassUtils.forName 会用 ClassUtils.getDefaultClassLoader()(线程上下文 ClassLoader,回退到 ClassUtils 自己的加载器)。

捕获的异常只有两个:IllegalArgumentException | ClassNotFoundException。

ClassUtils.forName 可能抛出的其他异常不会被捕获:

  • LinkageError(类加载失败,如 NoClassDefFoundError)
  • ExceptionInInitializerError(静态初始化失败)

这些会直接冒泡,导致启动失败。这是合理的——"类存在但加载不了"是严重错误,不该被降级当包名处理。

8.3 尝试资源:loadAsResources

private boolean loadAsResources(String resolvedSource) {
    boolean foundCandidate = false;
    Resource[] resources = findResources(resolvedSource);
    for (Resource resource : resources) {
        if (isLoadCandidate(resource)) {
            foundCandidate = true;
            load(resource);
        }
    }
    return foundCandidate;
}

注意 foundCandidate 的语义:只要有至少一个候选就返回 true(并已加载它们)。这不是"全部成功"——其中某个 load() 抛异常会直接中断整个循环。

findResources:

private Resource[] findResources(String source) {
    ResourceLoader loader = (this.resourceLoader != null) ? this.resourceLoader
            : new PathMatchingResourcePatternResolver();
    try {
        if (loader instanceof ResourcePatternResolver resolver) {
            return resolver.getResources(source);
        }
        return new Resource[] { loader.getResource(source) };
    }
    catch (IOException ex) {
        throw new IllegalStateException("Error reading source '" + source + "'");
    }
}

关键:PathMatchingResourcePatternResolver.getResources() 会做通配符匹配。所以:

spring.main.sources=classpath*:com/example/**/*.xml

理论上可以加载一批 XML。但注意 §11 的陷阱。

另一个反直觉点:PathMatchingResourcePatternResolver.getResource()(非 pattern)对于不存在的位置也返回一个 Resource 对象(exists() 为 false),而不是返回空数组。所以 findResources("不存在的东西") 很可能返回长度 1 的数组,然后由 isLoadCandidate() 过滤掉(因为 !resource.exists())→ foundCandidate = false → 继续降级到包名。

8.4 尝试包名:findPackage 的"抓一个类"技巧

private @Nullable Package findPackage(CharSequence source) {
    Package pkg = getClass().getClassLoader().getDefinedPackage(source.toString());
    if (pkg != null) {
        return pkg;
    }
    try {
        // Attempt to find a class in this package
        ResourcePatternResolver resolver =
                new PathMatchingResourcePatternResolver(getClass().getClassLoader());
        Resource[] resources = resolver.getResources(
                ClassUtils.convertClassNameToResourcePath(source.toString()) + "/*.class");
        for (Resource resource : resources) {
            String filename = resource.getFilename();
            Assert.state(filename != null, "No filename available");
            String className = StringUtils.stripFilenameExtension(filename);
            load(Class.forName(source + "." + className));
            break;
        }
    }
    catch (Exception ex) {
        // swallow exception and continue
    }
    return getClass().getClassLoader().getDefinedPackage(source.toString());
}

这段代码的逻辑相当不寻常,逐段拆解:

第一段:直接查已定义的包
Package pkg = getClass().getClassLoader().getDefinedPackage(source.toString());
if (pkg != null) {
    return pkg;
}

ClassLoader.getDefinedPackage(name) 是 JDK 9+ 的方法,返回该加载器已定义的具名包(需要 Package 已被创建,通常是有类被加载过)。

重要:对于未命名包(default package)或者尚未加载任何类的包,返回 null。

第二段:"抓一个类"来强制创建 Package 对象

如果第一段失败,就扫描该包下的 *.class,加载第一个类,然后 break:

Resource[] resources = resolver.getResources(
        ClassUtils.convertClassNameToResourcePath(source) + "/*.class");
for (Resource resource : resources) {
    String className = StringUtils.stripFilenameExtension(resource.getFilename());
    load(Class.forName(source + "." + className));   // ← 注意:这里直接 load 了!
    break;
}

几个非常值得注意的点:

  1. 它调用的是 load(Class) 而不是 Scanner——即这个"探针类"被直接注册成了 Bean 定义!这让代码看起来很奇怪,但目的是:加载一个类会让 JVM 创建该包的 Package 对象,使得第三段能成功。

  2. break 只加载第一个类——目的是最小化副作用,但"第一个"是未定义的(取决于文件系统/类加载器的枚举顺序)。这意味着行为在不同环境可能不同。

  3. 副作用:那个被 load() 的类会被注册成 Bean 定义(如果 isEligible)。也就是说,当你写 spring.main.sources=com.example 时,除了扫描整个包,还会额外把某个不确定的类单独注册一次——不过由于 ClassExcludeFilter(§9)和 checkCandidate 的"同源不冲突"判定(§12.7),不会真的重复。

  4. 子包不会被扫描:/*.class 只匹配直接子级,com/example/**/*.class 才会递归。所以 spring.main.sources=com.example 不会扫 com.example.sub。

第三段:再查一次
return getClass().getClassLoader().getDefinedPackage(source.toString());

如果第二段成功加载了类,这次就能拿到 Package;否则返回 null → 外层抛 IllegalArgumentException。

一个隐藏的 bug 倾向

getClass().getClassLoader() —— 用的是 BeanDefinitionLoader 自己的类加载器,而不是应用上下文的。在 Boot 的可执行 jar(LaunchedClassLoader,第 26 章)场景下,BeanDefinitionLoader 由 LaunchedClassLoader 加载,所以 getClass().getClassLoader() 就是 LaunchedClassLoader。这在绝大多数情况下是对的,但对于自定义 ClassLoader 场景(如 OSGi、插件系统)可能不匹配 resourceLoader 的 ClassLoader。

8.5 完整决策表

输入字符串① 占位符② 类?③ 资源?④ 包?结果
com.example.App无变化✅——注册为注解 Bean
com.example无变化❌ CNFE❌ 不存在✅扫描该包(+ 额外注册一个探针类)
classpath:beans.xml无变化❌(含 : 不是合法类名)✅—XML 加载
beans.groovy无变化❌✅—Groovy 加载
${foo}(未定义)原样保留❌❌❌IllegalArgumentException
com.example.NotExists无变化❌❌❌(无 .class)IllegalArgumentException
com.example.App(接口)无变化✅ 加载成功——isEligible=false → 静默跳过

最后一行是一个高危静默失败:把接口名写进 spring.main.sources,什么都不发生,也不报错。


9. ClassExcludeFilter:防止源被重复注册

private static class ClassExcludeFilter extends AbstractTypeHierarchyTraversingFilter {

    private final Set<String> classNames = new HashSet<>();

    ClassExcludeFilter(Object... sources) {
        super(false, false);
        for (Object source : sources) {
            if (source instanceof Class<?> classSource) {
                this.classNames.add(classSource.getName());
            }
        }
    }

    @Override
    protected boolean matchClassName(String className) {
        return this.classNames.contains(className);
    }
}

9.1 它解决什么问题

场景:

new SpringApplication(App.class)
    .setSources(Set.of("com.example"))     // 扫描 com.example 包
    .run(args);

App.class 也在 com.example 包下。扫描时会再次发现 App.class,如果没有排除过滤器,就会第二次注册。虽然 checkCandidate 的 "same source" 判定(§12.7)通常会兜住,但在某些路径下(如 Class.forName 探针 vs 扫描,两者的 source 不同)会导致 ConflictingBeanDefinitionException。

ClassExcludeFilter 明确把所有 Class 类型的 source 排除出扫描结果。

9.2 super(false, false) 的含义

AbstractTypeHierarchyTraversingFilter(considerInherited, considerInterfaces):

  • considerInherited=false → 不遍历父类链
  • considerInterfaces=false → 不遍历接口

两个都 false → 只做精确的类名匹配。这是性能最优的选择(不需要读继承层次),也符合语义("就是这个类,不管它的子类")。

9.3 为什么用 HashSet<String> 而不是 Set<Class<?>>

因为 matchClassName(String className) 收到的是字符串(来自 ASM 读到的字节码),此时类还没被加载。用字符串比较避免了强制类加载——这正是 ASM-based 扫描的价值(不用加载类就能知道类名)。

9.4 一个不对称:只收集 Class 类型的 source

for (Object source : sources) {
    if (source instanceof Class<?> classSource) {     // ← 只有 Class
        this.classNames.add(classSource.getName());
    }
}

Resource / Package / String 类型的 source 不会被加入排除集。这意味着:

  • 如果字符串源降级成了 Package 并扫描,扫描结果里不会排除其他 Class source(本来就没有)
  • 如果字符串源降级成了 Class(通过 findPackage 的探针),那个类会被注册一次,且如果它同时被包扫描命中……它被 Class.forName 加载时不在 classNames 里(因为是运行时才决定的),所以可能被注册两次。不过 checkCandidate 会兜住(§12.7)。

10. isLoadCandidate:ClassPathResource 的目录伪装陷阱

private boolean isLoadCandidate(@Nullable Resource resource) {
    if (resource == null || !resource.exists()) {
        return false;
    }
    if (resource instanceof ClassPathResource classPathResource) {
        // A simple package without a '.' may accidentally get loaded as an XML
        // document if we're not careful. The result of getInputStream() will be
        // a file list of the package content. We double-check here that it's not
        // actually a package.
        String path = classPathResource.getPath();
        if (path.indexOf('.') == -1) {
            try {
                return getClass().getClassLoader().getDefinedPackage(path) == null;
            }
            catch (Exception ex) {
                // Ignore
            }
        }
    }
    return true;
}

这段注释讲了一个真实的坑,值得完整还原:

10.1 坑的成因

考虑 spring.main.sources=com.example(一个不含点号的简单包名,比如 sources=demo):

  1. ② 尝试类名:ClassUtils.forName("demo") → ClassNotFoundException
  2. ③ 尝试资源:findResources("demo") → PathMatchingResourcePatternResolver.getResources("demo")
    • 这不是 pattern(不含 *),走 getResourceLoader().getResource("demo") → 返回 ClassPathResource("demo")
    • ClassPathResource.exists():对于 ClassLoader.getResource("demo"),如果 classpath 下真的有 demo/ 目录,JVM 会把目录当作资源返回!exists() 返回 true
    • 如果调用 getInputStream(),会打开这个"目录资源",在 jar 场景下会返回一个目录条目的列表(这就是注释里说的 "a file list of the package content")
  3. 于是 load(Resource) 被调用 → filename = "demo" → 不以 .groovy 结尾 → xmlReader.loadBeanDefinitions(resource) → 把目录列表当 XML 解析 → 必然失败,且错误信息极其难懂

10.2 防御逻辑

if (path.indexOf('.') == -1) {     // 只处理"不含点号"的路径
    try {
        return getClass().getClassLoader().getDefinedPackage(path) == null;
    }
    catch (Exception ex) {
        // Ignore
    }
}
  • 只对不含 . 的路径做检查 —— 因为含 . 的路径(com/example/App.class、beans.xml)不可能是包名(包名带点说明是多级包,而多级包路径含 /)
    • 等等,实际上这里判断的是 path.indexOf('.'),对于 com.example 这种字符串源,path 就是 "com.example",含点 → 不检查
    • 对于简单包名 demo,path = "demo",不含点 → 检查
  • getDefinedPackage(path) == null 才返回 true:如果 "demo" 是一个已定义的包 → 返回 false(不当资源加载)→ 降级到 ④ 包名处理 ✅
  • 如果不是包 → 返回 true → 当资源加载

注意 catch (Exception ex) { // Ignore }:如果 getDefinedPackage 抛异常(比如安全管理器),就落到 return true(方法末尾),即"当资源加载"。这是保守的默认。

10.3 这个陷阱的现实意义

为什么今天很少遇到?

因为 getDefinedPackage() 只对已被类加载器定义过的包返回非 null。在现代 Boot 应用里:

  • 如果 demo 包下有类被加载过(几乎必然),getDefinedPackage("demo") 非 null → 防御生效
  • 如果从没加载过 → 返回 null → 仍然会掉进坑

所以这是一个不完美的防御,但覆盖了绝大多数情况。

给我们的启示:spring.main.sources 里写包名时,最好加 classpath*: 或写成明确的 *.class 模式,避免触发这条歧义路径。


11. 一个延伸:XML source 在今天还有意义吗

XmlBeanDefinitionReader 在 Boot 4.1.1 里依然被无条件创建(注意:没有任何 isXmlPresent() 检查,与 Groovy 不同)。但实践中几乎没人用 spring.main.sources=classpath:beans.xml:XML Bean 定义在 JavaConfig 出现后就边缘化了,且 AOT / Native 对 XML 支持有限。

它唯一的不可替代用途是遗留系统迁移:一个 2008 年的 Spring 2.5 应用,几百个 XML 文件,用 spring.main.sources=classpath*:legacy/**/*.xml 可以最低成本接入 Boot。


12. 【本章核心】两个 ClassPathBeanDefinitionScanner

这是全章最重要的结论,值得单独成节。

12.1 问题陈述

看这两段代码:

// ① BeanDefinitionLoader 构造
this.scanner = new ClassPathBeanDefinitionScanner(registry);
this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));

// ② @SpringBootApplication 注解
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication { ... }

它们是两个完全独立的 scanner 实例,作用于不同的时机、不同的输入。

12.2 对比表

Scanner A(BeanDefinitionLoader.scanner)Scanner B(@ComponentScan 的 scanner)
创建者BeanDefinitionLoader 构造函数ConfigurationClassParser.doProcessConfigurationClass() → ComponentScanAnnotationParser.parse()
创建时机prepareContext()(refresh 之前)refresh() → invokeBeanFactoryPostProcessors() → ConfigurationClassPostProcessor
输入Package 类型的 source(来自字符串降级)@ComponentScan 的 basePackages / basePackageClasses(默认 = 标注类的包)
exclude 过滤器ClassExcludeFilterTypeExcludeFilter + AutoConfigurationExcludeFilter + @ComponentScan.excludeFilters
BeanNameGeneratorloader.setBeanNameGenerator() 设的(仅当 SpringApplication.setBeanNameGenerator 被调用)优先用 @ComponentScan.nameGenerator,回退到 CONFIGURATION_BEAN_NAME_GENERATOR 单例
是否处理 @Conditional否(Scanner A 不解析配置类)是(扫描结果作为配置类候选,后续被 ConfigurationClassParser 解析)

12.3 为什么这个区分很重要

场景 1:你自定义了 TypeExcludeFilter

@Bean
public static TypeExcludeFilter myFilter() {
    return new MyTypeExcludeFilter();
}

这个 filter 只影响 Scanner B(因为它来自 @SpringBootApplication 的 @ComponentScan)。如果你用 spring.main.sources=com.example 触发了 Scanner A,你的 filter 完全不生效。

场景 2:测试切片的机制

@WebMvcTest、@DataJpaTest 等切片测试之所以能"只加载部分 Bean",靠的就是 TypeExcludeFilter 的委派机制(§12.5)。这些测试用的 @SpringBootConfiguration 主类走的是 Scanner B。

场景 3:spring.main.sources 与 @ComponentScan 同时用

@SpringBootApplication(scanBasePackages = "com.a")
public class App { }

// application.yaml
spring.main.sources: com.b
  • com.a 由 Scanner B 扫(在 refresh 时)
  • com.b 由 Scanner A 扫(在 refresh 前)

两者注册时机不同 → 在 invokeBeanFactoryPostProcessors 阶段,com.b 的定义已经存在,com.a 的还没。BeanDefinitionRegistryPostProcessor 看到的是"半张表"。这通常无害,但如果你写了一个 BeanDefinitionRegistryPostProcessor 依赖扫描完整性,就会出问题。

12.4 @SpringBootApplication 的两个 excludeFilter

@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
12.4.1 AutoConfigurationExcludeFilter
public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoaderAware {

    private volatile @Nullable List<String> autoConfigurations;

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
            throws IOException {
        return isConfiguration(metadataReader) && isAutoConfiguration(metadataReader);
    }

    private boolean isConfiguration(MetadataReader metadataReader) {
        return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
    }

    private boolean isAutoConfiguration(MetadataReader metadataReader) {
        boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata()
            .isAnnotated(AutoConfiguration.class.getName());
        return annotatedWithAutoConfiguration
                || getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
    }

    protected List<String> getAutoConfigurations() {
        List<String> autoConfigurations = this.autoConfigurations;
        if (autoConfigurations == null) {
            ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.beanClassLoader);
            autoConfigurations = importCandidates.getCandidates();
            this.autoConfigurations = autoConfigurations;
        }
        return autoConfigurations;
    }
}

两个判定条件(AND):

  1. isConfiguration —— 被 @Configuration 标注(含元注解)。注意 @AutoConfiguration 本身就被 @Configuration(proxyBeanMethods = false) 标注(见 AutoConfiguration.java:63),所以自动配置类一定满足这条。
  2. isAutoConfiguration —— 两个条件之一(OR):
    • 被 @AutoConfiguration 标注(4.x 的标准写法)
    • 或者类名出现在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里

为什么要两套判定?

这是迁移兼容性设计。Boot 2.7 之前,自动配置类只用 @Configuration 标注并写在 spring.factories 里。2.7 引入 @AutoConfiguration 注解 + *.imports 文件后,为了兼容老的第三方 starter(它们用 @Configuration + spring.factories),Boot 保留了"类名在 import 列表里"这一条判定。

关键细节 —— 用的是 ImportCandidates,不是 SpringFactoriesLoader:

ImportCandidates.load(AutoConfiguration.class, this.beanClassLoader)

ImportCandidates 读的是(本仓库 ImportCandidates.java:42):

private static final String LOCATION = "META-INF/spring/%s.imports";

即 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。

所以 4.x 里,如果一个自动配置类写在 spring.factories 的 EnableAutoConfiguration 键下(老写法),它不会被 AutoConfigurationExcludeFilter 排除! 这会导致:

  • 自动配置类被 @ComponentScan 扫到(如果它恰好在扫描包下)→ 提前注册 → 跳过条件求值 → 条件注解失效

这是一个真实的迁移陷阱。 4.x 已经完全移除了 spring.factories 的 EnableAutoConfiguration 支持(第 04 章讲过),所以理论上不存在;但如果你依赖的第三方库还在用老格式,就会踩到。

12.4.2 volatile 的懒加载
private volatile @Nullable List<String> autoConfigurations;

volatile + 双重检查(虽然这里不是严格的 DCL 加锁,只是"谁先算谁赢")。因为 AutoConfigurationExcludeFilter 实例本身是被 @ComponentScan 反射创建的(每个 scanner 一个新实例?实际上 ComponentScanAnnotationParser 会用 BeanUtils.instantiateClass 创建 exclude filter 实例),多次创建就会有多次 ImportCandidates.load()——这是可观的 I/O(遍历 classpath 上所有 jar 找那个文件)。

volatile 保证多线程下可见性(虽然扫描通常单线程)。

12.5 TypeExcludeFilter:委派机制与"必须实现 equals"的硬约束

public class TypeExcludeFilter implements TypeFilter, BeanFactoryAware {

    private BeanFactory beanFactory;
    private @Nullable Collection<TypeExcludeFilter> delegates;

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
            throws IOException {
        if (this.beanFactory instanceof ListableBeanFactory && getClass() == TypeExcludeFilter.class) {
            for (TypeExcludeFilter delegate : getDelegates()) {
                if (delegate.match(metadataReader, metadataReaderFactory)) {
                    return true;
                }
            }
        }
        return false;
    }

    private Collection<TypeExcludeFilter> getDelegates() {
        Collection<TypeExcludeFilter> delegates = this.delegates;
        if (delegates == null) {
            delegates = ((ListableBeanFactory) this.beanFactory)
                    .getBeansOfType(TypeExcludeFilter.class).values();
            this.delegates = delegates;
        }
        return delegates;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        throw new IllegalStateException("TypeExcludeFilter " + getClass() + " has not implemented equals");
    }

    @Override
    public int hashCode() {
        throw new IllegalStateException("TypeExcludeFilter " + getClass() + " has not implemented hashCode");
    }
}
12.5.1 getClass() == TypeExcludeFilter.class 守卫

这一行的作用:只有"正好是 TypeExcludeFilter 本身"的实例才做委派;如果它是某个子类(用户自定义的 filter),就走"普通 filter"路径(返回 false,让子类自己的 match 决定)。

为什么需要? 因为 getBeansOfType(TypeExcludeFilter.class) 会包含子类。如果没有这个守卫,就会无限递归:filter 实例自己被当作 delegate,调用自己的 match,再触发 getDelegates()……

12.5.2 equals / hashCode 直接抛异常

这是极其罕见的设计——一个类故意让 equals 和 hashCode 抛出 IllegalStateException。

原因(类注释里写了):

They should also implement a valid hashCode and equals methods so that they can be used as part of Spring test's application context caches.

Spring TestContext 框架的上下文缓存 key 由一组"上下文配置属性"组成,其中就包括 @ComponentScan 的 excludeFilters。如果 filter 的 equals/hashCode 不能正确区分不同的 filter 实例,两个本应不同的测试上下文会被误判为相同 → 复用错误的上下文 → 测试结果诡异。

所以 Boot 强制所有 TypeExcludeFilter 子类实现 equals/hashCode,方法是在基类里直接抛异常——如果子类忘了实现,一用就炸,不会静默出错。

这是"用失败代替静默"的设计范式,与第 12 章讲的"诊断工具坏了不能掩盖真正的错误"一脉相承。

12.5.3 delegates 的缓存与生命周期
private @Nullable Collection<TypeExcludeFilter> delegates;
// 只在第一次 match 时填充,之后永久缓存

注意:getBeansOfType 会触发这些 delegate Bean 的实例化。类注释明确警告:

Note that TypeExcludeFilters are initialized very early in the application lifecycle, they should generally not have dependencies on any other beans.

因为扫描发生在 refresh() 的早期(invokeBeanFactoryPostProcessors 里的第一步),此时普通 Bean 还没初始化。如果 TypeExcludeFilter 依赖了别的 Bean,要么拿到未完全初始化的实例,要么直接失败。

实践要求:TypeExcludeFilter 的 @Bean 方法必须 static 且无参数。

12.6 默认 include 过滤器与 candidate 判定(Framework 行为)

诚实标注:以下描述的是 Spring Framework 7.0.9 的 ClassPathScanningCandidateComponentProvider 行为,本仓库不含其源码。我基于长期稳定的公开行为描述,读者如需逐行核实请查看 spring-context 源码。

ClassPathBeanDefinitionScanner 构造时默认 useDefaultFilters=true,registerDefaultFilters() 会加入:

  1. AnnotationTypeFilter(Component.class) —— 覆盖 @Component 及其所有元注解(@Service、@Repository、@Controller、@Configuration、@RestController……)
  2. AnnotationTypeFilter(jakarta.annotation.ManagedBean) —— 仅当 classpath 存在
  3. AnnotationTypeFilter(jakarta.inject.Named) —— 仅当 classpath 存在(JSR-330)

候选判定(isCandidateComponent)要求同时满足:

metadata.isIndependent()  &&  ( metadata.isConcrete()
                                || (metadata.isAbstract() && hasAnnotatedMethods(@Lookup)) )
  • isIndependent() = 顶层类或静态嵌套类(不依赖外部类实例就能构造)
  • isConcrete() = 非接口、非抽象
  • 例外:抽象类如果有 @Lookup 标注的方法也算候选(因为 Spring 会用 CGLIB 生成子类来实现方法注入)

常见踩坑:

@Service
public class Outer {
    @Service                       // ← 非静态内部类
    public class Inner { }         // ❌ isIndependent() == false,永远扫不到
}

改成 static class Inner 才行。

资源模式:默认 **/*.class,可通过 scanner.setResourcePattern() 改。@ComponentScan 没有暴露这个属性,所以 Boot 应用里基本固定。

12.7 checkCandidate:什么算"冲突"

扫描到候选后,ClassPathBeanDefinitionScanner.doScan 会调 checkCandidate(beanName, bd):

// Spring Framework 行为(本仓库无源码)
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
    if (!this.registry.containsBeanDefinition(beanName)) {
        return true;
    }
    BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
    BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
    if (originatingDef != null) {
        existingDef = originatingDef;
    }
    if (isCompatible(beanDefinition, existingDef)) {
        return false;   // 兼容 → 不重复注册,也不报错
    }
    throw new ConflictingBeanDefinitionException(...);
}

protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) {
    return (!(existingDefinition instanceof ScannedGenericBeanDefinition)  // 显式注册覆盖扫描
        || (newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource()))
        || newDefinition.equals(existingDefinition));
}

三种"不算冲突"的情形:

  1. 已存在的定义不是 ScannedGenericBeanDefinition(即它是显式注册的,如 primary source)→ 兼容
  2. 两个定义的 source 相同(同一个 .class 文件被扫了两次)→ 兼容
  3. 两个定义 equals(等价)→ 兼容

实践含义:primary source(AnnotatedGenericBeanDefinition)+ 扫描命中同一个类 → 兼容(情形 1);同一个 .class 被两个 scanner 扫到 → 兼容(情形 2);两个不同 jar 里同名同类的类 → 冲突,抛 ConflictingBeanDefinitionException——这就是常见的 "shading 事故"。

12.8 @Indexed 与组件索引

SpringBootConfiguration 上有 @Indexed:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration { ... }

@Indexed 是 Spring Framework 5.0 引入的编译期组件索引机制:

  1. 引入 spring-context-indexer(annotation processor)
  2. 编译期在 META-INF/spring.components 生成一个索引文件:
    com.example.App=org.springframework.stereotype.Component
    com.example.Foo=org.springframework.stereotype.Component
    
  3. 运行时 ClassPathScanningCandidateComponentProvider 检测到此文件,用它代替 classpath 扫描

收益:大型应用启动时间显著缩短(避免遍历所有 jar 的所有 .class)。

注意:Boot 自己的 spring-boot jar 不带 spring.components(本仓库只在测试里见过手写该文件的用例:module/spring-boot-web-server/src/test/.../ServletComponentScanIntegrationTests.java:127)。所以索引要靠你自己的项目引入 indexer。

代价:只能索引自己的源码(jar 依赖不会被索引);索引不完整时(如 Lombok 生成的类)会漏扫。Spring Framework 6.1+ 已标记其"收益有限",AOT 时代价值更低。


13. Bean 命名:四层优先级

13.1 第 0 层:显式注解 value

任何 stereotype 注解的 value 属性都可以指定 Bean 名:

@Component("myFoo")
@Service("myFoo")
@Configuration("myConfig")
@AutoConfiguration("myAutoConfig")
public class Foo { }

AnnotationBeanNameGenerator.determineBeanNameFromAnnotation() 会:

  1. 遍历类的所有注解(含元注解)
  2. 筛出"是 stereotype 且有 value 属性"的注解
  3. 取非空的 value 字符串
  4. 如果多个注解给出不同的名字 → 抛 IllegalStateException

13.2 第 1 层:默认生成(短类名 + decapitalize)

// AnnotationBeanNameGenerator.buildDefaultBeanName(Framework 行为)
protected String buildDefaultBeanName(BeanDefinition definition) {
    String beanClassName = definition.getBeanClassName();
    Assert.state(beanClassName != null, "No bean class name set");
    String shortClassName = ClassUtils.getShortName(beanClassName);
    return Introspector.decapitalize(shortClassName);
}

ClassUtils.getShortName 对于 com.example.URLProvider 返回 URLProvider;Introspector.decapitalize("URLProvider"):

// java.beans.Introspector.decapitalize
public static String decapitalize(String name) {
    if (name == null || name.length() == 0) return name;
    if (name.length() > 1 && Character.isUpperCase(name.charAt(0))
            && Character.isUpperCase(name.charAt(1))) {
        return name;                       // ← 前两个字符都大写 → 原样返回
    }
    return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}

这就是那个著名的"双大写特例":

类名Bean 名说明
UserServiceuserService首字符小写
Foofoo
URLProviderURLProvider前两字符都大写 → 原样
HTTPClientHTTPClient原样
aFooaFoo首字符本来就是小写,不变

实践陷阱:

@Service
public class URLProvider { }

@Autowired
private URLProvider urlProvider;    // ⚠️ 按类型注入没问题

但如果你写:

@Resource(name = "uRLProvider")     // ❌ 名字不对!实际是 "URLProvider"

或者两个同类型 Bean 时用 @Qualifier("uRLProvider") → NoSuchBeanDefinitionException。

排查技巧:启动时打印 ctx.getBeanDefinitionNames(),或者用 Actuator 的 /actuator/beans。

13.3 第 2 层:FullyQualifiedAnnotationBeanNameGenerator

Spring 提供另一个实现:生成完整类名作为 Bean 名。

@SpringBootApplication(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)

结果:com.example.URLProvider → Bean 名 "com.example.URLProvider"。

用途:避免同名冲突(不同包下的同名类)。代价是 Bean 名很长,@Qualifier 写起来丑。

13.4 第 3 层:SpringApplication.setBeanNameGenerator()

new SpringApplication(App.class)
    .setBeanNameGenerator(new MyBeanNameGenerator())
    .run(args);

有两条生效路径:

路径 A(直接设置):

// SpringApplication.load(): 688
if (this.beanNameGenerator != null) {
    loader.setBeanNameGenerator(this.beanNameGenerator);
}

→ 影响 annotatedReader、scanner(A)、xmlReader → 只影响 BeanDefinitionLoader 直接注册的那些定义

路径 B(容器单例):

// SpringApplication.postProcessApplicationContext(): 591-595
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
    if (this.beanNameGenerator != null) {
        context.getBeanFactory().registerSingleton(
                AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
    }
    ...
}

AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR = "org.springframework.context.annotation.internalConfigurationBeanNameGenerator"。

这个单例会被 ConfigurationClassPostProcessor 读取,用于 @ComponentScan 创建的 scanner(即 Scanner B)。

所以路径 B 才是覆盖 @ComponentScan 的关键。

13.5 四层优先级总结

优先级(高 → 低)

  ④ 显式注解 value                 @Component("x") / @Service("x")
       ↓ 无
  ③ @ComponentScan.nameGenerator   = @SpringBootApplication(nameGenerator=...)
       ↓ 默认 BeanNameGenerator.class(哨兵)
  ② CONFIGURATION_BEAN_NAME_GENERATOR 单例
       (= SpringApplication.setBeanNameGenerator() 注册)
       ↓ 无
  ① AnnotationBeanNameGenerator(框架默认)
       ↓
     buildDefaultBeanName → decapitalize(shortClassName)

关于 ③ 的哨兵设计:

@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

默认值是 BeanNameGenerator.class 这个接口本身,而不是 AnnotationBeanNameGenerator.class。为什么?

因为 ComponentScanAnnotationParser 需要区分"用户没指定"和"用户指定了默认实现"。如果默认是 AnnotationBeanNameGenerator.class,就无法判断用户是否显式要求"用 AnnotationBeanNameGenerator"——而显式要求在继承场景下是有意义的(子类想覆盖父类的自定义生成器)。

用一个不可能被实例化的接口作哨兵,就能精确表达"未指定"。这是一个干净的设计。

Javadoc 里也写明了:

The default value of the BeanNameGenerator interface itself indicates that the scanner used to process this @SpringBootApplication annotation should use its inherited bean name generator, e.g. the default AnnotationBeanNameGenerator or any custom instance supplied to the application context at bootstrap time.

13.6 内部类与 Bean 名

@Configuration
public class Outer {
    @Configuration
    static class Inner { }        // Bean 名?
}

ClassUtils.getShortName("com.example.Outer$Inner") → 返回 "Outer.Inner"(Spring 的 getShortName 会把 $ 转成 .)。

所以 Bean 名是 "outer.Inner"?

不——decapitalize("Outer.Inner") = "outer.Inner"(首字符小写,其余保留)。

这是一个极其容易被忽略的细节:嵌套类的 Bean 名里带一个点。如果你写 @Qualifier("outerInner") 找不到。

实践建议:内部类做配置类时,显式给名字:

@Configuration("innerConfig")
static class Inner { }

13.7 Bean 名冲突与 allowBeanDefinitionOverriding

// prepareContext: 391-393
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
    listableBeanFactory.setAllowBeanDefinitionOverriding(
            this.properties.isAllowBeanDefinitionOverriding());
}
  • 默认 false(Boot 2.1 起改的,之前是 true)
  • 同名覆盖时抛 BeanDefinitionOverrideException
  • 注意:只覆盖"同名 Bean 定义注册",不影响 @Bean 方法重载(@Bean 方法名本身就是 Bean 名,重载会导致同名)

为什么 Boot 把它默认关了?

因为在自动配置体系里,同名覆盖意味着"两个自动配置抢同一个 Bean 名"——这几乎总是 Bug(要么是类路径冲突,要么是用户误写了与自动配置同名的 @Bean)。静默覆盖会让问题延迟到运行时才暴露,且极难排查。

但有些场景必须开:

spring:
  main:
    allow-bean-definition-overriding: true

典型场景:测试里用 @TestConfiguration 覆盖某个 Bean、或者集成遗留代码。

一个重要限制:这个开关在 prepareContext() 里设置,而自动配置的注册发生在 refresh() 之后。所以它对自动配置也有效。但 spring.main.* 的绑定发生在 prepareEnvironment(),早于 prepareContext() → 时序正确。✅


14. AutoConfigurationPackages:自动配置的"扫描起点"记录

14.1 它解决什么问题

@ComponentScan 的扫描范围是 @SpringBootApplication 类所在的包。但其他框架(JPA 实体扫描、Spring Data Repository 扫描、MyBatis mapper 扫描)也需要知道"扫描起点"——它们不能直接用 @ComponentScan 的结果(因为实体类没有 @Component)。

AutoConfigurationPackages 就是把这个"起点"存下来,供其他模块查询。

14.2 存储形式:一个特殊的 Bean

private static final String BEAN = AutoConfigurationPackages.class.getName();
// = "org.springframework.boot.autoconfigure.AutoConfigurationPackages"

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    if (registry.containsBeanDefinition(BEAN)) {
        addBasePackages(registry.getBeanDefinition(BEAN), packageNames);
    }
    else {
        RootBeanDefinition beanDefinition = new RootBeanDefinition(BasePackages.class);
        beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        addBasePackages(beanDefinition, packageNames);
        registry.registerBeanDefinition(BEAN, beanDefinition);
    }
}

注意:

  1. Bean 名是类的全限定名 —— 这在 Spring 里很少见(通常用短名)。好处是绝对不会与用户的 Bean 重名。
  2. BeanDefinition.ROLE_INFRASTRUCTURE —— 标记为基础设施(§16.3 会因这条被排除出懒初始化)
  3. 包名通过构造器参数值(ConstructorArgumentValues)传递,而不是字段注入

14.3 追加语义与 distinct

private static void addBasePackages(BeanDefinition beanDefinition, String[] additionalBasePackages) {
    ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
    if (constructorArgumentValues.hasIndexedArgumentValue(0)) {
        ValueHolder indexedArgumentValue = constructorArgumentValues.getIndexedArgumentValue(0, String[].class);
        Assert.state(indexedArgumentValue != null, "'indexedArgumentValue' must not be null");
        String[] existingPackages = (String[]) indexedArgumentValue.getValue();
        Stream<String> existingPackagesStream = (existingPackages != null) ? Stream.of(existingPackages)
                : Stream.empty();
        constructorArgumentValues.addIndexedArgumentValue(0,
                Stream.concat(existingPackagesStream, Stream.of(additionalBasePackages))
                    .distinct()
                    .toArray(String[]::new));
    }
    else {
        constructorArgumentValues.addIndexedArgumentValue(0, additionalBasePackages);
    }
}

关键:用 Stream.concat(...).distinct() → 多次 register 是追加且去重的。

这支持了"多个 @AutoConfigurationPackage 来源"的场景:

@SpringBootApplication
@AutoConfigurationPackage(basePackages = "com.other")
public class App { }

→ BasePackages 里有两个包:com.example(来自主类包)和 com.other。

注意 distinct() 保留首次出现顺序 → 顺序稳定。

14.4 PackageImports:三种来源

PackageImports(AnnotationMetadata metadata) {
    AnnotationAttributes attributes = AnnotationAttributes.fromMap(
            metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
    Assert.state(attributes != null, "'attributes' must not be null");
    List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
    for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
        packageNames.add(basePackageClass.getPackage().getName());
    }
    if (packageNames.isEmpty()) {
        packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
    }
    this.packageNames = Collections.unmodifiableList(packageNames);
}

优先级(注意不是覆盖,是全部收集):

  1. @AutoConfigurationPackage(basePackages = {...})
  2. @AutoConfigurationPackage(basePackageClasses = {...}) → 每个类的所在包
  3. 都为空 → 用标注类的包(ClassUtils.getPackageName(metadata.getClassName()))

@SpringBootApplication 为什么有这个能力?

因为 @EnableAutoConfiguration 上有 @AutoConfigurationPackage:

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { ... }

(第 14 章会完整讲 EnableAutoConfiguration。)

所以 @SpringBootApplication → @EnableAutoConfiguration → @AutoConfigurationPackage → AutoConfigurationPackages.Registrar → 记录主类所在包。

14.5 Registrar 与 DeterminableImports

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImports(metadata));
    }
}

DeterminableImports 是 Boot 2.7 引入的接口(本仓库 org.springframework.boot.context.annotation.DeterminableImports),用于 AOT:

AOT 阶段需要"在不实际执行 ImportBeanDefinitionRegistrar 的情况下,预测它会导入什么"。determineImports 返回一个可比较的、确定性的对象(这就是为什么 PackageImports 实现了 equals/hashCode)。

Boot 的 AOT 引擎会比较两次运行(构建期生成 vs 运行时)的 determineImports() 结果,如果不一致就报错。

这也解释了为什么 PackageImports 要 Collections.unmodifiableList 且实现 equals/hashCode。

14.6 has() 与 get()

public static boolean has(BeanFactory beanFactory) {
    return beanFactory.containsBean(BEAN) && !get(beanFactory).isEmpty();
}

public static List<String> get(BeanFactory beanFactory) {
    try {
        return beanFactory.getBean(BEAN, BasePackages.class).get();
    }
    catch (NoSuchBeanDefinitionException ex) {
        throw new IllegalStateException("Unable to retrieve @EnableAutoConfiguration base packages");
    }
}

注意 has() 的短路:containsBean(BEAN) && !get(...).isEmpty() —— 如果 Bean 存在但包列表为空,has() 返回 false。

什么时候包列表会为空?

BasePackages(String... names) {
    List<String> packages = new ArrayList<>();
    for (String name : names) {
        if (StringUtils.hasText(name)) {
            packages.add(name);
        }
    }
    this.packages = packages;
}

只有所有名字都是空串/空白时。实际上 PackageImports 保证至少一个(兜底用类名包),所以基本不会发生。

JPA 的用法(JpaBaseConfiguration / EntityScanPackages 会先查 AutoConfigurationPackages.has(),见第 22 章)。

14.7 【高危】主类放置位置的影响

因为自动配置包 = 主类所在包,所以:

com.example.MyApplication          → 扫描 com.example.**(含所有子包)
com.MyApplication                  → 扫描 com.**(扫全站!)
myapp.MyApplication                → 扫描 myapp.**

把启动类放在 com 或过浅的包下,会导致:

  • 扫描到第三方 jar 里恰好在 com. 下的类
  • 扫描耗时暴增
  • 意外注册不该注册的 Bean

Boot 官方建议:启动类放在根包(如 com.example.myapp),所有业务代码在其子包下。这样"约定优于配置"的扫描范围恰到好处。

反过来:如果启动类在 com.example.myapp.app,而业务代码在 com.example.myapp.service,service 不会被扫到!这是初学者最常见的"Bean 注入失败"原因。

修复方式:

@SpringBootApplication(scanBasePackages = "com.example.myapp")

但注意:这只改 @ComponentScan,不改 AutoConfigurationPackages!JPA 实体扫描仍然看不到 com.example.myapp 下的实体。要同时改:

@SpringBootApplication(scanBasePackages = "com.example.myapp")
@EntityScan("com.example.myapp")
@EnableJpaRepositories("com.example.myapp")

这个"两套包配置"的割裂是 Boot 的一个长期痛点,值得记住。


15. prepareContext 里其他与 Bean 定义相关的动作

15.1 循环引用与定义覆盖开关

ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
    autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences());
    if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
        listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
    }
}

两层 instanceof 嵌套:

  • 外层 AbstractAutowireCapableBeanFactory → 因为 setAllowCircularReferences 定义在它上面
  • 内层 DefaultListableBeanFactory → 因为 setAllowBeanDefinitionOverriding 定义在它上面

(ConfigurableListableBeanFactory ← DefaultListableBeanFactory ← AbstractAutowireCapableBeanFactory ← AbstractBeanFactory)

两者默认都是 false(Boot 2.6 把循环引用默认关了)。

// ApplicationProperties.java
private boolean allowBeanDefinitionOverriding;   // 默认 false
private boolean allowCircularReferences;         // 默认 false

为什么循环引用默认关?(Boot 2.6 起)

  • 循环引用是设计问题的信号(Spring 团队的观点)
  • 允许它会强制 Spring 用早期对象引用(三级缓存的第二级),某些 AOP 代理场景行为诡异
  • 关闭后启动更快(不需要维护 earlySingletonObjects)

15.2 springApplicationArguments 单例

beanFactory.registerSingleton("springApplicationArguments", applicationArguments);

这是一个"假 Bean"——直接注册实例,没有 BeanDefinition。

后果:

  • getBeanDefinitionNames() 里看不到它
  • getBeanNamesForType(ApplicationArguments.class) 能找到它
  • 它不会被 BeanFactoryPostProcessor 处理(因为已经实例化完毕)
  • 它不支持懒初始化(§16 只处理 BeanDefinition)

用途:让应用代码可以注入 ApplicationArguments 拿到命令行参数:

@Component
public class MyBean {
    public MyBean(ApplicationArguments args) {
        System.out.println(args.getOptionNames());
    }
}

15.3 springBootBanner 单例

if (printedBanner != null) {
    beanFactory.registerSingleton("springBootBanner", printedBanner);
}

同上,且条件注册(banner 为 OFF 时不注册)。所以:

@Autowired(required = false)
private Banner banner;    // banner.mode=off 时为 null

15.4 KeepAlive(回顾)

if (this.properties.isKeepAlive()) {
    context.addApplicationListener(new KeepAlive());
}

第 12 章讲过:一个非 daemon 线程 + Thread.sleep(Long.MAX_VALUE),只为让 JVM 活到 CRaC checkpoint。这里补充它与 Bean 定义的关系:无(它是 ApplicationListener,不是 Bean)。

15.5 PropertySourceOrderingBeanFactoryPostProcessor

private static class PropertySourceOrderingBeanFactoryPostProcessor
        implements BeanFactoryPostProcessor, Ordered {

    private final ConfigurableApplicationContext context;

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        DefaultPropertiesPropertySource.moveToEnd(this.context.getEnvironment());
    }
}

它做什么:把 defaultProperties 这个 PropertySource 移到 Environment 的末尾(最低优先级)。

为什么需要?

SpringApplication.setDefaultProperties() / SpringApplicationBuilder.properties() 添加的 defaultProperties 在 configurePropertySources() 里被 DefaultPropertiesPropertySource.addOrMerge() 加到了最前面(addFirst)——因为此时还没有其他源。

但语义上"默认属性"应该是兜底(优先级最低),所以必须在所有源都装配完(ConfigData、命令行等)之后,把它移到末尾。

这个"移到末尾"的动作必须在 refresh() 之前完成(因为 refresh() 期间会大量读取属性),BeanFactoryPostProcessor 是 refresh 里最早能插手的时机之一。

HIGHEST_PRECEDENCE 的意义:保证它比任何其他 BFPP 先跑(比如 ConfigurationPropertiesBindingPostProcessor 需要先读属性)。

DefaultPropertiesPropertySource.moveToEnd(第 06 章讲过):

public static void moveToEnd(ConfigurableEnvironment environment) {
    MutablePropertySources sources = environment.getPropertySources();
    PropertySource<?> propertySource = sources.remove(NAME);   // "defaultProperties"
    if (propertySource != null) {
        sources.addLast(propertySource);
    }
}

注意内部类的 static:private static class ... —— 避免持有 SpringApplication 实例的隐式引用(虽然这里也没用到)。

15.6 两个 BFPP 都用 HIGHEST_PRECEDENCE 会冲突吗

LazyInitializationBeanFactoryPostProcessor(§16)和 PropertySourceOrderingBeanFactoryPostProcessor 都返回 Ordered.HIGHEST_PRECEDENCE。

不会冲突——因为 PostProcessorRegistrationDelegate 对同 order 的 BFPP 按注册顺序稳定排序(实际上是稳定的 sort)。注册顺序是:

  1. LazyInitializationBeanFactoryPostProcessor(行 405)
  2. PropertySourceOrderingBeanFactoryPostProcessor(行 409)

但它们的语义互不依赖(一个改 lazyInit,一个改 PropertySource 顺序),所以顺序无所谓。

真正需要关心的是:LazyInitializationBeanFactoryPostProcessor 必须在所有 Bean 定义注册完成后才能跑——它遍历 getBeanDefinitionNames()。而自动配置的 Bean 定义是在 ConfigurationClassPostProcessor(一个 BeanDefinitionRegistryPostProcessor)里注册的。

BeanDefinitionRegistryPostProcessor 先于 BeanFactoryPostProcessor 执行(Spring 的两阶段设计),所以:

refresh()
  → invokeBeanFactoryPostProcessors()
      阶段 1: BeanDefinitionRegistryPostProcessor
                - ConfigurationClassPostProcessor  ← 自动配置在这里注册
      阶段 2: BeanFactoryPostProcessor
                - LazyInitializationBeanFactoryPostProcessor   ← 此时能看到全部定义 ✅
                - PropertySourceOrderingBeanFactoryPostProcessor

时序正确。✅


16. 懒初始化:谁被排除,为什么

16.1 主流程

public final class LazyInitializationBeanFactoryPostProcessor
        implements BeanFactoryPostProcessor, Ordered {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Collection<LazyInitializationExcludeFilter> filters = getFilters(beanFactory);
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            if (beanDefinition instanceof AbstractBeanDefinition abstractBeanDefinition) {
                postProcess(beanFactory, filters, beanName, abstractBeanDefinition);
            }
        }
    }
    ...
}

三点:① 只处理 AbstractBeanDefinition(覆盖了 RootBeanDefinition / GenericBeanDefinition / ScannedGenericBeanDefinition / AnnotatedGenericBeanDefinition;若自定义实现直接实现 BeanDefinition 接口则被跳过);② getBeanDefinitionNames() 返回当前已注册的所有名字(含自动配置注册的,见 §15.6);③ getBeanDefinition(beanName) 会触发定义合并(若该名字对应子定义)。

16.2 排除条件 1:显式设置过 lazyInit

Boolean lazyInit = beanDefinition.getLazyInit();
if (lazyInit != null) {
    return;      // 已经显式设置过 → 尊重用户
}

AbstractBeanDefinition.lazyInit 是 Boolean(不是 boolean),null = 未设置。这正是"三态"设计。

所以 @Lazy 标注的类/方法不受 spring.main.lazy-initialization=true 影响(无论是 @Lazy(true) 还是 @Lazy(false))。

16.3 排除条件 2:SmartInitializingSingleton

private Collection<LazyInitializationExcludeFilter> getFilters(ConfigurableListableBeanFactory beanFactory) {
    // Take care not to force the eager init of factory beans when getting filters
    ArrayList<LazyInitializationExcludeFilter> filters = new ArrayList<>(
            beanFactory.getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values());
    filters.add(LazyInitializationExcludeFilter.forBeanTypes(SmartInitializingSingleton.class));
    filters.add(new InfrastructureRoleLazyInitializationExcludeFilter());
    return filters;
}

SmartInitializingSingleton.afterSingletonsInstantiated() 只有在单例被实例化时才会调用。如果这个 Bean 是懒加载的且从未被访问,回调永远不触发 → 依赖该回调的功能(如 ObjectProvider 的延迟初始化、某些框架的"启动完成后自检")会静默失效。

所以 Boot 强制排除它们。

16.4 排除条件 3:ROLE_INFRASTRUCTURE

private static final class InfrastructureRoleLazyInitializationExcludeFilter
        implements LazyInitializationExcludeFilter {

    @Override
    public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType) {
        return beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE;
    }
}

ROLE_INFRASTRUCTURE 的 Bean 包括:

  • 各种 *PostProcessor(ConfigurationClassPostProcessor 等)
  • AutoConfigurationPackages 的 BasePackages(§14.2)
  • @ConfigurationProperties 的注册处理器
  • Spring 内部的 Environment 相关 Bean

这些必须在启动时就位,否则整个容器无法工作。

实践含义:你自己的 Bean 如果设了 @Role(BeanDefinition.ROLE_INFRASTRUCTURE),就会自动免于懒初始化。这是一个逃生舱——如果某个第三方 Bean 因为懒初始化出问题,你可以:

@Bean
public static BeanDefinitionRegistryPostProcessor fixLazy(SomeClass c) { ... }

不过更简单的做法是用 LazyInitializationExcludeFilter:

@Bean
public static LazyInitializationExcludeFilter myFilter() {
    return LazyInitializationExcludeFilter.forBeanTypes(MyCriticalBean.class);
}

注意必须是 static(类注释明确要求),因为它在容器极早期被实例化。

16.5 getBeansOfType(..., false, false) 的两个 false

beanFactory.getBeansOfType(LazyInitializationExcludeFilter.class, false, false)

签名是 getBeansOfType(Class<T> type, boolean includeNonSingletons, boolean allowEagerInit):

  • includeNonSingletons=false → 只要单例
  • allowEagerInit=false → 不允许提前初始化 FactoryBean

源码注释写得很清楚:

Take care not to force the eager init of factory beans when getting filters

为什么重要? 因为 getBeansOfType 默认会调用 getType(name),对于 FactoryBean 类型的定义,这会触发 FactoryBean.getObjectType(),进而可能实例化 FactoryBean。在 BFPP 阶段这是非法的(会创建未完全初始化的 Bean)。

16.6 懒初始化的代价(常被忽略)

开启 spring.main.lazy-initialization=true 的收益是启动变快、内存占用低;代价则常被忽略:

  • 配置错误延迟暴露:本来启动时就报的错(缺依赖、类型不匹配),现在要到第一次访问该 Bean 才报
  • 首次请求变慢:HTTP 服务第一个请求可能超时
  • @PostConstruct / InitializingBean 延迟:依赖这些钩子的预热逻辑失效
  • SmartInitializingSingleton 全部强制非懒(§16.3),收益被削弱
  • Actuator 健康检查可能误报:HealthIndicator 依赖的 Bean 还没初始化

Boot 官方立场:懒初始化是"为特定场景准备的",不是默认推荐。生产环境开启前务必做全链路压测。



17. AOT / Native 模式下:load() 被整个跳过

17.1 跳过点

if (!AotDetector.useGeneratedArtifacts()) {
    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
    load(context, sources.toArray(new Object[0]));
}

AotDetector 是 Spring Framework(本仓库不含其源码)提供的检测器,判定方式大致是检查一个系统属性 / 类路径标记(Spring 6 起是 spring.aot.enabled 系统属性 + 生成的 META-INF/spring/aot.factories)。

当以 AOT 产物运行(java -Dspring.aot.enabled=true -jar app.jar)或 Native Image 时:

  • BeanDefinitionLoader 根本不会被创建
  • AnnotatedBeanDefinitionReader 的副作用(注册注解处理器,§5.5)也不会发生
  • 所有 Bean 定义由 AOT 生成的 Java 源码提供

17.2 替代者:__ApplicationContextInitializer

private void addAotGeneratedInitializerIfNecessary(List<ApplicationContextInitializer<?>> initializers) {
    if (AotDetector.useGeneratedArtifacts()) {
        List<ApplicationContextInitializer<?>> aotInitializers = new ArrayList<>(
                initializers.stream().filter(AotApplicationContextInitializer.class::isInstance).toList());
        if (aotInitializers.isEmpty()) {
            Assert.state(this.mainApplicationClass != null, "No application main class found");
            String initializerClassName = this.mainApplicationClass.getName() + "__ApplicationContextInitializer";
            if (!ClassUtils.isPresent(initializerClassName, getClassLoader())) {
                throw new AotInitializerNotFoundException(this.mainApplicationClass, initializerClassName);
            }
            aotInitializers.add(AotApplicationContextInitializer.forInitializerClasses(initializerClassName));
        }
        initializers.removeAll(aotInitializers);
        initializers.addAll(0, aotInitializers);      // ← 放到最前面
    }
    if (NativeDetector.inNativeImage()) {
        NativeImageRequirementsException.throwIfNotMet();
    }
}

几个关键设计:

  1. 命名约定:{主类名}__ApplicationContextInitializer。这里的 mainApplicationClass 来自 §3.5 的 StackWalker 推断——所以 AOT 模式下 mainApplicationClass 推断必须正确,否则找不到生成的类。

  2. initializers.addAll(0, aotInitializers) —— AOT initializer 被放到最前面。为什么?因为它要注册几乎所有 Bean 定义,其他 initializer 可能需要在其上做微调。

    这是本系列反复出现的"逆序插入 = 后声明者优先"的反面用例:正序插到 0 位 = 最先执行。提醒读者:不要机械套用第五定律,要看是"执行顺序"还是"优先级顺序"。

  3. removeAll 后再 addAll(0, ...) —— 先去重再前插,避免重复执行。

  4. NativeImageRequirementsException.throwIfNotMet() —— Native Image 下会额外检查(如 native.image 相关条件)。

17.3 AotInitializerNotFoundException

if (!ClassUtils.isPresent(initializerClassName, getClassLoader())) {
    throw new AotInitializerNotFoundException(this.mainApplicationClass, initializerClassName);
}

最常见的触发原因:

  • 用 java -jar app.jar 直接跑了 AOT 处理过的 jar,但没加 -Dspring.aot.enabled=true → 走的是普通路径,不会到这里
  • 反过来,加了 -Dspring.aot.enabled=true 但 jar 里没有 AOT 产物 → 到这里
  • 主类名推断错误(§3.5 的 main 方法匹配问题)

17.4 对使用者的三条实践结论

  1. AOT 模式下 spring.main.sources 无效——因为它只影响 BeanDefinitionLoader。如果你的应用用 spring.main.sources 加载 Bean,AOT 会静默丢失它们(AOT 处理时会尝试捕获,但字符串源的降级探测在构建期无法完全模拟)。
  2. AOT 模式下 SpringApplication.setBeanNameGenerator() 仍然有效——因为路径 B(CONFIGURATION_BEAN_NAME_GENERATOR 单例注册)在 postProcessApplicationContext() 里,不受 load() 跳过影响。但路径 A(直接设置 loader)无效。
  3. AOT 模式下 primary source 上的 @Conditional 语义可能不同——因为求值者变了(AOT 生成的 __ApplicationContextInitializer vs AnnotatedBeanDefinitionReader.conditionEvaluator)。

18. spring.main.* 完整清单与绑定时机

18.1 绑定点

// prepareEnvironment() 末尾(行 361)
bindToSpringApplication(environment);

protected void bindToSpringApplication(ConfigurableEnvironment environment) {
    try {
        Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));
    }
    catch (Exception ex) {
        throw new IllegalStateException("Cannot bind to SpringApplication", ex);
    }
}

Bindable.ofInstance(...) —— 这是"绑定到已存在的实例"(第 08 章讲过 Bindable.ofInstance vs Bindable.of(Class) 的区别)。所以 ApplicationProperties 的字段是原地修改的,不是新建对象。

时序:prepareEnvironment() → 早于 createApplicationContext() 和 prepareContext()。所以:

  • ✅ spring.main.web-application-type 在 createApplicationContext() 之前生效
  • ✅ spring.main.allow-bean-definition-overriding 在 prepareContext() 之前生效
  • ✅ spring.main.sources 在 load() 之前生效
  • ❌ 但晚于 SpringApplication 构造里做的那些(如 WebApplicationType.deduce()、getSpringFactoriesInstances)

18.2 完整属性表

来自本仓库 ApplicationProperties.java + additional-spring-configuration-metadata.json:

属性类型默认生效点本章小节
spring.main.sourcesSet<String>空getAllSources()§3.2 / §8
spring.main.web-application-typeWebApplicationType推断createApplicationContext()第 16 章
spring.main.allow-bean-definition-overridingbooleanfalseprepareContext()§13.7 / §15.1
spring.main.allow-circular-referencesbooleanfalseprepareContext()§15.1
spring.main.lazy-initializationbooleanfalseprepareContext()§16
spring.main.keep-alivebooleanfalseprepareContext()第 12 章
spring.main.banner-modeBanner.Mode见下printBanner()第 12 章
spring.main.log-startup-infobooleantrueprepareContext()第 12 章
spring.main.register-shutdown-hookbooleantruerun() 开头第 12 章
spring.main.cloud-platformCloudPlatform推断CloudPlatform.getActive()第 10 章
spring.main.show-bannerbooleantrue—已弃用(1.3.0 起,改用 banner-mode)
spring.main.web-environmentboolean——已弃用(2.0.0 起,改用 web-application-type)

两个"不在 ApplicationProperties 里"的特殊成员:

  1. spring.main.cloud-platform —— 在元数据里注册了(第 565 行),但 ApplicationProperties 没有对应字段。它由 CloudPlatform 自己读:

    // CloudPlatform.java: 187
    private static final String PROPERTY_NAME = "spring.main.cloud-platform";
    

    (第 10 章详述:强制指定走 Binder,探测走 Environment)

  2. spring.main.environment-prefix —— 根本不在元数据里,而且被硬禁止:

    // SpringApplication.java: 359
    Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
            ...);
    

    原因:环境变量前缀决定了环境变量如何映射到属性名。如果它能通过配置文件设置,就会出现"先有鸡还是先有蛋"——读配置文件之前必须先知道前缀。所以只能通过编程式 SpringApplication.setEnvironmentPrefix() 设置。

    这是一个"元属性"(meta-property):它影响属性系统本身,因此不能由属性系统配置。

18.3 bannerMode 的动态默认

Mode getBannerMode(Environment environment) {
    if (this.bannerMode != null) {
        return this.bannerMode;
    }
    String applicationPropertyName = LoggingSystemProperty.CONSOLE_STRUCTURED_FORMAT.getApplicationPropertyName();
    Assert.state(applicationPropertyName != null, "applicationPropertyName must not be null");
    boolean structuredLoggingEnabled = environment.containsProperty(applicationPropertyName);
    return (structuredLoggingEnabled) ? Mode.OFF : Banner.Mode.CONSOLE;
}

默认不是常量,而是条件计算:如果启用了结构化日志(第 23 章),banner 会被自动关掉——因为 banner 的 ANSI 输出会污染结构化日志流。

这是一个"环境感知的默认值"设计范式,值得学习。

18.4 ApplicationPropertiesRuntimeHints

static class ApplicationPropertiesRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
        BindableRuntimeHintsRegistrar.forTypes(ApplicationProperties.class).registerHints(hints, classLoader);
    }
}

BindableRuntimeHintsRegistrar(第 09 章讲过)会为 ApplicationProperties 的所有字段类型生成反射提示——保证 Native 下 spring.main.* 仍能绑定。


19. 端到端追踪:new SpringApplication(App.class) 到 Bean 定义落地

把全章串起来。假设:

@SpringBootApplication          // 在 com.example.app
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

application.yaml:

spring:
  main:
    sources: com.example.legacy

阶段 1–3:构造 → 环境 → 上下文

  1. primarySources = LinkedHashSet{App.class}
  2. properties.webApplicationType = WebApplicationType.deduce() → SERVLET(有 spring-webmvc)
  3. bootstrapRegistryInitializers / initializers / listeners ← spring.factories
  4. mainApplicationClass = App.class(StackWalker,§3.5)
  5. prepareEnvironment():Environment 创建、ConfigData 加载(第 07/10/11 章)
  6. bindToSpringApplication(environment) → properties.sources = {"com.example.legacy"}
  7. createApplicationContext() → AnnotationConfigServletWebServerApplicationContext;其构造器里 new AnnotatedBeanDefinitionReader(this) + new ClassPathBeanDefinitionScanner(this) → 注解处理器已注册(§5.5)

阶段 4:prepareContext()

context.setEnvironment → postProcessApplicationContext(beanNameGenerator 为 null,跳过,§13.4)→ addAotGeneratedInitializerIfNecessary(非 AOT,跳过,§17)→ setAllowCircularReferences(false) / setAllowBeanDefinitionOverriding(false)(§15.1)→ applyInitializers → contextPrepared → bootstrapContext.close(第 12 章)→ logStartupInfo → registerSingleton("springApplicationArguments")(§15.2)→ registerSingleton("springBootBanner")(§15.3)→ lazy-initialization=false 故不加 LazyInit BFPP(§16)→ keep-alive=false 故不加 KeepAlive → 加 PropertySourceOrderingBFPP(§15.5)→ AotDetector 为 false 故进 load 分支(§17)→ getAllSources() = [App.class, "com.example.legacy"](§3)→ load(context, sources)(§4)→ contextLoaded。

阶段 5:load() 内部

BeanDefinitionLoader 构造
  ├─ annotatedReader = new AnnotatedBeanDefinitionReader(registry)
  │     └─ 【副作用】再注册一遍注解处理器(幂等,因同名会被判定为兼容)
  ├─ xmlReader = new XmlBeanDefinitionReader(registry)
  ├─ groovyReader = null(无 Groovy)
  └─ scanner(A) = new ClassPathBeanDefinitionScanner(registry)
        └─ addExcludeFilter(ClassExcludeFilter{App.class})  ← 只收 Class 类型的 source

三个 setter 全部跳过(`beanNameGenerator` / `resourceLoader` / `environment` 均为 null)——最后一个就是 §8.1 的伏笔。

loader.load() ├─ source[0] = App.class (Class 分支) │ ├─ groovyReader == null → 跳过 Groovy 段 │ ├─ isEligible(App.class):非匿名、非 closure、有构造器 → true │ └─ annotatedReader.register(App.class) │ ├─ 构造 AnnotatedGenericBeanDefinition │ ├─ 读 @Scope/@Lazy/@Primary/@DependsOn/@Role/@Description │ ├─ conditionEvaluator.shouldSkip(metadata) → false │ └─ beanName = "app"(decapitalize("App"))→ registerBeanDefinition │ └─ source[1] = "com.example.legacy" (CharSequence 分支) ├─ ① resolvePlaceholders → 无占位符,原样返回 ├─ ② ClassUtils.forName → ClassNotFoundException → 吞掉 ├─ ③ loadAsResources → ClassPathResource 不存在 → false ├─ ④ findPackage("com.example.legacy") │ ├─ getDefinedPackage → null(还没加载过) │ ├─ 扫 com/example/legacy/.class → Class.forName 第一个 │ │ └─ 【副作用】该探针类被单独 load(Class) 一次(§8.4) │ └─ getDefinedPackage → 现在非 null └─ load(Package) → scanner(A).scan("com.example.legacy") ├─ 扫 com/example/legacy/**/.class ├─ 排除 ClassExcludeFilter(只有 App.class,跨包无影响) └─ 逐个 isCandidateComponent → 生成 beanName → checkCandidate → 注册


### 阶段 6:`refresh()`(下一章的主场,此处仅列相关)

| 步 | 动作 |
|---|---|
| 28 | `invokeBeanFactoryPostProcessors()` |
| 29 | 阶段 1:`ConfigurationClassPostProcessor`(`BeanDefinitionRegistryPostProcessor`) |
| 30 | → 解析 `App` 这个配置类 |
| 31 | → 处理 `@ComponentScan`(Scanner **B**,排除 `TypeExcludeFilter` + `AutoConfigurationExcludeFilter`) |
| 32 | → 处理 `@EnableAutoConfiguration`(`AutoConfigurationImportSelector`,第 14 章) |
| 33 | → 处理 `@AutoConfigurationPackage` → `Registrar` → `AutoConfigurationPackages.register(...)` = `com.example.app` |
| 34 | 阶段 2:`PropertySourceOrderingBFPP`(`defaultProperties` 移到末尾) |
| 35 | 阶段 2:其他 BFPP |
| 36 | `registerBeanPostProcessors()` |
| 37 | `finishBeanFactoryInitialization()` —— 实例化所有非懒单例 |

### 阶段 7:回答开篇的问题

> "`@SpringBootApplication` 标注的类,为什么既是配置类又是扫描起点?它自己被注册了几次?"

- **配置类**:因为 `@SpringBootApplication` 元标注了 `@SpringBootConfiguration` → `@Configuration`
- **扫描起点**:因为 `@SpringBootApplication` → `@EnableAutoConfiguration` → `@AutoConfigurationPackage`,`Registrar` 记录了它的包
- **注册次数**:
  1. `BeanDefinitionLoader` 用 `annotatedReader.register(App.class)` → 1 次(名为 `app`)
  2. Scanner B 扫 `com.example.app` 时会**再次命中** `App.class` → `checkCandidate("app", sbd)` → 已存在的是 `AnnotatedGenericBeanDefinition`(**不是** `ScannedGenericBeanDefinition`)→ `isCompatible` 走第一条 → 返回 `false` → **不重复注册**
  3. 所以最终 **1 个 Bean 定义,名为 `app`**

如果主类名是 `URLProvider`(双大写),Bean 名就是 `URLProvider` 而不是 `uRLProvider`(§13.2)。

---

## 20. 反直觉结论汇总(17 条)

### 20.1 `BeanDefinitionLoader` 是包级私有类,但它的工厂方法是 `protected`
→ 包外子类无法覆写 `createBeanDefinitionLoader()`。这是自 Boot 1.0 延续的可见性瑕疵,也说明**不要依赖这个类**。

### 20.2 【最重要】`BeanDefinitionLoader.scanner` 与 `@ComponentScan` 的 scanner 是两个实例
→ 自定义 `TypeExcludeFilter` 对 `spring.main.sources` 触发的扫描**无效**;两者的 `BeanNameGenerator` 来源也不同。

### 20.3 `spring.main.sources` 里的 `${}` 占位符解析用的是**新建的 `StandardEnvironment`**
→ `application.yaml` 里的属性**解析不到**,只有 system properties / system environment 能解析。(§8.1,标注为高置信推演)

### 20.4 把**接口名**写进 source 会被静默跳过
→ `hasNoConstructors()` 对接口返回 true → `isEligible` 返回 false → 不注册也不报错。

### 20.5 字符串源的降级链会"顺手注册一个探针类"
→ `findPackage()` 为了创建 `Package` 对象,会 `Class.forName` 加载包下**第一个**类并 `load()` 它。这引入了一个**顺序不确定**的额外 Bean 定义(通常无害,因为 `checkCandidate` 会兜住)。

### 20.6 `spring.main.sources=com.example` **不会**扫描子包
→ `findPackage` 用的是 `<pkg>/*.class`,不是 `**/*.class`。

### 20.7 主类不会被注册两次,靠的是 `isCompatible` 的第一条
→ "已存在的定义不是 `ScannedGenericBeanDefinition`" 就算兼容。所以 primary source 天然"赢"过扫描结果。

### 20.8 `AutoConfigurationExcludeFilter` 比对的是 `*.imports`,不是 `spring.factories`
→ 用老式 `spring.factories` 声明的自动配置类**可能**被 `@ComponentScan` 扫到并提前注册,导致条件注解失效。

### 20.9 `TypeExcludeFilter` 的 `equals`/`hashCode` **直接抛异常**
→ 强制子类实现。目的是保证 Spring TestContext 的上下文缓存 key 正确。这是"用失败代替静默"的范式。

### 20.10 `getClass() == TypeExcludeFilter.class` 是防递归守卫
→ 没有它,`getBeansOfType(TypeExcludeFilter.class)` 会把 filter 自己当 delegate,无限递归。

### 20.11 双大写类名的 Bean 名**首字母不大写**
→ `URLProvider` → `"URLProvider"`(受 `Introspector.decapitalize` 特例影响)。`@Qualifier("uRLProvider")` 会失败。

### 20.12 嵌套配置类的 Bean 名带一个点
→ `Outer.Inner` → `"outer.Inner"`。

### 20.13 `@SpringBootApplication(scanBasePackages=...)` **不改** `AutoConfigurationPackages`
→ JPA 实体扫描、Spring Data Repository 扫描仍然按主类包走。这是 Boot 的长期割裂点。

### 20.14 `springApplicationArguments` / `springBootBanner` 没有 BeanDefinition
→ 用 `registerSingleton` 注册,`getBeanDefinitionNames()` 看不到,但 `getBeanNamesForType()` 能找到。

### 20.15 两个 Boot 自带的 BFPP 都用 `HIGHEST_PRECEDENCE`,不冲突
→ 同 order 时按注册顺序稳定排序,且两者语义互不依赖。

### 20.16 懒初始化下 `SmartInitializingSingleton` 与 `ROLE_INFRASTRUCTURE` 强制非懒
→ 前者因为回调必须触发,后者因为容器离不开它们。

### 20.17 `spring.main.environment-prefix` 被硬断言禁止从配置文件设置
→ 它是"元属性",影响属性系统本身,只能编程式设置。

### 20.18(附加)`spring.main.cloud-platform` 不在 `ApplicationProperties` 里
→ 元数据注册了,但由 `CloudPlatform` 自己读 Environment。

---

## 21. 动手验证清单

### 21.1 验证 Bean 名生成规则

```java
@Component
public class URLProvider { }

@Component
public class UserService { }

@Configuration
public class Outer {
    @Component
    static class Inner { }
}

期望:URLProvider、userService、outer.Inner

21.2 验证 spring.main.sources 的占位符作用域

my:
  extra: com.example.ExtraConfig
spring:
  main:
    sources: ${my.extra}

期望:启动失败,IllegalArgumentException: Invalid source '${my.extra}'

对照实验:

java -Dmy.extra=com.example.ExtraConfig -jar app.jar

期望:成功加载 ExtraConfig。

若两次结果相同(都成功或都失败),说明我对 scanner.getEnvironment() 的推演有误,请记录实际行为。

21.3 验证接口被静默跳过

public interface MyMarker { }
new SpringApplication(App.class).setSources(Set.of("com.example.MyMarker")).run(args);

期望:无异常,MyMarker 不在任何 Bean 列表里。

21.4 验证两个 scanner 的独立性

启动后 SCANNED-B 打印的类名只包含 @ComponentScan 覆盖的包。再加 spring.main.sources=com.other,com.other 下的类不会打印(说明走的是 Scanner A)。

21.5 验证主类只注册一次

System.out.println(ctx.getBeanDefinitionCount());
System.out.println(Arrays.asList(ctx.getBeanNamesForType(App.class)));

期望:getBeanNamesForType(App.class) 只有 1 个元素。

21.6 验证 AutoConfigurationPackages

System.out.println(AutoConfigurationPackages.get(ctx.getBeanFactory()));

期望:[com.example.app](主类所在包)。

再试加 @AutoConfigurationPackage(basePackages = "com.other"):

期望:[com.example.app, com.other](追加,顺序 = 注解声明顺序)。

21.7 验证懒初始化的排除

spring.main.lazy-initialization: true

**期望**:启动时就打印 `SMART CALLED`(没被懒初始化)。

对照:普通 `@Component` 不会在启动时被实例化。

### 21.8 验证 `PropertySourceOrdering`

```java
SpringApplication app = new SpringApplication(App.class);
./gradlew :myapp:aot          # 或 mvn -Pnative spring-boot:process-aot
java -Dspring.aot.enabled=true -jar build/libs/myapp.jar

期望:spring.main.sources 指定的类(若不在主类扫描包下)不存在。

21.10 验证 ClassExcludeFilter

new SpringApplication(App.class).setSources(Set.of("com.example")).run(args);  // App.class 就在 com.example

期望:无 ConflictingBeanDefinitionException,App 只注册一次。若移除 primary source 只留字符串源,App 经 findPackage 探针 + 扫描两条路径命中,仍只注册一次(靠 checkCandidate 的 source 相同判定)。


22. 源码索引

主题文件关键行/方法
加载入口org/springframework/boot/SpringApplication.java380–419 prepareContext;683–698 load;747 createBeanDefinitionLoader
源集合同上1187–1196 getAllSources;211 primarySources
属性载体org/springframework/boot/ApplicationProperties.java全文
属性绑定SpringApplication.java550–557 bindToSpringApplication
主类推断同上286–296 deduceMainApplicationClass / findMainClass
环境前缀断言同上359
后处理器注册同上591–607 postProcessApplicationContext
defaultProperties 归位同上1594–1612 PropertySourceOrderingBeanFactoryPostProcessor
AOT initializer同上421–439 addAotGeneratedInitializerIfNecessary
加载门面org/springframework/boot/BeanDefinitionLoader.java全文(336 行)
源排除过滤器同上306–324 ClassExcludeFilter
目录伪装防御同上237–257 isLoadCandidate
包探测同上259–281 findPackage
懒初始化org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java全文
懒初始化排除org/springframework/boot/LazyInitializationExcludeFilter.java全文
扫描排除(委派)org/springframework/boot/context/TypeExcludeFilter.java全文
扫描排除(自动配置).../autoconfigure/AutoConfigurationExcludeFilter.java全文
import 文件读取org/springframework/boot/context/annotation/ImportCandidates.java42 LOCATION;82 load
自动配置包.../autoconfigure/AutoConfigurationPackages.java全文
启动注解.../autoconfigure/SpringBootApplication.java54–57(两个 excludeFilter)
配置注解org/springframework/boot/SpringBootConfiguration.java全文(含 @Indexed)
自动配置注解.../autoconfigure/AutoConfiguration.java62–63(@Configuration(proxyBeanMethods=false))
cloud platformorg/springframework/boot/cloud/CloudPlatform.java187 PROPERTY_NAME
属性元数据spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json545–625(spring.main.*)
测试参考OverrideSourcesTests.java(core/spring-boot/src/test)54–62

本章未能直接核实的外部依赖(Spring Framework 7.0.9,本仓库不含源码):

  • ClassPathScanningCandidateComponentProvider 的默认 include 过滤器与 isCandidateComponent
  • ClassPathBeanDefinitionScanner.checkCandidate / isCompatible
  • AnnotationBeanNameGenerator 与 Introspector.decapitalize 的交互
  • AnnotatedBeanDefinitionReader 构造器的 registerAnnotationConfigProcessors 副作用
  • AotDetector.useGeneratedArtifacts() 的判定细节

以上均已在正文中以"Framework 行为"或"基于接口契约"标注,未与仓库内源码事实混同。


23. 本章未展开的悬念

悬念将在
AutoConfigurationImportSelector 如何读 *.imports 并排序第 14 章
@ConditionalOnXxx 的求值时机与 ConditionEvaluationReport第 15 章
AutoConfigurationSorter 的拓扑排序第 15 章
JPA 实体扫描如何消费 AutoConfigurationPackages第 22 章
ServletComponentScan(@WebServlet 等)的自定义扫描器第 18 章
AOT 生成的 __ApplicationContextInitializer 结构第 27 章
BeanDefinitionLoader 在 spring-boot-test 里的替代路径第 28 章
SpringApplicationBuilder 的 sources 处理第 28 章

24. 本章未逐行核实 / 待你实测的项

  1. BeanDefinitionLoader 的五种源未全部实测(只跑了代表性用例)。
  2. AnnotationBeanNameGenerator 的命名规则未穷举(内部类、包级重名等边界)。
  3. TypeExcludeFilter 的注册细节在第 29 章,本章只提到。
  4. 组件扫描的 excludeFilters / includeFilters 组合行为未逐个验证。
  5. AOT 下组件扫描的等效生成代码未读(第 28 章)。

25. 下一章预告

第 13 章解决了"原始 Bean 定义从哪来"。但这个答案里有一个巨大的空洞:

App 注册之后,@EnableAutoConfiguration 是怎么变成 100+ 个自动配置类的?它们凭什么知道该不该生效?为什么我自己写的 DataSource Bean 能让自动配置的 DataSource 退让?

这就是自动配置引擎:*.imports 怎么被读出来、ImportCandidates 与 AutoConfigurationEntry 的数据结构、去重与排除(spring.autoconfigure.exclude)、@AutoConfigureBefore/After 排序,以及**AutoConfigurationImportSelector 为什么实现 DeferredImportSelector**(这个接口选择是整套机制的枢纽)。再往下是条件注解体系——@ConditionalOnClass / @ConditionalOnMissingBean / @ConditionalOnProperty 的求值顺序与"条件求值顺序会影响结果"这一著名陷阱。

第 14 章:自动配置引擎 —— AutoConfigurationImportSelector 全流程。