Spring Boot 4.1.1 深度源码解析-第 16 章 · 排序、元数据与报告:`AutoConfigurationSorter`、配置元数据、条件报告

Spring Boot 4.1.1 深度源码解析-第 16 章 · 排序、元数据与报告:AutoConfigurationSorter、配置元数据、条件报告

前三章回答了"自动配置怎么决定生不生效 "(第 15 章)、"自动配置类怎么被选出并排序 "(第 14 章)、 "原始 Bean 定义从哪来 "(第 13 章)。 本章回答剩下的问题------怎么知道结果:

  • 排序信息是靠什么跨越编译期到运行期 的?(spring-autoconfigure-metadata.properties)
  • spring-configuration-metadata.json 是谁生成的、给谁用的?
  • ConditionEvaluationReport 怎么变成 /actuator/conditions 的 JSON?
  • 启动失败时那句 "Consider defining a bean of type ... in your configuration" 是怎么算出来的?

这四件事看似分散,其实是同一条信息链的四个环节:

bash 复制代码
编译期(javac + 注解处理器)
   ↓  写出
   ├─ META-INF/spring-autoconfigure-metadata.properties   → 支撑"排序 + 预筛"
   └─ META-INF/spring-configuration-metadata.json         → 支撑"IDE 补全 + 文档"
运行期(启动)
   ↓  执行
   条件求值 → ConditionEvaluationReport
   ↓  暴露
   ├─ /actuator/conditions(成功时)
   └─ FailureAnalyzer(失败时,读同一份 report 生成人类可读诊断)

本章的核心洞察 :Boot 把"诊断能力"当成一等公民。 条件报告不是为了好看------它是失败分析器的数据源。 一个启动失败能告诉你"哪个自动配置因为什么条件没生效",靠的就是这条链。


1. 排序信息的载体:@AutoConfiguration 的四个属性

第 14 章讲了 AutoConfigurationSorter 的三轮排序,但留了一个口子: @AutoConfigureBefore / @AutoConfigureAfter 这两个注解,和 @AutoConfiguration 自己的 before / after / beforeName / afterName 是什么关系?

答案在 @AutoConfiguration 的声明里:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore
@AutoConfigureAfter
public @interface AutoConfiguration {

    @AliasFor(annotation = Configuration.class)
    String value() default "";

    @AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
    Class<?>[] before() default {};

    @AliasFor(annotation = AutoConfigureBefore.class, attribute = "name")
    String[] beforeName() default {};

    @AliasFor(annotation = AutoConfigureAfter.class, attribute = "value")
    Class<?>[] after() default {};

    @AliasFor(annotation = AutoConfigureAfter.class, attribute = "name")
    String[] afterName() default {};
}

关键点:

  1. @AutoConfiguration 自己被 @AutoConfigureBefore 和 @AutoConfigureAfter(空值 )标注。 这是一个**"声明我有这个元注解"**的技巧,让 @AliasFor 有目标可指。
  2. before / after 是 Class<?>[],beforeName / afterName 是 String[]。 通过 @AliasFor 分别映射到 @AutoConfigureBefore.value / @AutoConfigureAfter.value 和 .name。

净效果:写

java 复制代码
@AutoConfiguration(after = DataSourceAutoConfiguration.class)

等价于写

java 复制代码
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)

为什么要有两套? 因为 @AutoConfiguration 是 2.7.0 才引入的"现代写法", 而 @AutoConfigureBefore/After 从 1.0 就有。@AliasFor 让二者完全等价 , 不需要改 AutoConfigurationSorter 的读取逻辑。

1.1 name 变体存在的理由

java 复制代码
/**
 * The names of the auto-configuration classes that should have already been applied.
 * In the unusual case that an auto-configuration class is not a top-level class, its
 * class name should use {@code $} to separate it from its containing class, for example
 * {@code com.example.Outer$NestedAutoConfiguration}.
 */
@AliasFor(annotation = AutoConfigureAfter.class, attribute = "name")
String[] afterName() default {};

两个用途:

  1. 类不在 classpath 上 时用字符串(与 @ConditionalOnClass 的 value vs name 同理)
  2. 嵌套类 必须用 Outer$Nested 形式------因为 Class<?> 常量无法表达"嵌套"以外的关系, 而 ASM 解析出的类名总是带 $

注意:Javadoc 说嵌套自动配置类是 "the unusual case"------ Boot 约定自动配置类是顶层类。

1.2 value 的隐藏语义

java 复制代码
/**
 * Explicitly specify the name of the Spring bean definition associated with the
 * {@code @AutoConfiguration} class. ...
 * The custom name applies only if the {@code @AutoConfiguration} class is picked up
 * through component scanning or supplied directly to an AnnotationConfigApplicationContext.
 * If the {@code @AutoConfiguration} class is registered as a traditional XML bean definition,
 * the name/id of the bean element will take precedence.
 */
@AliasFor(annotation = Configuration.class)
String value() default "";

@AutoConfiguration("myName") → 等价于 @Configuration("myName"),指定 Bean 名。

但 Javadoc 明确说了它只在"组件扫描或直接注册"时生效 ------ 而自动配置类是通过 ImportCandidates + ConfigurationClassParser 导入的, 不走 AnnotationBeanNameGenerator......那么它到底生效吗?

严谨声明 :我没有在 Boot 源码中找到直接证据说明 ConfigurationClassBeanDefinitionReader 是否读取 @Configuration 的 value 作为 Bean 名。按我对 Framework 的理解, 自动配置类的 Bean 名由 ConfigurationClassBeanDefinitionReader 用 ConfigurationClassParser 解析出的 beanName 决定,而该名字来自 ImportCandidates 的类名(经 AnnotationBeanNameGenerator 之类),@Configuration(value=...) 是否参与 取决于 Framework 的实现细节。本章不断言结论 ,标注为待核实。 实践建议:不要依赖 @AutoConfiguration("name") 给自动配置类改名。

1.3 proxyBeanMethods = false 是强制的

java 复制代码
@Configuration(proxyBeanMethods = false)
public @interface AutoConfiguration {

自动配置类永远不被 CGLIB 代理。原因:

  1. 自动配置类之间的 @Bean 方法互相调用没有意义(它们是给"用户配置"用的特性)
  2. 代理会显著拖慢启动(每个配置类都要生成一个子类)
  3. AOT / Native 下代理更复杂

这是"约定优于配置"的又一例:不是给你选项,而是直接钉死。


2. 编译期元数据之一:spring-autoconfigure-metadata.properties

第 14 章讲过 AutoConfigurationMetadataLoader 读 META-INF/spring-autoconfigure-metadata.properties,第 15 章讲过三个 AutoConfigurationImportFilter 靠它做预筛。那么这个文件是谁写的?

答案:core/spring-boot-autoconfigure-processor 模块里的 AutoConfigureAnnotationProcessor------一个 javac 注解处理器。

2.1 支持的注解(8 个)

java 复制代码
@SupportedAnnotationTypes({
    "org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
    "org.springframework.boot.autoconfigure.condition.ConditionalOnBean",
    "org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate",
    "org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication",
    "org.springframework.boot.autoconfigure.AutoConfigureBefore",
    "org.springframework.boot.autoconfigure.AutoConfigureAfter",
    "org.springframework.boot.autoconfigure.AutoConfigureOrder",
    "org.springframework.boot.autoconfigure.AutoConfiguration" })
public class AutoConfigureAnnotationProcessor extends AbstractProcessor {

    protected static final String PROPERTIES_PATH = "META-INF/spring-autoconfigure-metadata.properties";

    private final Map<String, String> properties = new TreeMap<>();

只有这 8 个注解被写入元数据。注意------

@ConditionalOnMissingBean 不在列表里。 这印证了第 15 章 §6.1 的结论:预筛阶段根本不处理"缺某个 Bean", 所以没必要为它生成元数据。

2.2 七个 PropertyGenerator

java 复制代码
protected List<PropertyGenerator> getPropertyGenerators() {
    List<PropertyGenerator> generators = new ArrayList<>();
    addConditionPropertyGenerators(generators);
    addAutoConfigurePropertyGenerators(generators);
    return generators;
}

private void addConditionPropertyGenerators(List<PropertyGenerator> generators) {
    String annotationPackage = "org.springframework.boot.autoconfigure.condition";
    generators.add(PropertyGenerator.of(annotationPackage, "ConditionalOnClass")
        .withAnnotation(new OnClassConditionValueExtractor()));
    generators.add(PropertyGenerator.of(annotationPackage, "ConditionalOnBean")
        .withAnnotation(new OnBeanConditionValueExtractor()));
    generators.add(PropertyGenerator.of(annotationPackage, "ConditionalOnSingleCandidate")
        .withAnnotation(new OnBeanConditionValueExtractor()));
    generators.add(PropertyGenerator.of(annotationPackage, "ConditionalOnWebApplication")
        .withAnnotation(ValueExtractor.allFrom("type")));
}

private void addAutoConfigurePropertyGenerators(List<PropertyGenerator> generators) {
    String annotationPackage = "org.springframework.boot.autoconfigure";
    generators.add(PropertyGenerator.of(annotationPackage, "AutoConfigureBefore", true)
        .withAnnotation(ValueExtractor.allFrom("value", "name"))
        .withAnnotation("AutoConfiguration", ValueExtractor.allFrom("before", "beforeName")));
    generators.add(PropertyGenerator.of(annotationPackage, "AutoConfigureAfter", true)
        .withAnnotation(ValueExtractor.allFrom("value", "name"))
        .withAnnotation("AutoConfiguration", ValueExtractor.allFrom("after", "afterName")));
    generators.add(PropertyGenerator.of(annotationPackage, "AutoConfigureOrder")
        .withAnnotation(ValueExtractor.allFrom("value")));
}

生成的 key 形如:

ini 复制代码
com.example.MyAutoConfiguration.ConditionalOnClass=com.example.SomeClass,com.example.Other
com.example.MyAutoConfiguration.ConditionalOnBean=com.example.Required
com.example.MyAutoConfiguration.ConditionalOnWebApplication=SERVLET
com.example.MyAutoConfiguration.AutoConfigureBefore=com.example.Before1
com.example.MyAutoConfiguration.AutoConfigureAfter=com.example.After1
com.example.MyAutoConfiguration.AutoConfigureOrder=100
com.example.MyAutoConfiguration=                     ← 空值,标记"这个类被处理过"

@AutoConfigureBefore / @AutoConfigureAfter 的第三个参数 true = omitEmptyValues:

java 复制代码
void applyToProperties(Map<String, String> properties, String className, List<Object> annotationValues) {
    if (this.omitEmptyValues && annotationValues.isEmpty()) {
        return;
    }
    mergeProperties(properties, className + "." + this.propertyName, toCommaDelimitedString(annotationValues));
}

为什么这两个要 omitEmptyValues,而条件注解不要?

  • @AutoConfiguration 上永远 带着空的 @AutoConfigureBefore / @AutoConfigureAfter (§1 说过,这是为了让 @AliasFor 有目标)。如果不过滤空值, 每个自动配置类都会写出 Xxx.AutoConfigureBefore= 这样的空条目------纯噪声。
  • 条件注解如果写空值,语义是"这个注解存在但没有指定类",与"注解不存在"在消费端 (AutoConfigurationMetadata.getSet 返回空集合)效果类似,留着无害。

2.3 值提取器的三个差异

(a) OnClassConditionValueExtractor:对结果排序

java 复制代码
static class OnClassConditionValueExtractor extends NamedValuesExtractor {

    OnClassConditionValueExtractor() { super("value", "name"); }

    @Override
    public List<Object> getValues(AnnotationMirror annotation) {
        List<Object> values = super.getValues(annotation);
        values.sort(this::compare);
        return values;
    }

    private int compare(Object o1, Object o2) {
        return Comparator.comparing(this::isSpringClass)
            .thenComparing(String.CASE_INSENSITIVE_ORDER)
            .compare(o1.toString(), o2.toString());
    }

