第十五章 · 开箱起馆:Spring Boot 自动配置与 starter
下一章第⼗五章 · 开箱起馆:Spring Boot ⾃动配置与 starter"你什么都没写,我就替你把馆开起来了。"——这是⼀个 starter 在 pom.xml ⾥写下⾃⼰名字时的全部台词。
商旅推⻔进来,只想喝⼝茶、换张⽂牒。可他还没坐下,前台掌柜已经在那⼉了,藏经阁的⻔也开着,差役调度房⾥也站好了⼈。他什么都没安排——驿站套装替他把⼀座馆从⽆到有地起好了。
这就是 Spring Boot starter 给 Spring AI 的承诺:约定优先,⼀⾏依赖起⼀座馆。但越是"开箱即⽤",越值得我们追问⼀句:这座馆到底是谁、在什么条件下、悄悄替我们搭起来的?谁⼜能在它搭好之前把它拦下、换成⾃⼰的⼈?
这⼀章,我们只盯住⼀个样本—— OpenAiChatAutoConfiguration ,把"⾃动配置"这件被⽆数博客⼀句话带过的事,拆到⻣头⾥。
⼀、三层之分:馆⼼ / 装配房 / 套装铺先把⼯程的⻣架⽴稳。Spring AI 的设计⽂档把整个仓库切成三层,这不是组织代码的随意分包,⽽是⼀条机器校验的硬边界( design/02-boot-modularity.adoc:9-
44 ):
馆⼼(core): spring-ai-openai 、 spring-ai-model 、 spring-ai-client-chat ……这些不依赖 Spring Boot,能在裸 Spring Framework、甚⾄纯
main() ⾥ new 出来。装配房(auto-configurations):依赖 Boot,⽤ @AutoConfiguration ⼲装配的脏活。设计⽂档要求它对底层库"classpath 不在则零影响",所有⾮测试依赖都标 optional 。
套装铺(starters):不含⼀⾏代码,只负责把"某个 autoconfig + 传递依赖"聚成⼀包。
最狠的是第三句:Spring AI 2.0 ⽤ maven-enforcer-plugin 的 bannedDependencies 规则,在编译期机器化地禁⽌"core 依赖 boot"( design/02-boot-modularity.adoc:37-44 )。也就是说,"馆⼼可裸跑"不是⼀句⼝号,是构建⼯具站岗守着的纪律。这⼀层我们下⼀章会专⻔展开,本章只需记住:autoconfig 是馆⼼与Boot 之间那道"装配房",starter 是把装配房和家具⼀起打包的铺⼦。
⼆、套装铺⾥没有家具:starter ⻓什么样打开 OpenAI 的套装铺 spring-ai-starter-model-openai/pom.xml ,你会发现它名副其实地"空"——通篇只有依赖,没有⼀个 .java :
<!-- starters/spring-ai-starter-model-openai/pom.xml:32-79 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId> <!--装配房 -->
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId> <!--馆⼼ -->
</dependency>
<dependency><groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
</dependencies>⼀张清单读下来,套装铺的职责清清楚楚:把 OpenAI 这条线要的"装配房 + 馆⼼ + 前台掌柜的装配房 + 起居注的装配房 + 跑 HTTP 的客户端"⼀并请进来。 它⾃⼰不⼲活,它只负责"把该来的⼈都叫⻬"。
🦴 ⻣灰级细节:starter ⾥为什么找不到 spring-boot-starter ?
很多⼈以为 model starter 会显式依赖那个最基础的 spring-boot-starter ——它才是把 spring-boot-autoconfigure 带进来、让 .imports ⽂件⽣效的关键。但这份 pom ⾥压根没有它,只有 spring-boot-starter-webclient 和 spring-boot-starter-restclient 。
奥妙在传递依赖:这两个 web 客户端 starter ⾃⼰就依赖了 spring-boot-starter ,于是 spring-boot-autoconfigure 被间接拉了进来。Spring AI 在这⾥偷了个巧——它知道 OpenAI 这条线⽆论如何都要发 HTTP,索性让 HTTP 客户端 starter 顺⼿把 Boot 的⾃动配置基建⼀并带⼊,省掉⼀⾏显式依赖。
代价是:这条传递链是隐式的。哪天有⼈把 webclient/restclient 这两个 starter <exclusion> 掉、换成别的 HTTP 栈, spring-boot-autoconfigure 可能也跟着没了, .imports 静默失效, OpenAiChatModel 这个 Bean 就凭空消失——⽽错误信息只会是冷冰冰的 NoSuchBeanDefinitionException ,不会有任何⼈告诉你"是因为你拆掉了那条隐式链"。便利与可追溯,这⾥做了⼀次取舍。
三、 .imports :装配房的花名册starter 把装配房请进了 classpath,Boot ⼜是怎么知道"装配房⾥有哪⼏位先⽣该上⼯"的?靠的是⼀张花名册—— META-
INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ( auto-configurations/.../model-
openai/.../AutoConfiguration.imports ):org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfigurationorg.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfigurationorg.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfigurationorg.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfigurationorg.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfigurationorg.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
⼀个 OpenAI 装配房⾥,六位"先⽣"各管⼀摊:聊天、嵌⼊、图像、语⾳合成、语⾳转写、内容审核。Boot 启动时扫到这张花名册,就把这六个候选类全部"登记在册"。
注意"登记"不等于"上⼯"。花名册只是把它们送进候选名单——真正决定谁上⼯、谁回家,是每个类身上挂的那⼀串条件注解。这正是下⼀节的主⻆。
四、三段式:⼀个 ChatModel Bean 是怎么炼成的现在,我们终于⾛到本章的主菜—— OpenAiChatAutoConfiguration 的全貌。它短得令⼈意外,却把"⾃动配置"的三段式范式演示得淋漓尽致:
// auto-configurations/.../OpenAiChatAutoConfiguration.java:53-92@AutoConfiguration
@EnableConfigurationProperties({ OpenAiCommonProperties.class, OpenAiChatProperties.class })
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OPENAI,
matchIfMissing = true)
public class OpenAiChatAutoConfiguration {@Bean@ConditionalOnMissingBean
public OpenAiChatModel openAiChatModel(OpenAiCommonProperties commonProperties, OpenAiChatProperties chatProperties,
ToolCallingManager toolCallingManager, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<MeterRegistry> meterRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention,
ObjectProvider<OpenAiHttpClientBuilderCustomizer> httpClientBuilderCustomizers) {
var resolvedProperties = OpenAiAutoConfigurationUtil.resolveCommonProperties(commonProperties, chatProperties);// ...构造官⽅ SDK的 sync/async client ...
var chatModel = OpenAiChatModel.builder()
.openAiClient(openAIClient)
.openAiClientAsync(openAIClientAsync)
.options(chatProperties.toOptions())
.toolCallingManager(toolCallingManager)
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.meterRegistry(meterRegistryToUse)
.build();
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
}把这块拆成三段,你就拿到了 Spring AI 所有 model autoconfig 的通⽤配⽅。
第⼀段: @AutoConfiguration + @EnableConfigurationProperties —— 绑定⽂牒条款
@AutoConfiguration 把这个类标成"⾃动配置候选"。 @EnableConfigurationProperties({OpenAiCommonProperties, OpenAiChatProperties}) 把⽤户在application.yml ⾥写的 spring.ai.openai.* (base-url、api-key、model、temperature……)绑进两个强类型属性对象——⼀个放跨模型的公共条款(base-
url/api-key),⼀个放聊天专属条款(temperature/maxTokens)。⽤译馆的话说:这⼀段是把客官⼿写在⽂牒上的条款,翻译成驿丞看得懂的标准表格。 commonProperties 与 chatProperties 之后会被
resolveCommonProperties 合并(common 兜底、chat 覆盖),再 chatProperties.toOptions() 成模型选项。
第⼆段: @ConditionalOnProperty(...matchIfMissing=true) —— 选哪位番邦
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, // = "spring.ai.model.chat"
havingValue = SpringAIModels.OPENAI, // = "openai"
matchIfMissing = true)
SpringAIModelProperties.CHAT_MODEL 解析出来是 spring.ai.model.chat ( spring-ai-
model/.../SpringAIModelProperties.java:27 ), SpringAIModels.OPENAI 是字符串 "openai" ( SpringAIModels.java:41 )。这是设计上最漂亮的⼀笔:同⼀类模型,所有 provider 共⽤同⼀个开关。你 classpath ⾥同时塞了 OpenAI 和 Anthropic 两套 starter,只要spring.ai.model.chat=anthropic ,OpenAI 这条就因条件不满⾜⽽整体不装配,Anthropic 那条上⼯。⼀个属性,在多个互斥的 provider 之间做单选——⼲净、可切换、零代码改动。
✨ 爽点: matchIfMissing = true 的"零配置即⽤"这四个字是"开箱即⽤"体验的灵魂。它的意思是:这个属性压根没写,也当作匹配。 于是新⼿ demo ⾥只放⼀个 OpenAI starter、 application.yml ⾥只填⼀个api-key,什么 spring.ai.model.chat 都不写,OpenAI 的聊天模型照样起得来。约定替你做了"你⼤概率就是要 OpenAI"这个默认判断。
第三段: @Bean @ConditionalOnMissingBean —— 造模型,且让你能夺权@Bean@ConditionalOnMissingBean
public OpenAiChatModel openAiChatModel(...) { ... }@ConditionalOnMissingBean 是整套约定优先哲学的"逃⽣⼝":只有当容器⾥还没有同类型 Bean 时,我才造⼀个默认的。 反过来说——只要你⾃⼰ @Bean 了⼀个 OpenAiChatModel (或更宽的 ChatModel ),⾃动配置就识趣地闭嘴,把控制权完整交还给你。约定不是枷锁,是"你不管我才管"。
Bean ⽅法体内,真正的造物逻辑⽤ core 的 OpenAiChatModel.builder() 完成。这⾥要点破⼀处"新旧之变":builder 上挂的是 .openAiClient(...) 和
.openAiClientAsync(...) ——Spring AI 2.0 已经封装了官⽅的 OpenAI Java SDK( com.openai.client.OpenAIClient ),⽽不再是早期那个⼿写的OpenAiApi 。 装配房的活⼉,从"⾃⼰缝 HTTP 客户端",退化成了"把官⽅ SDK 配好塞进去",这是个值得记⼀笔的演进。
五、 ObjectProvider :软注⼊的艺术第三段⾥最⻅功⼒的,是那⼀串 ObjectProvider<...> 参数。它们对应译馆⾥⼏个"有则⽤、⽆则免"的配套设施:账房( ObservationRegistry 观测)、度量
( MeterRegistry )、⾃定义观测约定( ChatModelObservationConvention )、HTTP 客户端定制器。为什么不直接注⼊ ObservationRegistry ,⾮要裹⼀层 ObjectProvider ?因为直接注⼊意味着"必须存在",缺了就启动失败。⽽ Spring AI 的 core 类⽴过⼀条规矩:它只依赖 Micrometer 的 ObservationRegistry 接⼝,构造期可以是 NOOP (空实现)。 ObjectProvider 正是把"必须有"软化成"有最好,没有给个安全默认"的⼯具:
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
getIfUnique(...) :容器⾥有且仅有⼀个 ObservationRegistry 才⽤它,否则退回 ObservationRegistry.NOOP 。于是——你没引⼊ Micrometer,模型照样跑,埋点变成空操作,缺依赖零影响;你引⼊了 Micrometer 并配了 registry,同⼀⾏代码⾃动把真实账房接上,模型调⽤就被观测包住了。这正是研究笔记⾥那条"core 不依赖 boot、autoconfig 负责把真实 registry 注⼊"的活体证据。
⽽这个被软注⼊的 registry,最终会在馆⼼深处的埋点调⽤点真正发挥作⽤:
// models/spring-ai-openai/.../OpenAiChatModel.java:212-217
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ChatCompletion chatCompletion = this.openAiClient.chat().completions().create(request);// ...
});observe(() -> { 真正的 HTTP 调⽤ }) ——观测像⼀层透明的纱,把对番邦的实际请求整个包住。装配房软注⼊的那个 registry,在这⾥决定了这层纱是真账本还是空⽓。
⚠ 暗礁⼀: ObjectProvider 软注⼊ ≠ 所有依赖都软
别被那⼀排 ObjectProvider 迷惑,以为 Bean ⽅法的每个参数都是"缺了也⾏"。看第⼀个⾮ Provider 参数:
ToolCallingManager toolCallingManager, //硬注⼊,不是 ObjectProvider!
toolCallingManager 是裸类型直注——它在容器⾥必须存在,缺了这个 Bean,整个 openAiChatModel ⽅法连参数都凑不⻬,启动直接报错。
那它从哪来?来⾃另⼀个装配房 ToolCallingAutoConfiguration ,它⽤ @Bean @ConditionalOnMissingBean 给你兜底造了⼀个带 NOOP 观测的默认
manager( auto-configurations/models/tool/.../ToolCallingAutoConfiguration.java )。也就是说,"差役调度房"的"缺失给默认"发⽣在另⼀处autoconfig,⽽不是在这个 Bean ⽅法签名⾥。
这是研究笔记需要被订正的⼀处:笔记把 toolCallingManager 也写成了" ObjectProvider 软注⼊、缺失给 NOOP",但源码⾥它是硬注⼊,默认值由ToolCallingAutoConfiguration 在别处提供。结论⼀样安全(⽤户什么都不配也能跑),但机制不同——⼀个是⽅法签名内的 getIfUnique ,⼀个是跨 autoconfig的 @ConditionalOnMissingBean 兜底 Bean。这种细节,正是"⻣灰级"和"差不多"的分⽔岭。
六、同构:VectorStore / Advisor / ChatClient 都是同⼀张配⽅这套三段式不是 OpenAI 聊天的专利,⽽是全⼯程的统⼀范式。换个⼦系统,换汤不换药:
// auto-configurations/vector-stores/.../PgVectorStoreAutoConfiguration.java:49,54
@ConditionalOnProperty(name = SpringAIVectorStoreTypes.TYPE, havingValue = SpringAIVectorStoreTypes.PGVECTOR,
matchIfMissing = true)// ...@Bean@ConditionalOnMissingBeanPgVectorStore ...
VectorStore 把第⼆段的开关从 spring.ai.model.chat 换成了 spring.ai.vectorstore.type ,选 provider 的逻辑⼀模⼀样; @Bean@ConditionalOnMissingBean 让你能换⾃⼰的藏经阁。
ChatClient(前台掌柜)的装配房则示范了更精细的条件编排:
// auto-configurations/.../ChatClientAutoConfiguration.java:67,99
@AutoConfiguration(after = ToolCallingAutoConfiguration.class) //排在差役调度房之后// ...@Bean@ConditionalOnMissingBean
@ConditionalOnBean(ToolCallingManager.class) //有调度房才装⼯具驿丞
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(...) { ... }
@AutoConfiguration(after = ...) 保证装配顺序——前台掌柜在差役调度房就位之后才上⼯; @ConditionalOnBean(ToolCallingManager.class) 则是"先有调度房,才配那位负责⼯具回路的驿丞"。注意这⾥也印证了⼀条版本演进:⼯具回路已经从 ChatModel 上移到 ChatClient 的 ToolCallingAdvisor ——所以"装⼯具驿丞"这件事,⾃然落在 ChatClient 的装配房⾥。
⼀张配⽅,横扫 model / vectorstore / advisor / chatclient。读懂了 OpenAI 这⼀个,你就读懂了全部。
⚠ 暗礁⼆: matchIfMissing 的默认魔法,对排错不友好
约定优先的甜,在排错时会反噬成苦。设想:你想⽤ Anthropic,却把开关误写成 spring.ai.model.chat=anthropci (拼错了)。因为 matchIfMissing=true 是按"属性值是否等于 openai"判断,你这个拼错的值既不等于 openai 也不等于 anthropic ——结果两边都不装配,你得到⼀个空容器和⼀句NoSuchBeanDefinitionException ,却完全看不出"是因为⼀个拼写错误"。
更隐蔽的是"该装的没装": @ConditionalOnMissingBean 让⾃动配置在你不知情时悄悄退让。某个测试⾥别⼈ @MockBean 了⼀个 ChatModel ,真实的openAiChatModel 就静默缺席,⾏为诡异却⽆任何报错。
还有⼀处需要明确点破的事实订正:研究笔记称 OpenAiChatAutoConfiguration 类上有 @ConditionalOnClass 守 classpath。但翻遍这个类
( OpenAiChatAutoConfiguration.java:53-57 ),类级别只有 @AutoConfiguration 、 @EnableConfigurationProperties 、 @ConditionalOnProperty 三个注解,并没有 @ConditionalOnClass 。守 classpath 这件事,在这条线上主要靠 starter 的依赖闭合性与"底层库不在则零影响"的⼯程约定,⽽⾮这个类⾃⼰的
@ConditionalOnClass ——对⽐ ChatClientAutoConfiguration 明明⽩⽩挂着 @ConditionalOnClass(ChatClient.class) ( :68 ),就更显出 OpenAiChat这条的"裸"。
这就是条件装配的双⾯性:它把"装不装、谁来装"这件事从你眼前藏了起来。 顺⻛时是魔法,逆⻛时是⿊箱。理解每⼀条 @Conditional* 的真实判据,是和这套⿊箱和解的唯⼀办法——这也是为什么这⼀章我们要逐个注解地抠。
🎯 三句带⾛
1. 三段式范式:model autoconfig = @AutoConfiguration + @EnableConfigurationProperties 绑属性 →
@ConditionalOnProperty(spring.ai.model.chat=<provider>, matchIfMissing=true) 选 provider → @Bean @ConditionalOnMissingBean ⽤core Builder 造 Model,VectorStore/Advisor/ChatClient 同构。
2. 软硬之分:观测/度量/约定等配套⽤ ObjectProvider.getIfUnique(NOOP) 软注⼊、缺则零影响;但 ToolCallingManager 是硬注⼊,其默认值由独⽴的ToolCallingAutoConfiguration 以 @ConditionalOnMissingBean 兜底——别误以为 Bean ⽅法每个参数都可缺。
3. starter 不含代码:只聚合 "autoconfig + 馆⼼ core + 传递依赖",靠 AutoConfiguration.imports 花名册让 Boot 登记候选; @ConditionalOnMissingBean 是逃⽣⼝——你⾃⼰ @Bean 任何同类型 Bean,即可整体覆盖默认装配。
承上启下我们⽤⼀整章,把"开箱起馆"这件被⼀句话糊弄过去的事,拆成了花名册、三段条件、软硬注⼊的明细账。但你或许已经隐隐察觉:三段式之所以成⽴,根⼦在那道"馆⼼ /装配房 / 套装铺"的三层切割—— @ConditionalOnMissingBean 能让你夺权,前提是馆⼼本就能脱离 Boot 独⽴成活; ObjectProvider 能软注⼊ NOOP,前提是core 只认 Micrometer 接⼝⽽不认容器。
是时候把镜头从单个装配房拉远,俯瞰整座译馆的总图了。馆⼼、账房、驿⻢、公⽂——这四样东⻄如何分层、为何这样分、Spring AI ⽤什么哲学把它们焊在⼀起⼜彼此解耦?下⼀章「第⼗六章 · 馆⼼·账房·驿⻢·公⽂:三层架构与设计哲学总览」,我们爬到译馆的最⾼处,看清这套架构真正的承重墙。