    private boolean isSpringClass(String type) {
        return type.startsWith("org.springframework.");
    }
}

比较器是 "Spring 类优先,然后按字母序(大小写不敏感)" 。 注意 Comparator.comparing(this::isSpringClass) 比较的是 boolean, 而 Boolean 的自然序是 false < true ------ 所以 Spring 类排在后面,不是前面!

等等,这与"优先"矛盾吗?不矛盾 ------排序的目的不是"优先级", 而是让文件稳定 (同一份源码每次编译产出相同内容,便于构建缓存和 diff)。 thenComparing(String.CASE_INSENSITIVE_ORDER) 才是主排序键的实际效果。

我在此明确标注 :isSpringClass 作为第一比较键的实际效果是 "非 Spring 类在前、Spring 类在后"。由于 OnClassCondition 读取时用的是 Set(AutoConfigurationMetadata.getSet),顺序对运行时行为没有影响 。 这个排序唯一的作用是输出确定性。

(b) OnBeanConditionValueExtractor:有 name 就放弃

java 复制代码
static class OnBeanConditionValueExtractor extends AbstractValueExtractor {

    @Override
    public List<Object> getValues(AnnotationMirror annotation) {
        Map<String, AnnotationValue> attributes = new LinkedHashMap<>();
        annotation.getElementValues()
            .forEach((key, value) -> attributes.put(key.getSimpleName().toString(), value));
        if (attributes.containsKey("name")) {
            return Collections.emptyList();
        }
        List<Object> result = new ArrayList<>();
        extractValues(attributes.get("value")).forEach(result::add);
        extractValues(attributes.get("type")).forEach(result::add);
        return result;
    }
}

如果写了 name,就不写元数据。

原因:第 15 章 §6.1 讲过,OnBeanCondition 的预筛只做 "这些类型在 classpath 上存在吗?"的检查。而 name 是 Bean 名 ,不是类型------ 类型存在性检查对 name 毫无意义。所以直接放弃。

这是一个"元数据只为特定用途生成"的精确判断: 不为用不上的信息付存储代价。

(c) ConditionalOnWebApplication:只取 type

java 复制代码
generators.add(PropertyGenerator.of(annotationPackage, "ConditionalOnWebApplication")
    .withAnnotation(ValueExtractor.allFrom("type")));

写的是枚举的 name()(SERVLET / REACTIVE / ANY), 对应第 15 章 §8.5 预筛里的字符串比较。

2.4 mergeProperties:跨编译轮次的累积

java 复制代码
private void mergeProperties(Map<String, String> properties, String key, String value) {
    String existingKey = properties.get(key);
    if (existingKey == null || existingKey.isEmpty()) {
        properties.put(key, value);
    }
    else if (!value.isEmpty()) {
        properties.put(key, existingKey + "," + value);
    }
}

javac 的注解处理是多轮 的(RoundEnvironment)。同一个类可能在多轮被处理 (比如生成的代码又触发新的注解处理),所以必须合并而不是覆盖。

这是"幂等累积"模式------与第 10 章讲的 ConfigData 不动点迭代有相似的哲学: 允许多次处理,结果收敛。

2.5 写入

java 复制代码
private void writeProperties() throws IOException {
    if (!this.properties.isEmpty()) {
        Filer filer = this.processingEnv.getFiler();
        FileObject file = filer.createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH);
        try (Writer writer = new OutputStreamWriter(file.openOutputStream(), StandardCharsets.UTF_8)) {
            for (Map.Entry<String, String> entry : this.properties.entrySet()) {
                writer.append(entry.getKey());
                writer.append("=");
                writer.append(entry.getValue());
                writer.append(System.lineSeparator());
            }
        }
    }
}

UTF-8 + TreeMap 顺序 + 手写 key=value (不用 Properties.store, 因为后者会加时间戳导致构建不确定)。

TreeMap 保证输出按 key 字典序------又一处"为构建确定性做的努力"。

2.6 消费端回顾

第 14 章讲过:

java 复制代码
// AutoConfigurationMetadataLoader
static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";

AutoConfigurationMetadata.wasProcessed(className) 为 false 时, OnClassCondition 等会降级到读字节码。所以:

元数据缺失不会导致功能错误,只会导致性能下降。 这是一个"可选优化 "的设计------第三方库不引入 spring-boot-autoconfigure-processor 也能正常工作。

对照第 14 章:这也是为什么 ConfigurationClassFilter 里 filter.match(candidates, metadata) 的实现要容忍 null outcome。


3. 编译期元数据之二:spring-configuration-metadata.json

这是 IDE 里 application.yaml 自动补全的数据源,也是 /actuator/configprops 之外最重要的"配置可发现性"基础设施。

3.1 四个文件路径常量

java 复制代码
class MetadataStore {

    static final String METADATA_PATH = "META-INF/spring-configuration-metadata.json";

    static final BiFunction<TypeElement, TypeUtils, String> SOURCE_METADATA_PATH = (type, typeUtils) ->
        "META-INF/spring/configuration-metadata/%s.json".formatted(typeUtils.getQualifiedName(type));

    private static final String ADDITIONAL_METADATA_PATH = "META-INF/additional-spring-configuration-metadata.json";

    static final BiFunction<TypeElement, TypeUtils, String> ADDITIONAL_SOURCE_METADATA_PATH = (type, typeUtils) ->
        "META-INF/spring/configuration-metadata/additional/%s.json".formatted(typeUtils.getQualifiedName(type));
路径 用途 谁写
META-INF/spring-configuration-metadata.json 自动生成的主文件 注解处理器
META-INF/additional-spring-configuration-metadata.json 手写补充 开发者(Boot 自己就用了 89 个)
META-INF/spring/configuration-metadata/{FQN}.json 单个 @ConfigurationPropertiesSource 类的独立文件(4.x 新) 注解处理器
META-INF/spring/configuration-metadata/additional/{FQN}.json 上者的手写补充 开发者

4.x 的新变化 :spring/configuration-metadata/ 目录下按全限定类名 拆分文件。 这是模块化时代的必然------大型项目里一个巨大的 JSON 文件不好维护, 按类拆分后可以增量编译、按需加载。

仓库实证 :全仓库有 89 个 additional-spring-configuration-metadata.json。 Boot 自己就是这套机制的最大用户------因为很多属性(比如 spring.profiles.validate、 spring.autoconfigure.exclude)无法通过注解推导出完整信息,必须手写补充。

3.2 处理器支持的注解(10 个)

java 复制代码
@SupportedAnnotationTypes({
    CONFIGURATION_PROPERTIES_ANNOTATION,              // @ConfigurationProperties
    CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION,       // @ConfigurationPropertiesSource
    AUTO_CONFIGURATION_ANNOTATION,                    // @AutoConfiguration
    CONFIGURATION_ANNOTATION,                         // @Configuration
    CONTROLLER_ENDPOINT_ANNOTATION,                   // Actuator 端点系列
    ENDPOINT_ANNOTATION,
    JMX_ENDPOINT_ANNOTATION,
    REST_CONTROLLER_ENDPOINT_ANNOTATION,
    SERVLET_ENDPOINT_ANNOTATION,
    WEB_ENDPOINT_ANNOTATION })
public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor {

Actuator 端点占了 6 个 ------ 因为端点也是"可配置的东西" (management.endpoint.xxx.enabled 等),需要生成元数据。

另有几个在处理过程中被识别但不触发处理的注解:

java 复制代码
static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "...NestedConfigurationProperty";
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "...DeprecatedConfigurationProperty";
static final String CONSTRUCTOR_BINDING_ANNOTATION = "...bind.ConstructorBinding";
static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired";
static final String DEFAULT_VALUE_ANNOTATION = "...bind.DefaultValue";
static final String READ_OPERATION_ANNOTATION = "...annotation.ReadOperation";

@DefaultValue 在这里很关键------第 09 章讲过它用于构造函数绑定, 而这里它被用来生成元数据里的 defaultValue 字段。 一个注解同时服务运行期绑定和编译期文档------这是"信息只写一次"的范例。

3.3 处理流程

java 复制代码
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    this.metadataCollectors.processing(roundEnv);
    TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement();
    if (annotationType != null) {                     // @ConfigurationProperties 可用?
        for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
            processElement(element);
        }
    }
    TypeElement sourceAnnotationType = this.metadataEnv.getConfigurationPropertiesSourceAnnotationElement();
    if (sourceAnnotationType != null) {               // @ConfigurationPropertiesSource 可用?
        for (Element element : roundEnv.getElementsAnnotatedWith(sourceAnnotationType)) {
            if (element instanceof TypeElement typeElement) {
                MetadataCollector metadataCollector = this.metadataCollectors.getMetadataCollector(typeElement);
                processSourceElement(metadataCollector, "", typeElement);
            }
        }
    }
    Set<TypeElement> endpointTypes = this.metadataEnv.getEndpointAnnotationElements();
    if (!endpointTypes.isEmpty()) {                   // 端点注解可用?
        for (TypeElement endpointType : endpointTypes) {
            getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint);
        }
    }
    if (roundEnv.processingOver()) {
        try {
            writeSourceMetadata();
            writeMetadata();
        }
        catch (Exception ex) {
            throw new IllegalStateException("Failed to write metadata", ex);
        }
    }
    return false;
}

三个"可用性检查" (annotationType != null): 注解处理器可能被用在没有 Boot 依赖 的项目里(比如只有 spring-context)。 此时不能因为找不到注解类型就崩溃------静默跳过。

return false ------ 不"消费"这些注解,让其他处理器也能看到它们。

processingOver() 时才写文件 ------ 必须等所有轮次结束,否则会漏掉 后续轮次生成的类的元数据。

3.4 数据模型

运行期侧的模型在 spring-boot-configuration-metadata 模块:

java 复制代码
public class ConfigurationMetadataProperty implements Serializable {
    private String id;                  // 全限定名,小写横线形式:my.group.simple-property
    private String name;                // 组内名:simple-property
    private String type;                // java.lang.String(基本类型用包装类;泛型带完整信息)
    private String description;         // 多行,来自 Javadoc
    private String shortDescription;    // 单行单句
    private Object defaultValue;
    private final Hints hints = new Hints();
    private Deprecation deprecation;
}

type 字段的规则(Javadoc 明说):

  • boolean → java.lang.Boolean(基本类型一律用包装类)
  • HashMap<String,Integer> → java.util.HashMap<java.lang.String,java.lang.Integer>
  • 可能是"从 String 转换而来"的复杂类型

description vs shortDescription ------ 前者是完整 Javadoc, 后者由 SentenceExtractor 抽取第一句。IDE 的悬浮提示用后者, 文档用前者。

Hints :值提示(ValueHint)和值提供者(ValueProvider), 用于 IDE 的"候选值下拉框"。

3.5 消费端:ConfigurationMetadataRepositoryJsonBuilder

java 复制代码
ConfigurationMetadataRepository repository = ConfigurationMetadataRepositoryJsonBuilder
    .create(inputStream1, inputStream2, ...)     // 多个 JSON 源
    .build();

它把 classpath 上所有 spring-configuration-metadata.json (自己和所有依赖的)合并成一个仓库。

谁用它?

  1. IDE(IntelliJ IDEA / VS Code 的 Spring 插件)
  2. Boot 自己的文档工具(见下)
  3. /actuator/configprops 之外的一些工具链

3.6 Boot 自己的应用:ChangelogGenerator

这是 Boot 仓库里一个很有意思的内部工具 (configuration-metadata/spring-boot-configuration-metadata-changelog-generator):

java 复制代码
public final class ChangelogGenerator {

    public static void main(String[] args) throws IOException {
        generate(new File(args[0]), new File(args[1]), new File(args[2]));
    }

    private static void generate(File oldDir, File newDir, File out) throws IOException {
        String oldVersionNumber = oldDir.getName();
        ConfigurationMetadataRepository oldMetadata = buildRepository(oldDir);
        String newVersionNumber = newDir.getName();
        ConfigurationMetadataRepository newMetadata = buildRepository(newDir);
        Changelog changelog = Changelog.of(oldVersionNumber, oldMetadata, newVersionNumber, newMetadata);
        try (ChangelogWriter writer = new ChangelogWriter(out)) {
            writer.write(changelog);
        }
    }

    static ConfigurationMetadataRepository buildRepository(File directory) {
        ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
        for (File file : directory.listFiles()) {
            try (JarFile jarFile = new JarFile(file)) {
                JarEntry metadataEntry = jarFile.getJarEntry("META-INF/spring-configuration-metadata.json");
                if (metadataEntry != null) {
                    builder.withJsonResource(jarFile.getInputStream(metadataEntry));
                }
            }
        }
        return builder.build();
    }
}

用法 :传两个目录(目录名即版本号),里面放两个版本的 jar, 它扫出各版本的元数据 JSON,diff,输出 AsciiDoc 格式的变更日志。

四种差异类型:

java 复制代码
enum DifferenceType {
    ADDED,             // 新增
    DEPRECATED,        // 被标记弃用(可能仍存在)
    DELETED,           // 删除
    DEFAULT_CHANGED    // 默认值变了
}

这个工具的意义 :Boot 官方发布说明里的"配置属性变更"章节是自动生成的 , 不是人写的。这保证了文档与代码的一致性 ------ 也说明元数据不是"锦上添花",而是发布流程的一环。

DEFAULT_CHANGED 这一条尤其重要:默认值变了但属性还在, 这种变更最容易被忽略、最容易造成升级事故。Boot 专门把它列为一类差异。


4. 运行期报告:/actuator/conditions

第 15 章讲了 ConditionEvaluationReport 的内部结构。本节讲它如何被暴露成 HTTP。

4.1 端点定义

java 复制代码
@Endpoint(id = "conditions")
public class ConditionsReportEndpoint {

    private final ConfigurableApplicationContext context;

    @ReadOperation
    public ConditionsDescriptor conditions() {
        Map<@Nullable String, ContextConditionsDescriptor> contextConditionEvaluations = new HashMap<>();
        ConfigurableApplicationContext target = this.context;
        while (target != null) {
            contextConditionEvaluations.put(target.getId(), new ContextConditionsDescriptor(target));
            target = getConfigurableParent(target);
        }
        return new ConditionsDescriptor(contextConditionEvaluations);
    }

while (target != null) 向上遍历父上下文 ------ 所以管理端口子上下文 的 /actuator/conditions 会同时显示主上下文的条件报告。

这与第 15 章 §6.8 讲的 SearchStrategy.ANCESTORS 是配套的: 既然条件判定会跨上下文,报告就必须跨上下文。

4.2 三层 JSON 结构

yaml 复制代码
ConditionsDescriptor
  └─ contexts: { contextId -> ContextConditionsDescriptor }
        ├─ positiveMatches: { 短类名 -> [MessageAndConditionDescriptor] }   ← MultiValueMap
        ├─ negativeMatches: { 短类名 -> MessageAndConditionsDescriptor }    ← 单值
        │                                ├─ notMatched: [...]
        │                                └─ matched:    [...]   ← 注意:否定组里也有 matched!
        ├─ exclusions: [...]
        ├─ unconditionalClasses: [...]
        └─ parentId

两个不对称:

  1. positiveMatches 是 MultiValueMap<String, ...>(一个名字可对应多条 ), negativeMatches 是 Map<String, MessageAndConditionsDescriptor>(一个名字一条, 但那条内部含多个条件)。
  2. negativeMatches 里的对象同时有 matched 和 notMatched 两个列表。

第 2 点很重要:一个类的条件里可能既有匹配的也有不匹配的 (只要有一个不匹配, 整体就不匹配)。所以"否定匹配"条目要同时列出哪些条件过了、哪些没过------ 这样才能回答"它为什么没生效"。

4.3 归类逻辑

java 复制代码
private void add(String source, ConditionAndOutcomes conditionAndOutcomes) {
    String name = ClassUtils.getShortName(source);
    if (conditionAndOutcomes.isFullMatch()) {
        conditionAndOutcomes.forEach((conditionAndOutcome) -> this.positiveMatches.add(name,
                new MessageAndConditionDescriptor(conditionAndOutcome)));
    }
    else {
        this.negativeMatches.put(name, new MessageAndConditionsDescriptor(conditionAndOutcomes));
    }
}

判据是 isFullMatch() (所有条件都匹配)。 source 是全限定名 或 类#方法,显示时转短类名。

注意一个副作用 :ClassUtils.getShortName 会丢掉包信息和方法信息。 如果两个不同包的同名类 都有条件记录,它们在 JSON 里会合并到同一个 key 下 (positiveMatches 是 MultiValueMap 所以是追加;negativeMatches 是 Map 所以是覆盖 )。 后者会丢数据。

我标注为按代码观察的推断:Boot 没有处理这个冲突的迹象, 实践中同名的自动配置类很少见,所以影响有限。

4.4 单条记录的消息兜底

java 复制代码
public MessageAndConditionDescriptor(ConditionAndOutcome conditionAndOutcome) {
    Condition condition = conditionAndOutcome.getCondition();
    ConditionOutcome outcome = conditionAndOutcome.getOutcome();
    this.condition = ClassUtils.getShortName(condition.getClass());
    if (StringUtils.hasLength(outcome.getMessage())) {
        this.message = outcome.getMessage();
    }
    else {
        this.message = outcome.isMatch() ? "matched" : "did not match";
    }
}

消息为空时兜底成 "matched" / "did not match"。

对照第 15 章 §12:ConditionOutcome.getMessage() 在消息为空时返回 null------ StringUtils.hasLength(null) 是 false,所以走兜底分支。两处设计严丝合缝。

4.5 @JsonInclude(Include.NON_EMPTY) 与 @JsonPropertyOrder

java 复制代码
@JsonInclude(Include.NON_EMPTY)
public static final class ContextConditionsDescriptor { ... }

@JsonPropertyOrder({ "notMatched", "matched" })
public static class MessageAndConditionsDescriptor { ... }

@JsonPropertyOrder({ "condition", "message" })
public static class MessageAndConditionDescriptor { ... }

NON_EMPTY ------ 空集合不出现在 JSON 里(减小响应体积)。 JsonPropertyOrder ------ 固定字段顺序,便于人读和做快照对比。


5. 失败分析器:FailureAnalyzer 体系

这是 Boot 最有"人情味"的子系统------它把 JVM 的异常栈翻译成人能看懂的诊断。

5.1 三个核心类型

java 复制代码
@FunctionalInterface
public interface FailureAnalyzer {
    @Nullable FailureAnalysis analyze(Throwable failure);
}
java 复制代码
public class FailureAnalysis {
    private final String description;   // "出了什么事"
    private final @Nullable String action;  // "你该怎么办"
    private final Throwable cause;

    public FailureAnalysis(@Nullable String description, @Nullable String action, Throwable cause) {
        this.description = (description != null) ? description : "";   // null → ""
        ...
    }
}
java 复制代码
public interface FailureAnalysisReporter {
    void report(FailureAnalysis analysis);
}

三段式 :description(症状)+ action(行动)+ cause(原始异常)。 这个结构决定了 Boot 的错误输出格式。

5.2 AbstractFailureAnalyzer:用泛型解析目标异常类型

java 复制代码
public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {

    @Override
    public @Nullable FailureAnalysis analyze(Throwable failure) {
        T cause = findCause(failure, getCauseType());
        return (cause != null) ? analyze(failure, cause) : null;
    }

    protected abstract @Nullable FailureAnalysis analyze(Throwable rootFailure, T cause);

    @SuppressWarnings("unchecked")
    protected Class<? extends T> getCauseType() {
        Class<? extends T> type = (Class<? extends T>) ResolvableType
            .forClass(AbstractFailureAnalyzer.class, getClass())
            .resolveGeneric();
        Assert.state(type != null, "Unable to resolve generic");
        return type;
    }

    protected final <E extends Throwable> @Nullable E findCause(@Nullable Throwable failure, Class<E> type) {
        while (failure != null) {
            if (type.isInstance(failure)) { return (E) failure; }
            failure = failure.getCause();
        }
        return null;
    }
}

两个关键机制:

  1. 泛型解析 :ResolvableType.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric() 从子类的声明里反推出 T 的具体类型。所以你写 class MyAnalyzer extends AbstractFailureAnalyzer<MyException> 就够了, 不需要手动传 Class 对象。
  2. findCause 沿 getCause() 链向下找 ------ 因为根异常往往是 BeanCreationException,真正的 MyException 埋在十几层下面。

这是一个"反射 + 约定"的经典权衡 :用泛型避免样板代码, 代价是类型信息在运行时靠 ResolvableType 解析(有一次反射开销,只在启动时发生)。

5.3 FailureAnalyzers:加载与调度

java 复制代码
final class FailureAnalyzers implements SpringBootExceptionReporter {

    private static final Log logger = LogFactory.getLog(FailureAnalyzers.class);
    private final SpringFactoriesLoader springFactoriesLoader;
    private final List<FailureAnalyzer> analyzers;

    public FailureAnalyzers(@Nullable ConfigurableApplicationContext context) {
        this(context, SpringFactoriesLoader
            .forDefaultResourceLocation((context != null) ? context.getClassLoader() : null));
    }

    private static List<FailureAnalyzer> loadFailureAnalyzers(@Nullable ConfigurableApplicationContext context,
            SpringFactoriesLoader springFactoriesLoader) {
        List<FailureAnalyzer> analyzers = new ArrayList<>(springFactoriesLoader.load(FailureAnalyzer.class,
                getArgumentResolver(context), FailureHandler.logging(logger)));
        analyzers.add(FailureAnalyzedException::analyze);
        return Collections.unmodifiableList(analyzers);
    }

    private static @Nullable ArgumentResolver getArgumentResolver(@Nullable ConfigurableApplicationContext context) {
        if (context == null) { return null; }
        ArgumentResolver argumentResolver = ArgumentResolver.of(BeanFactory.class, context.getBeanFactory());
        argumentResolver = argumentResolver.and(Environment.class, context.getEnvironment());
        return argumentResolver;
    }

四条设计:

  1. ArgumentResolver 注入构造器参数 ------ 分析器可以声明 MyAnalyzer(BeanFactory beanFactory) 或 MyAnalyzer(Environment environment), 由 SpringFactoriesLoader 自动注入。 这是第 11 章讲过的 4.x 新 API 的又一个应用。
  2. FailureHandler.logging(logger) ------ 单个分析器加载失败 (类找不到、 构造器抛异常)时只记日志,不影响其他分析器 。 "诊断工具坏了不能掩盖真正的错误"(第 12 章的同一原则)。
  3. analyzers.add(FailureAnalyzedException::analyze) ------ 最后追加一个 "异常自己实现了 FailureAnalyzedException 接口"的兜底。 这让任意异常 都能自带诊断信息,无需注册到 spring.factories。
  4. context == null 时 ArgumentResolver 为 null ------ 早期失败(上下文还没创建)时也能分析,只是分析器拿不到 BeanFactory/Environment。

5.4 调度循环:第一个非 null 胜出

java 复制代码
@Override
public boolean reportException(Throwable failure) {
    FailureAnalysis analysis = analyze(failure, this.analyzers);
    return report(analysis);
}

private @Nullable FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) {
    for (FailureAnalyzer analyzer : analyzers) {
        try {
            FailureAnalysis analysis = analyzer.analyze(failure);
            if (analysis != null) {
                return analysis;
            }
        }
        catch (Throwable ex) {
            logger.trace(LogMessage.format("FailureAnalyzer %s failed", analyzer), ex);
        }
    }
    return null;
}

private boolean report(@Nullable FailureAnalysis analysis) {
    List<FailureAnalysisReporter> reporters = this.springFactoriesLoader.load(FailureAnalysisReporter.class);
    if (analysis == null || reporters.isEmpty()) {
        return false;
    }
    for (FailureAnalysisReporter reporter : reporters) {
        reporter.report(analysis);
    }
    return true;
}

三条语义:

  1. 顺序敏感 :spring.factories 里的顺序决定优先级。 第一个返回非 null 的分析器胜出。
  2. 分析器抛异常 → catch (Throwable) + TRACE 日志,继续下一个 。 "分析器本身不能让诊断失败"。
  3. report 返回 boolean ------ 告诉调用方"有没有产出诊断"。 第 12 章讲过 handleRunFailure 里的 reportFailure 在 finally 块中, 这个返回值决定是否继续走默认的异常打印路径。

注意 report 里 reporters 是每次重新 load ------ 没有缓存。 失败分析只发生一次,缓存没有意义。

5.5 LoggingFailureAnalysisReporter:那个经典格式

java 复制代码
public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter {

    private static final Log logger = LogFactory.getLog(LoggingFailureAnalysisReporter.class);

    @Override
    public void report(FailureAnalysis failureAnalysis) {
        if (logger.isDebugEnabled()) {
            logger.debug("Application failed to start due to an exception", failureAnalysis.getCause());
        }
        if (logger.isErrorEnabled()) {
            logger.error(buildMessage(failureAnalysis));
        }
    }

    private String buildMessage(FailureAnalysis failureAnalysis) {
        StringBuilder builder = new StringBuilder();
        builder.append(String.format("%n%n"));
        builder.append(String.format("***************************%n"));
        builder.append(String.format("APPLICATION FAILED TO START%n"));
        builder.append(String.format("***************************%n%n"));
        builder.append(String.format("Description:%n%n"));
        builder.append(String.format("%s%n", failureAnalysis.getDescription()));
        if (StringUtils.hasText(failureAnalysis.getAction())) {
            builder.append(String.format("%nAction:%n%n"));
            builder.append(String.format("%s%n", failureAnalysis.getAction()));
        }
        return builder.toString();
    }
}

那三行星号框起来的 APPLICATION FAILED TO START 就是这里拼的。

两个细节:

  1. error 只输出 description + action,不输出异常栈 ; 完整异常栈在 debug 级别。所以默认日志里看不到 stack trace ------ 想看的话要开 DEBUG。这是刻意的:诊断信息比栈更有用,栈会淹没它。
  2. action 为空时不输出 "Action:" 段。

5.6 全仓库 37 个分析器清单

模块 数量 分析器
spring-boot 18 ConfigDataNotFound、IncompatibleConfiguration、NotConstructorBoundInjection、AotInitializerNotFound、BeanCurrentlyInCreation、BeanDefinitionOverride、BeanNotOfRequiredType、Bind、BindValidation、InvalidConfigurationPropertyName、InvalidConfigurationPropertyValue、MissingParameterNames、MutuallyExclusiveConfigurationProperties、NoSuchMethod、NoUniqueBeanDefinition、PatternParse、UnboundConfigurationProperty、ValidationException
spring-boot-autoconfigure 2 NoSuchBeanDefinition、BundleContentNotWatchable
spring-boot-testcontainers 1 DockerEnvironmentNotFound
spring-boot-data-redis 1 DataRedisUrlSyntax
spring-boot-jdbc 2 DataSourceBeanCreation、HikariDriverConfiguration
spring-boot-jooq 2 JaxbNotAvailableException、NoDslContextBean
spring-boot-jpa 1 BootstrapExecutorRequired
spring-boot-liquibase 1 LiquibaseChangelogMissing
spring-boot-mail 1 NoSuchMailSenderBean
spring-boot-micrometer-metrics 1 Validation
spring-boot-r2dbc 4 ConnectionFactoryBeanCreation、MissingR2dbcPoolDependency、MultipleConnectionPoolConfigurations、NoConnectionFactoryBean
spring-boot-tomcat 1 ConnectorStart
spring-boot-web-server 2 PortInUse、MissingWebServerFactoryBean
合计 37

分布规律 :核心(spring-boot)负责框架级 问题(绑定、循环引用、属性名非法); 各技术模块负责领域级 问题(端口占用、驱动缺失、连接池多重配置)。 这是一个很好的扩展点设计范例 ------你的 starter 也应该配一个 FailureAnalyzer。


6. 深度拆解:NoSuchBeanDefinitionFailureAnalyzer

这是唯一一个把条件报告和失败分析连起来的分析器,也是本章"信息链"的终点。

6.1 构造:提前抓取 report

java 复制代码
class NoSuchBeanDefinitionFailureAnalyzer extends AbstractInjectionFailureAnalyzer<NoSuchBeanDefinitionException> {

    private final ConfigurableListableBeanFactory beanFactory;
    private final MetadataReaderFactory metadataReaderFactory;
    private final ConditionEvaluationReport report;

    NoSuchBeanDefinitionFailureAnalyzer(BeanFactory beanFactory) {
        Assert.isTrue(beanFactory instanceof ConfigurableListableBeanFactory,
                "'beanFactory' must be a ConfigurableListableBeanFactory");
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
        this.metadataReaderFactory = new CachingMetadataReaderFactory(this.beanFactory.getBeanClassLoader());
        // Get early as won't be accessible once context has failed to start
        this.report = ConditionEvaluationReport.get(this.beanFactory);
    }

那句注释是本章最重要的一句:

"Get early as won't be accessible once context has failed to start"

为什么必须提前抓? 因为 handleRunFailure 会调用 context.close(), 而 close() 会 destroySingletons()------autoConfigurationReport 是 registerSingleton 注册的单例,会被销毁 。销毁后再 get() 会拿到一个空的新 report。

所以必须在分析器构造时(此时上下文还活着)就把 report 引用抓在手里。

这是一个"生命周期竞态"的经典案例,也是"诊断能力需要提前布局"的证明。

6.2 关联逻辑:从 Bean 缺失反查条件失败

java 复制代码
private List<AutoConfigurationResult> getAutoConfigurationResults(NoSuchBeanDefinitionException cause) {
    List<AutoConfigurationResult> results = new ArrayList<>();
    collectReportedConditionOutcomes(cause, results);
    collectExcludedAutoConfiguration(cause, results);
    return results;
}

private void collectReportedConditionOutcomes(NoSuchBeanDefinitionException cause,
        List<AutoConfigurationResult> results) {
    this.report.getConditionAndOutcomesBySource()
        .forEach((source, sourceOutcomes) -> collectReportedConditionOutcomes(cause, new Source(source),
                sourceOutcomes, results));
}

private void collectReportedConditionOutcomes(NoSuchBeanDefinitionException cause, Source source,
        ConditionAndOutcomes sourceOutcomes, List<AutoConfigurationResult> results) {
    if (sourceOutcomes.isFullMatch()) {
        return;                                     // 全匹配 → 不是它的问题
    }
    BeanMethods methods = new BeanMethods(source, cause);
    for (ConditionAndOutcome conditionAndOutcome : sourceOutcomes) {
        if (!conditionAndOutcome.getOutcome().isMatch()) {
            for (MethodMetadata method : methods) {
                results.add(new AutoConfigurationResult(method, conditionAndOutcome.getOutcome()));
            }
        }
    }
}

算法:

  1. 遍历 report 里所有 source
  2. 跳过"全匹配"的(它成功了,不是嫌疑)
  3. 对每个 source,找出它的哪些 @Bean 方法的返回类型 是缺失的那个类型 (BeanMethods 做这个匹配,靠 metadataReaderFactory 读字节码)
  4. 对每个匹配的方法,记录它的失败条件

这就是那句诊断的来源:

arduino 复制代码
The following candidates were found but could not be injected:
	- Bean method 'dataSource' in 'DataSourceAutoConfiguration' not loaded because
	  @ConditionalOnClass did not find required class '...'

6.3 Source 类的字符串切分

java 复制代码
private static class Source {
    private final String className;
    private final @Nullable String methodName;

    Source(String source) {
        String[] tokens = source.split("#");
        this.className = (tokens.length > 1) ? tokens[0] : source;
        this.methodName = (tokens.length != 2) ? null : tokens[1];
    }
}

用 # 切分 ------ 对应第 15 章 §2.3 里 getClassOrMethodName 的 类#方法 格式。 两处设计必须一致,否则解析失败。

注意 tokens.length != 2 而不是 < 2 ------ 如果字符串里有两个 #(理论上不会),methodName 会是 null(保守)。

6.4 排除项也算嫌疑

java 复制代码
private void collectExcludedAutoConfiguration(NoSuchBeanDefinitionException cause,
        List<AutoConfigurationResult> results) {
    for (String excludedClass : this.report.getExclusions()) {
        Source source = new Source(excludedClass);
        BeanMethods methods = new BeanMethods(source, cause);
        for (MethodMetadata method : methods) {
            String message = String.format("auto-configuration '%s' was excluded",
                    ClassUtils.getShortName(excludedClass));
            results.add(new AutoConfigurationResult(method, new ConditionOutcome(false, message)));
        }
    }
}

被 spring.autoconfigure.exclude 排除的自动配置也会被列为"候选但没注入"。 这是对的------用户的排除也是"它没生效"的原因之一,必须告诉他。

new ConditionOutcome(false, message) ------ 造一个假的 ConditionOutcome 来表示"被排除"。这里用的是 String 重载构造器(第 15 章 §12 讲过)。

6.5 Action 文案的条件分支

java 复制代码
String action = String.format("Consider %s %s in your configuration.",
        (!autoConfigurationResults.isEmpty() || !userConfigurationResults.isEmpty())
                ? "revisiting the entries above or defining" : "defining",
        getBeanDescription(cause));

两种文案:

  • 有候选 → "Consider revisiting the entries above or defining a bean of type 'X' in your configuration."
  • 无候选 → "Consider defining a bean of type 'X' in your configuration."

这就是那句人人见过的话。 它的措辞由"有没有找到候选"决定------ 如果找到了被条件挡住的候选,会提示你去复核上面的条目(暗示"改条件"), 而不仅仅是"自己定义一个"。

6.6 注入点注解的附加信息

java 复制代码
InjectionPoint injectionPoint = findInjectionPoint(rootFailure);
if (injectionPoint != null) {
    Annotation[] injectionAnnotations = injectionPoint.getAnnotations();
    if (injectionAnnotations.length > 0) {
        message.append(String.format("%nThe injection point has the following annotations:%n"));
        for (Annotation injectionAnnotation : injectionAnnotations) {
            message.append(String.format("\t- %s%n", injectionAnnotation));
        }
    }
}

如果失败发生在注入点(@Autowired 字段/构造参数),会列出该注入点上的注解------ 比如 @Qualifier("xxx")。"你指定了 qualifier,但没有匹配的 Bean" 这类问题 靠这个信息才能诊断。

6.7 findInjectionPoint 的链路

java 复制代码
private @Nullable InjectionPoint findInjectionPoint(Throwable failure) {
    UnsatisfiedDependencyException unsatisfiedDependencyException = findCause(failure,
            UnsatisfiedDependencyException.class);
    if (unsatisfiedDependencyException == null) {
        return null;
    }
    return unsatisfiedDependencyException.getInjectionPoint();
}

NoSuchBeanDefinitionException 通常被包在 UnsatisfiedDependencyException 里, 而后者持有 InjectionPoint。沿异常链找上去。

6.8 numberOfBeansFound != 0 时放弃

java 复制代码
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause,
        @Nullable String description) {
    if (cause.getNumberOfBeansFound() != 0) {
        return null;
    }
    ...
}

NoSuchBeanDefinitionException 有两种:

  1. 找不到任何 Bean(numberOfBeansFound == 0)
  2. 找到了多个 但都不匹配(numberOfBeansFound > 1,通常是类型不匹配)

本分析器只处理第 1 种 。第 2 种交给 BeanNotOfRequiredTypeFailureAnalyzer 等。

这是"分析器之间按异常状态分工"的例子 ------同一个异常类, 不同状态由不同分析器处理,靠 analyze 返回 null 让位。


7. 三者串联:一次失败的完整诊断链

把所有环节串起来,看一次真实的启动失败:

python 复制代码
【编译期】
DataSourceAutoConfiguration 上有:
  @AutoConfiguration(after = ...)
  @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
  @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })

javac 跑 AutoConfigureAnnotationProcessor →
  spring-autoconfigure-metadata.properties 写入:
    ...DataSourceAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType
    ...DataSourceAutoConfiguration.ConditionalOnMissingBean=...      ← 注意:不在支持列表,不写
    ...DataSourceAutoConfiguration=

【运行期 · 启动】
1. ImportCandidates 读出候选(178 个 imports 文件)
2. AutoConfigurationSorter 排序(读 .properties 的 AutoConfigureAfter/Before/Order)
3. 预筛:OnClassCondition 读 .properties 的 ConditionalOnClass
   → 用户没加 JDBC 依赖 → EmbeddedDatabaseType 不存在 → 判死
   → 记录到 ConditionEvaluationReport(作为 negative match)
4. 自动配置被剔除,@Bean dataSource() 从未注册

【运行期 · 用户代码】
@Autowired DataSource dataSource;   → NoSuchBeanDefinitionException

【运行期 · 失败分析】
handleRunFailure → FailureAnalyzers.reportException
  → NoSuchBeanDefinitionFailureAnalyzer(构造时已抓住 report)
     → 遍历 report,找 DataSourceAutoConfiguration
     → 它的条件不全匹配 → 匹配它的 @Bean 方法返回类型 == DataSource → 命中
  → 输出:
     Description:
     Field dataSource in ... required a bean of type 'javax.sql.DataSource' that could not be found.
     The following candidates were found but could not be injected:
       - Bean method 'dataSource' in 'DataSourceAutoConfiguration' not loaded because
         @ConditionalOnClass did not find required class '...EmbeddedDatabaseType'
     Action:
     Consider revisiting the entries above or defining a bean of type 'javax.sql.DataSource'...

这条链上有四个"必须提前布局"的点:

  1. 编译期必须写出 ConditionalOnClass 的类型清单(否则预筛无法工作)
  2. 预筛必须在正式求值之前跑(否则性能崩塌)
  3. 预筛的否定结果必须写进 report(否则失败分析无数据)
  4. 失败分析器必须在上下文关闭前抓住 report(否则拿到空对象)

四个点缺一个,最后那句人性化的诊断就出不来。 这就是 Boot "诊断能力是一等公民"的具体含义------它不是事后加的, 而是从编译期就规划好的一条链。

8. 实战:怎么读懂 /actuator/conditions

拿一个真实场景走一遍。假设你加了 spring-boot-starter-data-redis 但没加 spring-boot-starter-jdbc,然后发现 JdbcTemplate 注入失败。

8.1 先看 negativeMatches

访问 /actuator/conditions,找到 contexts.{你的 contextId}.negativeMatches, 搜索 JdbcTemplateAutoConfiguration:

json 复制代码
"JdbcTemplateAutoConfiguration": {
  "notMatched": [
    {
      "condition": "OnClassCondition",
      "message": "@ConditionalOnClass did not find required class 'org.springframework.jdbc.core.JdbcTemplate'"
    }
  ],
  "matched": []
}

怎么读:

字段 含义
condition: "OnClassCondition" 挡住它的条件是类检查
message 缺的类是 JdbcTemplate
matched: [] 没有其他条件被求值------因为它在预筛阶段就被打死了

第三条是关键 :matched 是空的,说明这个自动配置的其他条件(如 @ConditionalOnSingleCandidate(DataSource.class))根本没跑 。 所以你看不到"它还需要 DataSource"这个信息------这正是第 15 章 §3.3 讲的 "预筛只在判否时记录"。

8.2 再看 positiveMatches

找一个生效的,比如 RedisAutoConfiguration:

json 复制代码
"RedisAutoConfiguration": [
  { "condition": "OnClassCondition", "message": "@ConditionalOnClass found required class 'org.springframework.data.redis.core.RedisOperations'" },
  { "condition": "OnBeanCondition",  "message": "@ConditionalOnMissingBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) did not find any beans" }
]

注意第二条的消息是 "did not find any beans ",但它出现在 positiveMatches------ 因为对 @ConditionalOnMissingBean 来说,"没找到"就是匹配。

这是读报告时最容易误解的一点 : 消息文字描述的是"条件看到了什么",不是"条件是否匹配"。 判断匹配与否要看它在 positiveMatches 还是 negativeMatches,不要看消息里有没有 "did not"。

8.3 unconditionalClasses 的用法

这个列表是"从未被任何条件判定的类 "。如果一个类出现在这里, 意味着它没有任何条件注解 ------即它一定会生效。

排障价值 :如果你怀疑某个自动配置"莫名其妙生效了",先看它是不是在 unconditionalClasses 里。如果是,说明它压根没写条件(这本身可能是 bug)。

对照第 15 章 §13.2:这个集合是"用排除法反向统计"出来的------ 候选全集减去所有有记录的。

8.4 exclusions

列出所有被 spring.autoconfigure.exclude(或 @EnableAutoConfiguration(exclude=...)) 排除的类。第 15 章 §6.4 讲过,NoSuchBeanDefinitionFailureAnalyzer 会把它们也算作 "候选但没注入"。

排障价值:如果你手动排除了某个自动配置,然后忘了, 导致 Bean 缺失------这里能立刻看到。

8.5 子上下文的 parentId

如果你开了管理端口(management.server.port), contexts 里会有两个 条目:主上下文和 ...management 子上下文。 子上下文条目的 parentId 指向主上下文。

注意 :子上下文的 positiveMatches 里可能包含主上下文的自动配置 (因为条件判定默认 SearchStrategy.ALL,会看到父上下文的 Bean)。 不要误以为那些配置在子上下文里生效了。


9. 实战:怎么写 additional-spring-configuration-metadata.json

9.1 什么时候需要手写

注解处理器能自动推导出大部分信息,但有四类它推不出来:

  1. 默认值不在代码里(比如来自第三方库的默认值)
  2. 描述需要人工润色(自动抽取的 Javadoc 不够清楚)
  3. 弃用信息 (@Deprecated 注解的语义太弱,需要 level 和 since)
  4. 属性来自第三方类(你无法给它的字段加 Javadoc)

9.2 真实样本(Boot 自己的)

json 复制代码
{
  "groups": [],
  "properties": [
    {
      "name": "spring.activemq.pool.block-if-full",
      "type": "java.lang.Boolean",
      "description": "Whether to block when a connection is requested and the pool is full. Set it to false to throw a \"JMSException\" instead.",
      "sourceType": "org.springframework.boot.jms.autoconfigure.JmsPoolConnectionFactoryProperties",
      "defaultValue": true
    },
    {
      "name": "spring.activemq.pool.create-connection-on-startup",
      "type": "java.lang.Boolean",
      "description": "Whether to create a connection on startup. Can be used to warm up the pool on startup.",
      "defaultValue": true,
      "deprecation": {
        "level": "error",
        "since": "2.1.0"
      }
    }
  ]
}

四个字段的讲究:

字段 说明
name 必须 是小写横线形式(block-if-full 不是 blockIfFull)
type 基本类型用包装类;Duration 等要写全限定名
sourceType 这个属性实际绑定的类 ------用于 IDE 跳转。注意它可能是"被 @NestedConfigurationProperty 引用的内部类"
defaultValue 真实默认值 。它会被 ChangelogGenerator 拿去做 DEFAULT_CHANGED 检测
deprecation.level warning / error。error 会让启动时报弃用警告
deprecation.since 从哪个版本开始弃用

9.3 deprecation.level = error 的实际效果

对照第 09 章:Boot 的 @ConfigurationProperties 校验里, 弃用属性会在绑定时 触发警告/错误。而元数据里的 level 决定 IDE 和文档如何呈现,也影响 Boot 内部的弃用检查。

严谨声明 :我没有逐行核实 level = "error" 是否会导致启动失败 (我推断它只产生 ERROR 级别日志,不中断启动------因为 Boot 要兼容老配置升级)。 标注为待实测。若必须确认,可写一个用了该属性的最小应用跑一次。

9.4 4.x 的新玩法:按类拆分

除了传统的 META-INF/additional-spring-configuration-metadata.json, 4.x 支持:

bash 复制代码
META-INF/spring/configuration-metadata/additional/{全限定类名}.json

配合 @ConfigurationPropertiesSource(第 09 章讲过), 可以把一个可复用属性类的元数据单独打包 , 被多个 @ConfigurationProperties 引用时共享。

适用场景 :多个 starter 共用的属性基类 (比如 spring-boot-web-server 里的 ServerProperties 族)。


10. 实战:写一个自己的 FailureAnalyzer

这是 Boot 里性价比最高的扩展点之一------几十行代码, 能把用户最常踩的坑变成一句人话。

10.1 最简形态(函数式)

java 复制代码
public class MyFailureAnalyzer implements FailureAnalyzer {

    @Override
    public FailureAnalysis analyze(Throwable failure) {
        MyException cause = findCause(failure);
        if (cause == null) {
            return null;                      // 不是我关心的 → 让位
        }
        return new FailureAnalysis(
            "My component failed because " + cause.getDetail() + ".",
            "Check that you have configured 'my.component.url'.",
            cause);
    }
}

注册:

properties 复制代码
# META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.MyFailureAnalyzer

10.2 推荐形态(泛型 + 构造器注入)

java 复制代码
class MyFailureAnalyzer extends AbstractFailureAnalyzer<MyException> {

    private final Environment environment;

    MyFailureAnalyzer(Environment environment) {       // 由 ArgumentResolver 注入
        this.environment = environment;
    }

    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, MyException cause) {
        String configured = this.environment.getProperty("my.component.url", "<none>");
        return new FailureAnalysis(
            "My component could not connect (configured url: " + configured + ").",
            "Verify 'my.component.url' points to a reachable endpoint.",
            cause);
    }
}

三个好处:

  1. AbstractFailureAnalyzer<MyException> 自动做异常链查找 ,不用自己写 findCause
  2. 构造器注入 Environment / BeanFactory,诊断时能带上运行时状态
  3. 返回 null 即可让位给其他分析器

10.3 五条写作准则

从 Boot 的 37 个分析器里总结出来的:

准则 说明
能分析就分析,不能就返回 null 永远不要把"不确定"包装成"可能是 X"。返回 null 让下一个分析器试
action 必须是可执行的动作 "Check your configuration" 太泛;"Verify 'my.url' points to a reachable endpoint" 才好
带上运行时状态 引用具体属性值、Bean 名、类名------抽象的建议没有价值
不要抛异常 你的分析器抛异常会被 catch (Throwable) 吞掉(只 trace)。如果你抛了,用户就得不到任何诊断
不要在分析器里改变状态 分析器应该用构造器注入拿到它需要的一切,因为上下文随时可能被关闭

最后一条正是 NoSuchBeanDefinitionFailureAnalyzer 那句 "Get early as won't be accessible once context has failed to start" 的推广。

10.4 让异常自带诊断

如果你不想注册到 spring.factories,可以让异常实现 FailureAnalyzedException:

java 复制代码
public class MyException extends RuntimeException implements FailureAnalyzedException {

    @Override
    public FailureAnalysis getFailureAnalysis(Throwable rootFailure) {
        return new FailureAnalysis("...", "...", rootFailure);
    }
}

FailureAnalyzers 在列表末尾追加了 FailureAnalyzedException::analyze, 所以这类异常无需注册也能被分析。

代价 :spring.factories 里的分析器先跑, 所以如果你的异常同时被某个已注册分析器识别,那个会胜出。


11. 十二条反直觉结论

# 结论
1 @ConditionalOnMissingBean 不生成编译期元数据 ------它不在 AutoConfigureAnnotationProcessor 支持的 8 个注解里
2 @AutoConfiguration(before=) 与 @AutoConfigureBefore 完全等价(@AliasFor),二选一即可
3 @AutoConfiguration 上的 before / beforeName 是两个不同属性 ,分别对应 @AutoConfigureBefore.value / .name
4 @AutoConfiguration 被空值 的 @AutoConfigureBefore / @AutoConfigureAfter 标注------这是为 @AliasFor 提供目标
5 OnClassConditionValueExtractor 的排序让 Spring 类排在后面 (Boolean 自然序 false < true),但顺序对运行时无影响 (消费端用 Set)
6 OnBeanConditionValueExtractor 看到 name 属性就放弃生成元数据(类型存在性检查对 Bean 名无意义)
7 spring-autoconfigure-metadata.properties 缺失只降性能,不影响正确性(会降级读字节码)
8 每个被处理的类都会写一条 类名= 的空值 条目------这是 wasProcessed() 的判据
9 /actuator/conditions 的 negativeMatches 条目里同时有 matched 和 notMatched,因为一个类可能部分条件通过
10 消息里的 "did not find" 不代表不匹配 ------@ConditionalOnMissingBean 的"did not find any beans"恰恰是匹配
11 NoSuchBeanDefinitionFailureAnalyzer 在构造时 就抓取 ConditionEvaluationReport,因为上下文关闭后单例会被销毁
12 LoggingFailureAnalysisReporter 默认不打印异常栈 (只在 DEBUG),以免淹没 Description / Action

12. 动手验证清单

12.1 验证编译期元数据的生成

在一个自己写的 @AutoConfiguration 类上加 @ConditionalOnClass(SomeAbsent.class), 编译后看 target/classes/META-INF/spring-autoconfigure-metadata.properties, 期望 能看到 你的类名.ConditionalOnClass=SomeAbsent的全限定名。

前提:项目要引入 spring-boot-autoconfigure-processor 依赖。 不引依赖做一次对照实验 ------文件不会生成,但应用仍能正常启动(只是启动略慢)。

12.2 验证 @ConditionalOnMissingBean 不进元数据

在同一个类上加 @ConditionalOnMissingBean(SomeType.class), 期望 :.properties 里没有 ConditionalOnMissingBean 这个 key。

12.3 验证 @AutoConfiguration(after=) 等价于 @AutoConfigureAfter

java 复制代码
@AutoConfiguration(after = A.class)      // 写法 1
// vs
@AutoConfiguration
@AutoConfigureAfter(A.class)             // 写法 2

两种写法编译出的 .properties 里都应出现 AutoConfigureAfter=...A,内容完全一致。

12.4 验证 /actuator/conditions 的父子上下文

开 management.server.port=8081,访问管理端口的 /actuator/conditions。 期望 :contexts 有 2 个条目,子上下文条目有非空的 parentId。

12.5 验证 negativeMatches 里的 matched

写一个自动配置类,同时带一个匹配的条件和一个不匹配的条件。 期望 :它在 negativeMatches 里,且 matched 数组非空(含那个匹配的条件)。

12.6 验证失败分析器能反查条件

java 复制代码
@SpringBootApplication
public class App {
    @Autowired JdbcTemplate jdbcTemplate;    // 不加 JDBC 依赖
}

期望输出包含:

lua 复制代码
The following candidates were found but could not be injected:
  - Bean method 'jdbcTemplate' in 'JdbcTemplateAutoConfiguration' not loaded because ...

再手动 spring.autoconfigure.exclude=...JdbcTemplateAutoConfiguration 试一次, 期望 输出变成 auto-configuration 'JdbcTemplateAutoConfiguration' was excluded。

12.7 验证 Action 文案的两种形态

  • 有候选(上面 12.6 的情况)→ "Consider revisiting the entries above or defining ..."
  • 无候选(注入一个完全没人提供的类型,比如 @Autowired MyOwnType)→ "Consider defining ..."

12.8 验证分析器加载失败的隔离

在 spring.factories 里注册一个构造就抛异常 的分析器, 然后制造一个正常的可分析失败。 期望 :真正的诊断仍然输出(坏分析器只留一条 TRACE 日志)。


13. 源码索引

主题 文件 关键位置
@AutoConfiguration 四属性 autoconfigure/AutoConfiguration.java 56--105(before/after/beforeName/afterName)
自动配置元数据生成 core/spring-boot-autoconfigure-processor/.../AutoConfigureAnnotationProcessor.java 57--64(8 个注解)、67(路径)、84--106(生成器)、174--187(写入)
245--286(两个值提取器)、325--340(mergeProperties)
元数据消费 autoconfigure/AutoConfigurationMetadataLoader.java PATH 常量
配置元数据生成 configuration-metadata/spring-boot-configuration-processor/.../ConfigurationMetadataAnnotationProcessor.java 66--75(10 个注解)、195--225(process)
元数据四个路径 configuration-metadata/.../MetadataStore.java 38--48
元数据模型 configuration-metadata/spring-boot-configuration-metadata/.../ConfigurationMetadataProperty.java 29--50
元数据仓库构建 .../ConfigurationMetadataRepositoryJsonBuilder.java 34--176
变更日志工具 configuration-metadata/.../changelog/ChangelogGenerator.java 41--71
.../changelog/DifferenceType.java 24--44
条件报告端点 module/spring-boot-actuator-autoconfigure/.../condition/ConditionsReportEndpoint.java 55--73、127--136、163--218
失败分析核心 core/spring-boot/.../diagnostics/FailureAnalyzer.java 30--40
.../diagnostics/FailureAnalysis.java 24--60
.../diagnostics/AbstractFailureAnalyzer.java 32--70
.../diagnostics/FailureAnalyzers.java 62--114
.../diagnostics/LoggingFailureAnalysisReporter.java 29--54
条件↔失败分析的桥 autoconfigure/.../diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java 62--72(提前抓 report)、154--180(反查)、99--113(Action 文案)
分析器注册表 core/spring-boot/src/main/resources/META-INF/spring.factories 55--78
各模块 spring.factories 共 13 个文件、37 个分析器
手写元数据样本 module/spring-boot-activemq/src/main/resources/META-INF/additional-spring-configuration-metadata.json 全文

14. 本章未能从本仓库核实的部分

项 说明
@AutoConfiguration(value=...) 是否真能改自动配置类的 Bean 名 §1.2。取决于 Framework 的 ConfigurationClassBeanDefinitionReader,本仓库无源码。不要依赖它
deprecation.level = "error" 是否导致启动失败 §9.3。我推断只产生 ERROR 日志不中断启动,标注为待实测
ConditionsReportEndpoint 同名短类冲突 §4.3。negativeMatches 用 Map 会覆盖。按代码观察的推断,未找到 Boot 的处理
SpringFactoriesLoader.load 对 FailureAnalyzer 是否排序 §5.4 说"第一个非 null 胜出",顺序来自 spring.factories 文件顺序。未确认是否有排序介入
MetadataCollector / JsonMarshaller 的字段细节 本章未逐行展开(共 1,373 行),只覆盖了路径与流程
BeanMethods 如何匹配方法返回类型 §6.2。它读字节码做类型匹配,具体算法未逐行核实


15. 深潜:配置元数据处理器如何"看见"你的属性

上一节讲了元数据文件的产出与消费。本节打开处理器内部, 回答一个有意思的问题:它凭什么知道 private int port = 8080; 的默认值是 8080?

15.1 四种 PropertyDescriptor

处理器把"一个可绑定属性"抽象成 PropertyDescriptor,有四个子类:

子类 对应形态 关键字段
JavaBeanPropertyDescriptor getter/setter(传统 JavaBean) getter、setter、field、factoryMethod
ConstructorParameterPropertyDescriptor 构造函数参数 参数索引
RecordParameterPropertyDescriptor record 组件 组件索引
LombokPropertyDescriptor Lombok @Data 生成 特殊处理
java 复制代码
class JavaBeanPropertyDescriptor extends PropertyDescriptor {

    private final ExecutableElement setter;
    private final VariableElement field;
    private final ExecutableElement factoryMethod;

    JavaBeanPropertyDescriptor(String name, TypeMirror type, TypeElement declaringElement,
            ExecutableElement getter, ExecutableElement setter, VariableElement field,
            ExecutableElement factoryMethod) {
        super(name, type, declaringElement, getter);
        this.setter = setter;
        this.field = field;
        this.factoryMethod = factoryMethod;
    }

    @Override
    protected Element getSourceElement() {
        return getGetter();          // ← Javadoc 从 getter 上读
    }

四个字段各自的作用:

  • getter ------ 读 Javadoc 的目标元素(所以 Javadoc 要写在 getter 上才生效)
  • setter ------ 判断"是否可写"(只有 getter 没有 setter 的属性不可绑定,元数据标注)
  • field ------ 读取字段上的默认值
  • factoryMethod ------ 识别 @Bean 方法创建的对象

getSourceElement() 返回 getter 是一个重要的隐含约定:

属性的 Javadoc 要写在 getter 上,不是字段上。 写在字段上的 Javadoc 会被忽略(除非没有 getter)。

这解释了为什么 Boot 自己的 @ConfigurationProperties 类 总是把 Javadoc 写在 getter 上,即使代码里字段和 getter 紧挨着。

15.2 默认值提取:反射进 javac 内部

这是整个处理器最"黑客"的部分。

java 复制代码
@FunctionalInterface
public interface FieldValuesParser {

    FieldValuesParser NONE = (element) -> Collections.emptyMap();

    Map<String, Object> getFieldValues(TypeElement element) throws Exception;
}

默认实现 JavaCompilerFieldValuesParser 在 fieldvalues/javac/ 包下, 它通过反射访问 com.sun.source.util.Trees------javac 的内部 API:

java 复制代码
final class Trees extends ReflectionWrapper {

    private Trees(Object instance) {
        super("com.sun.source.util.Trees", instance);
    }

    Tree getTree(Element element) throws Exception {
        Object tree = findMethod("getTree", Element.class).invoke(getInstance(), element);
        return (tree != null) ? new Tree(tree) : null;
    }

    static Trees instance(ProcessingEnvironment env) throws Exception {
        try {
            ClassLoader classLoader = env.getClass().getClassLoader();
            Class<?> type = findClass(classLoader, "com.sun.source.util.Trees");
            Method method = findMethod(type, "instance", ProcessingEnvironment.class);
            return new Trees(method.invoke(null, env));
        }
        catch (Exception ex) {
            return instance(unwrap(env));        // ← 剥一层代理再试
        }
    }

    private static ProcessingEnvironment unwrap(ProcessingEnvironment wrapper) throws Exception {
        Field delegateField = wrapper.getClass().getDeclaredField("delegate");
        delegateField.setAccessible(true);
        return (ProcessingEnvironment) delegateField.get(wrapper);
    }
}

为什么必须这么绕?

标准注解处理 API(javax.lang.model)只能看到声明,看不到初始化表达式的值 。 Element 上有 getConstantValue(),但它只对 static final 编译期常量 有效。 而 private int port = 8080; 是实例字段,getConstantValue() 返回 null。

要拿到 8080,必须访问 javac 的语法树 (VariableTree 的 initializer), 而语法树 API 在 com.sun.source.util.Trees ------ 一个 JDK 内部包。

三条防御措施:

  1. ReflectionWrapper 用反射包装(而不是直接 import), 这样处理器在没有该类的 JDK上也能加载(只是功能降级)
  2. unwrap 处理 ProcessingEnvironment 被包装的情况(Gradle 的增量编译器会包装)
  3. FieldValuesParser.NONE 是"什么都不返回"的兜底实现

这是一个"能力探测 + 优雅降级"的范例 : Boot 需要 javac 内部 API 才能做到最好,但不依赖它也能工作 。 代价是拿不到默认值(元数据里 defaultValue 缺失),仅此而已。

--add-exports 的必要性 :在 JDK 9+ 模块化之后,访问 com.sun.source.util 需要 --add-exports jdk.compiler/com.sun.source.util=ALL-UNNAMED。 如果你在构建时看到元数据里 defaultValue 全丢了,多半是这个参数没配。

实践提示 :Boot 的 Gradle 插件会自动加这个参数。 但如果你用 Maven 且手动配置 annotationProcessorPaths,可能漏掉它------ 表现为"IDE 里的默认值提示没了"。

15.3 MetadataGenerationEnvironment:处理器的"运行时上下文"

这个类的职责是把所有注解类型解析成 TypeElement ,并缓存。 关键在于它对每个注解都做 null 检查 ------因为处理器可能运行在 没有对应依赖的项目里(§3.3 的三个 if 就是靠它实现的)。

它还提供了几个"元注解搜索"能力:

  • getElementsAnnotatedOrMetaAnnotatedWith(element, annotation) ------ 支持组合注解
  • getNestedConfigurationPropertyAnnotation(field) ------ 找 @NestedConfigurationProperty

@NestedConfigurationProperty 的作用 :告诉处理器 "这个字段是一个嵌套对象,请把它的属性展开到当前命名空间下"。 不加的话,处理器会把它当成一个整体类型。

对照第 09 章:这个注解也影响 Binder 的行为(嵌套绑定)。 又是一个"一个注解同时服务运行期和编译期"的例子。

15.4 一个完整的推导流程

以这个类为例:

java 复制代码
@ConfigurationProperties("my.server")
public class MyServerProperties {

    /**
     * Port to listen on.
     */
    private int port = 8080;

    public int getPort() { return this.port; }
    public void setPort(int port) { this.port = port; }
}

处理器的动作序列:

ini 复制代码
1. roundEnv.getElementsAnnotatedWith(@ConfigurationProperties)
   → 找到 MyServerProperties

2. PropertyDescriptorResolver 扫描成员
   → 发现 getPort/setPort/port 三者 → 构造 JavaBeanPropertyDescriptor
     name = "port"(从 getPort 剥离 get 前缀并 decapitalize)
     type = int(来自 getter 返回类型)

3. 读 Javadoc:getSourceElement() = getPort → 找它上面的 Javadoc
   → description  = "Port to listen on."
   → shortDescription = SentenceExtractor 抽第一句 = "Port to listen on."

4. 读默认值:FieldValuesParser.getFieldValues(MyServerProperties)
   → 走 javac Trees → 找到 VariableTree(port) 的 initializer
   → defaultValue = 8080

5. 检查标志:
   → 有 setter → 可写
   → 无 @Deprecated / @DeprecatedConfigurationProperty → 不弃用
   → 无 @NestedConfigurationProperty → 不是嵌套

6. 拼 key:prefix("my.server") + "." + name("port") → "my.server.port"
   转成小写横线形式(本例无需转换)

7. 写 ItemMetadata,最终 marshall 成 JSON

第 3 步和第 4 步是"魔法"的来源------ 一个是 Javadoc 解析,一个是 javac 内部反射。两者都失败时, 你会得到"有属性名、有类型、但没描述、没默认值"的元数据。


16. 更多失败分析器深潜

第 7 节拆了 NoSuchBeanDefinitionFailureAnalyzer。本节再看五个代表性分析器, 它们覆盖了五类不同的诊断技巧。

16.1 PortInUseFailureAnalyzer:最简洁的范例

java 复制代码
class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {

    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
        return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
                "Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
                        + "application to listen on another port.",
                cause);
    }
}

全文 15 行,但信息完整:

  • description 带具体端口号 (cause.getPort())
  • action 给了两条可执行路径(杀进程 / 换端口)

这是"分析器最小可用形态"的教科书: 不需要 BeanFactory,不需要 Environment,只要异常自带的信息就够了。

关键前提 :PortInUseException 必须携带 port 字段。 异常设计是诊断质量的基础------你的自定义异常也要把关键状态放进去。

16.2 DataSourceBeanCreationFailureAnalyzer:用 Environment 做分支诊断

java 复制代码
class DataSourceBeanCreationFailureAnalyzer extends AbstractFailureAnalyzer<DataSourceBeanCreationException> {

    private final Environment environment;

    DataSourceBeanCreationFailureAnalyzer(Environment environment) {
        this.environment = environment;
    }

    private String getDescription(DataSourceBeanCreationException cause) {
        StringBuilder description = new StringBuilder();
        description.append("Failed to configure a DataSource: ");
        if (!StringUtils.hasText(cause.getProperties().getUrl())) {
            description.append("'url' attribute is not specified and ");
        }
        description.append(String.format("no embedded datasource could be configured.%n"));
        description.append(String.format("%nReason: %s%n", cause.getMessage()));
        return description.toString();
    }

    private String getAction(DataSourceBeanCreationException cause) {
        StringBuilder action = new StringBuilder();
        action.append(String.format("Consider the following:%n"));
        if (EmbeddedDatabaseConnection.NONE == cause.getConnection()) {
            action.append(String.format("\tIf you want an embedded database (H2, HSQL or Derby), please put it on the classpath.%n"));
        }
        else {
            action.append(String.format("\tReview the configuration of %s%n.", cause.getConnection()));
        }
        action.append("\tIf you have database settings to be loaded from a particular "
                + "profile you may need to activate it").append(getActiveProfiles());
        return action.toString();
    }
}

三条诊断技巧:

  1. Action 按状态分支 ------ "classpath 上没有任何嵌入式数据库" vs "有嵌入式数据库但配置错了",给的建议完全不同
  2. getActiveProfiles() 用 Environment ------ 提示"你的数据库配置可能在某个没激活的 profile 里"。 这是只有拿到 Environment 才能给出的诊断
  3. 描述里带上"哪个属性没配" ------ 'url' attribute is not specified

第 2 条尤其值得学习 :很多"配置不生效"的问题根因是 profile 没激活, 而用户在错误信息里看不到任何 profile 线索。Boot 主动把当前激活的 profile 打出来。

16.3 BeanCurrentlyInCreationFailureAnalyzer:从异常栈重建循环依赖

这是技术上最复杂的一个------它要从嵌套的异常栈 里重建出依赖环。

java 复制代码
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, BeanCurrentlyInCreationException cause) {
    DependencyCycle dependencyCycle = findCycle(rootFailure);
    if (dependencyCycle == null) {
        return null;
    }
    return new FailureAnalysis(buildMessage(dependencyCycle), action(), cause);
}

findCycle 的算法:

java 复制代码
private @Nullable DependencyCycle findCycle(Throwable rootFailure) {
    List<BeanInCycle> beansInCycle = new ArrayList<>();
    Throwable candidate = rootFailure;
    int cycleStart = -1;
    while (candidate != null) {
        BeanInCycle beanInCycle = BeanInCycle.get(candidate);
        if (beanInCycle != null) {
            int index = beansInCycle.indexOf(beanInCycle);
            if (index == -1) {
                beansInCycle.add(beanInCycle);
            }
            cycleStart = (cycleStart != -1) ? cycleStart : index;
        }
        candidate = candidate.getCause();
    }
    if (cycleStart == -1) {
        return null;
    }
    ...
}

思路:

  1. 沿 getCause() 链从上往下走,每遇到一个"能识别出 Bean"的异常就记录
  2. 如果某个 Bean 第二次出现 (indexOf != -1),说明成环了
  3. cycleStart 记录环的起点

为什么循环依赖的异常栈是这样的? 因为 Spring 创建 Bean 时, 每个"正在创建中"的 Bean 都会在下层触发新的 BeanCurrentlyInCreationException, 层层嵌套。栈的深度 = 环的长度,栈底是最内层的 Bean。

Action 也有分支:

java 复制代码
private String action() {
    if (this.beanFactory != null && this.beanFactory.isAllowCircularReferences()) {
        return "Despite circular references being allowed, the dependency cycle between beans could not be "
                + "broken. Update your application to remove the dependency cycle.";
    }
    return "Relying upon circular references is discouraged and they are prohibited by default. "
            + "Update your application to remove the dependency cycle between beans. "
            + "As a last resort, it may be possible to break the cycle automatically by setting "
            + "spring.main.allow-circular-references to true.";
}

两种文案对应两种状态:

  • 已经允许循环引用了还失败 → "即使允许也破不了环"(比如构造器注入的环)
  • 默认禁止 → 提示可以开 spring.main.allow-circular-references

对照第 13 章:Boot 默认 setAllowCircularReferences(false)。 所以第二条是默认路径。

注意措辞 : "Relying upon circular references is discouraged " + "As a last resort " ------ Boot 在给退路的同时明确表达不推荐。 这是"给用户绳子但不鼓励上吊"的文案艺术。

16.4 NoUniqueBeanDefinitionFailureAnalyzer:列出所有候选

java 复制代码
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoUniqueBeanDefinitionException cause,
        @Nullable String description) {
    String[] beanNames = extractBeanNames(cause);
    if (beanNames == null) {
        return null;
    }
    StringBuilder message = new StringBuilder();
    message.append(String.format("%s required a single bean, but %d were found:%n",
            (description != null) ? description : "A component", beanNames.length));
    for (String beanName : beanNames) {
        buildMessage(message, beanName);
    }
    ...
    return new FailureAnalysis(message.toString(), action.toString(), cause);
}

private void buildMessage(StringBuilder message, String beanName) {
    try {
        BeanDefinition definition = this.beanFactory.getMergedBeanDefinition(beanName);
        message.append(getDefinitionDescription(beanName, definition));
    }
    catch (NoSuchBeanDefinitionException ex) {
        message.append(String.format("\t- %s: a programmatically registered singleton%n", beanName));
    }
}

private String getDefinitionDescription(String beanName, BeanDefinition definition) {
    if (StringUtils.hasText(definition.getFactoryMethodName())) {
        return String.format("\t- %s: defined by method '%s' in %s%n", beanName,
                definition.getFactoryMethodName(), getResourceDescription(definition));
    }
    return String.format("\t- %s: defined in %s%n", beanName, getResourceDescription(definition));
}

两个技巧:

  1. getMergedBeanDefinition ------ 合并父子定义,拿到完整信息
  2. catch (NoSuchBeanDefinitionException) → "programmatically registered singleton" ------ 手动 registerSingleton 的 Bean 没有定义,但它也是候选,必须列出来。 (对照第 15 章 §6.11 的"双通道"设计------同一个问题,同一种处理。)

Action 给三条路:

sql 复制代码
Consider marking one of the beans as @Primary,
updating the consumer to accept multiple beans,
or using @Qualifier to identify the bean that should be consumed

这是一个"穷举所有可行解"的 action 范例------不替用户做决定, 而是把所有方案列出来。

16.5 AbstractInjectionFailureAnalyzer:自动补全"谁要求注入"

前四个分析器里多次出现 description 参数,它来自这个基类:

java 复制代码
public abstract class AbstractInjectionFailureAnalyzer<T extends Throwable> extends AbstractFailureAnalyzer<T> {

    @Override
    protected final @Nullable FailureAnalysis analyze(Throwable rootFailure, T cause) {
        return analyze(rootFailure, cause, getDescription(rootFailure));
    }

    private @Nullable String getDescription(Throwable rootFailure) {
        UnsatisfiedDependencyException unsatisfiedDependency = findMostNestedCause(rootFailure,
                UnsatisfiedDependencyException.class);
        if (unsatisfiedDependency != null) {
            return getDescription(unsatisfiedDependency);
        }
        BeanInstantiationException beanInstantiationException = findMostNestedCause(rootFailure,
                BeanInstantiationException.class);
        if (beanInstantiationException != null) {
            return getDescription(beanInstantiationException);
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private <C extends Exception> @Nullable C findMostNestedCause(Throwable root, Class<C> type) {
        Throwable candidate = root;
        C result = null;
        while (candidate != null) {
            if (type.isInstance(candidate)) {
                result = (C) candidate;
            }
            candidate = candidate.getCause();
        }
        return result;
    }
}

findMostNestedCause 与 findCause 的区别:

方法 行为
AbstractFailureAnalyzer.findCause 找到第一个匹配就返回(最外层)
AbstractInjectionFailureAnalyzer.findMostNestedCause 找到最后一个匹配(最内层)

为什么这里要最内层? 因为 UnsatisfiedDependencyException 的嵌套链是 "A 依赖 B,B 依赖 C,C 依赖 D(失败)"。真正需要注入的是 D, 也就是最内层的那个。取最外层会错误地描述成"A 要求..."。

getDescription 的两种形态(节选):

java 复制代码
private @Nullable String getDescription(UnsatisfiedDependencyException ex) {
    InjectionPoint injectionPoint = ex.getInjectionPoint();
    if (injectionPoint != null) {
        if (injectionPoint.getField() != null) {
            return String.format("Field %s in %s", injectionPoint.getField().getName(),
                    injectionPoint.getField().getDeclaringClass().getName());
        }
        ...
    }
}

这就产生了那句经典的:

bash 复制代码
Field dataSource in com.example.MyService required a bean of type '...' that could not be found.

"最内层 + 注入点信息"的组合 让诊断精确到字段级。 这是 Boot 诊断质量高的一个关键技术点。


17. 接入点:SpringBootExceptionReporter 是怎么被装配的

前面讲了分析器本身,本节讲它怎么被 SpringApplication 调用。

17.1 接口

java 复制代码
public interface SpringBootExceptionReporter {
    boolean reportException(Throwable failure);
}

FailureAnalyzers 是它的唯一内置实现。

17.2 装配时机

在第 04 章讲的 SpringApplication.run() 里,异常报告器在构造阶段就被加载:

scss 复制代码
new SpringApplication(...)
  → this.exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, ...)

注意 spring.factories 里注册的是 FailureAnalyzers,不是分析器本身:

properties 复制代码
# core/spring-boot/src/main/resources/META-INF/spring.factories:29-30
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

FailureAnalyzers 的构造需要 ConfigurableApplicationContext , 而 run() 构造阶段还没有上下文。所以:

  • 构造时机:SpringApplication 实例化时(传 null context)
  • 使用时机:handleRunFailure 时重新构造一个带 context 的实例

严谨声明 :FailureAnalyzers 的公开构造器接受 @Nullable ConfigurableApplicationContext, 但 SpringBootExceptionReporter 由 SpringFactoriesLoader 实例化时 只会用无参或 ArgumentResolver 可解析的构造器 。 按我对代码的理解,reportFailure 路径会新建一个 FailureAnalyzers(context)。 本章未能逐行核实 SpringApplication.reportFailure 的具体实现 (第 12 章讲过它在 finally 块中), 标注为待核实。

17.3 为什么"在 finally 里"很重要

第 12 章讲过:

java 复制代码
private RuntimeException handleRunFailure(...) {
    try {
        // ... 各种清理
    }
    finally {
        reportFailure(this.exceptionReporters, failure);
    }
    ...
}

finally 保证两件事:

  1. 即使 listeners.failed() 抛异常,诊断仍然输出
  2. 即使 context.close() 抛异常,诊断仍然输出

这与 §5.3 讲的 FailureHandler.logging、§5.4 讲的 catch (Throwable) 是同一条原则的三次体现:

诊断链路本身绝不能失败沉默。

三次防御:

层级 防御
分析器加载失败 FailureHandler.logging(logger) ------ 跳过这一个
分析器执行抛异常 catch (Throwable) + TRACE ------ 跳过这一个
报告流程整体异常 finally ------ 无论如何都尝试

18. 一个完整的 /actuator/conditions 响应示例

下面是带注释的简化响应(字段顺序按 @JsonPropertyOrder):

json 复制代码
{
  "contexts": {
    "application": {
      "positiveMatches": {
        "RedisAutoConfiguration": [
          {
            "condition": "OnClassCondition",
            "message": "@ConditionalOnClass found required class 'org.springframework.data.redis.core.RedisOperations'"
          },
          {
            "condition": "OnBeanCondition",
            "message": "@ConditionalOnMissingBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) did not find any beans"
          }
        ]
      },
      "negativeMatches": {
        "DataSourceAutoConfiguration": {
          "notMatched": [
            {
              "condition": "OnClassCondition",
              "message": "@ConditionalOnClass did not find required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'"
            }
          ],
          "matched": []
        },
        "JdbcTemplateAutoConfiguration": {
          "notMatched": [
            {
              "condition": "OnClassCondition",
              "message": "@ConditionalOnClass did not find required class 'org.springframework.jdbc.core.JdbcTemplate'"
            }
          ],
          "matched": [
            {
              "condition": "OnBeanCondition",
              "message": "@ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans"
            }
          ]
        }
      },
      "exclusions": [],
      "unconditionalClasses": [
        "org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration"
      ],
      "parentId": null
    }
  }
}

逐条解读:

条目 读法
RedisAutoConfiguration 在 positive 生效了。注意第二条消息里有 "did not find any beans" 但它是匹配
DataSourceAutoConfiguration 的 matched: [] 预筛阶段被打死 ------后续条件(@ConditionalOnMissingBean)根本没跑
JdbcTemplateAutoConfiguration 的 matched 有 1 条 它通过了某个条件(@ConditionalOnSingleCandidate... 等等)

等等,最后一条要小心 :@ConditionalOnSingleCandidate(DataSource.class) 的消息是 "did not find any beans",按 §16.5 的判据它应该是不匹配 (isAllMatched() 为 false)。 但它出现在 matched 数组里------

这说明我对 JdbcTemplateAutoConfiguration 的实际条件组合是臆测的 。 上面这个 JSON 是我为说明结构而构造的例子 ,不是真实输出。 真实场景下 matched 里出现的应该是真正匹配 的条件。 我在此明确说明:这是示意,不是实测快照。 真实输出请用 §12.4 的方法自己抓一次。

unconditionalClasses 里那条 ConfigurationPropertiesAutoConfiguration 是真实的(第 09 章讲过它无条件注册 ConfigurationPropertiesBinder 等基础设施)。


19. 一条常被忽略的实践建议

把本章的四块内容放在一起,得到一个可操作的结论:

给你的 Starter 配齐三样东西,spring-configuration-metadata.json、 additional-spring-configuration-metadata.json、一个 FailureAnalyzer。

理由:

  1. 元数据决定用户在 IDE 里的体验(有没有补全、有没有文档)
  2. 补充元数据决定那些"自动推导不出来"的信息(默认值、弃用)
  3. 失败分析器决定用户踩坑时能不能自救

Boot 自己的 128 个模块每一个 都至少配了前两样, 37 个分析器覆盖了最常见的踩坑场景。 这不是" nice to have",是 Starter 的交付标准。

反过来,如果你引入一个第三方 starter 发现:

  • 写配置没有补全 → 它没引入 spring-boot-configuration-processor
  • 报错是裸的 NoSuchBeanDefinitionException 没有"Consider defining..." → 它没配 FailureAnalyzer(或者它的自动配置压根没条件,所以无从分析)

这两条也可以用来快速评估一个 starter 的质量。


20. 元数据的第三块:hints(值提示)

前面两节讲了 properties(属性清单)和 groups(分组)。 元数据文件还有第三块:hints------告诉 IDE 一个属性可以填哪些值。

20.1 三个类型

类型 位置 作用
ItemHint(处理器侧)/ Hints(运行期侧) 顶层 hints 数组 一个属性的值提示容器
ValueHint ItemHint.values 一个具体候选值 + 描述
ValueProvider ItemHint.providers 一个动态值提供者(名字 + 参数)
java 复制代码
public class ItemHint implements Comparable<ItemHint> {
    private final String name;             // 属性名
    private final List<ValueHint> values;
    private final List<ValueProvider> providers;
}

ItemHint 实现 Comparable(按 name 排序)------又一处"输出确定性"的保证。

20.2 值提示 vs 值提供者

java 复制代码
public class ValueHint implements Serializable {
    private Object value;              // 候选值本身
    private String description;        // 完整描述
    private String shortDescription;   // 单句描述
}

public class ValueProvider implements Serializable {
    private String name;                                    // 提供者名
    private final Map<String, Object> parameters = new LinkedHashMap<>();
}

核心区别:

  • ValueHint 是静态枚举 ------ 值的集合在编译期就固定了。 比如 logging.level.* 的 TRACE/DEBUG/INFO/WARN/ERROR。
  • ValueProvider 是动态查询 ------ 值的集合在运行时才知道。 比如 spring.profiles.active 的候选值是"当前存在的所有 profile", 或者 logging.level.<logger-name> 的候选是"所有 logger 名"。

ValueProvider 的 Javadoc 说得很明确:

Each provider is defined by a name and can have an arbitrary number of parameters. The available providers are defined in the Spring Boot documentation.

也就是说,provider 的名字是一个开放的命名空间 ------ Boot 定义了一批(spring-boot-configuration-metadata 里的 ValueProvider 只是数据载体, 不含任何 provider 实现),IDE 插件按名字识别。

这带来一个约束 :你不能自定义 ValueProvider 实现并期望 IDE 支持------ 只能用 Boot 文档里列出的那些名字。这和 ValueHint 的"随便写"形成对比。

20.3 Map 类型的 .keys / .values 后缀规则

ItemHint 的类级 Javadoc 有一条重要规则:

The name of the hint is the name of the related property with one major exception for map types as both the keys and values of the map can have hints . In such a case, the hint should be suffixed by .keys or .values respectively. Creating a hint for a map using its property name is therefore invalid.

json 复制代码
{
  "hints": [
    {
      "name": "logging.level.keys",      // ← Map 的键提示
      "providers": [{ "name": "logger-names" }]
    },
    {
      "name": "logging.level.values",    // ← Map 的值提示
      "values": [
        { "value": "trace", "description": "..." },
        { "value": "debug", "description": "..." }
      ]
    }
  ]
}

为什么必须加后缀? 因为 logging.level 是一个 Map<String, String>:

  • 键 (.keys)= logger 名(com.example.Foo)
  • 值 (.values)= 日志级别(debug)

两者的候选集合完全不同,用同一个 hint 无法表达。

"用属性名本身给 Map 加 hint 是无效的" ------ 这是硬规则, 违反了不会报错,但 IDE 不会生效(静默失效)。

20.4 ItemHint 的名字规范化

java 复制代码
private String toCanonicalName(String name) {
    int dot = name.lastIndexOf('.');
    if (dot != -1) {
        String prefix = name.substring(0, dot);
        String originalName = name.substring(dot);
        return prefix + ConventionUtils.toDashedCase(originalName);
    }
    return ConventionUtils.toDashedCase(name);
}

只把最后一段转成横线形式,前缀保持原样。

原因:.keys / .values 这些后缀 和包路径不能 被转成横线。 比如 logging.level.keys ------ 若整体转横线,.keys 会变成 .keys(本来就是), 但 spring.my-map.keys 里的 my-map 必须保持。 保守处理:只动最后一段。

applyPrefix 用于嵌套属性组:

java 复制代码
public ItemHint applyPrefix(String prefix) {
    return new ItemHint(ConventionUtils.toDashedCase(prefix) + "." + this.name, this.values, this.providers);
}

20.5 ItemMetadata 的完整字段

处理器侧的属性模型:

java 复制代码
public class ItemMetadata {
    private final ItemType itemType;      // PROPERTY / GROUP
    private String name;
    private String type;
    private String description;
    private String sourceType;            // 声明这个属性的类的全限定名
    private String sourceMethod;          // 若属性由 @Bean 方法暴露,这里记方法签名
    private Object defaultValue;
    private ItemDeprecation deprecation;
}

sourceMethod 是一个容易被忽略但很有用的字段:

当一个属性的来源不是字段而是一个 @Bean 方法的返回类型时 (比如 Actuator 端点的属性),sourceMethod 记录方法签名, 让 IDE 能跳到方法而不是类。

itemType 区分 PROPERTY 和 GROUP ------ 对应 JSON 里的 properties 和 groups 两个数组。 GROUP 条目没有 type(它只是一个命名空间)。

20.6 一个带 hints 的完整示例

json 复制代码
{
  "groups": [
    {
      "name": "my.server",
      "type": "com.example.MyServerProperties",
      "sourceType": "com.example.MyServerProperties"
    }
  ],
  "properties": [
    {
      "name": "my.server.port",
      "type": "java.lang.Integer",
      "description": "Port to listen on.",
      "sourceType": "com.example.MyServerProperties",
      "defaultValue": 8080
    },
    {
      "name": "my.server.protocol",
      "type": "java.lang.String",
      "description": "Protocol to use.",
      "sourceType": "com.example.MyServerProperties",
      "defaultValue": "http"
    }
  ],
  "hints": [
    {
      "name": "my.server.protocol",
      "values": [
        { "value": "http",  "description": "Plain HTTP." },
        { "value": "https", "description": "HTTP over TLS." }
      ]
    }
  ]
}

IDE 的表现:

  • 输入 my.server. → 列出 port 和 protocol
  • 悬浮 port → 显示 "Port to listen on." + "Default: 8080"
  • 输入 my.server.protocol= → 弹出 http / https 下拉

三块数据缺一不可 :只有 properties 没有 hints,用户不知道能填什么; 只有 hints 没有 properties,IDE 不知道有这个属性。

20.7 为什么元数据要区分"编译期模型"和"运行期模型"

注意这里有两套模型:

模块 用途
编译期 spring-boot-configuration-processor 的 metadata/ 包(ItemMetadata、ItemHint...) 注解处理器写 JSON
运行期 spring-boot-configuration-metadata 的 ConfigurationMetadataProperty、Hints... 读 JSON

为什么要两套? 因为它们的职责不同:

  • 编译期模型要能增量修改 (MetadataCollector 累积、mergeProperties 合并), 所以字段可变、有 setter
  • 运行期模型是只读快照 ,面向查询(getAllProperties()、findHints())

但它们之间有一个隐含耦合 :JSON schema 必须一致。 Boot 用 JsonConverter(211 行)在编译期模型和运行期 JSON 结构之间转换。

这是一种"用中间格式(JSON)解耦两个模块"的设计------ 编译期模块不依赖运行期模块,反之亦然。 代价是 schema 演进要同时改两边(Boot 用测试覆盖这一点)。

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

  1. AutoConfigurationSorter 留下的口子本章已标注待核实,未给出结论。
  2. 配置元数据注解处理器的完整输出格式未展开。
  3. FailureAnalyzer 的全集未统计。
  4. ConditionEvaluationReport 在 native 下是否可用未验证。
  5. 排序与 spring-autoconfigure-metadata.properties 的交互未实测。

22. 下一章

卷四(13--16 章)到此结束。四章串起来是一条完整的主线:

复制代码
13:原始 Bean 定义从哪来        (BeanDefinitionLoader、组件扫描、Bean 命名)
14:自动配置类怎么被选出并排序  (ImportCandidates、ImportSelector、Sorter)
15:每个类怎么决定生不生效      (条件注解体系)
16:怎么知道结果                (编译期元数据、条件报告、失败分析)

这条主线只回答了一半的问题。 前四章讲的是"Bean 定义怎么被造出来", 但容器启动之后发生了什么还没讲:

  • WebApplicationType 是怎么推断的?三种类型分别创建什么上下文?
  • 内嵌服务器(Tomcat / Jetty / Netty)什么时候启动、怎么拿到端口?
  • 为什么 Servlet 环境下 DispatcherServlet 是懒注册的?
  • WebFlux 的 ReactiveWebServerApplicationContext 与 Servlet 的差别在哪?
  • 4.x 新增的 gRPC 模块是怎么接入这套体系的?

第 17 章:应用类型推断与上下文工厂 ------ WebApplicationType 与 ApplicationContextFactory。

相关推荐
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 14 章 · 自动配置引擎:`AutoConfigurationImportSelector` 全流程
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 24 章 · 日志系统:`LoggingApplicationListener` 与结构化日志
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 22 章 · 数据源、连接池与事务
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 17 章 应用类型推断与上下文工厂:`WebApplicationType`
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 23 章 · JPA / Hibernate 与持久化模块
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 12 章 · 事件总线与 Bootstrap 上下文
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 05 章 · `ApplicationProperties` 与 `spring.main.*`:启动配置
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 07 章 · ConfigData 体系深潜:位置解析、profile 迭代求解
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 20 章 · WebFlux 与 Reactor Netty:响应式栈与编解码
java·后端·开源