<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>XLabs | 技术博客</title><description>卫星实验室（XLabs Club）用开源探索边界，用分享传递价值。专注平台工程、云原生、DevOps、AIOps、DataOps、FinOps 与 Java 开发的工程实践与技术分享。</description><link>https://www.xlabs.club/</link><language>zh-CN</language><atom:link rel="self" href="https://www.xlabs.club/blog/rss.xml"/><atom:link rel="prev-archive" href="https://www.xlabs.club/blog/rss/2026-10.xml"/><item><title>MCP 工具返回值的两道隐藏边界：10MB 帧上限、outputSchema 校验（1.32.1 实测）</title><link>https://www.xlabs.club/blog/mcp-tool-result-limits/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-tool-result-limits/</guid><description>MCP 工具返回值实测：结果帧超 10MiB 断连只报 Connection closed；outputSchema 校验失败被包成 isError，客户端还会抛异常。</description><pubDate>Thu, 08 Oct 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-2026-07-28-deprecation-sdk-lag/&quot;&gt;MCP 2026-07-28 弃用扫描&lt;/a&gt; 我 grep 的是 SDK 有没有跟上 spec。这篇 grep 另一件事：&lt;strong&gt;我 server 返回的工具结果，在客户端那边会经历什么&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;四条结论都不在文档里，也不在类型定义里，跑出来才知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stdio 下，结果帧超过 &lt;strong&gt;10,485,760 字节&lt;/strong&gt; 就断连，调用方只看到 &lt;code dir=&quot;auto&quot;&gt;MCP error -32000: Connection closed&lt;/code&gt;，真实原因只在 &lt;code dir=&quot;auto&quot;&gt;transport.onerror&lt;/code&gt; 里。&lt;/li&gt;
&lt;li&gt;这个上限属于&lt;strong&gt;读的一方&lt;/strong&gt;（客户端）。服务端把 &lt;code dir=&quot;auto&quot;&gt;maxBufferSize&lt;/code&gt; 调多大都没用。&lt;/li&gt;
&lt;li&gt;服务端 &lt;code dir=&quot;auto&quot;&gt;outputSchema&lt;/code&gt; 校验失败变成一个 &lt;code dir=&quot;auto&quot;&gt;isError: true&lt;/code&gt; 的&lt;strong&gt;正常 result&lt;/strong&gt;，不是 JSON-RPC error，&lt;code dir=&quot;auto&quot;&gt;try/catch&lt;/code&gt; 抓不到。&lt;/li&gt;
&lt;li&gt;客户端只在调过 &lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 之后才校验 &lt;code dir=&quot;auto&quot;&gt;structuredContent&lt;/code&gt;；而且 &lt;code dir=&quot;auto&quot;&gt;isError: true&lt;/code&gt; 时它照校验不误，会把错误结果整个抛成异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;环境&quot;&gt;环境&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.32.1&lt;/code&gt;（npm 当前 latest）+ &lt;code dir=&quot;auto&quot;&gt;zod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Node v26.3.0，stdio transport，全部默认配置&lt;/li&gt;
&lt;li&gt;每个用例开新客户端进程，避免上一轮的断连污染下一轮&lt;/li&gt;
&lt;li&gt;单文件复现脚本在文末，&lt;code dir=&quot;auto&quot;&gt;node mcp-result-limits.mjs&lt;/code&gt; 直接跑&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;一outputschema校验失败不是错误&quot;&gt;一、outputSchema：校验失败不是错误&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;工具声明 &lt;code dir=&quot;auto&quot;&gt;outputSchema: { count: z.number() }&lt;/code&gt;，handler 返回 &lt;code dir=&quot;auto&quot;&gt;{ count: &apos;one&apos; }&lt;/code&gt;。服务端 &lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 确实拦了，但拦下来的处理方式是这样：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:181&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (error) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createToolError&lt;/span&gt;&lt;span&gt;(error &lt;/span&gt;&lt;span&gt;instanceof&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; error.message &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(error));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;createToolError&lt;/code&gt; 返回 &lt;code dir=&quot;auto&quot;&gt;{ content: [{type:&apos;text&apos;, text: msg}], isError: true }&lt;/code&gt;。抓包确认，线上是一个 &lt;code dir=&quot;auto&quot;&gt;result&lt;/code&gt; 帧，没有 &lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; 键：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;jsonrpc&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;result&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;: [{ &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;boom&quot;&lt;/span&gt;&lt;span&gt; }],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;structuredContent&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;count&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;one&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;isError&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;三条失败路径的原话：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;no_structured =&gt; MCP error -32602: Output validation error: Tool no_structured has an output schema but no structured content was provided&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;bad_type      =&gt; MCP error -32602: Output validation error: Invalid structured content for tool bad_type: Invalid input: expected number, received string at count&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;对调用方的直接影响是&lt;strong&gt;没有异常可捕获&lt;/strong&gt;：包在 &lt;code dir=&quot;auto&quot;&gt;try/catch&lt;/code&gt; 里的 &lt;code dir=&quot;auto&quot;&gt;callTool&lt;/code&gt; 正常返回，业务代码拿到 &lt;code dir=&quot;auto&quot;&gt;structuredContent: null&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;content&lt;/code&gt; 是报错文案的结果。下游直接读 &lt;code dir=&quot;auto&quot;&gt;structuredContent.count&lt;/code&gt; 就是 &lt;code dir=&quot;auto&quot;&gt;Cannot read properties of undefined&lt;/code&gt;；下游要是把它喂给模型，模型看到的是 “Output validation error: …” 这种它读不懂的字符串，然后开始猜——和我上一篇 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-tool-failure-recovery/&quot;&gt;工具失败回灌实测&lt;/a&gt; 里 &lt;code dir=&quot;auto&quot;&gt;empty_ok&lt;/code&gt; 触发幻觉是同一类问题。&lt;/p&gt;
&lt;p&gt;唯一可靠的观测点是 &lt;code dir=&quot;auto&quot;&gt;isError&lt;/code&gt;。但它同时承载了「业务失败」和「我 schema 写错了」两件事，监控上分不开。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;客户端校验取决于调没调过-toolslist&quot;&gt;客户端校验取决于调没调过 tools/list&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;同一个工具，客户端 &lt;code dir=&quot;auto&quot;&gt;listTools&lt;/code&gt; 之前和之后是两个世界：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具返回值&lt;/th&gt;
&lt;th&gt;未 listTools&lt;/th&gt;
&lt;th&gt;已 listTools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;no_structured&lt;/code&gt;（有 schema 没给 structuredContent）&lt;/td&gt;
&lt;td&gt;isError=true&lt;/td&gt;
&lt;td&gt;isError=true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bad_type&lt;/code&gt;（structuredContent 类型错）&lt;/td&gt;
&lt;td&gt;isError=true&lt;/td&gt;
&lt;td&gt;isError=true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;extra_field&lt;/code&gt;（多一个诊断字段）&lt;/td&gt;
&lt;td&gt;OK，原样返回&lt;/td&gt;
&lt;td&gt;抛 -32602&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;iserror_bad&lt;/code&gt;（isError + 不合规 structuredContent）&lt;/td&gt;
&lt;td&gt;OK，原样返回&lt;/td&gt;
&lt;td&gt;抛 -32602&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;diverge&lt;/code&gt;（text 说 5，structured 说 7）&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;客户端在 &lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 时缓存每个工具的 outputSchema validator（&lt;code dir=&quot;auto&quot;&gt;client/index.js&lt;/code&gt; 的 &lt;code dir=&quot;auto&quot;&gt;cacheToolMetadata&lt;/code&gt;），&lt;code dir=&quot;auto&quot;&gt;callTool&lt;/code&gt; 里再用它校验。所以只 &lt;code dir=&quot;auto&quot;&gt;callTool&lt;/code&gt; 从不 &lt;code dir=&quot;auto&quot;&gt;listTools&lt;/code&gt; 的客户端等于&lt;strong&gt;完全没有返回值校验&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而正常走 &lt;code dir=&quot;auto&quot;&gt;listTools&lt;/code&gt; 的客户端会拒绝多出来的字段，因为 Zod 转出来的 JSON Schema 带 &lt;code dir=&quot;auto&quot;&gt;additionalProperties: false&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;count&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;number&quot;&lt;/span&gt;&lt;span&gt; } }, &lt;/span&gt;&lt;span&gt;&quot;required&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;count&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;http://json-schema.org/draft-07/schema#&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;additionalProperties&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;extra_field / 已 listTools =&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MCP error -32602: Structured content does not match the tool&apos;s output schema: data must NOT have additional properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;「多给点信息」在这里会打断整个调用。这是最容易踩、也最难自己发现的一条——本地写 server 的调试客户端往往不调 &lt;code dir=&quot;auto&quot;&gt;listTools&lt;/code&gt;，你测不出来。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;iserror-是双重标准&quot;&gt;isError 是双重标准&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;服务端遇到 &lt;code dir=&quot;auto&quot;&gt;isError: true&lt;/code&gt; 就跳过 outputSchema 校验（&lt;code dir=&quot;auto&quot;&gt;server/mcp.js:237&lt;/code&gt; 的 &lt;code dir=&quot;auto&quot;&gt;if (result.isError) return;&lt;/code&gt;），好让错误结果不必满足成功时的 schema。客户端不是这样。它的注释写着 &lt;code dir=&quot;auto&quot;&gt;// Only validate structured content if present (not when there&apos;s an error)&lt;/code&gt;（&lt;code dir=&quot;auto&quot;&gt;client/index.js:492&lt;/code&gt;），但代码只跳过了「structuredContent 缺失」那一种情况——&lt;strong&gt;只要 structuredContent 存在就照样校验&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是：一个上报错误、顺手带了不合规 structuredContent 的工具，服务端放行，客户端抛异常。调用方拿到的是异常，不是那句错误文案。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;iserror_bad / 未 listTools =&gt; {&quot;isError&quot;:true,&quot;structured&quot;:{&quot;count&quot;:&quot;one&quot;},&quot;text&quot;:&quot;上游 502&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;iserror_bad / 已 listTools  =&gt; MCP error -32602: Structured content does not match the tool&apos;s output schema: data/count must be number&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;反过来，错误结果里&lt;strong&gt;不放&lt;/strong&gt; structuredContent，两边都放行。这是错误返回唯一稳妥的形态。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;text-和-structuredcontent-不一致没人查&quot;&gt;text 和 structuredContent 不一致，没人查&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;工具返回 &lt;code dir=&quot;auto&quot;&gt;content: [{type:&apos;text&apos;, text:&apos;count=5&apos;}]&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;structuredContent: {count: 7}&lt;/code&gt;，服务端校验通过，客户端校验通过。模型读 text，程序读 structuredContent，拿到两个不同的数，四层实现里没有一层会发现。&lt;/p&gt;
&lt;p&gt;同类的还有一个：声明 outputSchema、返回 &lt;code dir=&quot;auto&quot;&gt;content: []&lt;/code&gt; + 合法 structuredContent，也一路通过——TS 这个版本的高层 API &lt;strong&gt;不会&lt;/strong&gt;自动补一份 text 序列化（Python SDK 文档写的是会自动填两个 channel，两者行为不一致）。只吃 text 的客户端拿到空结果，但代码里看不出任何异常。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;二10mb结果帧超了就断连&quot;&gt;二、10MB：结果帧超了就断连&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;真正把连接打死的是这个常量：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js:2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;STDIO_DEFAULT_MAX_BUFFER_SIZE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ReadBuffer.append&lt;/code&gt; 累积超过它就 &lt;code dir=&quot;auto&quot;&gt;throw&lt;/code&gt;，抛出位置在 transport 的 ondata 回调里，冒泡上去的结果是 transport 关闭。逐个测：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;text 9437184B   =&gt; OK，318ms&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;text 10485760B  =&gt; -32000: Connection closed / ReadBuffer exceeded maximum size of 10485760 bytes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;text 12582912B  =&gt; -32000: Connection closed / ReadBuffer exceeded maximum size of 10485760 bytes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;断连之后同一个 client 再调任何工具都是 &lt;code dir=&quot;auto&quot;&gt;Error: Not connected&lt;/code&gt;。&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Connection closed&lt;/code&gt; 这个报错没有任何信息量&lt;/strong&gt;，真正的原因要去 &lt;code dir=&quot;auto&quot;&gt;transport.onerror&lt;/code&gt; 里捞；宿主客户端不一定会把它写进日志。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;三个被低估的点&quot;&gt;三个被低估的点&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. 上限算字节，不算字符。&lt;/strong&gt; 换成中文：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cjk 3000000 字  =&gt; OK（9,000,000 字节）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cjk 3500000 字  =&gt; 断连（10,500,000 字节）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;翻成中文以后，同样「三百万字」的摘要、diff、日志片段就撞墙。按字符数估容量必然翻车。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. base64 让二进制内容再打七五折。&lt;/strong&gt; 图片/音频走 &lt;code dir=&quot;auto&quot;&gt;data&lt;/code&gt; 字段 base64，膨胀 4/3：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;blob 原始 7340032B =&gt; OK（base64 后 9,786,712 字符）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;blob 原始 8388608B =&gt; 断连（base64 后 11,184,810 字符）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;截图类工具实际能返回的图片上限约 7.5 MiB 原始字节，不是 10 MiB。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 上限属于读的一方，服务端改不了。&lt;/strong&gt; 最反直觉的一条：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;客户端 maxBuffer=1MiB，返回 1MiB            =&gt; 断连（ReadBuffer exceeded maximum size of 1048576 bytes）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;客户端 maxBuffer=32MiB，返回 16MiB          =&gt; OK，768ms&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;服务端 maxBuffer=64MiB，客户端默认，返回 12MiB =&gt; 仍然断连（10485760 bytes）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;服务端把 &lt;code dir=&quot;auto&quot;&gt;StdioServerTransport&lt;/code&gt; 的 &lt;code dir=&quot;auto&quot;&gt;maxBufferSize&lt;/code&gt; 调大，只影响它&lt;strong&gt;读&lt;/strong&gt;客户端请求的能力，对「客户端能不能读下我的结果」毫无帮助。这不是服务端单方面能修的：宿主客户端（Claude Desktop 这类）用的就是默认值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它也不是协议限制。&lt;/strong&gt; 同一份 server 代码换 InMemory transport，32 MiB 用 1ms 过去；stdio 下连续三次返回 4 MiB 全部成功——上限按&lt;strong&gt;单个未结束的帧&lt;/strong&gt;算，不累积。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么会这样&quot;&gt;为什么会这样&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;spec 把「谁校验输出」留给实现：server 声明 outputSchema 后 client SHOULD 校验，client 拿到不符的结果 SHOULD/MUST 报错。TS SDK 选了「服务端把失败包成 isError + 客户端校验时抛异常」这套组合，两种策略叠在一起就出现了「同一个错误，取决于是不是调过 tools/list，是异常还是结果」。&lt;/p&gt;
&lt;p&gt;10 MiB 则是另一回事：它是 stdio 读取层防 OOM 的工程默认值，写在实现里，不在任何 spec、文档或类型里。没有文档的地方，失败模式就只能是静默。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;落地清单&quot;&gt;落地清单&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;给所有工具结果设自己可控的上限&lt;/strong&gt;，超了不回原文，回 &lt;code dir=&quot;auto&quot;&gt;resource_link&lt;/code&gt; 加摘要：&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MAX_INLINE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// 1 MiB，给 JSON framing 留足余量&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inlineResult&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Buffer.&lt;/span&gt;&lt;span&gt;byteLength&lt;/span&gt;&lt;span&gt;(text, &lt;/span&gt;&lt;span&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 必须按字节，不是 text.length&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (bytes &lt;/span&gt;&lt;span&gt;&amp;#x3C;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MAX_INLINE&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; [{ type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text }];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;`结果 ${&lt;/span&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;span&gt;} 字节，已截断，完整内容见 resource`&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ type: &lt;/span&gt;&lt;span&gt;&quot;resource_link&quot;&lt;/span&gt;&lt;span&gt;, uri, name: &lt;/span&gt;&lt;span&gt;&quot;full result&quot;&lt;/span&gt;&lt;span&gt;, mimeType: &lt;/span&gt;&lt;span&gt;&quot;text/plain&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;错误结果不要带 structuredContent&lt;/strong&gt;，只给 text + &lt;code dir=&quot;auto&quot;&gt;isError: true&lt;/code&gt;（两边都放行的唯一形态）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;outputSchema 只写会稳定返回的字段&lt;/strong&gt;。额外的诊断信息走 resource 或日志，否则被 &lt;code dir=&quot;auto&quot;&gt;additionalProperties: false&lt;/code&gt; 拒掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回 structuredContent 时自己再给一份 text 序列化&lt;/strong&gt;，别指望 SDK 补——只读 text 的客户端拿不到 structuredContent。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自己先校验再返回&lt;/strong&gt;。服务端失败会变成 &lt;code dir=&quot;auto&quot;&gt;isError&lt;/code&gt;，客户端失败会变成异常，两种都不是你想看到的报错方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端侧一定挂 &lt;code dir=&quot;auto&quot;&gt;transport.onerror&lt;/code&gt;&lt;/strong&gt;，否则 &lt;code dir=&quot;auto&quot;&gt;-32000: Connection closed&lt;/code&gt; 等于没信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI 加一条大结果回归&lt;/strong&gt;：造一个 12 MiB 返回值，断言 &lt;code dir=&quot;auto&quot;&gt;callTool&lt;/code&gt; 不抛异常、或断言错误文案含 &lt;code dir=&quot;auto&quot;&gt;ReadBuffer exceeded&lt;/code&gt;。别等用户报「工具偶尔没反应」。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;只测 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.32.1&lt;/code&gt; + Node v26.3.0 + stdio + 默认配置。Streamable HTTP 不走 &lt;code dir=&quot;auto&quot;&gt;shared/stdio.js&lt;/code&gt; 这套 ReadBuffer，上限另说，未测。&lt;/li&gt;
&lt;li&gt;Python SDK 未测。它文档里写过「会同时填 content 和 structured_content」，与 TS 高层 API 不一致，不能互相套用结论。&lt;/li&gt;
&lt;li&gt;10 MiB 是当前默认值，不是协议承诺，换版本可能就变。要长期依赖就自己定义上限，别依赖这个数。&lt;/li&gt;
&lt;li&gt;断连后服务端进程状态未验证（stderr 为空、拿不到 exitCode），只确认客户端侧连接关闭、后续调用 &lt;code dir=&quot;auto&quot;&gt;Not connected&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;复现脚本&quot;&gt;复现脚本&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;单文件，&lt;code dir=&quot;auto&quot;&gt;npm i @modelcontextprotocol/sdk@1.32.1 zod&lt;/code&gt; 之后直接跑。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// mcp-result-limits.mjs（节选）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 1) 子进程 server：一台机器上放 3 类工具&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fs.&lt;/span&gt;&lt;span&gt;writeFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;CHILD&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import { McpServer } from &apos;@modelcontextprotocol/sdk/server/mcp.js&apos;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import { StdioServerTransport } from &apos;@modelcontextprotocol/sdk/server/stdio.js&apos;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import { z } from &apos;zod&apos;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const s = new McpServer({ name: &apos;limits&apos;, version: &apos;0.0.1&apos; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const Cout = { count: z.number() };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const reg = (name, cfg, fn) =&gt; s.registerTool(name, { description: name, inputSchema: {}, ...cfg }, fn);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;ok&apos;,            { outputSchema: Cout }, async () =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;count=1&apos; }], structuredContent: { count: 1 } }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;no_structured&apos;, { outputSchema: Cout }, async () =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;count=1&apos; }] }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;bad_type&apos;,      { outputSchema: Cout }, async () =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;count=one&apos; }], structuredContent: { count: &apos;one&apos; } }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;extra_field&apos;,   { outputSchema: Cout }, async () =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;x&apos; }], structuredContent: { count: 1, extra: &apos;x&apos; } }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;iserror_bad&apos;,   { outputSchema: Cout }, async () =&gt; ({ isError: true, content: [{ type: &apos;text&apos;, text: &apos;上游 502&apos; }], structuredContent: { count: &apos;one&apos; } }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reg(&apos;diverge&apos;,       { outputSchema: Cout }, async () =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;count=5&apos; }], structuredContent: { count: 7 } }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s.registerTool(&apos;text&apos;, { description: &apos;text&apos;, inputSchema: { bytes: z.number() } }, async (a) =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;A&apos;.repeat(a.bytes) }] }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s.registerTool(&apos;cjk&apos;,  { description: &apos;cjk&apos;,  inputSchema: { chars: z.number() } }, async (a) =&gt; ({ content: [{ type: &apos;text&apos;, text: &apos;字&apos;.repeat(a.chars) }] }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s.registerTool(&apos;blob&apos;, { description: &apos;blob&apos;, inputSchema: { bytes: z.number() } }, async (a) =&gt; ({ content: [{ type: &apos;image&apos;, data: Buffer.alloc(a.bytes, 7).toString(&apos;base64&apos;), mimeType: &apos;image/png&apos; }] }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await s.connect(new StdioServerTransport(process.env.MAXBUF ? { maxBufferSize: Number(process.env.MAXBUF) } : undefined));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 2) 每个用例开新客户端：list 决定校验开关，maxBufferSize 决定帧上限，onerror 留住真实报错&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;call&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;, { &lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;clientMaxBuf&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {}) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;StdioClientTransport&lt;/span&gt;&lt;span&gt;({ command: &lt;/span&gt;&lt;span&gt;&quot;node&quot;&lt;/span&gt;&lt;span&gt;, args: [&lt;/span&gt;&lt;span&gt;CHILD&lt;/span&gt;&lt;span&gt;], stderr: &lt;/span&gt;&lt;span&gt;&quot;pipe&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;(clientMaxBuf &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; { maxBufferSize: clientMaxBuf } &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {}) });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;transportErrors&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;t.&lt;/span&gt;&lt;span&gt;onerror&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; transportErrors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(e?.message &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; e));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Client&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&quot;limits-client&quot;&lt;/span&gt;&lt;span&gt;, version: &lt;/span&gt;&lt;span&gt;&quot;0.0.1&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(t);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (list) &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;listTools&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;callTool&lt;/span&gt;&lt;span&gt;({ name: tool, arguments: args &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; {} });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { ok: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, isError: res.isError &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, structured: res.structuredContent &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, text: res.content?.[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]?.text?.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;96&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { ok: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;, code: e.code, message: e.message, transportError: transportErrors[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;] };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;完整文件（含 InMemory 对照与读者侧/服务端侧配置对比）我跑在这台机器上，输出就是上面各节的原始日志。想复用的话，把 &lt;code dir=&quot;auto&quot;&gt;call()&lt;/code&gt; 直接拿去做回归测试也行——第 7 条清单里的 12 MiB 用例就是这么写的。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;MCP 的 server / client 实现和周边工具，可以顺着 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 AI Coding 分类&lt;/a&gt;往下翻。同环境同 SDK 的前几篇：&lt;a href=&quot;https://www.xlabs.club/blog/mcp-2026-07-28-version-negotiation/&quot;&gt;版本协商实测&lt;/a&gt;、&lt;a href=&quot;https://www.xlabs.club/blog/mcp-streamable-http-sdk-behavior/&quot;&gt;Streamable HTTP 行为&lt;/a&gt;、&lt;a href=&quot;https://www.xlabs.club/blog/mcp-tool-failure-recovery/&quot;&gt;工具失败回灌&lt;/a&gt;。&lt;/p&gt;</content:encoded><category>MCP</category><category>outputSchema</category><category>structuredContent</category><category>stdio</category><category>TypeScript SDK</category><category>断连排查</category><category>ReadBuffer</category></item><item><title>MCP 2026-07-28 弃用 Roots/Sampling/Logging，SDK 还在裸奔（1.32.1 实测）</title><link>https://www.xlabs.club/blog/mcp-2026-07-28-deprecation-sdk-lag/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-2026-07-28-deprecation-sdk-lag/</guid><description>实测 @modelcontextprotocol/sdk@1.32.1：spec 已弃用 Roots/Sampling/Logging，但 SDK 零警告零 @deprecated，要主动 audit。</description><pubDate>Tue, 06 Oct 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;昨天翻 MCP 2026-07-28 changelog，看到一条以前没注意的：&lt;strong&gt;Roots、Sampling、Logging 三个 feature 整体弃用&lt;/strong&gt;（SEP-2577），且新增了 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt; 方法和强制 &lt;code dir=&quot;auto&quot;&gt;Mcp-Method&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;Mcp-Name&lt;/code&gt; 请求头（SEP-2243）。文档说弃用是 annotation-only，wire protocol 不变，给 12 个月迁移期。&lt;/p&gt;
&lt;p&gt;我立刻去看自己的 server 代码——里面正用着 &lt;code dir=&quot;auto&quot;&gt;sampling/createMessage&lt;/code&gt; 做 Agent 内嵌推理。如果 SDK 已经有 deprecation 警告，我得在升级前看到它。所以拿了 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.32.1&lt;/code&gt;（2026-10-05 发布，npm 上当前最新）实测了一把。&lt;/p&gt;
&lt;p&gt;结论：&lt;strong&gt;SDK 还没跟上 spec。这三个 feature 在 1.32.1 上裸奔，没有任何运行时或类型层警告。&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实测环境&quot;&gt;实测环境&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.32.1&lt;/code&gt;（npm，2026-10-05）&lt;/li&gt;
&lt;li&gt;Node v22&lt;/li&gt;
&lt;li&gt;InMemory transport，client 端声明 &lt;code dir=&quot;auto&quot;&gt;roots: { listChanged: true }&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;sampling: {}&lt;/code&gt; 能力并 stub 了 handler&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;实测三个弃用-feature-的当前行为&quot;&gt;实测：三个弃用 feature 的当前行为&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;写了一个 probe server，单个工具内部直发三种「弃用」请求，捕获 &lt;code dir=&quot;auto&quot;&gt;console.warn&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;console.error&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;process.on(&apos;warning&apos;)&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 节选，完整 probe.mjs 在文末&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.roots &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendRequest&lt;/span&gt;&lt;span&gt;({ method: &lt;/span&gt;&lt;span&gt;&quot;roots/list&quot;&lt;/span&gt;&lt;span&gt;, params: {} }, RootsResultZod);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;roots: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.sampling &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;method: &lt;/span&gt;&lt;span&gt;&quot;sampling/createMessage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;params: { messages: [{ role: &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;, content: { type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;&quot;hi&quot;&lt;/span&gt;&lt;span&gt; } }], maxTokens: &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SamplingResultZod&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sampling: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendNotification&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;method: &lt;/span&gt;&lt;span&gt;&quot;notifications/message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;params: { level: &lt;/span&gt;&lt;span&gt;&quot;info&quot;&lt;/span&gt;&lt;span&gt;, data: &lt;/span&gt;&lt;span&gt;&quot;deprecated-log&quot;&lt;/span&gt;&lt;span&gt;, logger: &lt;/span&gt;&lt;span&gt;&quot;probe&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.log &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;sent&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;logging: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;跑出来的结果：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;roots&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;roots&quot;&lt;/span&gt;&lt;span&gt;: [{ &lt;/span&gt;&lt;span&gt;&quot;uri&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;file:///tmp/x&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;x&quot;&lt;/span&gt;&lt;span&gt; }] },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;sampling&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;role&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;model&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;stub&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;stub-reply&quot;&lt;/span&gt;&lt;span&gt; } },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;log&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;errors&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;logging: Server does not support logging (required for notifications/message)&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// --- warnings captured ---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// (none)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;读出来的事实：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;roots/list&lt;/code&gt; 工作正常，无任何警告&lt;/strong&gt;。SDK 不知道也不在乎 spec 已弃用它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;sampling/createMessage&lt;/code&gt; 工作正常，无任何警告&lt;/strong&gt;。同上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;notifications/message&lt;/code&gt; 被拦了，但拦它的不是 deprecation&lt;/strong&gt;——是 SDK 自带的能力协商：server 没声明 &lt;code dir=&quot;auto&quot;&gt;logging&lt;/code&gt; 能力，&lt;code dir=&quot;auto&quot;&gt;sendNotification&lt;/code&gt; 直接抛错。也就是说，即使你今天不小心用 logging，得到的提示也不是「这个 API 要没了」，而是「你没声明能力」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果只看运行时行为，没人能猜到这三个 feature 已经进入 12 个月倒计时。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;类型层也没有-deprecated&quot;&gt;类型层也没有 &lt;code dir=&quot;auto&quot;&gt;@deprecated&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SEP-2577 明确说要在 &lt;code dir=&quot;auto&quot;&gt;schema/draft/schema.ts&lt;/code&gt; 给以下类型加 &lt;code dir=&quot;auto&quot;&gt;@deprecated&lt;/code&gt;：Root、ListRootsRequest、CreateMessageRequest、SamplingMessage、ModelPreferences、LoggingLevel、LoggingMessageNotification 等等二十多个。&lt;/p&gt;
&lt;p&gt;我在 SDK 1.32.1 的 &lt;code dir=&quot;auto&quot;&gt;types.d.ts&lt;/code&gt; 里搜了一遍：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ grep -n &quot;@deprecated&quot; node_modules/@modelcontextprotocol/sdk/dist/esm/types.d.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;匹配到的全部是无关的旧 API 迁移提示（&lt;code dir=&quot;auto&quot;&gt;isJSONRPCResultResponse&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;ResourceTemplateReferenceSchema&lt;/code&gt; 之类），&lt;strong&gt;没有任何 Roots/Sampling/Logging 类型被标弃用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也就是说，IDE 里写 &lt;code dir=&quot;auto&quot;&gt;server.createMessage(...)&lt;/code&gt; 时，IntelliSense 不会标黄、不会跳删除线。开发者不看 spec 就完全感知不到。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;sdk-也没实现-spec-的新增项&quot;&gt;SDK 也没实现 spec 的新增项&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;spec 2026-07-28 另外引入了几个服务端点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;：客户端可以问一下服务器的版本、能力、身份，不再依赖 initialize&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Mcp-Method&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;Mcp-Name&lt;/code&gt; 请求头：让 CDN/网关可以按方法路由，不解析 body&lt;/li&gt;
&lt;li&gt;MRTR（Multi Round-Trip Request）：把 server-initiated 的 roots/list、sampling/createMessage、elicitation/create 改成「result 里嵌 inputRequests + 客户端 retry 时回带 inputResponses」的模式，摆脱长连接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 1.32.1 的 dist/esm 里 grep：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ grep -rn &quot;server/discover&quot; node_modules/@modelcontextprotocol/sdk/dist/esm/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ grep -rn &quot;Mcp-Method\|Mcp-Name&quot; node_modules/@modelcontextprotocol/sdk/dist/esm/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ grep -rn &quot;input_required\|InputRequiredResult\|inputRequests&quot; node_modules/@modelcontextprotocol/sdk/dist/esm/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;前两个零匹配。第三个只命中 &lt;code dir=&quot;auto&quot;&gt;TaskStatus = &quot;...&quot; | &quot;input_required&quot; | ...&lt;/code&gt;，是 Task 系统的状态枚举，&lt;strong&gt;不是 MRTR 的 InputRequiredResult&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也就是说 SDK 1.32.1 还是 2025-11-25 时代的实现，没有接 spec 的新机制。这跟我在 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-2026-07-28-version-negotiation/&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;mcp-2026-07-28-version-negotiation&lt;/code&gt;&lt;/a&gt; 里实测的版本协商一致——SDK 目前默认仍走旧协议。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么这是个隐患&quot;&gt;为什么这是个隐患&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果你今天在写新的 MCP server，看到 types.ts 里 &lt;code dir=&quot;auto&quot;&gt;createMessage&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;listRoots&lt;/code&gt; 都是正常的公开 API，IDE 没警告，runtime 没警告，spec 文档里说「annotation-only deprecation」又不会主动跳出来——&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你只会写出来一个 12 个月后要重写的 server。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更要命的是 spec 给的迁移路径是&lt;strong&gt;结构性的&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;弃用&lt;/th&gt;
&lt;th&gt;替代方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Roots&lt;/td&gt;
&lt;td&gt;把目录作为工具参数 / resource URI / server 配置传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sampling&lt;/td&gt;
&lt;td&gt;直接调 LLM provider 的 API（让开发者自己持有 API key）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;stderr（stdio）/ OpenTelemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这不是改名，是&lt;strong&gt;架构调整&lt;/strong&gt;。Roots 替代方案要求 server 自己管 scope，不再依赖 client 提供；Sampling 替代方案要求 server 直接持有 LLM 凭据，不再是 client 代理。这两种迁移都不能拖到最后一周做。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;工程清单&quot;&gt;工程清单&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;今天就得做的事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主动 audit server 代码&lt;/strong&gt;。grep &lt;code dir=&quot;auto&quot;&gt;createMessage&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;listRoots&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;sendLoggingMessage&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;logging/setLevel&lt;/code&gt;，列出来，对每一处的迁移路径做决定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要等 SDK 警告才动&lt;/strong&gt;。SDK 1.32.1 已经证不会警告你。把 SEP-2577 加进 upgrade checklist。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;新代码默认避开这三个 feature&lt;/strong&gt;。即使他们还能跑，写新代码时也用工具参数传 scope、直接调 LLM、用 stderr/OTel。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;盯 SDK 后续版本的 changelog&lt;/strong&gt;。SDK 团队总会在某个版本加 &lt;code dir=&quot;auto&quot;&gt;@deprecated&lt;/code&gt; 和 runtime 警告——那一天会是大范围告警的日子，提前一步。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果非要继续用 Sampling，把 model_preferences 写好&lt;/strong&gt;。spec 明确说 server hint 是 advisory、client 可以无视。写好 fallback 链路。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;只测了 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.32.1&lt;/code&gt; 一个版本。Python SDK（&lt;code dir=&quot;auto&quot;&gt;py.sdk.modelcontextprotocol.io&lt;/code&gt;）从文档看已经在 sampling 函数上加了 deprecation warning（“Deprecated by the 2026-07-28 specification”），跟 TypeScript SDK 节奏不一样。&lt;/li&gt;
&lt;li&gt;只测了 InMemory transport。Streamable HTTP 传输层的行为没测。&lt;/li&gt;
&lt;li&gt;SEP-2577 的 12 个月期限从「包含该 SEP 的 spec 版本发布日」起算——changelog 上写 spec 2026-07-28 GA，&lt;strong&gt;所以最晚 2027-07-28 可能被移除&lt;/strong&gt;。具体日历看官方 deprecated features registry。&lt;/li&gt;
&lt;li&gt;MRTR 我没实测，因为 SDK 还没实现 InputRequiredResult。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;probemjs-完整源码&quot;&gt;probe.mjs 完整源码&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;放出来给需要自己复现的人：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { McpServer } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@modelcontextprotocol/sdk/server/mcp.js&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Client } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@modelcontextprotocol/sdk/client/index.js&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { InMemoryTransport } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@modelcontextprotocol/sdk/inMemory.js&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { ListRootsRequestSchema, CreateMessageRequestSchema } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;@modelcontextprotocol/sdk/types.js&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { z } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;zod&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;warnLog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;origWarn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; console.warn;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;warnLog.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(a.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot; &quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;origWarn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;a);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;process.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;warning&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; warnLog.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(w.name &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; w.message));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;McpServer&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&quot;probe-server&quot;&lt;/span&gt;&lt;span&gt;, version: &lt;/span&gt;&lt;span&gt;&quot;0.0.1&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;server.&lt;/span&gt;&lt;span&gt;registerTool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;use-deprecated&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;description: &lt;/span&gt;&lt;span&gt;&quot;Calls listRoots + createMessage + sendLoggingMessage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;inputSchema: {}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;extra&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { roots: &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, sampling: &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, log: &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, errors: [] };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.roots &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendRequest&lt;/span&gt;&lt;span&gt;({ method: &lt;/span&gt;&lt;span&gt;&quot;roots/list&quot;&lt;/span&gt;&lt;span&gt;, params: {} }, z.&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;({ roots: z.&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;(z.&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;({ uri: z.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(), name: z.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;() })) }));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;roots: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.sampling &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;method: &lt;/span&gt;&lt;span&gt;&quot;sampling/createMessage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;params: { messages: [{ role: &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;, content: { type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;&quot;hi&quot;&lt;/span&gt;&lt;span&gt; } }], maxTokens: &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;z.&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;({ role: z.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(), model: z.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(), content: z.&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;() })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sampling: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; extra.&lt;/span&gt;&lt;span&gt;sendNotification&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;method: &lt;/span&gt;&lt;span&gt;&quot;notifications/message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;params: { level: &lt;/span&gt;&lt;span&gt;&quot;info&quot;&lt;/span&gt;&lt;span&gt;, data: &lt;/span&gt;&lt;span&gt;&quot;deprecated-log&quot;&lt;/span&gt;&lt;span&gt;, logger: &lt;/span&gt;&lt;span&gt;&quot;probe&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.log &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;sent&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;out.errors.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;logging: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; e.message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { content: [{ type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(out) }] };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;ct&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;st&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; InMemoryTransport.&lt;/span&gt;&lt;span&gt;createLinkedPair&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Client&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&quot;probe-client&quot;&lt;/span&gt;&lt;span&gt;, version: &lt;/span&gt;&lt;span&gt;&quot;0.0.1&quot;&lt;/span&gt;&lt;span&gt; }, { capabilities: { roots: { listChanged: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; }, sampling: {} } });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client.&lt;/span&gt;&lt;span&gt;setRequestHandler&lt;/span&gt;&lt;span&gt;(ListRootsRequestSchema, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;roots: [{ uri: &lt;/span&gt;&lt;span&gt;&quot;file:///tmp/x&quot;&lt;/span&gt;&lt;span&gt;, name: &lt;/span&gt;&lt;span&gt;&quot;x&quot;&lt;/span&gt;&lt;span&gt; }]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client.&lt;/span&gt;&lt;span&gt;setRequestHandler&lt;/span&gt;&lt;span&gt;(CreateMessageRequestSchema, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;role: &lt;/span&gt;&lt;span&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;model: &lt;/span&gt;&lt;span&gt;&quot;stub&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content: { type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;&quot;stub-reply&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; server.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(st);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(ct);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;callTool&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&quot;use-deprecated&quot;&lt;/span&gt;&lt;span&gt;, arguments: {} });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(result.content[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;].text);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;--- warnings ---&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(warnLog.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; warnLog.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;(none)&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; server.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;p&gt;MCP 相关的开源 server / client 实现，可以看 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 AI Coding 分类&lt;/a&gt;。我前两天写的 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-tool-failure-recovery/&quot;&gt;MCP 工具失败回灌实测&lt;/a&gt; 是同环境同 SDK 系列。&lt;/p&gt;</content:encoded><category>MCP</category><category>MCP 2026-07-28</category><category>Roots</category><category>Sampling</category><category>Logging</category><category>MRTR</category><category>server/discover</category><category>SEP-2577</category></item><item><title>MCP 工具挂了之后：empty_ok 比报错更危险，4 个模型实测</title><link>https://www.xlabs.club/blog/mcp-tool-failure-recovery/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-tool-failure-recovery/</guid><description>3 种失败回灌 × 4 模型实测 MCP 工具失败后的行为：empty_ok 时 MiniMax 编出 4 个原因，deepseek 协议层直接 400。</description><pubDate>Mon, 05 Oct 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MCP 工具调用失败之后，宿主把错误回灌给模型，模型会怎么反应？这个问题的答案决定你的 Agent 在生产环境里是「报错后重试」还是「编一段看似合理的话糊弄过去」。&lt;/p&gt;
&lt;p&gt;我搭了一个 12 工具的 MCP server（桩实现），让 4 个模型在真实 MCP 循环里跑失败场景，最多 3 轮，记录每一次调用和最终结局。三种失败回灌方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;informative&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;isError=true&lt;/code&gt;，content 里带可行动线索（&lt;code dir=&quot;auto&quot;&gt;Error: pods &quot;payment-api-9f8e7d6c5-x9zzz&quot; not found in namespace &quot;prod&quot;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;generic&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;isError=true&lt;/code&gt;，但错误信息没线索（&lt;code dir=&quot;auto&quot;&gt;Error: upstream request failed&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;empty_ok&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;isError=false&lt;/code&gt;，content 为空数组——协议层「成功」，语义层什么都没说&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;结局表&quot;&gt;结局表&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;informative&lt;/th&gt;
&lt;th&gt;generic&lt;/th&gt;
&lt;th&gt;empty_ok&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;http_error_400&lt;/strong&gt;（1 轮就断）&lt;/td&gt;
&lt;td&gt;http_error_400&lt;/td&gt;
&lt;td&gt;http_error_400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.8-flash&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax-M3&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;text_only（编故事）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-3.8-flash&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;td&gt;loop_exhausted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;loop_exhausted&lt;/code&gt; 的意思是模型在 3 轮内不断换工具重试，没给出最终答案。&lt;code dir=&quot;auto&quot;&gt;text_only&lt;/code&gt; 的意思是模型放弃调用工具，直接输出文本。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现一empty_ok-是幻觉触发器&quot;&gt;发现一：empty_ok 是幻觉触发器&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;MiniMax-M3 在 empty_ok 场景下的输出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下是 &lt;code dir=&quot;auto&quot;&gt;prod&lt;/code&gt; 命名空间下 &lt;code dir=&quot;auto&quot;&gt;payment-api-9f8e7d6c5-x9zzz&lt;/code&gt; 最近 200 行日志的拉取结果：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;日志内容为空(0 行)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可能的原因有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pod 刚刚启动，还没有产生任何日志输出&lt;/strong&gt; — 如果应用本身没有主动打日志，或者日志都被输出到了 stdout 之外的某些文件，这里就会取不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用把日志重定向到了文件而非 stdout/stderr&lt;/strong&gt; — Kubernetes &lt;code dir=&quot;auto&quot;&gt;kubectl logs&lt;/code&gt; 只能拿到容器标准输出流的内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志采集走了 sidecar/agent，容器内 stdout 本身没东西&lt;/strong&gt; — 例如日志被 stdout 转发到了某个日志收集器，&lt;code dir=&quot;auto&quot;&gt;kubectl logs&lt;/code&gt; 取不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod 处于 CrashLoopBackOff / 已重启过多次&lt;/strong&gt; — 当前容器实例的日志为空&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;这段输出读起来像资深 SRE 的排查思路，但&lt;strong&gt;每一个字都是编的&lt;/strong&gt;。工具协议层返回了 &lt;code dir=&quot;auto&quot;&gt;isError=false&lt;/code&gt;，content 为空——模型把「空结果」解释成了「日志为空」，然后基于这个虚假前提展开推理。&lt;/p&gt;
&lt;p&gt;empty_ok 的问题在于：它&lt;strong&gt;不是错误&lt;/strong&gt;，但&lt;strong&gt;没有信息&lt;/strong&gt;。模型收到的是一个「成功但无内容」的信号，而 LLM 的训练目标让它必须给出有意义的回复，于是它开始脑补。&lt;/p&gt;
&lt;p&gt;informative 场景下 MiniMax 没有编故事，而是正经地调了 &lt;code dir=&quot;auto&quot;&gt;k8s_list_pods&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;k8s_list_pods&lt;/code&gt; 尝试换路径。有线索的错误反而引导了正确的恢复行为。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现二deepseek-的-400-是客户端问题不是模型问题&quot;&gt;发现二：deepseek 的 400 是客户端问题，不是模型问题&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;deepseek-v4-flash 在三种失败场景下全部 &lt;code dir=&quot;auto&quot;&gt;http_error_400&lt;/code&gt;，第一轮就断。抓包看请求体：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;The `reasoning_content` in the thinking mode must be passed back to the API.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;invalid_request_error&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;deepseek 的 thinking 模式要求：assistant 消息里的 &lt;code dir=&quot;auto&quot;&gt;reasoning_content&lt;/code&gt; 必须在后续调用中原样回传。实验代码的 &lt;code dir=&quot;auto&quot;&gt;assistant_msg()&lt;/code&gt; 做了这个处理，但只在 &lt;code dir=&quot;auto&quot;&gt;reasoning_content&lt;/code&gt; 非空时才传键——如果某轮 deepseek 返回了空 reasoning（空字符串或 None），下一轮请求就缺了这个字段，直接 400。&lt;/p&gt;
&lt;p&gt;这不是 MCP 的问题，是 deepseek thinking 模式 + 工具调用 + 多轮对话的组合坑。如果你的 Agent 框架用 deepseek，遇到工具调用失败后的多轮恢复，&lt;strong&gt;必须检查 reasoning_content 是否被正确透传&lt;/strong&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现三qwen-和-gemini-的重试是无效循环&quot;&gt;发现三：qwen 和 gemini 的「重试」是无效循环&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;qwen3.8-flash 在 informative 场景下的 trace：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 0: k8s_get_pod_logs(namespace=prod, pod=payment-api-9f8e7d6c5-x9zzz, tail=200)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 1: k8s_list_pods(namespace=prod, labelSelector=app=payment-api)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 2: k8s_list_pods(namespace=prod)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;pod 不存在 → 换 list 找 pod → list 没找到（因为 label 不对）→ 再 list 一次不带 label。3 轮用完，没有结论。&lt;/p&gt;
&lt;p&gt;gemini-3.8-flash 在 informative 场景下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 0: k8s_get_pod_logs(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 1: k8s_list_pods(namespace=prod)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;turn 2: k8s_list_deployments(namespace=prod)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;pod 找不到 → list pods → list deployments。它把「pod 不存在」理解成了「也许 deployment 层面有问题」，方向错了。&lt;/p&gt;
&lt;p&gt;两个模型的共同问题：&lt;strong&gt;有线索的错误没有触发「放弃当前路径」的决策&lt;/strong&gt;。它们把「换工具」当成了「解决错误」本身，而不是「验证假设」的手段。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现四命名空间猜测模型的性格差异&quot;&gt;发现四：命名空间猜测——模型的性格差异&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;多轮实验 MT2：用户问「payment-api-7d9f4c8b6-x2knp 现在 CPU 和内存用了多少」，文本里没有 namespace。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;th&gt;轮数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;default → payment → payments → prod → production → kube-system → prod&lt;/td&gt;
&lt;td&gt;10 轮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.8-flash&lt;/td&gt;
&lt;td&gt;default → production → staging → default → payment&lt;/td&gt;
&lt;td&gt;6 轮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax-M3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;直接反问：「请问部署在哪个命名空间？」&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 轮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-3.8-flash&lt;/td&gt;
&lt;td&gt;default → payment → production → prod&lt;/td&gt;
&lt;td&gt;7 轮&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MiniMax 的 0 轮反问是&lt;strong&gt;最优策略&lt;/strong&gt;：它识别出信息缺口，选择不猜。deepseek 和 qwen 的枚举是&lt;strong&gt;最劣策略&lt;/strong&gt;：它们把「猜 namespace」当成了「解决问题」，消耗大量 token 和延迟，最终可能还是错的。&lt;/p&gt;
&lt;p&gt;gemini 第 2 轮就试到 prod，但那是运气，不是策略。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;工程清单&quot;&gt;工程清单&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具失败时，回灌必须有可行动线索&lt;/strong&gt;。&lt;code dir=&quot;auto&quot;&gt;Error: upstream request failed&lt;/code&gt; 是垃圾输入，&lt;code dir=&quot;auto&quot;&gt;Error: pods &quot;xxx&quot; not found in namespace &quot;prod&quot;&lt;/code&gt; 能帮助模型换路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;empty_ok（协议层成功但语义为空）必须显式标记&lt;/strong&gt;。在宿主层把空结果转成 &lt;code dir=&quot;auto&quot;&gt;isError=true&lt;/code&gt; + 提示文本，或者至少加一条 &lt;code dir=&quot;auto&quot;&gt;content: [{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;查询成功但无结果&quot;}]&lt;/code&gt;。否则模型会编原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deepseek thinking 模式 + 多轮工具调用，检查 reasoning_content 透传&lt;/strong&gt;。缺了就是 400，一轮就断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信息缺口时，「反问」比「枚举」好&lt;/strong&gt;。如果工具 schema 里 namespace 是必填，而用户没给，好的 Agent 应该问，而不是猜。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 轮重试上限要配死&lt;/strong&gt;。qwen 和 gemini 的 loop_exhausted 说明没有上限时模型会一直换工具，消耗 token 不产出结论。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;4 个模型、3 种失败形态、12 个工具，样本量小。不同 SDK 版本、不同 gateway 的行为可能不同。&lt;/li&gt;
&lt;li&gt;工具是桩实现，返回固定文本。真实 K8s API 的错误格式可能触发不同的模型行为。&lt;/li&gt;
&lt;li&gt;没测「empty_ok 但 content 里有空字符串」的变体——那种情况可能更隐蔽。&lt;/li&gt;
&lt;li&gt;MiniMax 的「反问」策略只在 s12 工具集下出现；换 s6 或 s24 是否一致，没测。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具清单见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 AI Coding 分类&lt;/a&gt;，里面有 MCP 相关的开源实现。&lt;/p&gt;</content:encoded><category>MCP</category><category>Tool Use</category><category>Function Calling</category><category>LLM 错误处理</category><category>AI Agent</category><category>多轮对话</category></item><item><title>AI 写 Playwright 选择器，5 个模型 5 种 DOM 变异实测：i18n 团灭</title><link>https://www.xlabs.club/blog/ai-selector-mutation-testing/</link><guid isPermaLink="true">https://www.xlabs.club/blog/ai-selector-mutation-testing/</guid><description>5 模型 × 4 选择器 × 5 种 DOM 变异实测：CSS Modules、多层包裹都免疫，换英文文案时 5 个 AI 全断；i18n 下仅 1 个模型存活。</description><pubDate>Wed, 30 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;让 AI 生成 Playwright 用例时，我关心的是它写的选择器在页面前端重构之后还能不能活。上一篇 &lt;a href=&quot;https://www.xlabs.club/blog/ai-generated-e2e-tests-mutation-testing/&quot;&gt;AI 生成的 E2E 测试能抓到真 bug 吗&lt;/a&gt; 测的是「能不能抓到注入的功能缺陷」，这篇测另一个维度——&lt;strong&gt;选择器本身在 UI 不变语义、只变结构/文案时的存活率&lt;/strong&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验设计&quot;&gt;实验设计&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;一个用户管理后台的静态 HTML，包含：新建按钮、搜索框、三行用户列表、行内编辑/删除按钮、分页器。把它喂给 5 个模型，让它们为 4 个操作各写一个 Playwright locator 表达式（&lt;code dir=&quot;auto&quot;&gt;getByRole(...)&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;page.locator(...)&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;然后做 5 个&lt;strong&gt;语义等价&lt;/strong&gt;的 DOM 变异，让选择器逐一在 6 个页面上跑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;M1 CSS Modules&lt;/strong&gt;：所有 class 换成 &lt;code dir=&quot;auto&quot;&gt;buttonPrimary_a3f9&lt;/code&gt; 这种带后缀的形式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M2 i18n 英文&lt;/strong&gt;：整页文案换成英文（「新建用户」→「Create User」）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M3 包裹一层&lt;/strong&gt;：按钮外包 &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;div class=&quot;action-area&quot;&gt;&amp;#x3C;div class=&quot;action-inner&quot;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M4 动态 id&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;id=&quot;create-user-btn&quot;&lt;/code&gt; 变成 &lt;code dir=&quot;auto&quot;&gt;id=&quot;create-user-btn-a1b2c3&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M5 aria 语义化&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;button&lt;/code&gt; 换成 &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;a role=&quot;button&quot;&gt;&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;span role=&quot;button&quot;&gt;&lt;/code&gt;，加 &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判定标准：locator 不仅能命中元素，而且命中的必须是&lt;strong&gt;语义正确的那一个&lt;/strong&gt;（防止 click 落到相邻行的编辑按钮上——这种错误比断掉更糟，因为测试还是绿的，bug 已经漏了）。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;结果i18n-团灭&quot;&gt;结果：i18n 团灭&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;每个模型 4 个选择器 × 6 页面，共 120 个执行格子：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;=== deepseek-v4.1-flash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;create_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;button&apos;, { name: &apos;新建用户&apos;, exact: true })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;search_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByPlaceholder(&apos;输入用户名搜索&apos;, { exact: true })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;edit_zhangwei   ✓ ✓ · ✓ ✓ ·  &amp;#x3C;- locator(&apos;tr[data-user-id=&quot;u001&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;编辑&apos;, exact: true })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;delete_wangfang ✓ ✓ · ✓ ✓ ·  &amp;#x3C;- locator(&apos;tr[data-user-id=&quot;u003&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;删除&apos;, exact: true })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;=== MiniMax-M3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;create_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;button&apos;, { name: &apos;新建用户&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;search_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByPlaceholder(&apos;输入用户名搜索&apos;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;edit_zhangwei   ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- page.locator(&apos;tr[data-user-id=&quot;u001&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;编辑&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;delete_wangfang ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- page.locator(&apos;tr[data-user-id=&quot;u003&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;删除&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;=== gpt-5.6-luna&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;create_user     ✓ ✓ ✓ ✓ · ✓  &amp;#x3C;- #create-user-btn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;search_user     ✓ ✓ ✓ ✓ · ✓  &amp;#x3C;- #search-input&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;edit_zhangwei   ✓ ✓ ✓ ✓ ✓ ✓  &amp;#x3C;- tr[data-user-id=&quot;u001&quot;] button.edit-btn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;delete_wangfang ✓ ✓ ✓ ✓ ✓ ✓  &amp;#x3C;- tr[data-user-id=&quot;u003&quot;] button.delete-btn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;=== kimi-k3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;create_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;button&apos;, { name: &apos;新建用户&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;search_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByPlaceholder(&apos;输入用户名搜索&apos;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;edit_zhangwei   ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- locator(&apos;tr[data-user-id=&quot;u001&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;编辑&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;delete_wangfang ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- locator(&apos;tr[data-user-id=&quot;u003&quot;]&apos;).getByRole(&apos;button&apos;, { name: &apos;删除&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;=== doubao-seed-2.0-pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;create_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;button&apos;, { name: &apos;新建用户&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;search_user     ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByPlaceholder(&apos;输入用户名搜索&apos;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;edit_zhangwei   ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;row&apos;, { name: &apos;张伟&apos; }).getByRole(&apos;button&apos;, { name: &apos;编辑&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;delete_wangfang ✓ ✓ · ✓ ✓ ✓  &amp;#x3C;- getByRole(&apos;row&apos;, { name: &apos;王芳&apos; }).getByRole(&apos;button&apos;, { name: &apos;删除&apos; })&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;每个变异页面让 20 个选择器活下来几个：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;base                 20/20   (基准)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;M1 CSS Modules       20/20   (没有人依赖被改的 class)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;M2 i18n 英文          4/20   (团灭)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;M3 包裹一层          20/20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;M4 动态 id            18/20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;M5 aria 语义化        18/20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;M1 满分有个&lt;strong&gt;实验偏差&lt;/strong&gt;：gpt-5.6-luna 用了 &lt;code dir=&quot;auto&quot;&gt;.edit-btn&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;.delete-btn&lt;/code&gt; 这两个 class，而我的 M1 变异里没改它们（只改了 &lt;code dir=&quot;auto&quot;&gt;btn-primary&lt;/code&gt; 等通用修饰）。如果 CSS Modules 改造是全量的，gpt-5.6-luna 的两个行内按钮选择器会一起断。真实项目里不要把 class 名当稳定契约——它随时可能被构建工具改写。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;三个判断&quot;&gt;三个判断&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. 模型写选择器已经普遍按 Playwright 推荐的 getByRole 路线走，但这条路线对文案是硬依赖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;deepseek-v4.1-flash、MiniMax-M3、kimi-k3、doubao-seed-2.0-pro 四家全部首选 &lt;code dir=&quot;auto&quot;&gt;getByRole(&apos;button&apos;, { name: &apos;...&apos; })&lt;/code&gt;。这是 Playwright 官方推荐、也是社区公认的「最稳」写法。前提是页面文案不变。&lt;/p&gt;
&lt;p&gt;M2（换英文文案）一上，这 16 个选择器全灭。只剩 gpt-5.6-luna 的 4 个——它用的是 &lt;code dir=&quot;auto&quot;&gt;#id&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;data-user-id&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这不是模型选错了策略，是 &lt;strong&gt;「文案稳定」这个假设在 i18n 场景根本不成立&lt;/strong&gt;。给国内单语言后台写用例时没事；一旦产品要出海或做多语言，所有 getByRole 用例需要成批重写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. gpt-5.6-luna 的 &lt;code dir=&quot;auto&quot;&gt;#id&lt;/code&gt; 风格在 i18n 下活下来，但死在了 M4（动态 id 后缀）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;#create-user-btn&lt;/code&gt; 在 M4（&lt;code dir=&quot;auto&quot;&gt;id=&quot;create-user-btn-a1b2c3&quot;&lt;/code&gt;）直接找不到元素。这是组件库多次实例化、或者构建工具自动加 hash 时的常态。一个页面只有 1 个新建按钮时没事，同一页面有两个「新建」入口时 id 一定会变。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它反而是 5 个模型里唯一在 i18n 变异下活下来的&lt;/strong&gt;——以 M4 的死为代价。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. M5（aria 语义化）只断 deepseek 一家，出乎意料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把编辑按钮从「无 aria-label」改成 &lt;code dir=&quot;auto&quot;&gt;aria-label=&quot;编辑用户&quot;&lt;/code&gt;，text content 不变。结果 deepseek-v4.1-flash 的 &lt;code dir=&quot;auto&quot;&gt;getByRole(&apos;button&apos;, { name: &apos;编辑&apos;, exact: true })&lt;/code&gt; 断了——Playwright 的 accessible name 计算优先取 &lt;code dir=&quot;auto&quot;&gt;aria-label&lt;/code&gt;，取到的是「编辑用户」而不是「编辑」，exact match 失配。&lt;/p&gt;
&lt;p&gt;其他四家都没问题：gpt-5.6-luna 用 class selector 完全不在乎 aria-label；MiniMax-M3、kimi-k3、doubao 用 &lt;code dir=&quot;auto&quot;&gt;name: &apos;编辑&apos;&lt;/code&gt;（不带 exact）substring 命中了「编辑用户」。&lt;/p&gt;
&lt;p&gt;讽刺的是：&lt;strong&gt;模型里只有 deepseek 加了 &lt;code dir=&quot;auto&quot;&gt;exact: true&lt;/code&gt;&lt;/strong&gt;——这是更严谨的写法，却反而更脆。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;工程建议&quot;&gt;工程建议&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果让 AI 写回归测试，把这几条塞进 prompt 或 checklist：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果产品会 i18n：禁止依赖 text content / &lt;code dir=&quot;auto&quot;&gt;getByPlaceholder&lt;/code&gt;。要求模型优先选 &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;data-user-id&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;aria-*&lt;/code&gt; 这类不变量。&lt;/li&gt;
&lt;li&gt;如果产品会拆分多语言团队开发：强制 &lt;code dir=&quot;auto&quot;&gt;data-testid&lt;/code&gt; 是唯一可靠的契约。&lt;/li&gt;
&lt;li&gt;不要把 &lt;code dir=&quot;auto&quot;&gt;exact: true&lt;/code&gt; 当默认值。它确实更严谨，但任何一次文案/aria-label 微调都会断。&lt;/li&gt;
&lt;li&gt;拿到 AI 生成的选择器后，至少跑一遍「换文案」「换 div 层级」的变异再入库。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;模型只生成一次，temperature=0；换一个 prompt 风格结果可能不同。我用的 prompt 没提「要考虑 i18n」，这是公平的真实场景——真实用户也不会提。&lt;/li&gt;
&lt;li&gt;M2 的英文翻译是我手工写的（「Delete this user?」对应「确认删除该用户？」），如果机翻成别的措辞，部分 substring 匹配可能蒙对。&lt;/li&gt;
&lt;li&gt;M1 的 CSS class 变异只改了部分 class（&lt;code dir=&quot;auto&quot;&gt;btn-primary&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;btn-danger&lt;/code&gt; 等通用修饰），没改 &lt;code dir=&quot;auto&quot;&gt;.edit-btn&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;.delete-btn&lt;/code&gt; 这类业务按钮 class，所以 M1 列的满分不能外推到「更激进的 CSS Modules 改造」。&lt;/li&gt;
&lt;li&gt;所有 5 个模型都在同一网关下 2026-10-01 实测，模型固化的版本是当时的快照。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;复现&quot;&gt;复现&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;关键的判定逻辑是「locator 必须命中&lt;strong&gt;语义正确&lt;/strong&gt;的元素」——只用 &lt;code dir=&quot;auto&quot;&gt;count() === 1&lt;/code&gt; 不够，因为有些选择器在错误页面上也能命中（比如定位到了相邻行的编辑按钮），那种情况下测试是绿的，bug 已经漏了。我用的是回读 &lt;code dir=&quot;auto&quot;&gt;data-user-id&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt; 前缀这些稳定特征做二次确认。&lt;/p&gt;
&lt;p&gt;同领域 AI 工程工具见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops&lt;/a&gt; 的 AI 分类。MCP/Agent 场景的实测在 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-streamable-http-sdk-behavior/&quot;&gt;MCP Streamable HTTP SDK 行为&lt;/a&gt; 一篇。&lt;/p&gt;</content:encoded><category>AI 测试</category><category>Playwright</category><category>选择器稳定性</category><category>E2E 测试</category><category>变异测试</category><category>LLM 代码生成</category></item><item><title>Playwright Test Agents 拆包：init-agents 落到磁盘的 3 个 agent 定义文件</title><link>https://www.xlabs.club/blog/playwright-test-agents-init-unpacked/</link><guid isPermaLink="true">https://www.xlabs.club/blog/playwright-test-agents-init-unpacked/</guid><description>实测 npx playwright init-agents 1.63.0：四个 loop 各自的落点和格式，healer 定义里「不允许问用户」和 fixme() 兜底条款的原文。</description><pubDate>Mon, 28 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Playwright 1.56 引入的 Planner / Generator / Healer 三个测试 agent，文档里只给了 &lt;code dir=&quot;auto&quot;&gt;npx playwright init-agents --loop=&amp;#x3C;client&gt;&lt;/code&gt; 一句话。这个命令到底往仓里写了什么、agent 的 prompt 里写了什么硬约束，没人拆开看过。&lt;/p&gt;
&lt;p&gt;我把 1.63.0 在空目录里跑了一遍，四个 loop（claude / codex / vscode / opencode）和 &lt;code dir=&quot;auto&quot;&gt;--prompts&lt;/code&gt; 参数各跑一次。下面是落盘产物对比和原文摘录。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;四个-loop-落点不一样&quot;&gt;四个 loop 落点不一样&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--loop=claude&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.claude/agents/playwright-test-planner.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.claude/agents/playwright-test-generator.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.claude/agents/playwright-test-healer.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.mcp.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;seed.spec.ts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;specs/README.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--loop=codex&lt;/code&gt; 把同一批内容写成 TOML，路径换成 &lt;code dir=&quot;auto&quot;&gt;.codex/agents/playwright_test_*.toml&lt;/code&gt;，且 &lt;code dir=&quot;auto&quot;&gt;.mcp.json&lt;/code&gt; 里的 server 名变成带 &lt;code dir=&quot;auto&quot;&gt;tools: [&quot;*&quot;]&lt;/code&gt; 显式白名单的写法。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--loop=vscode&lt;/code&gt; 产物最多：&lt;code dir=&quot;auto&quot;&gt;.github/agents/*.agent.md&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;.vscode/mcp.json&lt;/code&gt; + 一个 &lt;code dir=&quot;auto&quot;&gt;.github/workflows/copilot-setup-steps.yml&lt;/code&gt;（Copilot coding agent 在 CI 镜像里跑 &lt;code dir=&quot;auto&quot;&gt;npm ci &amp;#x26;&amp;#x26; npx playwright install --with-deps&lt;/code&gt; 的引导）。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--loop=opencode&lt;/code&gt; 类似 claude，但落到 &lt;code dir=&quot;auto&quot;&gt;.opencode/&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;公共三件套是 &lt;code dir=&quot;auto&quot;&gt;.mcp.json&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;seed.spec.ts&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;specs/README.md&lt;/code&gt;。&lt;code dir=&quot;auto&quot;&gt;.mcp.json&lt;/code&gt; 永远只声明一个 server：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;playwright-test&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;playwright&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;run-test-mcp-server&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;seed.spec.ts&lt;/code&gt; 是 7 行空 &lt;code dir=&quot;auto&quot;&gt;test.describe&lt;/code&gt;，作用是给 agents 一个「能跑起来」的最小锚点。&lt;code dir=&quot;auto&quot;&gt;specs/README.md&lt;/code&gt; 一句话：「directory for test plans」—— Planner 的产出落在这里。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--prompts&lt;/code&gt; 标志额外多写四个文件，对应四个动作的调用模板（&lt;code dir=&quot;auto&quot;&gt;.claude/prompts/playwright-test-plan.md&lt;/code&gt; 等），内容是 5-9 行的 front matter + 一句提示词，比如 heal 的就一句「Run all my tests and fix the failing ones」。文件本身没有信息密度，主要价值是把入口参数格式固定在仓里。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;healer-定义里两条值得抄出来的条款&quot;&gt;Healer 定义里两条值得抄出来的条款&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;三个 &lt;code dir=&quot;auto&quot;&gt;*.agent.md&lt;/code&gt; 里最硬的两条都长在 healer（&lt;code dir=&quot;auto&quot;&gt;.claude/agents/playwright-test-healer.md&lt;/code&gt;）里，原文摘录：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- If the error persists and you have high level of confidence that the test is correct,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mark this test as test.fixme() so that it is skipped during the execution. Add a comment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;before the failing step explaining what is happening instead of the expected behavior.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Do not ask user questions, you are not interactive tool, do the most reasonable thing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;possible to pass the test.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Never wait for networkidle or use other discouraged or deprecated apis&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;三条各自的含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;test.fixme()&lt;/code&gt; 是合法出口&lt;/strong&gt;。Healer 修不动时会主动选择「跳过 + 注释说明现象」，而不是继续瞎改。这是把「无法修复」显式编码进产物的条款——比静默改成宽断言要诚实。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「不要问用户」是被写死的&lt;/strong&gt;。Agent 在无人值守的 CI 循环里跑，遇到歧义自己选最合理解。这意味着同一个 healer 跑两次，第二次的 patch 可能跟第一次不同——它不被要求幂等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;networkidle&lt;/code&gt; 被点名禁用&lt;/strong&gt;。Planner/Generator 的 prompt 里没有这条，Healer 单独提，作者踩过 &lt;code dir=&quot;auto&quot;&gt;networkidle&lt;/code&gt; 把单页应用卡死的坑。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同时 healer 拿到的工具列表是三个 agent 里&lt;strong&gt;唯一带 &lt;code dir=&quot;auto&quot;&gt;test_debug&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;test_run&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;test_list&lt;/code&gt;&lt;/strong&gt; 的——planner 和 generator 都没有这两个 MCP 工具，意味着它们不能执行已生成的 spec，只能操作浏览器。生成器写完代码没法自己跑一遍验证，这事是 healer 的活。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;generator-强制契约一个文件一个测试&quot;&gt;Generator 强制契约：一个文件一个测试&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Generator 的 agent.md 把产出格式钉死了：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- File should contain single test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- File name must be fs-friendly scenario name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Test must be placed in a describe matching the top-level test plan item&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Test title must match the scenario name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Includes a comment with the step text before each step execution&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;外加 &lt;code dir=&quot;auto&quot;&gt;generator_read_log&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;generator_write_test&lt;/code&gt; 两个专属工具——生成不是「模型自己写 spec 文件」，而是「模型在浏览器里把每一步重放一遍，把动作日志回收，再让模型对照日志写最终的 .spec.ts」。从设计稿上能看出作者想避免的事：写代码时不查日志、编造没真跑过的选择器。&lt;/p&gt;
&lt;p&gt;这同时是一个隐性约束：&lt;strong&gt;Generator 每产一个文件就要走一次完整 setup_page → 回放 → read_log → write_test&lt;/strong&gt;。一个 plan 里 20 个场景就是 20 次完整循环。模型费用、耗时、token 都是线性乘上去的，没有增量压缩机制。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;codex-的差异在-sandbox&quot;&gt;Codex 的差异在 sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Codex 版的 healer 定义开头多了 &lt;code dir=&quot;auto&quot;&gt;sandbox_mode = &quot;workspace-write&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对照我自己的 Codex 记忆（full-auto 沙箱下 git fetch / GitLab API 都不通），这条沙箱声明意味着 healer 在 Codex 里跑时，&lt;strong&gt;写文件可以、但网络出仓是受约束的&lt;/strong&gt;。当 spec 里要命中后端 API 拿数据、&lt;code dir=&quot;auto&quot;&gt;test_debug&lt;/code&gt; 要拿 trace 时，能不能出得了沙箱要另算。其他三个 loop 的 agent 定义里没有这个声明。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;这个实测能带走什么&quot;&gt;这个实测能带走什么&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三个 agent 是不对称的&lt;/strong&gt;：Planner/Generator 不调 &lt;code dir=&quot;auto&quot;&gt;test_run&lt;/code&gt;，Healer 不调 &lt;code dir=&quot;auto&quot;&gt;generator_write_test&lt;/code&gt;。想让 Generator 写完自己跑验证 → 没有，那是 Healer 的职责边界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;init-agents 不会污染你的 &lt;code dir=&quot;auto&quot;&gt;playwright.config.ts&lt;/code&gt;&lt;/strong&gt;，所有产物是新建文件，删 &lt;code dir=&quot;auto&quot;&gt;.claude/agents/&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;.mcp.json&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;specs/&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;seed.spec.ts&lt;/code&gt; 就能干净回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;升级 Playwright 后要重跑 init-agents&lt;/strong&gt;。&lt;code dir=&quot;auto&quot;&gt;node_modules/playwright/lib/agents/*.agent.md&lt;/code&gt; 是模板源头，1.56 → 1.63 之间模板本身在演化，仓里那份是当时的快照，不会自动跟着 npm 升。&lt;/li&gt;
&lt;li&gt;**Healer 的「不问用户」**意味着它不适合放到默认 CI：一个歧义场景它可能选错「最合理动作」，把真 bug 改成 &lt;code dir=&quot;auto&quot;&gt;fixme()&lt;/code&gt; 跳过。文档把它放到 IDE 循环里是有原因的。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;三个 Playwright MCP server 的工具集见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops AI Coding 分类&lt;/a&gt;。MCP 协议本身的握手和会话机制实测见本站 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-streamable-http-sdk-behavior/&quot;&gt;MCP Streamable HTTP 实测&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>Playwright</category><category>Test Agents</category><category>E2E</category><category>MCP</category><category>Claude Code</category><category>AI Agent</category></item><item><title>MCP Streamable HTTP 实测：Accept 少一项就 406，第二次 initialize 会吃掉整个 Server</title><link>https://www.xlabs.club/blog/mcp-streamable-http-sdk-behavior/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-streamable-http-sdk-behavior/</guid><description>实测 MCP SDK 1.30.0：Accept 必须同时含 json 和 SSE；单 transport 只服务第一个客户端，第二个 initialize 被 -32600 拒掉。</description><pubDate>Tue, 22 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MCP 把 SSE 改成 Streamable HTTP 之后，文档把 Accept、&lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt;、DELETE、405、404 这一整套状态机讲了一遍。我把官方 SDK 1.30.0（Node 26）跑起来实际抓了一遍，结论先放在这里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accept 少一项就是 406&lt;/strong&gt;，错误码 &lt;code dir=&quot;auto&quot;&gt;-32000&lt;/code&gt;，但消息明确写「Client must accept both application/json and text/event-stream」——这是好事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt; 只在 initialize 响应里下发一次&lt;/strong&gt;，之后每个请求/每条流都必须带；不带就是 400，带错/带旧就是 404，DELETE 之后也是 404。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话模式下，&lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 一个实例只接受一次 initialize&lt;/strong&gt;。第二个客户端的 initialize 拿到的是 400 + &lt;code dir=&quot;auto&quot;&gt;-32600 &quot;Only one initialization request is allowed&quot;&lt;/code&gt;——这是 SDK 层面的限制，不是协议的限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议允许 JSON-RPC batch&lt;/strong&gt;，但 SDK 拒绝「一个 batch 里既有 initialize 又有别的方法」，错误消息是「Only one initialization request is allowed」。先把 initialize 单独发完，后续 batch &lt;code dir=&quot;auto&quot;&gt;tools/call&lt;/code&gt; 是正常 200 的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;sessionIdGenerator: undefined&lt;/code&gt; 是另一个世界&lt;/strong&gt;：无会话模式下 GET / DELETE 全部 500，连「不支持」都不愿意告诉你。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面把每一组实验贴出来，curl 形态的你拿去就能复现。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;环境&quot;&gt;环境&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.30.0&lt;/code&gt;，Node 26，&lt;code dir=&quot;auto&quot;&gt;node:http&lt;/code&gt; 起服务（不占端口时让内核分配，跑完即退）。&lt;/li&gt;
&lt;li&gt;客户端就是 &lt;code dir=&quot;auto&quot;&gt;fetch&lt;/code&gt;，没引 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/client&lt;/code&gt;——目的是看裸 HTTP 行为。&lt;/li&gt;
&lt;li&gt;服务实现：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;McpServer&lt;/span&gt;&lt;span&gt;({ name: &lt;/span&gt;&lt;span&gt;&quot;probe&quot;&lt;/span&gt;&lt;span&gt;, version: &lt;/span&gt;&lt;span&gt;&quot;0.1.0&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;server.&lt;/span&gt;&lt;span&gt;tool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;echo&quot;&lt;/span&gt;&lt;span&gt;, { text: z.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;() }, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content: [{ type: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;, text: &lt;/span&gt;&lt;span&gt;`echo:${&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt; }]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;transport&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;StreamableHTTPServerTransport&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;sessionIdGenerator&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; crypto.&lt;/span&gt;&lt;span&gt;randomUUID&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; server.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(transport);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;createServer&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; transport.&lt;/span&gt;&lt;span&gt;handleRequest&lt;/span&gt;&lt;span&gt;(req, res)).&lt;/span&gt;&lt;span&gt;listen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;127.0.0.1&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;实验一accept-是硬检查不是建议&quot;&gt;实验一：Accept 是硬检查，不是建议&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;POST &lt;code dir=&quot;auto&quot;&gt;/&lt;/code&gt; 发 initialize，分别给四个不同的 Accept：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Accept&lt;/th&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;application/json, text/event-stream&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;正常返回 SSE 帧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;406&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;-32000 Not Acceptable: Client must accept both application/json and text/event-stream&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;text/event-stream&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;406&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缺省&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;空响应体，没错误消息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;GET 同理，只是更狠：&lt;strong&gt;GET 只接受 &lt;code dir=&quot;auto&quot;&gt;text/event-stream&lt;/code&gt;&lt;/strong&gt;——&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GET /&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Accept: application/json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Mcp-Session-Id: &amp;#x3C;sid&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 406&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;error&quot;:{&quot;code&quot;:-32000,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;message&quot;:&quot;Not Acceptable: Client must accept text/event-stream&quot;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;id&quot;:null}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;所以 MCP client 不能写「Accept: application/json 就行」这种代码。两条 accept 都得列上。&lt;/p&gt;
&lt;p&gt;Content-Type 也是硬检查：&lt;code dir=&quot;auto&quot;&gt;text/plain&lt;/code&gt; 拿到 415 &lt;code dir=&quot;auto&quot;&gt;Unsupported Media Type: Content-Type must be application/json&lt;/code&gt;。这一条符合常识，写出来是因为 500 和 406 的差距只在 SDK 里几行，不要把 500 当成「服务器内部错」。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验二mcp-session-id-是单-transport-的生死状&quot;&gt;实验二：&lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt; 是单 transport 的生死状&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;initialize 响应：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 200&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Content-Type: text/event-stream&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Mcp-Session-Id: f3952143-bf9e-4507-b3b9-6a769d36b9c3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;event: message&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data: {&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2025-03-26&quot;,...},&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;之后每个请求的行为：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;请求&lt;/th&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST 带正确 session&lt;/td&gt;
&lt;td&gt;200，SSE 帧&lt;/td&gt;
&lt;td&gt;正常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST 缺 session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;-32000 Bad Request: Mcp-Session-Id header is required&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST 带伪造 session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;-32001 Session not found&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST 带 DELETE 过的 session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET 带 session（Accept: text/event-stream）&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;打开 SSE 流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET 缺 session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同 POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET 带伪造 session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;404&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE 带 session&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;关闭会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE 缺 session&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;同一个 &lt;code dir=&quot;auto&quot;&gt;-32000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE 重复一次&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;幂等失败语义&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;DELETE 是真实的&lt;/strong&gt;：删完之后 session 立刻失效，再发任何东西都 404。规范里写「server MAY return 405」意思是「可以不支持 DELETE」，但 SDK 是支持 DELETE 的，所以 405 在这个实现里不会出现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt; 的大小写&lt;/strong&gt;：Node 的 HTTP 头不区分大小写，&lt;code dir=&quot;auto&quot;&gt;mcp-session-id&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt; 等价。抓包时看到的是小写。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验三一个-mcpserver-只接受一次-initialize&quot;&gt;实验三：一个 &lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 只接受一次 initialize&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这是&lt;strong&gt;坑&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我用同一个 &lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 实例、同一个 &lt;code dir=&quot;auto&quot;&gt;StreamableHTTPServerTransport&lt;/code&gt;，先让客户端 A initialize 拿到 &lt;code dir=&quot;auto&quot;&gt;s1&lt;/code&gt;，再让客户端 B initialize：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 400&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;error&quot;:{&quot;code&quot;:-32600,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;message&quot;:&quot;Invalid Request: Server already initialized&quot;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;id&quot;:null}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;-32600 是 JSON-RPC 标准的 Invalid Request。文档里没写这一条，但 SDK 就是这么实现的——一个 server 实例只服务一个逻辑客户端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多客户端的正确姿势是给每个会话建一个 transport，挂到同一个 endpoint 上&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;transports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;createServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sid&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; req.headers[&lt;/span&gt;&lt;span&gt;&quot;mcp-session-id&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; transport;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (sid &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; transports.&lt;/span&gt;&lt;span&gt;has&lt;/span&gt;&lt;span&gt;(sid)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;transport &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; transports.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(sid);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;sid &lt;/span&gt;&lt;span&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span&gt; req.method &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;POST&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 新会话&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;transport &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;StreamableHTTPServerTransport&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;sessionIdGenerator&lt;/span&gt;&lt;span&gt;: () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; crypto.&lt;/span&gt;&lt;span&gt;randomUUID&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;onsessioninitialized&lt;/span&gt;&lt;span&gt;: (&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; { transports.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(id, transport); },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;transport.&lt;/span&gt;&lt;span&gt;onclose&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (transport.sessionId) transports.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(transport.sessionId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;buildServer&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(transport);   &lt;/span&gt;&lt;span&gt;// buildServer() 返回新实例&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;res.&lt;/span&gt;&lt;span&gt;writeHead&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;); res.&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* Bad Request */&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; transport.&lt;/span&gt;&lt;span&gt;handleRequest&lt;/span&gt;&lt;span&gt;(req, res);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}).&lt;/span&gt;&lt;span&gt;listen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这是 TypeScript SDK &lt;code dir=&quot;auto&quot;&gt;examples/&lt;/code&gt; 里的标准模式。要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个 transport 配一个独立的 &lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 实例&lt;/strong&gt;，不是共享。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;onsessioninitialized&lt;/code&gt; 把新 session 注册进 Map，&lt;code dir=&quot;auto&quot;&gt;onclose&lt;/code&gt; 把它摘掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DELETE 触发 &lt;code dir=&quot;auto&quot;&gt;onclose&lt;/code&gt;&lt;/strong&gt;，Map 自动清，不需要手动管过期。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实测两个会话同时存在、互不影响：s1 DELETE 之后 s2 还能 &lt;code dir=&quot;auto&quot;&gt;tools/call&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验四无会话模式sessionidgenerator-undefined是半成品&quot;&gt;实验四：无会话模式（&lt;code dir=&quot;auto&quot;&gt;sessionIdGenerator: undefined&lt;/code&gt;）是半成品&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;不设 &lt;code dir=&quot;auto&quot;&gt;sessionIdGenerator&lt;/code&gt; 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;initialize 200，&lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt; 不下发（正确）；&lt;/li&gt;
&lt;li&gt;tools/list 200（正确）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GET 直接 500&lt;/strong&gt;，没错误消息；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DELETE 直接 500&lt;/strong&gt;，没错误消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;规范说 server 可以返回 405 表明不支持 SSE stream / 不支持 DELETE。SDK 没这么做，500 是字面意义的「Internal Server Error」，客户端无法区分「服务器崩了」和「无会话模式不支持」。&lt;/p&gt;
&lt;p&gt;如果一定要做无会话 server（比如内部网关只做 tool proxy），&lt;strong&gt;GET / DELETE 你自己在 HTTP 框架层面接住&lt;/strong&gt;，返回 405 + &lt;code dir=&quot;auto&quot;&gt;Allow: POST&lt;/code&gt;，别放给 SDK。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验五json-rpc-batch-的隐藏约束&quot;&gt;实验五：JSON-RPC batch 的隐藏约束&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;标准 JSON-RPC 支持 batch（一个数组里多个请求）。SDK 支持 batch，但&lt;strong&gt;initialize 必须单独发&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;POST /&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Content-Type: application/json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Accept: application/json, text/event-stream&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;initialize&quot;,&quot;params&quot;:{...}},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:2,&quot;method&quot;:&quot;tools/list&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 400&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;error&quot;:{&quot;code&quot;:-32600,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;message&quot;:&quot;Invalid Request: Only one initialization request is allowed&quot;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;id&quot;:null}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;但同一个会话上，batch 两个 &lt;code dir=&quot;auto&quot;&gt;tools/call&lt;/code&gt; 是合法的：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 200&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Content-Type: text/event-stream&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;event: message&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data: {&quot;result&quot;:{&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;echo:a&quot;}]},...,&quot;id&quot;:10}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;event: message&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data: {&quot;result&quot;:{&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;echo:b&quot;}]},...,&quot;id&quot;:11}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;两个 SSE &lt;code dir=&quot;auto&quot;&gt;message&lt;/code&gt; 事件按序返回。&lt;strong&gt;这个行为对 client 有影响&lt;/strong&gt;：你得同时按 id 分发响应，而不是按行序列化处理。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验六协议版本协商传一个未来版本会怎么样&quot;&gt;实验六：协议版本协商——传一个未来版本会怎么样&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;发 &lt;code dir=&quot;auto&quot;&gt;protocolVersion: &quot;2999-01-01&quot;&lt;/code&gt; 给 SDK 1.30.0：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 200&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;event: message&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data: {&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2025-11-25&quot;,...}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;不抛错，而是返回 server 支持的最新版本&lt;/strong&gt;（1.30.0 是 2025-11-25）。客户端要不要再连下去，是它的事——规范允许这么干，但绝大多数 client 拿到不匹配的版本会直接断开。&lt;/p&gt;
&lt;p&gt;发老版本 &lt;code dir=&quot;auto&quot;&gt;2024-11-05&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HTTP/1.1 200&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data: {&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2024-11-05&quot;,...}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;回的是老版本&lt;/strong&gt;——server 降级接受了。注意响应头里&lt;strong&gt;没有&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;Mcp-Protocol-Version&lt;/code&gt;（这个头只对 2025-03-26 之后的版本才强制，老版本协商时不出现在响应头里）。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;给-mcp-client-的五条硬规约&quot;&gt;给 MCP Client 的五条硬规约&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果你要写一个能连上任意 MCP Server 的 client：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;永远发 &lt;code dir=&quot;auto&quot;&gt;Accept: application/json, text/event-stream&lt;/code&gt;&lt;/strong&gt;，少一项就 406。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;永远单独发 initialize&lt;/strong&gt;，不要 batch。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;initialize 响应里如果有 &lt;code dir=&quot;auto&quot;&gt;Mcp-Session-Id&lt;/code&gt;，之后每个请求都带上&lt;/strong&gt;（包括 GET、DELETE、notification）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;protocolVersion&lt;/code&gt; 用 client 支持的版本，不要写「最新」&lt;/strong&gt;。server 可能接受也可能拒绝，看它版本新旧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DELETE 之后再发请求会 404&lt;/strong&gt;。client 应该捕获 404 + &lt;code dir=&quot;auto&quot;&gt;-32001&lt;/code&gt; 自动重新 initialize，不要让用户看见红色的错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;给-mcp-server-的四条建议&quot;&gt;给 MCP Server 的四条建议&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每个会话一个 transport&lt;/strong&gt;，每个 transport 一个 &lt;code dir=&quot;auto&quot;&gt;McpServer&lt;/code&gt; 实例——不要共享。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;onsessioninitialized&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;onclose&lt;/code&gt; 必须实现&lt;/strong&gt;，不然 session 表漏泄。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;sessionIdGenerator&lt;/code&gt; 用 &lt;code dir=&quot;auto&quot;&gt;crypto.randomUUID()&lt;/code&gt;&lt;/strong&gt;，不要自增。规范要求密码学安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无会话模式自己处理 GET / DELETE&lt;/strong&gt;。SDK 默认 500 不友好，客户端会当故障处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;实验只测了 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.30.0&lt;/code&gt; 的 Node 实现。Python SDK / Rust rmcp 行为不同（比如 rmcp 的 &lt;code dir=&quot;auto&quot;&gt;SessionId&lt;/code&gt; 是 UUID 类型，校验比 TS 严）。&lt;/li&gt;
&lt;li&gt;没有测 SSE 流上的服务器主动推送（&lt;code dir=&quot;auto&quot;&gt;notifications/*&lt;/code&gt; 那条线）——本轮只用了 request/response，没有模拟 server 推送。&lt;/li&gt;
&lt;li&gt;没有测 DNS rebinding / Origin 校验。SDK 有 &lt;code dir=&quot;auto&quot;&gt;enableDnsRebindingProtection&lt;/code&gt; 选项，默认开，但只在 stdio→HTTP 转发场景有意义。&lt;/li&gt;
&lt;li&gt;错误消息可能随 SDK 版本变化。比如 &lt;code dir=&quot;auto&quot;&gt;-32001 Session not found&lt;/code&gt; 这个码不是 JSON-RPC 标准，是 SDK 自定义的——未来版本可能改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更多 MCP / AI Agent 工具见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-infra&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 AI Infra 分类&lt;/a&gt;。站内之前那篇 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-2026-07-28-version-negotiation/&quot;&gt;MCP 2026-07-28 实测：initialize 握手没了&lt;/a&gt; 是从抓包看协议演进；这篇是从 SDK 行为看会话管理和状态机，两边对着看能拼出一个完整图景。&lt;/p&gt;</content:encoded><category>MCP</category><category>Streamable HTTP</category><category>JSON-RPC</category><category>AI Agent</category></item><item><title>AI 生成的 E2E 测试能抓到真 bug 吗：5 个模型 41 个用例的变异实测</title><link>https://www.xlabs.club/blog/ai-generated-e2e-tests-mutation-testing/</link><guid isPermaLink="true">https://www.xlabs.club/blog/ai-generated-e2e-tests-mutation-testing/</guid><description>5 模型 × 41 用例 × 8 注入缺陷实测：变异体击杀 36/40，搜索大小写敏感 4 家全漏，12% 用例零击杀。</description><pubDate>Sun, 20 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;让模型写 E2E 用例，跑出来一片绿。问题在于「绿」本身不携带信息。我在一个真实的单页应用上注入了 8 个真缺陷，让 5 个模型各自生成 Playwright 用例，然后用变异检测的方式判断哪些用例真的在防回归。&lt;/p&gt;
&lt;p&gt;结论先放这里：40 个在正确页面上能通过的用例里，&lt;strong&gt;5 个（12%）在整个实验里从未失败过一次&lt;/strong&gt;；8 个缺陷里有 1 个只有 1 家抓到，其余 4 家全漏；用例数最多的模型花了 1.5 倍的输出 token，只多抓到 1 个缺陷。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;环境与实验设计&quot;&gt;环境与实验设计&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;网关：内网 OpenAI 兼容网关 &lt;code dir=&quot;auto&quot;&gt;https://aihub.firstshare.cn/v1&lt;/code&gt;，2026-09-21 实测，&lt;code dir=&quot;auto&quot;&gt;temperature=0&lt;/code&gt;，每个模型只生成一次。&lt;/li&gt;
&lt;li&gt;运行：Playwright 1.63.0 + 系统自带的 Chromium 152。不下载 Playwright 自带浏览器，直接 &lt;code dir=&quot;auto&quot;&gt;launchOptions.executablePath&lt;/code&gt; 指向系统 chromium：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;playwright.config.js&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;testDir: &lt;/span&gt;&lt;span&gt;&quot;./specs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;use: { headless: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;, launchOptions: { executablePath: &lt;/span&gt;&lt;span&gt;&quot;/usr/bin/chromium&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;被测页面：单文件 HTML，内联 JS，无框架，5.6 KB。包含登录（空字段时按钮禁用，&lt;code dir=&quot;auto&quot;&gt;admin&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;secret&lt;/code&gt; 通过）、成员表（12 条数据、每页 5 条、「共 N 条」计数、点姓名表头切换排序、按姓名搜索且大小写不敏感）、添加成员（新增后清空输入框）。&lt;/li&gt;
&lt;li&gt;页面用 &lt;code dir=&quot;auto&quot;&gt;file://&lt;/code&gt; 打开，整个实验没有任何常驻进程和监听端口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;8 个变异体，每个只改一行&lt;/strong&gt;，都是外部可观察行为的改变：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;改动&lt;/th&gt;
&lt;th&gt;可观察差异&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;搜索去掉 &lt;code dir=&quot;auto&quot;&gt;toLowerCase()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;搜 &lt;code dir=&quot;auto&quot;&gt;ALICE&lt;/code&gt; 由 1 条变 0 条&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m2&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;PAGE_SIZE = 5&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每页行数与「第 1 / 3 页」变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m3&lt;/td&gt;
&lt;td&gt;错误提示文案改掉&lt;/td&gt;
&lt;td&gt;断言固定文案的用例失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m4&lt;/td&gt;
&lt;td&gt;添加成员后不清空输入框&lt;/td&gt;
&lt;td&gt;输入框 value 变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m5&lt;/td&gt;
&lt;td&gt;排序开关失效，永远升序&lt;/td&gt;
&lt;td&gt;首次点击表头不再变降序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m6&lt;/td&gt;
&lt;td&gt;登录按钮不再按空字段禁用&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;toBeDisabled()&lt;/code&gt; 失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m7&lt;/td&gt;
&lt;td&gt;末页「下一页」不再禁用&lt;/td&gt;
&lt;td&gt;边界态变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m8&lt;/td&gt;
&lt;td&gt;「共 N 条」改用全量数据&lt;/td&gt;
&lt;td&gt;搜索后计数不更新&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;变异体和基线页面由脚本从同一份 &lt;code dir=&quot;auto&quot;&gt;app.html&lt;/code&gt; 做单点字符串替换生成，每个锚点在基线中唯一，改动内容可复核。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;协议&lt;/strong&gt;：把页面源码全文放进 prompt，要求输出单个 &lt;code dir=&quot;auto&quot;&gt;@playwright/test&lt;/code&gt; 文件，覆盖登录校验、登录成功、搜索、分页、排序、添加成员。生成时告诉模型应用在 &lt;code dir=&quot;auto&quot;&gt;http://localhost:3000/&lt;/code&gt;，跑之前只做一件事——把 spec 里的这个地址替换成本地 &lt;code dir=&quot;auto&quot;&gt;file://&lt;/code&gt; 路径，其他一字不改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判定&lt;/strong&gt;：只有「在正确页面上通过、在该变异体上失败」的用例才算真的抓到了缺陷。基线就失败的用例不算击杀——它只是坏的用例碰对了。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;结果&quot;&gt;结果&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;用例&lt;/th&gt;
&lt;th&gt;断言&lt;/th&gt;
&lt;th&gt;击杀&lt;/th&gt;
&lt;th&gt;漏掉&lt;/th&gt;
&lt;th&gt;输出 token&lt;/th&gt;
&lt;th&gt;生成耗时&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax-M3&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8/8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;8502&lt;/td&gt;
&lt;td&gt;44.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-sonnet-4-6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;7879&lt;/td&gt;
&lt;td&gt;25.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-pro&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;10478&lt;/td&gt;
&lt;td&gt;36.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;11023&lt;/td&gt;
&lt;td&gt;35.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.8-flash&lt;/td&gt;
&lt;td&gt;6（5 个基线通过）&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;m1&lt;/td&gt;
&lt;td&gt;5336&lt;/td&gt;
&lt;td&gt;75.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;m2 到 m8 这 7 个缺陷被 5 家全部击杀；唯一活下来的是 m1。5 家合计击杀 36/40。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;唯一漏掉的缺陷搜索的大小写&quot;&gt;唯一漏掉的缺陷：搜索的大小写&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;m1 的改动就是这一行：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 基线&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; m.name.&lt;/span&gt;&lt;span&gt;toLowerCase&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;indexOf&lt;/span&gt;&lt;span&gt;(keyword.&lt;/span&gt;&lt;span&gt;toLowerCase&lt;/span&gt;&lt;span&gt;()) &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 变异体&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; m.name.&lt;/span&gt;&lt;span&gt;indexOf&lt;/span&gt;&lt;span&gt;(keyword) &lt;/span&gt;&lt;span&gt;&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;只有 MiniMax-M3 写了这条用例，它也是整轮里唯一杀死 m1 的用例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;搜索应大小写不敏感&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#search&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;ALICE&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#count-info&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;共 1 条&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.cell-name&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt;()).&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;alice&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;另外 4 家都测了搜索，输入全是小写 &lt;code dir=&quot;auto&quot;&gt;alice&lt;/code&gt;。差异不在工具、不在框架、也不在覆盖率——5 家拿到的是同一份源码，&lt;code dir=&quot;auto&quot;&gt;toLowerCase()&lt;/code&gt; 就写在第 30 行。差的是模型有没有想到「用户会开着大写锁定敲搜索框，或者直接粘贴一段大写文本」。&lt;/p&gt;
&lt;p&gt;这是 AI 生成用例最像人的地方：覆盖面取决于它的联想，而不是取决于被测代码里有什么。指望用覆盖率指标找出这种缺口是不行的，&lt;code dir=&quot;auto&quot;&gt;fill(&apos;ALICE&apos;)&lt;/code&gt; 和不写这条用例，行覆盖完全一样。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;12-的用例一个缺陷都没抓到&quot;&gt;12% 的用例一个缺陷都没抓到&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;40 个有效用例里有 5 个在 8 个变异体上全部通过，意思是它们对「有没有被改坏」不敏感。典型例子：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;两个字段都填写时登录按钮应启用&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#username&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;admin&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#password&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;secret&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#login-btn&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;toBeEnabled&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;它断言的是一个从来没有被破坏的状态。这种用例进 CI 唯一的作用是让报告变长。注意它们的通过率是 100%，任何按「通过率」「用例数」来评价生成质量的看板都会给它满分。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;基线就挂的用例模型把数据数错了&quot;&gt;基线就挂的用例：模型把数据数错了&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;qwen3.8-flash 有一条用例在&lt;strong&gt;正确页面&lt;/strong&gt;上就失败：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#search&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;a&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page.&lt;/span&gt;&lt;span&gt;locator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#count-info&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;toHaveText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;共 6 条&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 实际：共 7 条&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;页面里 12 个名字中含 &lt;code dir=&quot;auto&quot;&gt;a&lt;/code&gt; 的是 7 个（alice、carol、dave、frank、grace、ivan、mallory），模型数成 6 个。这条用例永远不可能变绿，但它会被算进「生成成功 / 用例数 +1」的统计里。&lt;/p&gt;
&lt;p&gt;所以生成用例的第一道门禁只能是「在正确代码上全绿」，第二道才是「对已知缺陷失败」。顺序反过来，你会把坏用例当覆盖。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;用例数量与断言密度&quot;&gt;用例数量与断言密度&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;MiniMax-M3 用 18 个用例、8502 输出 token 换到 8 个击杀，其中 5 个用例从未失败。&lt;/li&gt;
&lt;li&gt;claude-sonnet-4-6 用 6 个用例、81 个断言换到 7 个击杀，断言密度 13.5/用例；MiniMax 是 2.9/用例。&lt;/li&gt;
&lt;li&gt;deepseek-v4-flash 用了 22 次 &lt;code dir=&quot;auto&quot;&gt;.nth(0)&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;.nth(4)&lt;/code&gt; 定位表格行。它能杀掉 m5（排序失效）靠的是断言内容，不是定位方式——一旦行序因为功能变化而改动，这 22 处会集体变成等待超时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;「多写用例」和「写深断言」不是同一件事。这一轮里，让击杀数上升的是后者。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么通过率不能当质量指标&quot;&gt;为什么通过率不能当质量指标&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;根因是 test oracle 的位置。模型只能从源码推断「预期行为」，它写下的断言就是它对这份代码的理解。代码是错的，它就照着错的写断言——文献里把这叫「验证 bug 的测试」：一项针对商用与开源生成工具的研究（arXiv 2412.14137）发现，带过滤/自动修复机制的生成工具，最终套件里最多 68.1% 的用例会在错误实现上通过、在正确实现上失败。&lt;/p&gt;
&lt;p&gt;另外两个对照，说明这个问题的量级不是本轮实验特有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ACL 2026 Findings 的 &lt;a href=&quot;https://aclanthology.org/2026.findings-acl.1976/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SWE-Mutation&lt;/a&gt; 在单元测试上测了 7 个模型，最好的 DeepSeek-V3.1 相对检出率只有 36.15%；而且语义级变异体比规则变异体难杀得多（平均检出率 71.04% → 39.81%）。&lt;/li&gt;
&lt;li&gt;本轮 E2E 的 90%（36/40）不能拿去和 36.15% 对比。这里每个变异体都改动了页面上的外部可观察行为，而 m8 那种「只改一句文案/计数」在单元测试里属于最容易杀的一档；而且模型拿到的是完整源码，是白盒。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正可操作的部分是：&lt;strong&gt;用变异检测来验收这批用例&lt;/strong&gt;。m8 只改了计数文案，覆盖率一点没变，通过率一点没变，只有变异检测能把它标出来。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;落地清单&quot;&gt;落地清单&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;生成完用例先跑一次变异检测——手工注入 8 个一行改动就够，不需要引 Stryker 这类完整工具链。&lt;/li&gt;
&lt;li&gt;门禁两条：基线全绿；对已知缺陷失败。两条都不满足的用例退回重写，不要合入。&lt;/li&gt;
&lt;li&gt;断言写页面状态（计数、文案、行序、按钮禁用态），不要只写 happy path 的可点击/可见。&lt;/li&gt;
&lt;li&gt;静态数据能数清的，让脚本生成期望值（如「共 N 条」按数据源算），不要靠模型心算——qwen 那条 &lt;code dir=&quot;auto&quot;&gt;共 6 条&lt;/code&gt; 就是这么挂的。&lt;/li&gt;
&lt;li&gt;定位器优先 id / role / label；&lt;code dir=&quot;auto&quot;&gt;.nth(索引)&lt;/code&gt; 定位表格行的地方标记出来，排序或分页一改就是一批超时。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再用哪家的生成能力是次要问题：这一轮 5 家的差距在「有没有想到那条边界」上，而不在能不能写出可运行的 Playwright 代码——5 家的用例都能跑起来。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;单个页面、8 个 UI 级变异体、每个模型只生成一次、每个变异体只跑一次，没有做重复与 flaky 统计。&lt;/li&gt;
&lt;li&gt;模型拿到完整源码（白盒）。只给需求文档的话，结果只会更差，但差多少没测。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;file://&lt;/code&gt; 打开页面，与真实 dev server 的差异在跨域和 fetch 上，本页没有这两类行为。&lt;/li&gt;
&lt;li&gt;8 个变异体都至少被一家击杀过，所以集合里没有等价变异体（改行为但不改观察结果的那种）混入。&lt;/li&gt;
&lt;li&gt;生成侧还有 2 家在本次调用里没跑通：glm 渠道返回 403 &lt;code dir=&quot;auto&quot;&gt;client_not_allowed&lt;/code&gt;，kimi-k3 拒绝 &lt;code dir=&quot;auto&quot;&gt;temperature=0&lt;/code&gt;（要求 &lt;code dir=&quot;auto&quot;&gt;temperature=1&lt;/code&gt;）。这两个是网关侧的接入约束，不代表模型能力。&lt;/li&gt;
&lt;li&gt;m1 只被 1 家击杀，只说明这一次生成里另外 4 家没写这条用例，不代表它们写不出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试与质量工具清单见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#testing-tools&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 Testing Tools 分类&lt;/a&gt;。站内相关：&lt;a href=&quot;https://www.xlabs.club/blog/ai-code-review-tools-comparison/&quot;&gt;AI Code Review 横评——CodeRabbit vs PR-Agent vs Copilot Review&lt;/a&gt; 讨论的是生成之后谁来看的问题，这篇讨论的是生成出来的东西怎么验。&lt;/p&gt;</content:encoded><category>AI 测试</category><category>Playwright</category><category>E2E 测试</category><category>变异测试</category><category>测试用例生成</category><category>LLM 质量门禁</category></item><item><title>LLM 可观测性实测：OpenAI 兼容网关的 usage 靠不靠得住</title><link>https://www.xlabs.club/blog/llm-observability-usage-fields/</link><guid isPermaLink="true">https://www.xlabs.club/blog/llm-observability-usage-fields/</guid><description>5 个模型实测同一台 OpenAI 兼容网关：include_usage 空操作、MiniMax 回传全 0 usage、断连丢 token、prompt_tokens 差 6.8 倍。</description><pubDate>Fri, 18 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;把 LLM 成本看板建在客户端拿到的 &lt;code dir=&quot;auto&quot;&gt;usage&lt;/code&gt; 上，会错多少？我在这台内网 OpenAI 兼容聚合网关上跑了约 80 次调用，答案不是”有点误差”，而是字段缺失和语义不一致。&lt;/p&gt;
&lt;p&gt;环境：一个 &lt;code dir=&quot;auto&quot;&gt;/v1/chat/completions&lt;/code&gt; 后面挂着多家的模型，2026-09-19 实测，&lt;code dir=&quot;auto&quot;&gt;temperature=0&lt;/code&gt;。同一个 20 字中文问题，非流式 &lt;code dir=&quot;auto&quot;&gt;max_tokens=256&lt;/code&gt; 打 5 个模型；流式另有 3 个模型 × 2 种参数 × 2 次重复的配对调用。全部用标准库 &lt;code dir=&quot;auto&quot;&gt;urllib&lt;/code&gt; 直连，限速 2.3s 避免打限流。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;一usage-里能拿到什么5-个模型给出-5-套字段&quot;&gt;一、usage 里能拿到什么，5 个模型给出 5 套字段&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;请求模型&lt;/th&gt;
&lt;th&gt;prompt_tokens&lt;/th&gt;
&lt;th&gt;completion_tokens&lt;/th&gt;
&lt;th&gt;其中 reasoning&lt;/th&gt;
&lt;th&gt;正文长度&lt;/th&gt;
&lt;th&gt;finish_reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;35 字&lt;/td&gt;
&lt;td&gt;stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.8-flash&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;268&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;td&gt;22 字&lt;/td&gt;
&lt;td&gt;stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glm-5.3&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;1 字&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;length&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.6-luna&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;33 字&lt;/td&gt;
&lt;td&gt;stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax-M3&lt;/td&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 字&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;length&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三字段（prompt / completion / total）是唯一的不变量。其余全按供应商各自的习惯：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存命中：deepseek 报 &lt;code dir=&quot;auto&quot;&gt;prompt_tokens_details.cached_tokens&lt;/code&gt;，同时另报一组 &lt;code dir=&quot;auto&quot;&gt;prompt_cache_hit_tokens&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;prompt_cache_miss_tokens&lt;/code&gt;；qwen 在 &lt;code dir=&quot;auto&quot;&gt;prompt_tokens_details&lt;/code&gt; 里给 &lt;code dir=&quot;auto&quot;&gt;cached_tokens&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;text_tokens&lt;/code&gt;；MiniMax 只有 &lt;code dir=&quot;auto&quot;&gt;cached_tokens&lt;/code&gt;；gpt-5.6-luna 连 &lt;code dir=&quot;auto&quot;&gt;prompt_tokens_details&lt;/code&gt; 都没有。&lt;/li&gt;
&lt;li&gt;推理 token 全都计入 &lt;code dir=&quot;auto&quot;&gt;completion_tokens&lt;/code&gt;。所以”输出 token”这个指标跨供应商不可比：同一句话 completion 从 68 到 268，其中 38～256 是推理。&lt;/li&gt;
&lt;li&gt;MiniMax 额外回传 &lt;code dir=&quot;auto&quot;&gt;input_sensitive&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;output_sensitive&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;base_resp&lt;/code&gt;，还带一个 &lt;code dir=&quot;auto&quot;&gt;total_characters&lt;/code&gt; 字段——实测恒为 0，别拿它算字数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;glm-5.3 那一行值得单独看：256 token 预算里 254 个花在推理上，正文只出了 1 个字，&lt;code dir=&quot;auto&quot;&gt;finish_reason=length&lt;/code&gt;。它在 HTTP 层是 200 成功。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;二同一句话的-prompt_tokens-差-68-倍&quot;&gt;二、同一句话的 prompt_tokens 差 6.8 倍&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;上表第一列就是答案：gpt-5.6-luna 29，MiniMax-M3 198。&lt;/p&gt;
&lt;p&gt;你没法用一个 tokenizer 离线估算全站成本。同一段文本的输入 token 数取决于路由到哪个供应商，还可能包含供应商自己追加的提示词（MiniMax 那 198 里，有 156 是服务端缓存命中，第一轮真实未命中约 42）。这也解释了为什么”按字符数估成本”在聚合网关下必然偏。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;三stream_optionsinclude_usage-在这台网关上是个空操作&quot;&gt;三、&lt;code dir=&quot;auto&quot;&gt;stream_options.include_usage&lt;/code&gt; 在这台网关上是个空操作&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;OpenAI 文档和各家云厂商文档的写法都是：流式下要加 &lt;code dir=&quot;auto&quot;&gt;stream_options: {&quot;include_usage&quot;: true}&lt;/code&gt; 才能拿到 usage。&lt;/p&gt;
&lt;p&gt;实测 12 次配对调用（deepseek-v4-flash / glm-5.3 / MiniMax-M3 × 加与不加 × 各 2 次），&lt;code dir=&quot;auto&quot;&gt;usage&lt;/code&gt; 块的个数、位置、内容完全一致。不加也一样有。&lt;/p&gt;
&lt;p&gt;所以这段配置在这台网关上既没有收益也没有副作用。但结论不要外推：换成直连 OpenAI 或别家的网关，行为可能相反。判断方法是抓一次 SSE 流，数一下 &lt;code dir=&quot;auto&quot;&gt;usage&lt;/code&gt; 非 null 的块有几个，而不是读文档。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;四usage-出现在哪个块4-种形态&quot;&gt;四、usage 出现在哪个块，4 种形态&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;流式块数&lt;/th&gt;
&lt;th&gt;前序块&lt;/th&gt;
&lt;th&gt;usage 块&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;65 块 &lt;code dir=&quot;auto&quot;&gt;usage: null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第 66 块，与 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 同块，没有独立 usage 块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.8-flash&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;30 块 &lt;code dir=&quot;auto&quot;&gt;usage: null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第 31 块，&lt;code dir=&quot;auto&quot;&gt;choices: []&lt;/code&gt;；&lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 在第 30 块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glm-5.3&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;18 块 &lt;code dir=&quot;auto&quot;&gt;usage: null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第 19 块，&lt;code dir=&quot;auto&quot;&gt;choices: []&lt;/code&gt;；&lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 在第 18 块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.6-luna&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;37 块&lt;strong&gt;连 &lt;code dir=&quot;auto&quot;&gt;usage&lt;/code&gt; 键都没有&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;第 38 块，&lt;code dir=&quot;auto&quot;&gt;choices: []&lt;/code&gt;；&lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 在第 37 块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax-M3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4 块 &lt;code dir=&quot;auto&quot;&gt;usage: {&quot;total_tokens&quot;:0,&quot;total_characters&quot;:0}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第 5 块是真值，并带 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一批条件（&lt;code dir=&quot;auto&quot;&gt;max_tokens=64&lt;/code&gt;，&lt;code dir=&quot;auto&quot;&gt;temperature=0&lt;/code&gt;）。块数随生成长度变化，看的是 usage 落在哪里。&lt;/p&gt;
&lt;p&gt;deepseek 把 usage 和 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 挤在同一块；另外三家在 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt; 之后再补一个 &lt;code dir=&quot;auto&quot;&gt;choices&lt;/code&gt; 为空的块。MiniMax 最阴：前面每一块都回传一个&lt;strong&gt;非 null、但全为 0&lt;/strong&gt; 的 usage 对象。&lt;/p&gt;
&lt;p&gt;这意味着”取第一个非空 usage”和”见到 usage 就认为请求结束”这两种常见写法，在 MiniMax 这条路径上记录下来的 token 数是 0。同时也不能把多块 usage 累加——那是把一次请求记成多次。&lt;/p&gt;
&lt;p&gt;正确写法只有一条：取最后一份非空 usage。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;usage, finish, content &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;, []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; line &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; resp:                     &lt;/span&gt;&lt;span&gt;# SSE 逐行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; line.startswith(&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;&quot;data:&quot;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;payload &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; line[&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;:].strip()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; payload &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;&quot;[DONE]&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;d &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; json.loads(payload)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;u &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; d.get(&lt;/span&gt;&lt;span&gt;&quot;usage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; u &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; u.get(&lt;/span&gt;&lt;span&gt;&quot;total_tokens&quot;&lt;/span&gt;&lt;span&gt;):    &lt;/span&gt;&lt;span&gt;# 跳过 MiniMax 的全 0 占位块&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;usage &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; u                      &lt;/span&gt;&lt;span&gt;# 只保留最后一份，不累加&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; ch &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; d.get(&lt;/span&gt;&lt;span&gt;&quot;choices&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; []:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;finish &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ch.get(&lt;/span&gt;&lt;span&gt;&quot;finish_reason&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; finish&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;content.append((ch.get(&lt;/span&gt;&lt;span&gt;&quot;delta&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; {}).get(&lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;五客户端断连这条请求的-token-就消失了&quot;&gt;五、客户端断连，这条请求的 token 就消失了&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;流式下收到 3 个内容块后客户端断开（模拟用户取消、前端切页、超时重试）：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;deepseek-v4-flash: content 块 3 个, usage 块 0 个&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;qwen3.8-flash:     content 块 3 个, usage 块 0 个&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;本地一行 token 数都拿不到，而网关侧已经消耗了 prompt token 并生成了一部分内容。把成本真源放在应用日志上，误差就和”用户耐心”正相关——取消得越多，你看到的成本越低。&lt;/p&gt;
&lt;p&gt;边界说明：我没有网关侧日志权限，无法验证这些断连请求在服务端是否照常计费。要验证得看网关自己的记账。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;六200-成功--空正文&quot;&gt;六、200 成功 + 空正文&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;把 &lt;code dir=&quot;auto&quot;&gt;max_tokens&lt;/code&gt; 压到 16：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;deepseek-v4-flash: 正文 0 字, completion_tokens=16 (全为 reasoning), finish_reason=length&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MiniMax-M3:        正文 0 字, completion_tokens=16 (全为 reasoning), finish_reason=length&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;MiniMax-M3 在 &lt;code dir=&quot;auto&quot;&gt;max_tokens=256&lt;/code&gt; 下也给出 &lt;code dir=&quot;auto&quot;&gt;content: &quot;&quot;&lt;/code&gt; 的 200 响应（推理吃光预算）。如果监控只有 HTTP 状态码和延迟，这一档请求全是”健康”的。&lt;/p&gt;
&lt;p&gt;站内上一篇 &lt;a href=&quot;https://www.xlabs.club/blog/prompt-regression-testing/&quot;&gt;Prompt 回归测试&lt;/a&gt; 踩过同一颗雷：&lt;code dir=&quot;auto&quot;&gt;content&lt;/code&gt; 为空先看 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt;，别先怀疑 prompt。那次是回归台，这次是生产网关——同样的判断顺序。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;七前缀缓存按前缀失效改开头等于全价&quot;&gt;七、前缀缓存按前缀失效，改开头等于全价&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;长 prompt（约 1122 token），同一模型连发 4 次：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变体&lt;/th&gt;
&lt;th&gt;prompt_tokens&lt;/th&gt;
&lt;th&gt;cached_tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原样第 1 次&lt;/td&gt;
&lt;td&gt;1122&lt;/td&gt;
&lt;td&gt;896&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原样第 2 次&lt;/td&gt;
&lt;td&gt;1122&lt;/td&gt;
&lt;td&gt;896&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只改结尾加一句&lt;/td&gt;
&lt;td&gt;1140&lt;/td&gt;
&lt;td&gt;896&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只改开头加一个标记&lt;/td&gt;
&lt;td&gt;1127&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;缓存是服务端自动做的，客户端不需要传任何参数。命中粒度是前缀长度，不是比例。&lt;/p&gt;
&lt;p&gt;表里”第 1 次”其实已经不冷了——同一段前缀在前面的调用里出现过，缓存早已建立，所以第一次就是 896 命中。真正有信息量的是最后一行：改开头一个标记，命中归零。&lt;/p&gt;
&lt;p&gt;工程含义直接：稳定内容（system prompt、长文档、规则）放前面，变量放后面。把动态内容拼在开头，等于每次全价。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;八响应体里的-model-不是请求的模型&quot;&gt;八、响应体里的 model 不是请求的模型&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;请求 deepseek-v4-flash   → 响应 model: deepseek-flash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;请求 deepseek-v4.1-flash → 响应 model: deepseek-flash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;请求 deepseek-v4-pro     → 响应 model: deepseek-flash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;三个不同价位的请求名，在响应体里合并成同一个字符串。按响应体 &lt;code dir=&quot;auto&quot;&gt;model&lt;/code&gt; 做成本归因，这条曲线就没有意义了——必须记请求侧的模型名，或者要求网关回传实际路由。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;system_fingerprint&lt;/code&gt; 只有 deepseek 回传，qwen、glm、gpt-5.6-luna、MiniMax 都没有。想靠它检测”后端静默换版本”，在这台网关上不可行。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;落地清单&quot;&gt;落地清单&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;流式取&lt;strong&gt;最后一份非空 usage&lt;/strong&gt;，不累加、不用第一份，显式跳过全 0 占位块。&lt;/li&gt;
&lt;li&gt;usage 缺失记 &lt;code dir=&quot;auto&quot;&gt;unknown&lt;/code&gt;，不要记 0。0 会把”断连”和”免费”混为一谈。&lt;/li&gt;
&lt;li&gt;成本真源放网关或代理侧；应用日志只作参考，用来看影响面而不是算钱。&lt;/li&gt;
&lt;li&gt;输出 token 拆成 reasoning / text 两个指标，跨供应商先分组再比。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt;（尤其 length 占比）和空正文率进同一张看板。&lt;/li&gt;
&lt;li&gt;成本归因按请求侧模型名，不按响应体。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;一台网关、5 个模型、约 80 次调用（含踩到 403 和解析 bug 的重跑）。不同网关的实现差异可能覆盖以上全部结论。&lt;/li&gt;
&lt;li&gt;没测非 200 响应（429、超时）是否扣 token。&lt;/li&gt;
&lt;li&gt;缓存命中只报了 token 数，没有代入单价，所以算不出省了多少钱。&lt;/li&gt;
&lt;li&gt;MiniMax 的 prompt_tokens 偏高一部分来自供应商内部链路，具体构成拆不出来。&lt;/li&gt;
&lt;li&gt;全部为单次观测加少量重复，没有做统计检验；上面的差异都是量级差异。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具清单见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#llm-and-agent-observability&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 LLM and Agent Observability 分类&lt;/a&gt;，那里面有采集和评测两端的开源选项。&lt;/p&gt;</content:encoded><category>LLM 可观测性</category><category>Token 统计</category><category>OpenAI 兼容</category><category>成本归因</category><category>流式输出</category><category>AI Gateway</category></item><item><title>Prompt 回归测试实测：同一份 prompt 在两个模型上错得不一样</title><link>https://www.xlabs.club/blog/prompt-regression-testing/</link><guid isPermaLink="true">https://www.xlabs.club/blog/prompt-regression-testing/</guid><description>11 条告警用例、4 个 prompt 版本、2 个模型共 198 次调用：JSON 合法但契约合规 0/33，temperature=0 判决仍翻转。</description><pubDate>Thu, 17 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;「prompt 改好了」这句话，通常来自一次运行、一个模型、肉眼看输出。&lt;/p&gt;
&lt;p&gt;我搭了一个 11 条告警日志的回归台，同一批用例跑 4 个 prompt 版本、2 个模型，共 198 次调用。主表是 &lt;code dir=&quot;auto&quot;&gt;deepseek-v4-flash&lt;/code&gt;，temperature=0.7，每题 3 次重复：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;prompt 版本&lt;/th&gt;
&lt;th&gt;调用&lt;/th&gt;
&lt;th&gt;裸输出能 &lt;code dir=&quot;auto&quot;&gt;json.loads&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;满足契约&lt;/th&gt;
&lt;th&gt;四字段全对&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1：只说「提取字段，用 JSON 输出」&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1b：v1 + error_type/severity 枚举取值域&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1c：v1 + 禁止 markdown 包裹&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2：工程化（取值域 + 类型 + 反例 + null 规则）&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;28（85%）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;v1 是最常见的写法：31/33 次输出了合法 JSON，看着挺好。但没有一次满足下游契约——JSON 合法不等于能被消费。&lt;/p&gt;
&lt;p&gt;（v1、v2 各 3 次重复；v1b、v1c 是消融版本，单轮 11 个样本。）&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实验怎么搭的&quot;&gt;实验怎么搭的&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;任务：从告警日志里抽 4 个字段，契约长这样：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;service&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;payment-api&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;error_type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;OOM&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;137&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;severity&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;P1&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;service&lt;/code&gt; 是字符串或 null，&lt;code dir=&quot;auto&quot;&gt;error_type&lt;/code&gt; 限 7 个枚举值，&lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 是 &lt;code dir=&quot;auto&quot;&gt;int|null&lt;/code&gt;，&lt;code dir=&quot;auto&quot;&gt;severity&lt;/code&gt; 限 &lt;code dir=&quot;auto&quot;&gt;P0&lt;/code&gt;–&lt;code dir=&quot;auto&quot;&gt;P3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;11 条用例覆盖 OOMKilled exit 137、Metaspace 溢出、MySQL 死锁 1213、JWT 过期 401、上游 429、DNS &lt;code dir=&quot;auto&quot;&gt;no such host&lt;/code&gt;、gRPC DeadlineExceeded、ENOSPC errno 28、readiness 探针 503 等，期望值都是在日志文本里能定位的客观事实。&lt;/p&gt;
&lt;p&gt;断言分三层，分别统计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;格式层：对原始输出做 &lt;code dir=&quot;auto&quot;&gt;json.loads&lt;/code&gt;，不先剥 markdown 代码块——下游解析器也不会剥。&lt;/li&gt;
&lt;li&gt;契约层：字段名、类型、枚举值。&lt;/li&gt;
&lt;li&gt;语义层：字段值是否等于期望。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;控制变量：v1 与 v2 都是单条 user message，不动 system prompt，温度、max_tokens、模型固定。实验台只用 Python 标准库（&lt;code dir=&quot;auto&quot;&gt;urllib&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;ThreadPoolExecutor&lt;/code&gt;），先把数字算出来，再谈要不要上评测平台。&lt;/p&gt;
&lt;p&gt;限流是设计的一部分：网关 30 req/min，本地把最小间隔设成 2.3 秒、并发 4，避免 429 重试拿回空输出——这类脏数据会记到 prompt 的账上，必须先按调用层噪声剔除。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现一两个模型的头号失败模式完全不同&quot;&gt;发现一：两个模型的头号失败模式完全不同&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;同一份 v1 prompt 换模型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code dir=&quot;auto&quot;&gt;deepseek-v4-flash&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code dir=&quot;auto&quot;&gt;glm-5.3-flash&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1 输出被 markdown 包裹&lt;/td&gt;
&lt;td&gt;2/33&lt;/td&gt;
&lt;td&gt;11/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1 能 parse&lt;/td&gt;
&lt;td&gt;31/33&lt;/td&gt;
&lt;td&gt;0/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1b（只加枚举取值域）能 parse&lt;/td&gt;
&lt;td&gt;11/11&lt;/td&gt;
&lt;td&gt;0/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1c（只禁 markdown 包裹）能 parse&lt;/td&gt;
&lt;td&gt;11/11&lt;/td&gt;
&lt;td&gt;11/11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;glm-5.3-flash&lt;/code&gt; 的 v1 输出每一条都以 &lt;code dir=&quot;auto&quot;&gt;```json&lt;/code&gt; 开头，deepseek 只有 2/33 这样。加一句「不要用 markdown 代码块包裹」，GLM 从 0/11 变 11/11；同一句话对 deepseek 几乎无事发生——它本来只有 2/33 被包裹，加完规则的 11 次里是 0 次。&lt;/p&gt;
&lt;p&gt;反过来，只加枚举取值域（v1b），deepseek 的 &lt;code dir=&quot;auto&quot;&gt;error_type&lt;/code&gt; 准确率从 0/33 到 11/11；GLM 还是 0/11，因为输出仍被包裹，改的那部分根本没机会生效。&lt;/p&gt;
&lt;p&gt;同一句话在两个模型上的边际收益差 100%。所以「优化 prompt」之前先做一件事：按失败模式分类统计现有输出，再决定改哪一句。&lt;/p&gt;
&lt;p&gt;顺带一个反例：GLM 的 v1b 有 1 次在 JSON 前加了一行 &lt;code dir=&quot;auto&quot;&gt;## 分析结果&lt;/code&gt;（包裹率 10/11，不是 11/11）。用宽松正则清洗模型输出只能对付已知模式；真正的护栏是让模型不产生这类输出。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现二修好一处弄坏另一处&quot;&gt;发现二：修好一处，弄坏另一处&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;v1b 把 &lt;code dir=&quot;auto&quot;&gt;error_type&lt;/code&gt; 修到 11/11，同时把 &lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 从 v1 的 13/33 打到 2/11。&lt;/p&gt;
&lt;p&gt;原因不在枚举，在类型。契约要求 &lt;code dir=&quot;auto&quot;&gt;int|null&lt;/code&gt;，v1 的 c01 输出数字 &lt;code dir=&quot;auto&quot;&gt;137&lt;/code&gt;，v1b 输出字符串 &lt;code dir=&quot;auto&quot;&gt;&quot;137&quot;&lt;/code&gt;。同一模型、同一用例，只多了一条枚举约束。&lt;/p&gt;
&lt;p&gt;v1b 的 11 次 &lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 实际输出：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c01 &quot;137&quot;  c05 &quot;1213&quot;  c06 &quot;401&quot;  c10 &quot;28&quot;  c11 &quot;503&quot;    ← 数字被序列化成字符串&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c02 &quot;java.lang.OutOfMemoryError: ...&quot;   c03 &quot;Metaspace&quot;   ← 文本塞进数值字段&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c08 &quot;NO_SUCH_HOST&quot;  c09 &quot;DeadlineExceeded&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c04 null   c07 429                                         ← 只有这两次满足契约&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;契约写的是 &lt;code dir=&quot;auto&quot;&gt;int|null&lt;/code&gt;，&lt;code dir=&quot;auto&quot;&gt;&quot;137&quot;&lt;/code&gt; 就是违约。判定在 Python 侧做（&lt;code dir=&quot;auto&quot;&gt;isinstance(obj[&quot;error_code&quot;], int)&lt;/code&gt;），强类型客户端我没跑——但 JSON 契约里的类型不是可以商量的事。&lt;/p&gt;
&lt;p&gt;而只看「模型答得对不对」，这 11 次里 &lt;code dir=&quot;auto&quot;&gt;service&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;error_type&lt;/code&gt; 几乎全对，很容易签字通过。&lt;/p&gt;
&lt;p&gt;这就是 prompt 变更必须过回归的原因：&lt;strong&gt;改动的副作用不是局部的&lt;/strong&gt;。模型的注意力是共享资源，你在「枚举值必须从这 7 个里选」上加了约束，它就可能把这份「照规矩输出」的理解挪到别的字段上，顺手把数字加了引号。单看一遍输出发现不了，因为两边的输出都很「像样」。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现三temperature0-不代表可复现&quot;&gt;发现三：temperature=0 不代表可复现&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;v2 在 temperature=0 下每题采样 4 次，temperature=0.7 下每题 3 次（P=通过，F=失败）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用例&lt;/th&gt;
&lt;th&gt;T=0（4 次）&lt;/th&gt;
&lt;th&gt;T=0.7（3 次）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;c04 JDBC 连接失败&lt;/td&gt;
&lt;td&gt;P F P P&lt;/td&gt;
&lt;td&gt;P F P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c05 MySQL 死锁 1213&lt;/td&gt;
&lt;td&gt;F P P F&lt;/td&gt;
&lt;td&gt;F P P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c07 上游 429&lt;/td&gt;
&lt;td&gt;F F P F&lt;/td&gt;
&lt;td&gt;F F F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其余 8 条&lt;/td&gt;
&lt;td&gt;4/4 全过&lt;/td&gt;
&lt;td&gt;3/3 全过&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;temperature=0 下依然有 3 题判决翻转，但原因各不相同，不能都算在模型头上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;c04 在 T=0.7 的那次 F 是 &lt;code dir=&quot;auto&quot;&gt;content&lt;/code&gt; 为空、&lt;code dir=&quot;auto&quot;&gt;finish_reason=length&lt;/code&gt;——输出被 max_tokens 截断，调用层噪声。&lt;/li&gt;
&lt;li&gt;c04 在 T=0 的那次 F 是正常结束（&lt;code dir=&quot;auto&quot;&gt;stop&lt;/code&gt;），模型把 DB 连接失败判成了 &lt;code dir=&quot;auto&quot;&gt;TIMEOUT&lt;/code&gt;——模型自身的不确定性，同时暴露用例有歧义：&lt;code dir=&quot;auto&quot;&gt;Communications link failure&lt;/code&gt; 读成超时也说得通，期望值是按「根因是数据库连接」写死的。&lt;/li&gt;
&lt;li&gt;c05 的 F 是漏取 1213，&lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 填了 &lt;code dir=&quot;auto&quot;&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;c07 是&lt;strong&gt;我的用例写错了&lt;/strong&gt;。日志里同时写了 &lt;code dir=&quot;auto&quot;&gt;upstream search-api returned 429&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;client search-web 触发限流&lt;/code&gt;，两种理解都成立，期望值只认 &lt;code dir=&quot;auto&quot;&gt;search-api&lt;/code&gt;；默认 max_tokens 下 7 次采样里 6 次判给了 &lt;code dir=&quot;auto&quot;&gt;search-web&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理方式是改用例文本、把服务归属写死，不是放宽断言。放宽断言就是把门禁改成常绿——它从此再也拦不住真的回归。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;发现四推理模型的思维链也算在-max_tokens-里&quot;&gt;发现四：推理模型的思维链也算在 max_tokens 里&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;同一批用例、同一份 v2 prompt，把 &lt;code dir=&quot;auto&quot;&gt;max_tokens&lt;/code&gt; 从 8192 降到 2048 再跑 11 次：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;finish_reason=length  6/11，这 6 次 content 全是空字符串&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reasoning token 其中 5 次恰好停在 2048 上限，1 次 2047&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;通过 5/11：截断的 6 题全挂，没截断的 5 题全过（同批单轮基线 9/11）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;不是模型不会答，是思维链把预算吃完了。默认 8192 下的 143 次 deepseek 调用，平均每次 2398 token，其中思维链 2029，占 84.6%，平均延迟 9.6 秒。4 个字段的抽取任务，钱主要花在推理上。&lt;/p&gt;
&lt;p&gt;这也直接给出回归频率的算法：一轮 11 题 × 3 次 = 33 次调用、约 7.9 万 token，进 CI 不心疼；用例集涨到 200 条就是 600 次调用、约 144 万 token 一轮，那时要决策的不是跑不跑，是多长时间跑一次。&lt;/p&gt;
&lt;p&gt;排错顺序记住一条：&lt;code dir=&quot;auto&quot;&gt;content&lt;/code&gt; 为空先看 &lt;code dir=&quot;auto&quot;&gt;finish_reason&lt;/code&gt;，别先怀疑 prompt。&lt;/p&gt;
&lt;p&gt;顺带一个观测口径的坑：同一个网关下 &lt;code dir=&quot;auto&quot;&gt;glm-5.3-flash&lt;/code&gt; 的 44 次调用里 &lt;code dir=&quot;auto&quot;&gt;reasoning_tokens&lt;/code&gt; 字段完全缺失（不是 0），平均 1352 token。拿 deepseek 的口径去套 GLM 的成本估算会错。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;落地一个最小回归台&quot;&gt;落地：一个最小回归台&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;prompt-regression-lab/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── cases.py          # 用例：日志文本 + 期望值（客观事实）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── prompts/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│   ├── v1.txt        # 每个版本一个文件，Git 就是 prompt 的版本历史&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│   └── v2.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── plan.json         # 跑哪些 (model, version, temperature, repeats)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── run.py            # 执行 + 限流 + 判定&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;└── analyze.py        # 按层聚合：parse / 契约 / 语义 / 漂移&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;run.py&lt;/code&gt; 判定部分的精简版：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;judge&lt;/span&gt;&lt;span&gt;(raw, case):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;r &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;parse_ok&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;schema_ok&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;json.loads(raw.strip())        &lt;/span&gt;&lt;span&gt;# 不剥 markdown 包裹&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; r                       &lt;/span&gt;&lt;span&gt;# 格式层直接判负&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;obj &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; json.loads(raw.strip())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;r[&lt;/span&gt;&lt;span&gt;&quot;parse_ok&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;r[&lt;/span&gt;&lt;span&gt;&quot;schema_ok&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(obj) &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;service&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;error_type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;severity&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; obj[&lt;/span&gt;&lt;span&gt;&quot;error_type&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;OOM&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;DB&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;AUTH&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;NETWORK&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;TIMEOUT&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;RATE_LIMIT&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;OTHER&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; (obj[&lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isinstance&lt;/span&gt;&lt;span&gt;(obj[&lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;], &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; obj[&lt;/span&gt;&lt;span&gt;&quot;severity&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;&quot;P0&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;P1&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;P2&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;P3&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;r[&lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; r[&lt;/span&gt;&lt;span&gt;&quot;schema_ok&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; obj[&lt;/span&gt;&lt;span&gt;&quot;service&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; case[&lt;/span&gt;&lt;span&gt;&quot;service&quot;&lt;/span&gt;&lt;span&gt;] \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; obj[&lt;/span&gt;&lt;span&gt;&quot;error_type&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; case[&lt;/span&gt;&lt;span&gt;&quot;error_type&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; obj[&lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; case[&lt;/span&gt;&lt;span&gt;&quot;error_code&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;v2 里真正干活的是两段规则（节选，示例部分省略）：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 取值规则&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- service：出错服务的名字。pod 名要去掉 ReplicaSet hash 后缀（payment-api-7d9f4c8b6-x2knp → payment-api）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- error_type 只能取以下之一：OOM | DB | AUTH | NETWORK | TIMEOUT | RATE_LIMIT | OTHER&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- error_code：只填日志中明确出现的 HTTP 状态码、进程 exit code 或 errno 数值；没有就填 null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;端口号、耗时毫秒数、行号、重试次数都不是 error_code&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- 一条日志里出现多个错误时，填离根因最近的那个&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 硬性约束&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- 回复的第一个字符必须是 {，最后一个字符必须是 }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- 不要用 markdown 代码块包裹，不要输出解释、注释或任何前后缀&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这些规则不是凑数：v1 的 33 次里 &lt;code dir=&quot;auto&quot;&gt;severity&lt;/code&gt; 出现过 7 种写法（critical 17 / warning 8 / WARN 3 / ERROR 2 / CRITICAL 1 / error 1 / high 1），0/33 命中 &lt;code dir=&quot;auto&quot;&gt;P0&lt;/code&gt;–&lt;code dir=&quot;auto&quot;&gt;P3&lt;/code&gt;；v1 有 2/33 次把 StatefulSet 的 &lt;code dir=&quot;auto&quot;&gt;log-collector-0&lt;/code&gt; 原样返回；&lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 在 v1 和 v1b 里都出现过用文本填数值字段（&lt;code dir=&quot;auto&quot;&gt;GC overhead limit exceeded&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;no such host&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;DeadlineExceeded&lt;/code&gt;）。规则不写死，模型就按自己的常识补。&lt;/p&gt;
&lt;p&gt;v2 是「一次改到位」的版本，我只对枚举取值域、禁 markdown 包裹做了单条消融，其余几段的收益没有单独归因——把它们从 v2 里拆出来再跑一遍才知道谁在干活。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;断言分层与门禁&quot;&gt;断言分层与门禁&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段类型&lt;/th&gt;
&lt;th&gt;断言方式&lt;/th&gt;
&lt;th&gt;失败时先改什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;格式层（能否 parse）&lt;/td&gt;
&lt;td&gt;严格&lt;/td&gt;
&lt;td&gt;prompt：禁 markdown 包裹、禁前后缀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;契约层（类型、枚举）&lt;/td&gt;
&lt;td&gt;严格&lt;/td&gt;
&lt;td&gt;prompt：写死取值域，写明类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自由文本（&lt;code dir=&quot;auto&quot;&gt;service&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;严格，但期望值必须无歧义&lt;/td&gt;
&lt;td&gt;先用例文本，再 prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主观字段（&lt;code dir=&quot;auto&quot;&gt;severity&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;只校验枚举，不做精确断言&lt;/td&gt;
&lt;td&gt;不进门禁&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;门禁三条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单次运行不下结论。&lt;/strong&gt; 看「k 次重复全过」的题数，不看单次 pass 率。本次 v2 在 T=0 和 T=0.7 下都是 11 题里 8 题全过，两个温度的数字一样，而中间有 3 题在翻转。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;锁定子集零容忍。&lt;/strong&gt; 挑 5–8 条确定性最强的用例，任何一次失败就拦；其余用例看 k 次全过率。规则简单到没人会想去绕过它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败分类先于归因。&lt;/strong&gt; 把 &lt;code dir=&quot;auto&quot;&gt;finish_reason=length&lt;/code&gt;、空 &lt;code dir=&quot;auto&quot;&gt;content&lt;/code&gt;、限流失败先归成调用层噪声，再统计 prompt 指标。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;只测了两个模型、一类任务（日志字段抽取）。跨任务、跨模型的推广是推测，不是结论。&lt;/li&gt;
&lt;li&gt;v1b、v1c 各只跑了 1 轮（11 个样本）；GLM 每个版本也只有 1 轮，只能看模式差异（包裹率 11/11 与 0/11），看不出漂移。但 parse 0/11 与 11/11、&lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt; 13/33 与 2/11 这种量级的差别，不需要统计检验。&lt;/li&gt;
&lt;li&gt;T=0 的 4 次采样跨了两个批次（1 + 3），批次间波动没能单独分离。&lt;/li&gt;
&lt;li&gt;没测 &lt;code dir=&quot;auto&quot;&gt;seed&lt;/code&gt; 参数；没有验证同一模型名背后被换版本造成的漂移。&lt;/li&gt;
&lt;li&gt;11 条用例的规模测不出 3 个百分点级别的改进——那需要上百条用例和更多重复次数。&lt;/li&gt;
&lt;li&gt;全部调用走同一个 OpenAI 兼容网关，自部署推理栈的数值不确定性未验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;prompt 变更的风险不在「这次改得对不对」，在「这次改动的影响是不是局部的」。v1b 修好 &lt;code dir=&quot;auto&quot;&gt;error_type&lt;/code&gt; 的同时打断 &lt;code dir=&quot;auto&quot;&gt;error_code&lt;/code&gt;，单次目测看不出来——只有 k 次重复的分层回归能把它变成一条红色断言。&lt;/p&gt;
&lt;p&gt;站内早前的 &lt;a href=&quot;https://www.xlabs.club/blog/ai-code-review-tools-comparison/&quot;&gt;AI Code Review 横评&lt;/a&gt; 有一致的坑：AI 审查输出的格式漂移比判断错误更难发现。prompt 版本管理、评测集与回归门禁的工具清单，见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#llm-and-agent-observability&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops 的 LLM and Agent Observability 分类&lt;/a&gt;。&lt;/p&gt;</content:encoded><category>Prompt Engineering</category><category>回归测试</category><category>LLM 评测</category><category>结构化输出</category><category>AIOps</category><category>Python</category></item><item><title>MCP 2026-07-28 实测：initialize 握手没了，老客户端还能连吗</title><link>https://www.xlabs.club/blog/mcp-2026-07-28-version-negotiation/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-2026-07-28-version-negotiation/</guid><description>实测 MCP 2026-07-28：initialize 握手被移除，改为每请求用 _meta 声明版本并强制 server/discover。用官方 v1/v2 SDK 与 mcporter 抓包看两代客户端如何协商，附 dual-era Server 实现与 tools/list 缓存字段报错。</description><pubDate>Mon, 14 Sep 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2026-07-28 这一版 MCP 把 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; 握手删了。协议版本不再是”协商一次、存在连接里”的状态，而是每个请求自己在 &lt;code dir=&quot;auto&quot;&gt;_meta&lt;/code&gt; 里声明；server 则必须实现 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实测下来四件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;老客户端（官方 v1 SDK &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk@1.29.0&lt;/code&gt;，&lt;code dir=&quot;auto&quot;&gt;LATEST_PROTOCOL_VERSION = &apos;2025-11-25&apos;&lt;/code&gt;）不做探测，首包永远是 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;新客户端（&lt;code dir=&quot;auto&quot;&gt;mcporter@0.13.10&lt;/code&gt;，依赖 v2 的 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/client@2.0.0&lt;/code&gt;）先发 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;，拿到非 modern 错误就回退 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;。&lt;strong&gt;只认老协议的 server 不用改，照样能被新客户端用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;老客户端撞上只支持 2026-07-28 的 server 会直接失败。它没有”向前兼容”机制，只能人肉改。&lt;/li&gt;
&lt;li&gt;最容易翻车的不是版本号，是缓存字段：&lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 漏 &lt;code dir=&quot;auto&quot;&gt;ttlMs&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;cacheScope&lt;/code&gt;，v2 客户端直接判定整个结果非法。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;先确认手上的包属于哪一代&quot;&gt;先确认手上的包属于哪一代&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# v1 线：还是 initialize 握手&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;require(&apos;@modelcontextprotocol/sdk/package.json&apos;).version&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;require(&apos;@modelcontextprotocol/sdk/dist/cjs/types.js&apos;).LATEST_PROTOCOL_VERSION&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# v2 线：实现 2026-07-28&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;require(&apos;@modelcontextprotocol/server/package.json&apos;).version&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;本机实测值（Node 26.3.0）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;包&lt;/th&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/sdk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.29.0&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;LATEST_PROTOCOL_VERSION = &apos;2025-11-25&apos;&lt;/code&gt;，SUPPORTED 一路到 &lt;code dir=&quot;auto&quot;&gt;2024-10-07&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/core&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;server&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.0.0&lt;/td&gt;
&lt;td&gt;2026-07-28，v2 是官方 README 里写明的 stable release line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;mcporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.13.10&lt;/td&gt;
&lt;td&gt;依赖 &lt;code dir=&quot;auto&quot;&gt;@modelcontextprotocol/client@^2.0.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一台机器上两代 SDK 并存，这就是断层。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;协议到底改了什么&quot;&gt;协议到底改了什么&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;legacy（≤ 2025-11-25）&lt;/th&gt;
&lt;th&gt;modern（2026-07-28）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;版本声明&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; 协商一次，结果算连接状态&lt;/td&gt;
&lt;td&gt;每个请求 &lt;code dir=&quot;auto&quot;&gt;_meta[&quot;io.modelcontextprotocol/protocolVersion&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;能力&lt;/td&gt;
&lt;td&gt;握手时交换一次&lt;/td&gt;
&lt;td&gt;每个请求带 &lt;code dir=&quot;auto&quot;&gt;clientCapabilities&lt;/code&gt;（MUST）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开场&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;notifications/initialized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可选 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;，但 server MUST 实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结果结构&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;{content, isError}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;resultType: &quot;complete&quot;&lt;/code&gt;，且 &lt;code dir=&quot;auto&quot;&gt;*/list&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;resources/read&lt;/code&gt; 必须带 &lt;code dir=&quot;auto&quot;&gt;ttlMs&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;cacheScope&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;版本不匹配&lt;/td&gt;
&lt;td&gt;协商降级&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;-32022&lt;/code&gt; 附 &lt;code dir=&quot;auto&quot;&gt;data.supported&lt;/code&gt; 列表，客户端换版本重试&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;为什么要这么改&lt;/strong&gt;：握手把”我们说的是哪个协议”变成了连接状态，于是任何要水平扩展的部署都得处理会话粘滞——多副本、serverless、中间的负载均衡和代理都得记住”这条连接协商过什么”。改成每请求自带元数据后，请求可以落在任意副本上；规范还明确允许 tools 集合随请求里的授权上下文变化（同一台 server 对不同 token 返回不同工具集），因为授权本来就是请求级输入，不是连接级状态。代价是所有实现方都得改一遍。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;抓包真实客户端到底发什么&quot;&gt;抓包：真实客户端到底发什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;MCP Inspector 只能当客户端用，看不到真实客户端的报文。写一个三十行的 stdio 透传代理就能抓：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// tap.mjs —— node tap.mjs --log /tmp/x.ndjson -- &amp;#x3C;server-cmd&gt; [args...]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { spawn } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;node:child_process&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createWriteStream } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;node:fs&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;argv&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; process.argv.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createWriteStream&lt;/span&gt;&lt;span&gt;(argv[argv.&lt;/span&gt;&lt;span&gt;indexOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;--log&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;], { flags: &lt;/span&gt;&lt;span&gt;&quot;w&quot;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; argv.&lt;/span&gt;&lt;span&gt;indexOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;--&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;cmd&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; argv.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(sep &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;child&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;spawn&lt;/span&gt;&lt;span&gt;(cmd, args, { stdio: [&lt;/span&gt;&lt;span&gt;&quot;pipe&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;pipe&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;inherit&quot;&lt;/span&gt;&lt;span&gt;] }); &lt;/span&gt;&lt;span&gt;// stderr 透传，别吞&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;dir&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;msg&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; log.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ dir, msg }) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;safe&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;line&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(line);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; { _raw: line };&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;process.stdin.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;c.&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(Boolean)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;l&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;c2s&quot;&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;safe&lt;/span&gt;&lt;span&gt;(l)));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;child.stdin.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(c);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;process.stdin.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;end&quot;&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; child.stdin.&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;child.stdout.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;c.&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(Boolean)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;l&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;s2c&quot;&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;safe&lt;/span&gt;&lt;span&gt;(l)));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;process.stdout.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(c);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;child.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;exit&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&gt;&lt;/span&gt;&lt;span&gt; process.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(code &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;两个细节：&lt;code dir=&quot;auto&quot;&gt;stderr: &apos;inherit&apos;&lt;/code&gt; 让 server 的日志照常输出但不进协议通道；解析不了的行记成 &lt;code dir=&quot;auto&quot;&gt;_raw&lt;/code&gt;，脏输出不会静默丢掉。&lt;/p&gt;
&lt;p&gt;用法是把客户端配置里的 command 指向代理，server 端一行都不用改：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;era-lab&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/tmp/mcp-era-lab/tapped-dual.sh&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;新客户端连一台只认老协议的 server，抓到的就是这套流程（报文截断显示）：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:&quot;server-discover-probe-1&quot;,&quot;method&quot;:&quot;server/discover&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;params&quot;:{&quot;_meta&quot;:{&quot;io.modelcontextprotocol/protocolVersion&quot;:&quot;2026-07-28&quot;, ...}}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:&quot;server-discover-probe-1&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;error&quot;:{&quot;code&quot;:-32601,&quot;message&quot;:&quot;method not found: server/discover&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;method&quot;:&quot;initialize&quot;,&quot;params&quot;:{&quot;protocolVersion&quot;:&quot;2025-11-25&quot;, ...},&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:0}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:0,&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2025-11-25&quot;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;capabilities&quot;:{&quot;tools&quot;:{}},&quot;serverInfo&quot;:{&quot;name&quot;:&quot;era-legacy&quot;,&quot;version&quot;:&quot;1.0.0&quot;}}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;notifications/initialized&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;method&quot;:&quot;tools/list&quot;,&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;result&quot;:{&quot;tools&quot;:[{&quot;name&quot;:&quot;echo&quot;, ...}]}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;探测请求的 id 是 &lt;code dir=&quot;auto&quot;&gt;server-discover-probe-1&lt;/code&gt;，回退规则照规范实现：&lt;code dir=&quot;auto&quot;&gt;DiscoverResult&lt;/code&gt; → 认定 modern；&lt;code dir=&quot;auto&quot;&gt;-32022&lt;/code&gt; 这类已定义的 modern 错误 → 认定 modern，用 &lt;code dir=&quot;auto&quot;&gt;supported&lt;/code&gt; 里的版本重试，&lt;strong&gt;不回退&lt;/strong&gt; &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;；其他错误或超时 → 认定 legacy，回退握手。&lt;/p&gt;
&lt;p&gt;这里最反直觉的是回退判据：&lt;strong&gt;不能只认某个错误码&lt;/strong&gt;。老 server 对未知方法回的是实现自定义的错误（常见 &lt;code dir=&quot;auto&quot;&gt;-32601&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;-32602&lt;/code&gt;），也可能干脆不回复。判错一次，你就把”版本不匹配”误当成”这是一台老 server”，然后在一个只支持新协议的 server 上反复发 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同一台新客户端连 dual-era server，&lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; 在抓包里出现 0 次：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s server/discover  (_meta: 2026-07-28)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c result {resultType:&quot;complete&quot;, supportedVersions:[&quot;2026-07-28&quot;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;capabilities:{tools:{}}, ttlMs:3600000, cacheScope:&quot;public&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s tools/list       (_meta: 2026-07-28)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c result {resultType:&quot;complete&quot;, tools:[...], ttlMs:3600000, cacheScope:&quot;public&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;而 v1 SDK 写的客户端连同一台 server，首包永远是 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;，没有任何探测：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;method&quot;:&quot;initialize&quot;,&quot;params&quot;:{&quot;protocolVersion&quot;:&quot;2025-11-25&quot;,&quot;capabilities&quot;:{},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;clientInfo&quot;:{&quot;name&quot;:&quot;legacy-client&quot;,&quot;version&quot;:&quot;1.0.0&quot;}},&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:0}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:0,&quot;result&quot;:{&quot;protocolVersion&quot;:&quot;2025-11-25&quot;,&quot;capabilities&quot;:{&quot;tools&quot;:{}},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;serverInfo&quot;:{&quot;name&quot;:&quot;era-dual&quot;,&quot;version&quot;:&quot;1.2.0&quot;}}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;method&quot;:&quot;notifications/initialized&quot;,&quot;jsonrpc&quot;:&quot;2.0&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;c2s {&quot;method&quot;:&quot;tools/list&quot;,&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;s2c {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;result&quot;:{&quot;tools&quot;:[{&quot;name&quot;:&quot;echo&quot;, ...}]}}   ← 注意：没有 resultType&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;两代客户端的开场方式不同，server 端必须两种都认。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;dual-era-server-的分流规则&quot;&gt;dual-era Server 的分流规则&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;只有一条规则：客户端怎么开场，就按哪一代语义服务。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (method &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;initialize&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// legacy 开场：协商出协议版本，本进程后续按 legacy 语义回复&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;// （结果不带 resultType，也不带缓存字段）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;legacyResult&lt;/span&gt;&lt;span&gt;(id, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;protocolVersion: &lt;/span&gt;&lt;span&gt;pick&lt;/span&gt;&lt;span&gt;(params.protocolVersion),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;capabilities: { tools: {} },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;serverInfo: &lt;/span&gt;&lt;span&gt;SERVER&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metaVersion&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; params._meta?.[&lt;/span&gt;&lt;span&gt;&quot;io.modelcontextprotocol/protocolVersion&quot;&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (metaVersion &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;MODERN_SUPPORTED&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(metaVersion)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fail&lt;/span&gt;&lt;span&gt;(id, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;32022&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;Unsupported protocol version&quot;&lt;/span&gt;&lt;span&gt;, { supported: &lt;/span&gt;&lt;span&gt;MODERN_SUPPORTED&lt;/span&gt;&lt;span&gt;, requested: metaVersion });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (params._meta[&lt;/span&gt;&lt;span&gt;&quot;io.modelcontextprotocol/clientCapabilities&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fail&lt;/span&gt;&lt;span&gt;(id, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;32602&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;missing required _meta field: io.modelcontextprotocol/clientCapabilities&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;两个我实际写错过的地方：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;clientCapabilities&lt;/code&gt; 是 MUST。&lt;/strong&gt; 第一版我只校验了 &lt;code dir=&quot;auto&quot;&gt;protocolVersion&lt;/code&gt;，一个只有版本字段的请求本该被拒：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:2,&quot;error&quot;:{&quot;code&quot;:-32602,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;message&quot;:&quot;missing required _meta field: io.modelcontextprotocol/clientCapabilities&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;legacy 分支的结果别带 modern 字段。&lt;/strong&gt; 我实测老客户端对多余字段是宽容的：在 legacy 结果里混入 &lt;code dir=&quot;auto&quot;&gt;resultType&lt;/code&gt;，v1 SDK 照常解析出 &lt;code dir=&quot;auto&quot;&gt;callTool = {&quot;content&quot;:[...],&quot;isError&quot;:false}&lt;/code&gt;。但那是运气不是保证，按分支给对应结构更省事。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; 的拒绝方式也有讲究。modern-only server 收到 &lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt;，规范建议把支持的版本写进错误信息——老客户端没有 fall-forward 机制，错误文本是用户唯一能看到的诊断：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:3,&quot;error&quot;:{&quot;code&quot;:-32601,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;message&quot;:&quot;initialize is not supported by this server; supported protocol versions: 2026-07-28&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;v1 SDK 客户端拿到后原样抛出：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;FAILED: McpError | MCP error -32601: initialize is not supported by this server; supported protocol versions: 2026-07-28&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;真正会卡住你的坑list-必须带缓存字段&quot;&gt;真正会卡住你的坑：&lt;code dir=&quot;auto&quot;&gt;*/list&lt;/code&gt; 必须带缓存字段&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;dual-era server 跑通了，但 v2 客户端一上来就把 &lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 判成非法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;error&quot;: &quot;Invalid result for tools/list: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{\&quot;expected\&quot;:\&quot;number\&quot;,\&quot;code\&quot;:\&quot;invalid_type\&quot;,\&quot;path\&quot;:[\&quot;ttlMs\&quot;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&quot;message\&quot;:\&quot;Invalid input: expected number, received undefined\&quot;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{\&quot;code\&quot;:\&quot;invalid_value\&quot;,\&quot;values\&quot;:[\&quot;public\&quot;,\&quot;private\&quot;],\&quot;path\&quot;:[\&quot;cacheScope\&quot;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&quot;message\&quot;:\&quot;Invalid option: expected one of \\\&quot;public\\\&quot;|\\\&quot;private\\\&quot;\&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;规范原文的要求是：&lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;prompts/list&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;resources/list&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;resources/templates/list&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;resources/read&lt;/code&gt; 返回的 &lt;code dir=&quot;auto&quot;&gt;resultType: &quot;complete&quot;&lt;/code&gt; 结果 &lt;strong&gt;MUST&lt;/strong&gt; 带 &lt;code dir=&quot;auto&quot;&gt;ttlMs&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;cacheScope&lt;/code&gt;，后者只能是 &lt;code dir=&quot;auto&quot;&gt;&quot;public&quot;&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;&quot;private&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我一开始只在 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt; 里带了这两个字段——它们语义上属于”缓存协商”，直觉上只跟发现阶段有关。补到每个 list 结果之后，v2 客户端立刻 &lt;code dir=&quot;auto&quot;&gt;&quot;status&quot;: &quot;ok&quot;&lt;/code&gt;，&lt;code dir=&quot;auto&quot;&gt;tools/call&lt;/code&gt; 也正常返回。&lt;/p&gt;
&lt;p&gt;这里没有渐进降级：字段缺失等于整个结果被拒，现象是”客户端拿不到工具列表”，而 &lt;strong&gt;server 日志里一行错误都没有&lt;/strong&gt;。要定位这种问题，只能从客户端侧看原始错误。&lt;/p&gt;
&lt;p&gt;顺带一句，&lt;code dir=&quot;auto&quot;&gt;cacheScope&lt;/code&gt; 不是随手填的值。&lt;code dir=&quot;auto&quot;&gt;&quot;public&quot;&lt;/code&gt; 意味着结果可能被客户端或共享网关跨授权上下文复用。&lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 如果按调用者权限过滤过，必须 &lt;code dir=&quot;auto&quot;&gt;&quot;private&quot;&lt;/code&gt;——否则不同 token 之间会互相看到工具集。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;一个被说反了的常识脏-stdout&quot;&gt;一个被说反了的常识：脏 stdout&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;“MCP server 往 stdout 写一行日志就会崩”——我实测没崩。让 server 启动时先 &lt;code dir=&quot;auto&quot;&gt;console.log(&quot;era-legacy 启动中，正在加载配置...&quot;)&lt;/code&gt;，v1 SDK 客户端和 mcporter（v2 客户端）&lt;strong&gt;都忽略了这行脏数据&lt;/strong&gt;，照常连接、列工具、调用工具。在 tap 的原始记录里它就是一条解析失败的行：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;dir&quot;:&quot;s2c&quot;,&quot;msg&quot;:{&quot;_raw&quot;:&quot;era-legacy 启动中，正在加载配置...&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;正确的结论不是”可以往 stdout 写日志”，而是&lt;strong&gt;别指望客户端替你兜住&lt;/strong&gt;。规范写得很直白：stdout 只允许出现合法的 MCP 消息，stderr 才是日志通道（&lt;code dir=&quot;auto&quot;&gt;MAY write UTF-8 strings to stderr for any logging purposes&lt;/code&gt;）。我这台机器上的两代 SDK 恰好都跳过了脏行，但社区里已经有宿主因为 server 的 stdout 噪音直接报 &lt;code dir=&quot;auto&quot;&gt;Invalid JSON-RPC messages&lt;/code&gt;。把日志写 stderr，成本是零。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;边界&quot;&gt;边界&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;全部在 stdio 传输下实测。Streamable HTTP 的版本协商走 &lt;code dir=&quot;auto&quot;&gt;MCP-Protocol-Version&lt;/code&gt; 请求头，回退判据是 &lt;code dir=&quot;auto&quot;&gt;400 Bad Request&lt;/code&gt; 响应体里有没有 modern 错误——我没有部署 HTTP server，这部分只读了规范，未实测。&lt;/li&gt;
&lt;li&gt;客户端侧只覆盖了官方 v1/v2 SDK 和 mcporter 0.13.10。Claude Code、Claude Desktop 这类宿主没在本机验证；规范对探测与回退用的是 SHOULD / RECOMMENDED，不是 MUST，各家实现可能有差异。&lt;/li&gt;
&lt;li&gt;缓存字段我按规范填的是保守值，没有做缓存命中率或 TTL 的实测。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;所以现在怎么做&quot;&gt;所以现在怎么做&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;server 端写 dual-era&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;initialize&lt;/code&gt; 和每请求 &lt;code dir=&quot;auto&quot;&gt;_meta&lt;/code&gt; 都认，直到确认所有客户端都能说 modern。分流成本就是一次判断，不需要维护会话状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;升级 SDK 前先抓一次真实流量&lt;/strong&gt;：一个透传代理比读文档快得多——你的客户端发不发 &lt;code dir=&quot;auto&quot;&gt;server/discover&lt;/code&gt;、协商到哪个版本，抓一次就有答案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别依赖会话状态&lt;/strong&gt;：modern 语义下版本、能力、授权上下文都随请求走。任何”握手时记住的东西”，在切到 modern 的那天都会失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MCP 的分层结构与三大原语见站内早前的 &lt;a href=&quot;https://www.xlabs.club/blog/mcp-protocol-deep-dive/&quot;&gt;MCP 协议深度解析&lt;/a&gt;；MCP Server 与 Agent 工作流相关工具清单在 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#agentic-workflow&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops&lt;/a&gt;。&lt;/p&gt;</content:encoded><category>MCP</category><category>Model Context Protocol</category><category>Protocol Version</category><category>server/discover</category><category>AI Agent</category><category>Node.js</category></item><item><title>K8S 集群平滑迁移：Service + 手工 EndpointSlice 跨集群引流</title><link>https://www.xlabs.club/blog/k8s-cross-cluster-migration/</link><guid isPermaLink="true">https://www.xlabs.club/blog/k8s-cross-cluster-migration/</guid><description>K8S 集群迁移的平滑方案：老集群 Service 开 NodePort，新集群建无 selector 同名 Service + 手工 EndpointSlice 指向老集群，服务逐个就绪后再切回。</description><pubDate>Sun, 02 Aug 2026 03:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;多集群迁移（机房搬迁、集群升级）的难点在于：新集群是从零搭的，Pod 不会一下子全就绪。要是直接把流量切过去，没就绪的服务会把调用全打失败。微服务之间靠 Service 做服务发现，迁移期间整条调用链不能断。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;思路&quot;&gt;思路&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Service 是流量入口（ClusterIP + DNS），EndpointSlice 决定流量转发到哪。流量并不要求转发目标一定在本集群——EndpointSlice 里写任何可达的 IP:Port 都行。&lt;/p&gt;
&lt;p&gt;所以做法是：新集群的 Service 先通过手工 EndpointSlice 把流量导到老集群，等服务逐个就绪再切回来。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;调用方 Pod (新集群)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│  order-service.default.svc.cluster.local&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;▼&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Service ClusterIP ─▶ EndpointSlice(手工) ─▶ 老集群 NodeIP:NodePort ─▶ 老集群 Service ─▶ 老集群 Pod&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;比 ExternalName（DNS CNAME，有缓存，同一个 Service 没法同时指两个集群）和改调用方配置（要发版）都省事。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-1老集群-service-开-nodeport&quot;&gt;Step 1：老集群 Service 开 NodePort&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;ClusterIP 只在本集群内可达，跨集群要用 NodePort。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;v1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;namespace&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;selector&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;NodePort&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;targetPort&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;nodePort&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;两个注意点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保持默认 &lt;code dir=&quot;auto&quot;&gt;externalTrafficPolicy: Cluster&lt;/code&gt;，请求可以转发到集群内任意 Node 上的 Pod，不用管 Pod 落在哪个节点。&lt;/li&gt;
&lt;li&gt;新集群的节点要能访问老集群节点的 &lt;code dir=&quot;auto&quot;&gt;内网IP:30080&lt;/code&gt;（内网/专线/VPN，安全组放行），迁移期间这条链路不能断。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;step-2新集群建同名-service不带-selector&quot;&gt;Step 2：新集群建同名 Service，不带 selector&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;name 和 namespace 跟老集群保持一致，调用方域名不变，代码不用动。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;v1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;namespace&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ClusterIP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;targetPort&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 故意不写 selector。带 selector 的 Service 会被 kube-controller-manager&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 自动生成并接管 EndpointSlice（带 ownerReference），手工创建的同名 EndpointSlice 会被覆盖。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;不带 selector，controller 就不介入，EndpointSlice 完全由我们控制。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-3手工创建-endpointslice-指到老集群&quot;&gt;Step 3：手工创建 EndpointSlice 指到老集群&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;discovery.k8s.io/v1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;EndpointSlice&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;order-service-proxy-old-cluster&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;namespace&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;labels&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;kubernetes.io/service-name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;addressType&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IPv4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protocol&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;TCP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;endpoints&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;addresses&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;10.1.2.3&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;10.1.2.4&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;conditions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;ready&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;kubernetes.io/service-name&lt;/code&gt; 这个 label 必须和 Service 同名，Service 靠它关联 EndpointSlice。&lt;/li&gt;
&lt;li&gt;没有 ownerReference，controller 不会自动删，直到我们手动删。&lt;/li&gt;
&lt;li&gt;Node IP 全写上，kube-proxy 会做负载均衡；只写一个也行，挂了会自动重试。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;step-4验证&quot;&gt;Step 4：验证&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kubectl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--rm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;debug&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--image=curlimages/curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--restart=Never&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://order-service.default.svc.cluster.local:8080/health&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;在新集群里能跑通这个，说明引流成功，这时新集群的调用方流量全走老集群。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-5切流量&quot;&gt;Step 5：切流量&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;新集群 Pod 全部 ready 之后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给 Service 补上 selector，controller 生成指向新 Pod 的 EndpointSlice。新旧两套并存，流量被分流。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kubectl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;patch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;svc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--type=json&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-p=&lt;/span&gt;&lt;span&gt;&apos;[{&quot;op&quot;:&quot;add&quot;,&quot;path&quot;:&quot;/spec/selector&quot;,&quot;value&quot;:{&quot;app&quot;:&quot;order-service&quot;}}]&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;看新 Pod 的日志和监控，确认正常处理流量。&lt;/li&gt;
&lt;li&gt;删掉指向老集群的 EndpointSlice，流量全切过来。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kubectl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;endpointslices&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;order-service-proxy-old-cluster&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Step 5 全程没有断点：切之前流量全在老集群，切的时候有一段两边并存的窗口，出问题随时补回手工 EndpointSlice 就能回滚。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;step-6收尾&quot;&gt;Step 6：收尾&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;老集群把 NodePort 撤掉（改回 ClusterIP 或直接删），迁移完成。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;灰度&quot;&gt;灰度&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;EndpointSlice 本来就能同时写老集群和新集群的地址。不靠 selector，把新集群 Pod IP 按比例加进手工 EndpointSlice 也行，观察指标再全量。缺点是 Pod IP 会随重启变化，得手动维护，所以一般还是用 selector 那套。上面 Step 5 借 selector 做了一次现成的全量切换。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;注意事项&quot;&gt;注意事项&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Service 别带 selector，最容易踩的坑。&lt;/li&gt;
&lt;li&gt;迁移是逐步的，老集群的调用方不受影响，老集群服务在撤 NodePort 前一直正常。&lt;/li&gt;
&lt;li&gt;这只解决流量层，数据/存储要单独迁。&lt;/li&gt;
&lt;li&gt;调用方如果缓存了 Service 地址，切完要重建；正常情况 ClusterIP 和 DNS 全程不变。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>k8s</category><category>集群迁移</category><category>EndpointSlice</category><category>NodePort</category><category>高可用</category></item><item><title>AI Code Review 横评——CodeRabbit vs PR-Agent vs Copilot Review</title><link>https://www.xlabs.club/blog/ai-code-review-tools-comparison/</link><guid isPermaLink="true">https://www.xlabs.club/blog/ai-code-review-tools-comparison/</guid><description>CodeRabbit、PR-Agent、GitHub Copilot Code Review 三款 AI 代码审查工具 10 维度实测对比：审查质量、误报率、配置成本、集成方式。附 GitHub Actions 配置和选型建议。</description><pubDate>Mon, 13 Jul 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI 写代码是热点，AI 审代码才是刚需。人工 Code Review 三个痛点：响应慢（等同事有空）、质量不稳（看人看心情）、覆盖率低（只审改动的 30%）。AI Code Review 解决的不是”替代人审”，是&lt;strong&gt;每次 PR 都有一致标准的先行审查&lt;/strong&gt;——人等会累，机器不会。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多 AI 开发工具见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops AI Coding 分类&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;三款工具一句话&quot;&gt;三款工具一句话&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;定位&lt;/th&gt;
&lt;th&gt;一句话&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CodeRabbit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全托管 SaaS&lt;/td&gt;
&lt;td&gt;「AI 审查 + 对话式修正 + 自动摘要」的无脑一体化方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PR-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;开源可自托管&lt;/td&gt;
&lt;td&gt;「各种 AI Provider 随便换」的审查框架，Codium 出品&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Copilot Review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub 原生&lt;/td&gt;
&lt;td&gt;「点一下就有」的零配置审查，但能力还在长&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-1集成方式&quot;&gt;维度 1：集成方式&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;coderabbitgithub-app--saas&quot;&gt;CodeRabbit：GitHub App + SaaS&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;CodeRabbit 是托管服务。安装 GitHub App → 授权仓库 → 开 PR → 自动审。不需要 CI runner，不需要管理 token。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# .coderabbit.yaml — 唯一的配置入口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;reviews&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;auto_review&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;drafts&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;poem&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# 关掉那个烦人的诗&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;path_instructions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;**.go&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;instructions&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;检查 error 是否被正确处理，context 是否正确传递&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;db/migrations/**&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;instructions&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;检查是否有破坏性变更，reviewers: @dba-team&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：零运维，摘要生成（PR Summary）质量好。&lt;strong&gt;缺点&lt;/strong&gt;：数据离仓，内部代码谨慎用。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;pr-agentcli--github-action&quot;&gt;PR-Agent：CLI + GitHub Action&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;PR-Agent 本质上是一个命令行工具 &lt;code dir=&quot;auto&quot;&gt;pr-agent&lt;/code&gt;，提供两种使用方式：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 方式 1：CLI 主动调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pr-agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--pr_url=https://github.com/org/repo/pull/42&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;review&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 方式 2：GitHub Action 自动触发&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.github/workflows/pr-agent.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;PR-Agent Review&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;types&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;opened&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;synchronize&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;permissions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;pull-requests&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;contents&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;review&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;runs-on&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ubuntu-latest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Codium-ai/pr-agent@v0.26&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;CONFIG.MODEL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;gpt-5&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_REVIEWER.EXTRA_INSTRUCTIONS&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;重点关注：SQL 注入风险、N+1 查询、未处理的 error&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_DESCRIPTION.ENABLE&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_REVIEW.INLINE_CODE_COMMENTS&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：可自托管，Provider 随便换（OpenAI / Azure / Bedrock / 本地 Ollama）。&lt;strong&gt;缺点&lt;/strong&gt;：需要自己维护 runner 和 API key。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;copilot-code-reviewgithub-原生按钮&quot;&gt;Copilot Code Review：GitHub 原生按钮&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;GitHub Copilot Code Review 目前提供两种模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PR 页面点击「Copilot Review」按钮&lt;/strong&gt; — 手动触发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot Workspace&lt;/strong&gt; — 自动触发（需 waitlist）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;没有 yaml 配置，没有自定义指令，就是点一下。审完输出评论列表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：零配置，GitHub 原生。&lt;strong&gt;缺点&lt;/strong&gt;：控制力最弱，只支持 GitHub，功能还在快速迭代中。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-2审查深度实测&quot;&gt;维度 2：审查深度实测&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我拿同一个 PR（包含 SQL 注入风险、N+1 查询、未处理的 error、拼写错误）分别让三款工具审，结果如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;发现项&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;CodeRabbit&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;PR-Agent&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Copilot Review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL 注入风险&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出 + 修复建议 + CWE 编号&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出 + 修复建议&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;⚠️ 只提示「拼接 SQL」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N+1 查询&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 识别循环内查询&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 识别并给出批量查询方案&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 未发现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;未处理 error&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拼写错误&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码风格不一致&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 指出 + 建议&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;误报&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1 处（误判 dead code）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;0&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;coderabbit最全面但有噪音&quot;&gt;CodeRabbit：最全面但有噪音&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;CodeRabbit 审得最细——拼写检查、许可证声明检查、CWE 安全映射都做了。但也产生了误报：把一个条件编译的 fallback 路径标为「可能不可达」，需要人工忽略。&lt;/p&gt;
&lt;p&gt;它的 &lt;code dir=&quot;auto&quot;&gt;path_instructions&lt;/code&gt; 机制是杀手功能：你可以针对不同目录、不同文件类型给 AI 不同的审查重点。比如 &lt;code dir=&quot;auto&quot;&gt;db/migrations/**&lt;/code&gt; 检查破坏性变更，&lt;code dir=&quot;auto&quot;&gt;**.go&lt;/code&gt; 检查 error handling。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;pr-agent精准但依赖模型质量&quot;&gt;PR-Agent：精准但依赖模型质量&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;PR-Agent 换不同模型结果差异明显。用 GPT-5 审出来 5 个有效问题，换 DeepSeek-V3 只审出来 3 个，SQL 注入风险漏了。结论：&lt;strong&gt;PR-Agent 的上限取决于你用什么模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它的 &lt;code dir=&quot;auto&quot;&gt;PR_REVIEWER.EXTRA_INSTRUCTIONS&lt;/code&gt; 等价于 CodeRabbit 的 &lt;code dir=&quot;auto&quot;&gt;path_instructions&lt;/code&gt;，但粒度更粗——只能给全局指令，不能按文件路径区分。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;copilot-code-review能用但不够深&quot;&gt;Copilot Code Review：能用但不够深&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Copilot Code Review 能发现明显问题——未处理的 error、拼接 SQL。但 N+1 查询、代码风格这类需要上下文理解的问题，它目前覆盖不到。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-3审查速度&quot;&gt;维度 3：审查速度&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;小型 PR（&amp;#x3C;100 行）&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;中型 PR（100-500 行）&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;大型 PR（&gt;500 行）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CodeRabbit&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;30s-1min&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1-3min&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;3-8min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR-Agent (GPT-5)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;20s-40s&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1-2min&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;3-5min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot Review&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10s-30s&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;30s-1min&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1-3min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Copilot 最快但审得最浅。CodeRabbit 最慢但最全。PR-Agent 速度受模型 API 延迟影响——用 Azure OpenAI 比用 OpenAI 直连快 30%。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-4审查结果交互&quot;&gt;维度 4：审查结果交互&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;coderabbit对话式&quot;&gt;CodeRabbit：对话式&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;CodeRabbit 的审查结果是&lt;strong&gt;对话线程&lt;/strong&gt;——每条评论你都可以回复追问。比如它标记了一个「潜在 SQL 注入」，你可以回复「给出修复代码」，它会在同一线程下给完整修复。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CodeRabbit Bot: ⚠️ 第 42 行：使用字符串拼接构造 SQL 查询（CWE-89）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;建议：使用参数化查询&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你: @coderabbitai 给出修复方案&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;CodeRabbit Bot: 替换为：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;db.Query(&quot;SELECT * FROM users WHERE id = ?&quot;, userID)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;pr-agent评论式&quot;&gt;PR-Agent：评论式&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;PR-Agent 默认输出&lt;strong&gt;一份汇总评论&lt;/strong&gt;，包含所有发现（&lt;code dir=&quot;auto&quot;&gt;PR_REVIEW.INLINE_CODE_COMMENTS: true&lt;/code&gt; 可改为行内评论）。不支持对话交互——问题发现就是发现，没有追问机制。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;copilot-code-review只读评论&quot;&gt;Copilot Code Review：只读评论&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Copilot Code Review 输出标准 PR 评论列表。同样不支持对话式交互。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-5成本&quot;&gt;维度 5：成本&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;免费额度&lt;/th&gt;
&lt;th&gt;付费方案&lt;/th&gt;
&lt;th&gt;大型团队月成本估算&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CodeRabbit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;开源项目免费&lt;/td&gt;
&lt;td&gt;Pro $12/月/人&lt;/td&gt;
&lt;td&gt;~$500/月（40人）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PR-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;完全开源免费&lt;/td&gt;
&lt;td&gt;只有 API 费用（自己出）&lt;/td&gt;
&lt;td&gt;~$100-300/月（API）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Copilot Review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copilot 订阅内含&lt;/td&gt;
&lt;td&gt;Copilot Business $19/月/人&lt;/td&gt;
&lt;td&gt;~$760/月（40人）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;隐藏成本&lt;/strong&gt;：CodeRabbit 的付费是按「审查的代码行数」计费（Pro plan），不是人头。如果你的团队 PR 频繁且改动量大，成本会显著上升。PR-Agent 的 API 费用取决于模型选择——用 GPT-5 每次中型 PR 审查约 $0.15-0.30，用 DeepSeek 约 $0.02-0.05。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-6支持语言&quot;&gt;维度 6：支持语言&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;三款工具都对主流语言（Python、JavaScript/TypeScript、Go、Java、Rust、C#）支持良好。差异在于小众语言：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CodeRabbit&lt;/strong&gt;：声称支持所有语言，但非主流语言审查质量明显下降（用了通用 prompt）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR-Agent&lt;/strong&gt;：依赖底层模型能力，GPT-5 系对 50+ 语言有不错理解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot Review&lt;/strong&gt;：GitHub 自家产品，&lt;code dir=&quot;auto&quot;&gt;copilot-language-support&lt;/code&gt; 列表内的质量最好&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-7安全与合规&quot;&gt;维度 7：安全与合规&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关注点&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;CodeRabbit&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;PR-Agent&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Copilot Review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;代码是否离仓&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 是（SaaS）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 是（默认用 OpenAI）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自托管选项&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOC 2&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;不适用（自托管）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;支持私有部署&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 可配 Azure/AWS Bedrock&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;如果你的代码不能出公司网络&lt;/strong&gt;：只有 PR-Agent 配合私有 LLM 部署（Ollama / vLLM / 内部 Azure OpenAI）能满足。CodeRabbit 有 Enterprise plan 提供数据隔离，但本质上还是 SaaS。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-8自动摘要和描述生成&quot;&gt;维度 8：自动摘要和描述生成&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;CodeRabbit 和 PR-Agent 都支持自动生成 PR 描述和摘要（walkthrough），Copilot Review 不支持。&lt;/p&gt;
&lt;p&gt;CodeRabbit 的摘要质量更高——它把改动按文件分组，标注影响范围：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Summary by CodeRabbit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;### Walkthrough&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- **auth/middleware.go**: 重构认证中间件，从 JWT 改为 session-based&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 新增 session store 初始化&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 移除 JWT 签名验证逻辑&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- **db/migrations/042_sessions.sql**: 新增 sessions 表&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 注意：包含 DROP TABLE IF EXISTS（破坏性）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;PR-Agent 的 &lt;code dir=&quot;auto&quot;&gt;PR_DESCRIPTION.ENABLE: true&lt;/code&gt; 也能生成摘要，但格式更简单，不标注风险项。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;选型决策&quot;&gt;选型决策&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你的代码能离开公司网络吗？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NO ──▶ PR-Agent + 私有 LLM（Ollama/Azure OpenAI）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;YES&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你愿意为「开箱即用」付费吗？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;YES ──▶ CodeRabbit（全托管，开箱即用）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NO&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你需要 GitHub 原生体验吗？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;YES ──▶ Copilot Code Review（如果你已有 Copilot 订阅）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NO ──▶ PR-Agent（灵活度最高，成本最低）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;一个实际的工作流配置&quot;&gt;一个实际的工作流配置&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;以下是我们团队使用的 PR-Agent 配置，覆盖 80% 的审查场景：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.github/workflows/pr-agent.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;AI Code Review&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;types&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;opened&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;synchronize&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;permissions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;pull-requests&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;contents&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;read&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;review&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;runs-on&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;ubuntu-latest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Codium-ai/pr-agent@v0.26&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;CONFIG.MODEL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;gpt-5&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_REVIEWER.EXTRA_INSTRUCTIONS&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;审查重点（按优先级）：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 安全问题：注入、XSS、认证绕过、敏感信息泄露&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 性能问题：N+1 查询、无索引查询、大对象加载&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. 错误处理：未处理的 error、panic 风险、超时设置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4. 代码质量：重复代码、过长函数（&gt;50行）、过度嵌套（&gt;3层）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;请忽略：测试文件中的命名风格、注释的措辞&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_REVIEW.INLINE_CODE_COMMENTS&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_REVIEW.NUM_OF_SUGGESTIONS&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;PR_DESCRIPTION.ENABLE&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;在这个配置下跑了一个月，数据如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均每个 PR 发现 3.2 个有效问题&lt;/li&gt;
&lt;li&gt;误报率 &amp;#x3C; 5%&lt;/li&gt;
&lt;li&gt;人工 Review 时间从平均 45 分钟降到 25 分钟（AI 先筛掉低级问题）&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;最后&quot;&gt;最后&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AI Code Review 不是替代人工审查，是&lt;strong&gt;把人从「找拼写错误」「检查 SQL 注入」「对代码规范」中解放出来&lt;/strong&gt;，让人专注于架构设计、业务逻辑正确性、长期可维护性——这些 AI 做不好的事。&lt;/p&gt;
&lt;p&gt;选择工具前先想清楚一个问题：&lt;strong&gt;你最烦人工 Code Review 的哪个环节？&lt;/strong&gt; 如果烦的是低级错误检查 → CodeRabbit；烦的是审查速度 → PR-Agent（最快模型）；烦的是同事从来不做 Review → 先解决文化问题，工具帮不了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;更多 AI 开发工具：&lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops AI Coding 分类&lt;/a&gt; · &lt;a href=&quot;https://www.xlabs.club&quot;&gt;xlabs.club 技术博客&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded><category>AI Code Review</category><category>CodeRabbit</category><category>PR-Agent</category><category>Copilot</category><category>GitHub Actions</category><category>CI/CD</category></item><item><title>MCP 协议深度解析——AI 工具互联的 USB-C 时刻</title><link>https://www.xlabs.club/blog/mcp-protocol-deep-dive/</link><guid isPermaLink="true">https://www.xlabs.club/blog/mcp-protocol-deep-dive/</guid><description>MCP（Model Context Protocol）是什么？为什么它是 AI Agent 时代的通用接口标准？深度解析 MCP 架构、Server/Client 模式、资源/工具/提示三大原语，以及与 Function Calling 的本质区别。</description><pubDate>Thu, 09 Jul 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2024 年底，Anthropic 发布了 &lt;strong&gt;MCP（Model Context Protocol）&lt;/strong&gt;——一个让 AI 模型安全、标准化地访问外部工具和数据源的开放协议。用 Anthropic 的话说：&lt;strong&gt;MCP 是 AI 应用的 USB-C 接口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;六个月过去，MCP 已经从 Anthropic 的实验变成了整个 AI 生态的基础设施：OpenAI 发布了自己的 MCP 支持，Google 的 Gemini 接入，微软在 Copilot 生态中推广，开源社区贡献了 500+ MCP Server。&lt;/p&gt;
&lt;p&gt;这篇文章不是翻译官方文档，而是我们从实现 MCP Server 和 Client 的过程中，对协议设计哲学的理解。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;问题function-calling-不够吗&quot;&gt;问题：Function Calling 不够吗？&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;在 MCP 之前，LLM 调用外部工具的标准方式是 &lt;strong&gt;Function Calling&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// OpenAI 风格 Function Calling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;tools&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;function&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;function&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;search_database&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;查询用户数据库&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;parameters&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;query&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;limit&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;integer&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个模式的三个致命缺陷：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Context Window 污染&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个工具的定义（名称、描述、参数 schema）都要塞进 System Prompt。10 个工具 = 500+ tokens。100 个工具 = 5000+ tokens。而这些 tokens 在&lt;strong&gt;每一轮对话中&lt;/strong&gt;都占据上下文窗口——即使用户这次只用了其中 2 个工具。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;用户问：&quot;今天天气怎么样？&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;LLM 实际收到的 Prompt：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;你是 AI 助手。你可以使用以下工具：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. search_database — 查询数据库 ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. create_ticket — 创建工单 ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. deploy_service — 部署服务 ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;... (95 个无关工具定义)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;用户：今天天气怎么样？&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;浪费了 4500+ tokens 在无关工具上。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. 工具发现是静态的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Function Calling 的工具列表在&lt;strong&gt;对话开始时就固定了&lt;/strong&gt;。如果对话中间新增了一个数据源或工具——比如用户说「查一下我 GitHub 上的 PR 状态」——你只能让 LLM 返回「我没有这个工具」，然后应用层重新发起一次带新工具的调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 安全和权限是「调用前」的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Function Calling 只有「能调用」和「不能调用」两个状态。如果 LLM 可以调用 &lt;code dir=&quot;auto&quot;&gt;deploy_service&lt;/code&gt;，它就能部署任何服务。没有「需要用户确认」或「只读」的粒度。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-的设计哲学&quot;&gt;MCP 的设计哲学&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;MCP 不是 Function Calling 的替代品，它是一个&lt;strong&gt;更高层次的抽象&lt;/strong&gt;。核心差异：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Function Calling&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;工具注册&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;静态列表，全量注入 Prompt&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;动态发现，按需加载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;工具描述&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;文本参数 schema&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;结构化 Resource/Tool/Prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文管理&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;LLM 负责&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;MCP Client 负责&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;权限模型&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;调用前二态&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;资源级 + 操作级 + 人机审查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传输层&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;HTTP/SSE&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;stdio / Streamable HTTP / WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;状态&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;无状态&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;有状态（Resource 订阅）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MCP 把工具抽象成三个原语（Primitive）：&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;原语一resource资源&quot;&gt;原语一：Resource（资源）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Resource 是 LLM 可以读取的数据。&lt;/strong&gt; 它不只是「文件」，而是任何结构化或非结构化的数据源。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// MCP Server 暴露的资源&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;resources&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;uri&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;db://users/active&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;活跃用户列表&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;过去 30 天内登录过的用户&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;mimeType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;uri&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;docs://internal/security-policy&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;安全策略文档&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;mimeType&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;text/markdown&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;关键设计：&lt;strong&gt;Resource 的 URI 是标准化的&lt;/strong&gt;——&lt;code dir=&quot;auto&quot;&gt;protocol://path&lt;/code&gt;。这让 LLM 可以用统一的语义引用任何数据源，而不是记住「用户列表是 &lt;code dir=&quot;auto&quot;&gt;get_users()&lt;/code&gt;，文档是 &lt;code dir=&quot;auto&quot;&gt;read_file()&lt;/code&gt;」。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;原语二tool工具&quot;&gt;原语二：Tool（工具）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Tool 是 LLM 可以执行的操作。&lt;/strong&gt; 和 Function Calling 的 function 类似，但多了&lt;strong&gt;服务器端描述&lt;/strong&gt;——Tool 的能力由 MCP Server 声明，Client 不需要硬编码。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;tools&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;create_issue&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;在 GitHub 仓库中创建 Issue&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;inputSchema&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;repo&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;body&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;和 Function Calling 的核心差异：&lt;strong&gt;Tool 列表不在 Prompt 中，在 MCP Client 的内存中&lt;/strong&gt;。Client 负责在 LLM 需要时动态注入相关 Tool 的 schema，而不是全部塞进去。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;原语三prompt提示模板&quot;&gt;原语三：Prompt（提示模板）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Prompt 是预定义的交互模板。&lt;/strong&gt; 比如「帮我 Review 这个 PR」或「为这个函数写单元测试」。这些模板由 MCP Server 提供，确保 LLM 以正确的格式理解用户意图。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;prompts&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;code_review&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;审查 Pull Request&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;arguments&quot;&lt;/span&gt;&lt;span&gt;: [{ &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;pr_url&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;PR 链接&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;required&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; }]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Prompt 模板的好处：你不需要在每次对话中都告诉 LLM「请以这种格式审查代码，关注 1)安全性 2)性能 3)可读性」——这些规则被封装在 MCP Server 的 Prompt 模板中。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-的架构&quot;&gt;MCP 的架构&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;┌──────────────────────────────────────────┐&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│              MCP Host (应用)               │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  ┌────────────────────────────────────┐   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  │        MCP Client (协议层)          │   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  │  • 管理多个 MCP Server 连接          │   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  │  • 动态发现 Resource/Tool/Prompt    │   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  │  • 按需注入上下文到 LLM             │   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│  └──────┬──────────┬──────────┬───────┘   │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│         │          │          │           │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│    ┌────▼───┐ ┌───▼────┐ ┌──▼──────┐     │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│    │Server A│ │Server B│ │Server C │     │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│    │(GitHub)│ │(Slack) │ │(Database)│    │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;│    └────────┘ └────────┘ └─────────┘     │&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;└──────────────────────────────────────────┘&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Client 是核心&lt;/strong&gt;。它不是简单的 proxy，而是&lt;strong&gt;上下文管理器&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;收到用户消息 → 分析意图&lt;/li&gt;
&lt;li&gt;从所有连接 Server 的 Tool 列表中匹配相关工具&lt;/li&gt;
&lt;li&gt;只将匹配的 Tool schema 注入 LLM 上下文&lt;/li&gt;
&lt;li&gt;LLM 返回 Tool Call → Client 路由到对应 Server 执行&lt;/li&gt;
&lt;li&gt;结果返回 LLM → 生成最终回复&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-的传输层&quot;&gt;MCP 的传输层&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;MCP 支持三种传输方式：&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;stdio标准输入输出&quot;&gt;stdio（标准输入输出）&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;claude_desktop_config.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;-y&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;@anthropic/mcp-server-github&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;MCP Client 以子进程方式启动 Server，通过 stdin/stdout 通信。&lt;strong&gt;适合本地工具&lt;/strong&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;streamable-http&quot;&gt;Streamable HTTP&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;MCP Server 作为 HTTP 服务运行，Client 通过 HTTP 请求获取工具列表、调用工具。支持 Server-Sent Events（SSE）推送。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client ──GET /mcp/tools──▶ Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client ◀──200 [{tool1}, {tool2}]── Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client ──POST /mcp/call──▶ Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{tool: &quot;create_issue&quot;, args: {...}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Client ◀──200 {result}── Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;适合公共服务&lt;/strong&gt;，任何支持 HTTP 的环境都可以对接。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;websocket实验性&quot;&gt;WebSocket（实验性）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;双向实时通信，适合需要推送通知的场景。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实战写一个-mcp-server&quot;&gt;实战：写一个 MCP Server&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;以下是一个完整的 MCP Server 示例——暴露一个「天气查询」工具：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;weather_server.py&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; mcp.server &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; Server, NotificationOptions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; mcp.server.models &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; InitializationCapabilities&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; httpx&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;server &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Server(&lt;/span&gt;&lt;span&gt;&quot;weather-server&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@server.list_tools&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;list_tools&lt;/span&gt;&lt;span&gt;():&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;get_weather&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;获取指定城市的当前天气&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;inputSchema&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;object&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;properties&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;&quot;city&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;城市名称（中文，如 &apos;北京&apos;）&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;required&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;city&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@server.call_tool&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;call_tool&lt;/span&gt;&lt;span&gt;(name: &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;, arguments: &lt;/span&gt;&lt;span&gt;dict&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; name &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;get_weather&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;city &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; arguments[&lt;/span&gt;&lt;span&gt;&quot;city&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;with&lt;/span&gt;&lt;span&gt; httpx.AsyncClient() &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; client:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;resp &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; client.get(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;https://api.weather.com/v1/current?city=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;city&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;apiKey&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;YOUR_KEY&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;data &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; resp.json()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;city&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;当前温度 &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;data[&lt;/span&gt;&lt;span&gt;&apos;temp&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;°C，&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;data[&lt;/span&gt;&lt;span&gt;&apos;condition&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__name__&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;__main__&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;server.run(&lt;/span&gt;&lt;span&gt;transport&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;启动：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;python&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;weather_server.py&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;然后在 Claude Desktop 的 &lt;code dir=&quot;auto&quot;&gt;claude_desktop_config.json&lt;/code&gt; 中添加：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;weather&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;python&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;weather_server.py&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Claude 就能查询天气了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：MCP Python SDK 仍在快速迭代。截至 2026 年 7 月，建议固定 SDK 版本。Server 端的异常处理要完善——任何未捕获的异常都会导致 stdio 连接断开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-vs-function-calling-vs-plugin&quot;&gt;MCP vs Function Calling vs Plugin&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Function Calling&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;ChatGPT Plugin&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;标准化&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;OpenAI 专有&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;OpenAI 专有&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;开放标准&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;模型无关&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ OpenAI only&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态发现&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 静态列表&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ Resource/Tool/Prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;权限粒度&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;二态&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;二态&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;资源级 + 操作级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传输层&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;API 注入&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;HTTP&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;stdio / HTTP / WS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生态&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;OpenAI 生态&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;已弃用&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;开放生态（500+ Server）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;ChatGPT Plugin 被 MCP 取代几乎是必然——Plugin 是封闭生态，MCP 是开放标准。类似 W3C 和 Flash 的关系。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-的问题与陷阱&quot;&gt;MCP 的问题与陷阱&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;陷阱-1schema-膨胀&quot;&gt;陷阱 1：Schema 膨胀&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;MCP Server 可以提供任意数量的 Tool。但如果你把 200 个 Tool 都注册到同一个 Server，Client 的匹配逻辑反而会退化——它不知道哪个 Tool 是相关的，只能把所有 Tool 的描述发给 LLM。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：一个 MCP Server 暴露 &lt;strong&gt;5-15 个 Tool&lt;/strong&gt;。超过 20 个就拆分成多个 Server。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;陷阱-2tool-描述的语言差异&quot;&gt;陷阱 2：Tool 描述的语言差异&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;如果你的 MCP Client 是中文应用，但 Server 的 Tool 描述是英文——LLM 在匹配用户中文意图和英文 Tool 描述时会出现理解偏差。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：Tool 描述和用户 prompt 使用同一语言。如果用户是中文，Tool 描述也用中文。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;陷阱-3资源泄露&quot;&gt;陷阱 3：资源泄露&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;MCP 使用 stdio 传输时，Server 是子进程。如果 Client 没有正确管理进程生命周期，每个对话都 spawn 一个新的 Server 进程 → 进程泄漏 → 内存耗尽。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：Client 使用进程池，复用 Server 进程。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;陷阱-4tool-调用依赖链&quot;&gt;陷阱 4：Tool 调用依赖链&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;用户：&quot;部署最新版本到生产环境&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ LLM 调用 list_services → 返回 50 个服务&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ LLM 调用 get_version(&quot;user-service&quot;) → 返回 v2.3.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;→ LLM 调用 deploy(&quot;user-service&quot;, &quot;v2.3.1&quot;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个链路需要 3 次 Tool 调用。如果 &lt;code dir=&quot;auto&quot;&gt;list_services&lt;/code&gt; 返回超时，整个链路失败。MCP 目前没有内置的 retry 或 fallback 机制——这些需要应用层处理。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp-的未来方向&quot;&gt;MCP 的未来方向&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;资源订阅（Resource Subscription）&lt;/strong&gt;：Server 推送资源变化（如「数据库有新记录」），Client 主动通知 LLM，而不是 LLM 每次轮询。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool 组合（Tool Composition）&lt;/strong&gt;：声明式组合多个 Tool 为一个工作流——「部署」= build → test → deploy → health check，LLM 不再需要知道这 4 步的细节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;跨会话记忆&lt;/strong&gt;：MCP Server 维护跨会话的状态，让 LLM 能「记住上次我们做到哪了」。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MCP 网关&lt;/strong&gt;：统一的 MCP Server 管理和路由层，处理认证、限流、监控、多租户——企业 IAM 的典型能力。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MCP 正在成为 AI Agent 基础设施的&lt;strong&gt;事实标准&lt;/strong&gt;——就像 HTTP 成为 Web 的标准、USB-C 成为设备的接口标准。它不是最完美的协议，但它在一个关键时间点出现：当所有人都开始把 LLM 连接到外部工具，但每个人都用不同的方式连接。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;协议需要实践检验。参考 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops MCP 生态&lt;/a&gt; 获取更多 MCP Server 和 Client 实现。&lt;/em&gt;&lt;/p&gt;</content:encoded><category>MCP</category><category>Model Context Protocol</category><category>AI Agent</category><category>Tool Integration</category><category>LLM</category><category>Anthropic</category></item><item><title>AI Coding 工具横评——Aider vs Continue vs Cline 谁更适合你</title><link>https://www.xlabs.club/blog/ai-coding-tools-comparison/</link><guid isPermaLink="true">https://www.xlabs.club/blog/ai-coding-tools-comparison/</guid><description>Aider、Continue、Cline 三大 AI Coding 工具 12 维度对比：工作流、代码编辑、上下文、模型支持、成本。附实战场景推荐和选型决策树。</description><pubDate>Thu, 09 Jul 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2026 年的 AI Coding 工具已经不再是「玩具」或「自动补全」那么简单。Aider、Continue、Cline 代表了三种不同的设计哲学：&lt;strong&gt;终端原生的结对编程&lt;/strong&gt;、&lt;strong&gt;IDE 内嵌的上下文感知&lt;/strong&gt;、&lt;strong&gt;VS Code 插件的 Agent 自动化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文从 12 个维度对比这三款工具，帮你选型——不是看谁功能多，是看谁的设计哲学契合你的工作流。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多 AI 基础设施工具参见 &lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai--inference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops AI 分类&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;一句话定位&quot;&gt;一句话定位&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;定位&lt;/th&gt;
&lt;th&gt;核心哲学&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;终端原生 AI 结对编程&lt;/td&gt;
&lt;td&gt;“AI 是你的 Pair，Git 是真相来源”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Continue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IDE 内嵌 AI 助手&lt;/td&gt;
&lt;td&gt;“AI 应该像 Tab 补全一样自然，不打断心流”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VS Code AI Agent&lt;/td&gt;
&lt;td&gt;“AI 应该能自己读代码、写代码、执行命令”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-1工作流适配&quot;&gt;维度 1：工作流适配&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;aider终端原生&quot;&gt;Aider：终端原生&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Aider 在终端运行，和 Git 深度集成。你不需要离开终端——&lt;code dir=&quot;auto&quot;&gt;aider&lt;/code&gt; 启动，AI 自动理解 Git 仓库的上下文。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ aider&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Aider v0.75.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Models: claude-sonnet-4-5 with diff edit format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Git repo: .git with 1,234 files&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Repo map: using 1024 tokens&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;──────────────────────────────────────────&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt; 把这个函数的错误处理改成 Result 类型&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# AI 直接在终端中输出 diff，你可以 /add、/drop、/undo&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;适合&lt;/strong&gt;：Vim/Neovim/终端重度用户、Git 工作流驱动的开发。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;continueide-内嵌&quot;&gt;Continue：IDE 内嵌&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Continue 安装为 VS Code / JetBrains 插件。它通过 &lt;code dir=&quot;auto&quot;&gt;@&lt;/code&gt; 命令引入上下文：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@file&lt;/code&gt; 引用文件&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@folder&lt;/code&gt; 引用目录&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@docs&lt;/code&gt; 引用文档&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@terminal&lt;/code&gt; 引入终端输出&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@git&lt;/code&gt; 引入 Git diff&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;适合&lt;/strong&gt;：IDE 重度用户，希望 AI 不离开编辑器。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;clineagent-模式&quot;&gt;Cline：Agent 模式&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Cline 的运行模式更像「自主 Agent」：你描述目标，它自己读代码 → 写代码 → 执行命令 → 检查输出 → 修正错误。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你：把这个 Express 后端迁移到 Hono&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Cline：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 读取 package.json → 理解依赖&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. 读取 src/ → 理解路由结构&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. 安装 hono → npm install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4. 重写路由文件 → 逐个文件替换&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5. 运行测试 → npm test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;6. 发现 1 个失败 → 读取错误日志 → 修复 → 重新测试&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;7. 全部通过 ✓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;适合&lt;/strong&gt;：希望 AI 自己完成多步骤任务，而不是每次对话都要手动描述上下文。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-2代码编辑能力&quot;&gt;维度 2：代码编辑能力&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Aider&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Continue&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Cline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;单文件编辑&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多文件编辑&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 强&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;搜索替换模式&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ SEARCH/REPLACE&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ Diff 模式&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自动格式化&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ &lt;code dir=&quot;auto&quot;&gt;--lint&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 依赖 IDE&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Git 提交&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 自动 &lt;code dir=&quot;auto&quot;&gt;git commit&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ 手动&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Undo/回滚&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ &lt;code dir=&quot;auto&quot;&gt;/undo&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;❌ IDE 原生 Undo&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ Checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;测试驱动修复&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ &lt;code dir=&quot;auto&quot;&gt;--test-cmd&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;⚠️ 手动&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;⚠️ 半自动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Aider 最独特的能力&lt;/strong&gt;：理解 Git 仓库的 Map（Repo Map）——它能自动生成仓库结构摘要发给 LLM，让 LLM 知道哪个文件干什么。这意味着当你问「修改认证逻辑」，Aider 不需要你手动 &lt;code dir=&quot;auto&quot;&gt;@file auth.ts&lt;/code&gt;，它自己知道应该改哪些文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cline 最独特的能力&lt;/strong&gt;：Plan → Act 模式。在修改代码前，Cline 会先输出一个执行计划（Plan Mode），你同意后它才执行（Act Mode）。这在改生产代码时特别有用。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-3上下文与内存&quot;&gt;维度 3：上下文与内存&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Aider&lt;/th&gt;
&lt;th&gt;Continue&lt;/th&gt;
&lt;th&gt;Cline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文来源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Git 仓库 + 对话历史&lt;/td&gt;
&lt;td&gt;手动 @ 引用&lt;/td&gt;
&lt;td&gt;自动读取项目文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文大小&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仓库 Map + 当前文件&lt;/td&gt;
&lt;td&gt;用户选择&lt;/td&gt;
&lt;td&gt;自动裁剪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;跨会话记忆&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;.aider.conf.yml&lt;/code&gt; 缓存&lt;/td&gt;
&lt;td&gt;⚠️ 对话无持久&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自定义规则&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;CONVENTIONS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;.continuerc.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code dir=&quot;auto&quot;&gt;.clinerules&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Aider&lt;/strong&gt; 的 Repo Map 是差异化优势——它持续分析整个仓库的代码结构，LLM 调用时附上摘要。如果你有 500 个源文件，LLM 不需要读 500 个文件，只需要读 1KB 的 Map 摘要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Continue&lt;/strong&gt; 的上下文方式更「手动但精确」——你控制 LLM 看到什么。好处是 Token 成本可控，坏处是每次都要手动选择上下文。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-4模型支持&quot;&gt;维度 4：模型支持&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Aider&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Continue&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Cline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.5&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 优秀&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;本地模型（Ollama）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自定义 Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ &lt;code dir=&quot;auto&quot;&gt;--openai-api-base&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅ 配置&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三款工具都支持自定义 API endpoint，可以对接任何 OpenAI 兼容接口（含自建代理）。这意味着你可以使用公司内部的 LLM 网关或成本更低的第三方 API。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;维度-5学习曲线与上手成本&quot;&gt;维度 5：学习曲线与上手成本&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Aider：    ████████░░  需要理解 Git 工作流 + Map 概念，但上手后极快&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Continue： ████░░░░░░  和 Tab 补全一样自然， 5 分钟上手&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Cline：    ██████░░░░  需要理解 Agent 模式，但 UI 引导清楚&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;实战场景推荐&quot;&gt;实战场景推荐&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;重构遗留代码--aider&quot;&gt;重构遗留代码 → Aider&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ aider --model claude-sonnet-4-5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt; 把 src/legacy/ 下的所有函数签名从 callback 模式改成 async/await&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt; 用 /run npm test 验证&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Aider 的 Repo Map 天然适合大规模重构——它自己知道所有受影响文件。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;日常开发--代码审查--continue&quot;&gt;日常开发 + 代码审查 → Continue&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;在 VS Code 中选中代码 → &lt;code dir=&quot;auto&quot;&gt;Cmd+L&lt;/code&gt; → “这段代码有什么问题？” → AI 在侧边栏回答。不需要离开编辑器，不需要复制粘贴。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;从零搭建新项目--cline&quot;&gt;从零搭建新项目 → Cline&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;用 Hono + Drizzle + SQLite 搭一个 REST API，包含用户注册/登录/CRUD&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Cline 会自动：创建项目结构 → 安装依赖 → 写代码 → 运行测试 → 修复问题。你只需要在关键步骤确认。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;成本和性能&quot;&gt;成本和性能&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;单次任务平均 Token&lt;/th&gt;
&lt;th&gt;月成本（轻度使用）&lt;/th&gt;
&lt;th&gt;月成本（重度使用）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;5K-20K&lt;/td&gt;
&lt;td&gt;$10-30&lt;/td&gt;
&lt;td&gt;$50-150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;2K-10K&lt;/td&gt;
&lt;td&gt;$5-20&lt;/td&gt;
&lt;td&gt;$30-80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cline&lt;/td&gt;
&lt;td&gt;10K-50K&lt;/td&gt;
&lt;td&gt;$20-50&lt;/td&gt;
&lt;td&gt;$100-300&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Cline 的 Agent 模式每次任务消耗更多 Token（因为自己读文件+执行命令+读取输出），但完成的任务也更多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;选择决策树&quot;&gt;选择决策树&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你是终端/Vim 用户？ ──YES──▶ Aider&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NO&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你喜欢 IDE 内嵌、低打扰？ ──YES──▶ Continue&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NO&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;你想 AI 自己完成多步骤任务？ ──YES──▶ Cline&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ALL → 三个都装，不同场景用不同工具&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;实际上很多开发者三个都装了——Aider 做重构、Continue 做日常、Cline 做自动化任务。它们不互斥。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;你的画像&lt;/th&gt;
&lt;th&gt;首选工具&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vim/Terminal 党，Git 工作流驱动&lt;/td&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE 党，希望 AI 像 Tab 一样自然&lt;/td&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;想要 AI Agent 自主完成任务&lt;/td&gt;
&lt;td&gt;Cline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;「我全都要」&lt;/td&gt;
&lt;td&gt;三个都装&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AI Coding 工具的选择不是技术问题，是&lt;strong&gt;工作流哲学&lt;/strong&gt;问题。选那个不打断你心流的工具。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;更多 AI 开发工具：&lt;a href=&quot;https://github.com/void-sleep/awesome-x-ops#ai-coding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;awesome-x-ops AI Coding 分类&lt;/a&gt; · &lt;a href=&quot;https://www.xlabs.club&quot;&gt;xlabs.club 技术博客&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded><category>AI Coding</category><category>Aider</category><category>Continue</category><category>Cline</category><category>LLM</category><category>Developer Tools</category></item><item><title>Keycloak 生产环境避坑指南——从单实例到高可用集群</title><link>https://www.xlabs.club/blog/keycloak-production-pitfalls/</link><guid isPermaLink="true">https://www.xlabs.club/blog/keycloak-production-pitfalls/</guid><description>Keycloak 生产环境避坑：H2 数据库、会话共享、反向代理、连接池、持久化、健康检查、优雅停机。实测配置和解决方案，从单实例到高可用集群。</description><pubDate>Thu, 09 Jul 2026 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Keycloak 的开发环境只需 &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt;，但生产环境远没有这么简单。本文整理了我们在多个项目中踩过的 7 个坑，每个都有可复制的配置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你还不熟悉 Keycloak 的基本概念（Realm、Client、Identity Provider），建议先阅读 &lt;a href=&quot;https://idaas.xlabs.club/docs/fundamentals/what-is-keycloak/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Keycloak 完全指南&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-1默认-h2-数据库&quot;&gt;坑 1：默认 H2 数据库&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 默认使用 H2 嵌入式数据库。开发环境没问题，&lt;strong&gt;生产环境绝对不能碰&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;症状&lt;/strong&gt;：重启后所有 Realm 配置消失、并发写入时数据损坏、无法做数据库级别备份。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;docker-compose.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;keycloak&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;quay.io/keycloak/keycloak:26.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;environment&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;KC_DB&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;postgres&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;KC_DB_URL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;jdbc:postgresql://postgres:5432/keycloak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;KC_DB_USERNAME&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;KC_DB_PASSWORD&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&amp;#x3C;secure-password&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;depends_on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;postgres&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;service_healthy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：生产环境第一天就切到 PostgreSQL。H2 → PostgreSQL 的迁移比想象中痛苦（用户密码哈希格式不同、自定义 SPI 数据表结构不兼容）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-2会话不共享多实例的噩梦&quot;&gt;坑 2：会话不共享——多实例的噩梦&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 26.x 默认使用 Infinispan 做分布式缓存，但&lt;strong&gt;单播发现模式（&lt;code dir=&quot;auto&quot;&gt;PING&lt;/code&gt;）在生产环境中不可靠&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;症状&lt;/strong&gt;：登录后刷新页面回到登录页、不同 Pod 之间 Session 不共享、偶尔出现 &lt;code dir=&quot;auto&quot;&gt;user session not found&lt;/code&gt; 错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;根本原因&lt;/strong&gt;：Keycloak Pod 之间没有发现彼此，每个 Pod 维护独立的 Infinispan 缓存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案——Kubernetes DNS 发现模式&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Keycloak 启动参数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--cache&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;ispn&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--cache-stack&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;kubernetes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--spi-connections-infinispan-quarkus-site&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&amp;#x3C;site-name&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;配合 Headless Service：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;keycloak-headless.yaml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;v1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak-discovery&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;clusterIP&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;selector&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;7800&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;jgroups-tcp&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;验证集群是否正常&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 进入 Keycloak Pod&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kubectl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;keycloak-0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/opt/keycloak/bin/kcadm.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;config&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;credentials&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--server&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--realm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;admin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 检查集群节点&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kubectl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;keycloak-0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/opt/keycloak/bin/kcadm.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--server&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;serverinfo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;jq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;.clusterInfo&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;深入理解 Keycloak 的 Infinispan 缓存和用户会话管理，参考 &lt;a href=&quot;https://idaas.xlabs.club/docs/advanced-topics/iam-session-management/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;IAM 会话管理深度解析&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-3反向代理配置混合端口噩梦&quot;&gt;坑 3：反向代理配置——混合端口噩梦&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 默认监听 8080（HTTP），但生产环境必须用 TLS。NGINX/Traefik 反向代理是标配，但配置不当会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;redirect_uri 不匹配&lt;/strong&gt;：Keycloak 以为自己是 &lt;code dir=&quot;auto&quot;&gt;http://localhost:8080&lt;/code&gt;，实际用户在访问 &lt;code dir=&quot;auto&quot;&gt;https://auth.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合内容&lt;/strong&gt;：HTML 通过 HTTPS，但 JS/CSS 引用的是 HTTP 地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket 断开&lt;/strong&gt;：Keycloak Admin Console 使用 WebSocket，反向代理必须支持升级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正确配置&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Keycloak 启动参数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--hostname&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;https://auth.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--http-enabled&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--proxy-headers&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;xforwarded&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Traefik IngressRoute&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;traefik.io/v1alpha1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;IngressRoute&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;entryPoints&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;websecure&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;routes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Host(`auth.example.com`)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Rule&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;tls&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;secretName&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;keycloak-tls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;--hostname&lt;/code&gt; 和反向代理的 &lt;code dir=&quot;auto&quot;&gt;Host&lt;/code&gt; 头必须一致。任何不一致都会导致 OpenID Connect Discovery 端点（&lt;code dir=&quot;auto&quot;&gt;/.well-known/openid-configuration&lt;/code&gt;）返回错误的 URL，所有依赖 OIDC 的应用都会故障。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-4数据库连接池耗尽&quot;&gt;坑 4：数据库连接池耗尽&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 使用 HikariCP 连接池，默认最大连接数 20。当 Realm 配置的 Identity Provider 数量多、或 User Federation 查询频繁时，20 个连接很快耗尽。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;症状&lt;/strong&gt;：Admin Console 卡住、登录延迟飙升、&lt;code dir=&quot;auto&quot;&gt;HikariPool-1 - Connection is not available&lt;/code&gt; 日志。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;调优&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 根据实际负载调整&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--db-pool-initial-size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--db-pool-min-size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--db-pool-max-size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--db-pool-idle-timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;300000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;同时确保 PostgreSQL 的 &lt;code dir=&quot;auto&quot;&gt;max_connections&lt;/code&gt; 远大于 Keycloak 的 pool size（建议 &lt;code dir=&quot;auto&quot;&gt;max_connections = keycloak_instances × pool_max_size + 50&lt;/code&gt;）。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-5会话持久化pod-重启后用户不掉线&quot;&gt;坑 5：会话持久化——Pod 重启后用户不掉线&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes 默认使 Pod 变成「牲畜」，随时可以被杀死。Keycloak 如果只依赖内存缓存（&lt;code dir=&quot;auto&quot;&gt;--cache=local&lt;/code&gt;），Pod 重启后所有在线用户的 Session 丢失。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案——启用持久会话（Persistent User Sessions）&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--spi-user-sessions-infinispan-offline-session-preload&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--spi-user-sessions-infinispan-offline-preload-entry-size&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;生产验证&lt;/strong&gt;：执行 &lt;code dir=&quot;auto&quot;&gt;kubectl delete pod keycloak-0&lt;/code&gt;，观察是否有活跃用户掉线。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;完整的 Keycloak 高可用 + 灾备方案，参考 &lt;a href=&quot;https://idaas.xlabs.club/docs/solution-blogs/keycloak-ha-dr/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Keycloak 高可用与灾备部署&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-6健康检查存活探针不当导致脑裂&quot;&gt;坑 6：健康检查——存活探针不当导致脑裂&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes 的健康检查（Liveness Probe）如果配置不当，可能会在 Keycloak 启动慢或 Infinispan 集群重组时误杀 Pod。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推荐配置&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;livenessProbe&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;httpGet&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/health/live&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;9000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;initialDelaySeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;periodSeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;failureThreshold&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# 连续 5 次失败才重启&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;readinessProbe&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;httpGet&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/health/ready&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;9000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;initialDelaySeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;periodSeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;failureThreshold&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;startupProbe&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;httpGet&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/health/started&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;9000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;initialDelaySeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;periodSeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;failureThreshold&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# 给足 5 分钟启动时间&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;/health/live&lt;/code&gt; 端点依赖 Infinispan 集群状态。在集群重组时，&lt;code dir=&quot;auto&quot;&gt;/health/live&lt;/code&gt; 会返回 &lt;code dir=&quot;auto&quot;&gt;DOWN&lt;/code&gt;。如果 &lt;code dir=&quot;auto&quot;&gt;failureThreshold&lt;/code&gt; 太小（默认 3），3 × 30 = 90 秒内 Keycloak 集群还没重组完成，Pod 就会被杀——导致重组永远完不成。&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;failureThreshold: 5&lt;/code&gt; 甚至 10 都不为过&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;坑-7优雅停机拒绝新请求--排空存量&quot;&gt;坑 7：优雅停机——拒绝新请求 + 排空存量&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 的关闭不是瞬时的。直接 &lt;code dir=&quot;auto&quot;&gt;kill -9&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;kubectl delete pod --grace-period=0&lt;/code&gt; 会导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正在处理的登录请求丢失&lt;/li&gt;
&lt;li&gt;正在写入的会话数据损坏&lt;/li&gt;
&lt;li&gt;Infinispan 集群成员突然消失 → 整个集群短暂不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正确配置&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Kubernetes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;120&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# 给足 2 分钟&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;containers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;lifecycle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;preStop&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/bin/sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;-c&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 1. 从负载均衡摘除自己（如调用 Traefik API）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 2. 等待存量请求排空&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sleep 30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;# 3. Keycloak 自身关闭（SIGTERM）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Keycloak 收到 &lt;code dir=&quot;auto&quot;&gt;SIGTERM&lt;/code&gt; 后会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;停止接受新连接&lt;/li&gt;
&lt;li&gt;等待活跃事务完成&lt;/li&gt;
&lt;li&gt;刷新 Infinispan 缓存到持久存储&lt;/li&gt;
&lt;li&gt;优雅退出&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;terminationGracePeriodSeconds&lt;/code&gt; 必须大于 Keycloak 的实际关闭时间。建议在测试环境实测后 + 30 秒安全边界。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;坑&lt;/th&gt;
&lt;th&gt;症状&lt;/th&gt;
&lt;th&gt;关键修复&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H2 数据库&lt;/td&gt;
&lt;td&gt;重启数据丢失&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;KC_DB=postgres&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话不共享&lt;/td&gt;
&lt;td&gt;登录状态丢失&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--cache-stack=kubernetes&lt;/code&gt; + Headless Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反向代理&lt;/td&gt;
&lt;td&gt;redirect_uri 不匹配&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--hostname=https://...&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;--proxy-headers=xforwarded&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连接池耗尽&lt;/td&gt;
&lt;td&gt;Admin Console 卡住&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--db-pool-max-size=50&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话丢失&lt;/td&gt;
&lt;td&gt;Pod 重启用户掉线&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--spi-user-sessions-infinispan-offline-session-preload=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;健康检查误杀&lt;/td&gt;
&lt;td&gt;集群重组时 Pod 被杀&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;failureThreshold: 5+&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;startupProbe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强制重启&lt;/td&gt;
&lt;td&gt;数据损坏&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;terminationGracePeriodSeconds: 120&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;生产环境的 Keycloak 不是一个 &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt; 的事。但填完上述 7 个坑后，它可以稳定运行数年。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;更多 Keycloak 深度内容：&lt;a href=&quot;https://idaas.xlabs.club/docs/fundamentals/what-is-keycloak/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Keycloak 完全指南&lt;/a&gt; · &lt;a href=&quot;https://idaas.xlabs.club/docs/solution-blogs/keycloak-ha-dr/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Keycloak 高可用与灾备&lt;/a&gt; · &lt;a href=&quot;https://idaas.xlabs.club&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;IDaaS Book&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded><category>Keycloak</category><category>IAM</category><category>Kubernetes</category><category>High Availability</category><category>Production</category><category>Infinispan</category></item><item><title>企业级智能代码重构探索：大规模自动化迁移实践与工具选型</title><link>https://www.xlabs.club/blog/smart-code/</link><guid isPermaLink="true">https://www.xlabs.club/blog/smart-code/</guid><description>介绍 OpenRewrite、Spring Boot Migrator 等工具在企业级自动化代码迁移与重构中的实践方法。</description><pubDate>Tue, 09 Dec 2025 14:08:54 GMT</pubDate><content:encoded>&lt;p&gt;作为一个以 Java 和 Spring 为主要技术栈的团队，在日常的软件开发中，我们经常会遇到一系列的组件升级和代码重构需求，在此过程我们期望能做到几个效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目级升级：整个项目（注此项目指 Maven Project）升级，而不是基于某个 Java 类或片段。&lt;/li&gt;
&lt;li&gt;不同版本跨度变更：比如从 Spring 项目迁移到 Spring Boot，从 Spring Boot 2.x 升级到 3.x，从 Java 8 升级到 Java 21。&lt;/li&gt;
&lt;li&gt;代码安全可靠：变更的代码一定是正确的，至少是逻辑正确的，至少不能像某 AI 助手一样设置一些根本不存在的属性。&lt;/li&gt;
&lt;li&gt;经验产品化：最佳实践就是产品，比如我并不（想）了解 Spring Boot 3.2 具体有哪些变更，但希望能一键从 3.0 自动升级到 3.2，直接告诉有哪些变更。&lt;/li&gt;
&lt;li&gt;自定义重构：对于某些自研代码，希望能自定义重构逻辑，一键自动重构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于以上背景，我们探索了 &lt;a href=&quot;https://docs.openrewrite.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenRewrite&lt;/a&gt;、&lt;a href=&quot;https://github.com/spring-projects-experimental/spring-boot-migrator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot Migrator&lt;/a&gt;、&lt;a href=&quot;https://github.com/windup&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Redhat Windup&lt;/a&gt;、&lt;a href=&quot;https://github.com/adoptium/emt4j&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;emt4j&lt;/a&gt; 和一众不便具名的 AI 代码助手，本文将分享我们使用 OpenRewrite、Spring Boot Migrator 进行代码重构和升级的一些使用经验和体验。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;openrewrite&quot;&gt;OpenRewrite&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.openrewrite.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenRewrite&lt;/a&gt; 是一个开源的代码重写工具，旨在帮助开发人员自动化大规模重构代码。&lt;/p&gt;
&lt;p&gt;它提供了一套强大的 API 和插件系统，可以通过静态分析和代码转换技术来解析、修改和生成代码。OpenRewrite 支持多种编程语言，包括 Java、C#、 TypeScript、Python、Kubernetes 等。通过使用 OpenRewrite，开发人员可以轻松地进行代码重构、性能优化、代码风格调整和代码迁移等操作，从而提高代码质量和可维护性。OpenRewrite 的开源性质使得开发人员可以自由地定制和扩展其功能，以满足特定项目的需求。&lt;/p&gt;
&lt;p&gt;比如 Java 领域一些热门的应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 版本升级：从 Java 8 到 Java 21，从 Java EE 到 Jakarta EE 。&lt;/li&gt;
&lt;li&gt;Spring 框架迁移：从 Spring 5 到 Spring 6，从 Spring Boot 2 到 Spring Boot 3。&lt;/li&gt;
&lt;li&gt;测试框架迁移： 从 Junit 4 到 Junit 5。&lt;/li&gt;
&lt;li&gt;依赖管理：自动更新 Java 项目的 Maven 或 Gradle 依赖，确保使用最新和最安全的库版本。&lt;/li&gt;
&lt;li&gt;代码清理和格式化：自动清理和格式化 Java 代码，确保符合项目或组织的编码标准和风格指南。&lt;/li&gt;
&lt;li&gt;修复安全漏洞：自动识别和修复 Java 代码中的已知安全漏洞，如使用了有安全问题的库或方法。&lt;/li&gt;
&lt;li&gt;代码异味检测和修复：识别并自动重构 Java 代码中的“代码异味”，以提高代码可维护性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相比于时下火热的 AI 代码工具，比如 GitHub Copilot、Amazon CodeWhisperer，我认为 OpenRewrite 的优势主要有以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以直接处理大型项目，截止我写此文的今天还没一个成熟的 AI 助手能将整个项目一次性迁移或重构。&lt;/li&gt;
&lt;li&gt;准确度较高，它通过预定义的规则来实现代码的精确修改，目前很多 AI 代码助手还在一本正经的瞎扯，尤其是凭空创造让人防不胜防。&lt;/li&gt;
&lt;li&gt;可自定义扩展，对于自研代码不可能经过自动学习达到精确的效果，此时可自定义实现。&lt;/li&gt;
&lt;li&gt;免费，可离线使用，便于 CI 集成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得一提的是，AWS 最近推出了 Amazon Q Code Transformation 专门做代码迁移和重构，需要开通 Amazon CodeWhisperer 企业版才能使用，可惜我没有钱去试用。&lt;/p&gt;
&lt;p&gt;更新：AWS 2025 年底推出了 AWS Transform Custom，这其实就是我想要的，比 Q Code 已经有质的提升，只是没有深度测试，有用过的小伙伴欢迎交流反馈。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;快速入门&quot;&gt;快速入门&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 一个最核心的概念是 &lt;code dir=&quot;auto&quot;&gt;Recipe&lt;/code&gt;，由于我是国内首个翻译这个单词的人，以下将直译为食谱。一个 Recipe 可以理解为是一套声明好的规则，OpenRewrite 按照固定规则进行重构。&lt;/p&gt;
&lt;p&gt;一个 Recipe 可以包含多个 Recipe，可以层级累加，比如 &lt;code dir=&quot;auto&quot;&gt;UpgradeSpringBoot_3_2&lt;/code&gt;可能包含对 pom.xml、application.properties、java main source、java test source 的升级改动。&lt;/p&gt;
&lt;p&gt;初次使用 OpenRewrite，可通过官方提供的 &lt;a href=&quot;https://docs.openrewrite.org/running-recipes/popular-recipe-guides&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;热门指导&lt;/a&gt; 找到自己感兴趣的按步骤操作。&lt;/p&gt;
&lt;p&gt;另外就是食谱很多，一个一个用很麻烦，可优先考虑 &lt;code dir=&quot;auto&quot;&gt;best practices&lt;/code&gt; 系列，里面聚合了多个食谱。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;./popular-recipe-guides.png&quot;&gt;&lt;img alt=&quot;popular-recipe-guides&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1582&quot; height=&quot;1350&quot; src=&quot;https://www.xlabs.club/_astro/popular-recipe-guides.D4JlwBrq_Z1Ylwsb.webp&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;更多功能可通过 &lt;a href=&quot;https://docs.openrewrite.org/recipes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;食谱分类&lt;/a&gt; 搜索，也可通过官网右上方的搜索按钮按照关键字搜索，也可通过 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:discover&lt;/code&gt; 列出可用的食谱。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;./recipes.png&quot;&gt;&lt;img alt=&quot;recipes&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1532&quot; height=&quot;1796&quot; src=&quot;https://www.xlabs.club/_astro/recipes.C3yAlawa_2kzev1.webp&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;运行方式&quot;&gt;运行方式&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;友情提示：运行前请把代码交给 Git 管理，并 checkout 一个全新的分支，便于比对自动修改了哪些代码。&lt;/p&gt;
&lt;p&gt;OpenRewrite 支持使用 maven、gradle 以及使用 SaaS 服务 Moderne cli &lt;code dir=&quot;auto&quot;&gt;mod&lt;/code&gt; 这三种方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用 maven、gradle 文件定制比较方便，dependency 管理也比较灵活，另外某些食谱只有 maven、gradle 支持，缺点则是需要修改 pom、gradle 文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 maven 命令行直接执行插件任务并指定属性，不用改 pom.xml 更方便，缺点是不方便管理 dependencies。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JakartaEE10&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.exportDatatables=true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 SaaS 服务提供的 Moderne CLI 最方便，比如 &lt;code dir=&quot;auto&quot;&gt;mod run . --recipe JakartaEE10&lt;/code&gt;, 需要注册 license。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;最佳实践和定制&quot;&gt;最佳实践和定制&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 对不同类型分别内置了一些最佳实践，可通过 &lt;code dir=&quot;auto&quot;&gt;best practices&lt;/code&gt; 关键字搜索。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;./best-practices.png&quot;&gt;&lt;img alt=&quot;best-practices&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1618&quot; height=&quot;1326&quot; src=&quot;https://www.xlabs.club/_astro/best-practices.CwTBBNMt_SMLBt.webp&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最佳实践包含了哪些规则呢，以 &lt;a href=&quot;https://docs.openrewrite.org/recipes/java/spring/boot3/springboot3bestpractices&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 3.x best practices&lt;/a&gt; 为例，在文档中能看到对应的食谱列表如下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;specs.openrewrite.org/v1beta/recipe&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot3.SpringBoot3BestPractices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;displayName&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Spring Boot 3.x best practices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Applies best practices to Spring Boot 3 applications.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;spring&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;recipeList&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot2.SpringBoot2BestPractices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.migrate.UpgradeToJava21&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;而其中每个食谱又可能包含多个，比如上面的 &lt;a href=&quot;https://docs.openrewrite.org/recipes/java/spring/boot2/springboot2bestpractices&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;org.openrewrite.java.spring.boot2.SpringBoot2BestPractices&lt;/a&gt; 又包含以下列表：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;specs.openrewrite.org/v1beta/recipe&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot2.SpringBoot2BestPractices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;displayName&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Spring Boot 2.x best practices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Applies best practices to Spring Boot 2 applications.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;spring&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;recipeList&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.NoRequestMappingAnnotation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.ImplicitWebAnnotationNames&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot2.UnnecessarySpringExtension&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.NoAutowiredOnConstructor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot2.RestTemplateBuilderRequestFactory&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;org.openrewrite.java.spring.boot2.ReplaceDeprecatedEnvironmentTestUtils&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果官方的某个食谱不和你的胃口，比如想在最佳实践中去掉或新增某个自定义食谱怎么办，可以自己定义一个 &lt;code dir=&quot;auto&quot;&gt;rewrite.yml&lt;/code&gt; 文件，类似上面的 yaml 文件，在文件中自行组合食谱。注意 &lt;code dir=&quot;auto&quot;&gt;rewrite.yml&lt;/code&gt; 文件要放到 maven 项目的根目录下，更多关于文件的说明参考 &lt;a href=&quot;https://docs.openrewrite.org/running-recipes/popular-recipe-guides/authoring-declarative-yaml-recipes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Refactoring with declarative YAML recipes&lt;/a&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;ci-集成&quot;&gt;CI 集成&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;通过 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:run&lt;/code&gt; 命令执行后，会直接修改代码。但是大部分 CI 场景下，我们可能只想做分析，不想真正改动代码。那么就可以把命令换成 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:dryRun&lt;/code&gt;，dryRun 会在控制台输出 warning 结果，并生成一个 rewrite.patch 文件。&lt;/p&gt;
&lt;p&gt;如果 dryRun 向控制台日志发出任何警告，或者生成了 rewrite.patch 文件，证明此次有需要变更的地方，则可视为 CI 失败。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;spring-boot-migrator&quot;&gt;Spring Boot Migrator&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 功能很强悍，但是仍缺失一项功能：将传统 Spring 项目一键迁移到 Spring Boot。&lt;/p&gt;
&lt;p&gt;Spring Boot Migrator 基于 OpenRewrite，是一个专门用于协助将传统的 Spring 应用迁移到基于 Spring Boot 的工具，同时也提供 Spring Boot 的升级。&lt;/p&gt;
&lt;p&gt;Spring Boot Migrator 程序运行起来后，通过 &lt;code dir=&quot;auto&quot;&gt;list&lt;/code&gt; 命令可以看到提供的 OpenRewrite recipes，其中主要关注的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;initialize-spring-boot-migration: 将项目初始化为 Spring Boot 应用。&lt;/li&gt;
&lt;li&gt;migrate-spring-xml-to-java-config：将 Spring xml 转换为 Java 注解。&lt;/li&gt;
&lt;li&gt;spring-context-xml-import：使用 Import xml 文件的形式初始化 Bean。&lt;/li&gt;
&lt;li&gt;boot-2.7-3.0-dependency-version-update：Spring Boot 2 升级到 Spring Boot 3。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;migrator:&gt; list&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Found these recipes:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1) remove-redundant-maven-compiler-plugin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Remove standard maven-compiler plugin for applications with boot parent.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2) initialize-spring-boot-migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Initialize an application as Spring Boot application.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3) migrate-jndi-lookup&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate JNDI lookup using InitialContext to Spring Boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4) migrate-jpa-to-spring-boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate JPA to Spring Boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5) migrate-ejb-jar-deployment-descriptor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Add or overrides @Stateless annotation as defined in ejb deployment descriptor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;6) migrate-weblogic-ejb-deployment-descriptor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate weblogic-ejb-jar.xml deployment descriptor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;7) mark-and-clean-remote-ejbs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Search @Stateless EJBs implementing a @Remote interface&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8) migrate-stateless-ejb&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migration of stateless EJB to Spring components.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;9) migrate-annotated-servlets&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Allow Spring Boot to deploy servlets annotated with @WebServlet&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10) migrate-jax-ws&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Jax Web-Service implementation to Spring Boot bases Web-Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;11) migrate-jax-rs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Any class has import starting with javax.ws.rs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;12) migrate-mule-to-boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Mulesoft 3.9 to Spring Boot.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;13) migrate-tx-to-spring-boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migration of @TransactionAttribute to @Transactionsl&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;14) spring-context-xml-import&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Import Spring Framework xml bean configuration into Java configuration without converting them.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;15) migrate-spring-xml-to-java-config&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Spring Framework xml bean configuration to Java configuration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;16) migrate-jms&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Convert JEE JMS app into Spring Boot JMS app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;17) documentation-actions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create Documentation for Actions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;18) migrate-jsf-2.x-to-spring-boot&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Use joinfaces to integrate JSF 2.x with Spring Boot.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;19) cn-spring-cloud-config-server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Externalize properties to Spring Cloud Config Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20) boot-2.4-2.5-upgrade-report&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create Upgrade Report for a Spring Boot 2.4 Application&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;21) boot-2.7-3.0-dependency-version-update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Bump spring-boot-starter-parent from 2.7.x to 3.0.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;22) boot-autoconfiguration-update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create org.springframework.boot.autoconfigure.AutoConfiguration.imports file for new spring 2.7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;23) boot-2.4-2.5-datasource-initializer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Replace deprecated spring.datasource.* properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;24) boot-2.4-2.5-spring-data-jpa&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Rename JpaRepository methods getId() and calls to getOne()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;25) boot-2.4-2.5-dependency-version-update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Update Spring Boot dependencies from 2.4 to 2.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;26) boot-2.7-3.0-upgrade-report&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create a report for Spring Boot Upgrade from 2.7.x to 3.0.0-M3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;27) boot-2.4-2.5-sql-init-properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Replace deprecated spring.datasource.* properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;28) sbu30-report&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create a report for Spring Boot Upgrade from 2.7.x to 3.0.x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;29) sbu30-upgrade-dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Upgrade dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30) sbu30-set-java-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Set java version property in build file&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;31) sbu30-add-milestone-repositories&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Add milestone repository for dependencies and plugins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;32) sbu30-migrate-spring-data-properties&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Migrate &apos;spring.data&apos; properties to new property names&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;33) sbu30-remove-construtor-binding&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Remove redundant @ConstructorBinding annotations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;34) sbu30-migrate-to-jakarta-packages&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Migrate javax packages to new jakarta packages&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;35) sbu30-johnzon-dependency-update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Specify version number for johnzon-core&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;36) sbu30-225-logging-date-format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Logging Date Format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;37) sbu30-auto-configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Move EnableAutoConfiguration Property from spring.factories to AutoConfiguration.imports&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;38) sbu30-upgrade-spring-cloud-dependency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Upgrade Spring Cloud Dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;39) sbu30-upgrade-boot-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Upgrade Spring Boot version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;40) sbu30-remove-image-banner&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Remove the image banner at src/main/resources&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;41) sbu30-paging-and-sorting-repository&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Add CrudRepository interface extension additionally to PagingAndSortingRepository&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;42) migrate-raml-to-spring-mvc&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create Spring Boot @RestController from .raml files.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;43) migrate-boot-2.3-2.4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate from Spring Boot 2.3 to 2.4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;44) upgrade-boot-1x-to-2x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate applications built on previous versions of Spring Boot to the latest Spring Boot 2.7 release.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;openrewrite-使用示例&quot;&gt;OpenRewrite 使用示例&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;下面使用 maven 方式演示几个例子，注意如果是 maven 多模块项目，请在父模块下执行，否则运行可能出错，参考官方说明：&lt;a href=&quot;https://docs.openrewrite.org/running-recipes/multi-module-maven&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Running Rewrite on a multi-module Maven project&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;友情提醒：初次使用会下载很多依赖 jar 包，速度可能比较慢，切换到你最快的 maven 仓库。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;升级到-java-21&quot;&gt;升级到 Java 21&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;从任意版本升级到 Java 21。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava21&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 或者手动指定版本&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:5.31.0:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:2.14.0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava21&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 会自动替换一些 Java 21 支持的新特性，如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大文本块 JEP 378: Text Blocks，自动优化格式化。&lt;/li&gt;
&lt;li&gt;模式匹配增强：支持在执行 instanceof 操作时同时定义指定类型的新变量，省却了类型转换的过程。&lt;/li&gt;
&lt;li&gt;String 类增加的新格式化方法： formatted(Object…​ args)。&lt;/li&gt;
&lt;li&gt;排序性集合：可以直接使用 getFirst() 和 getLast() 来获取第一个和最后一个集合元素了。参考： JEP 431: Sequenced Collections。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;base64-方法替换&quot;&gt;Base64 方法替换&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Base64 常用的实现有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apache Common Codec，依然可用，可以选择是否转换成 java.util.Base64。&lt;/li&gt;
&lt;li&gt;sun.misc 包下提供的实现类，从 Java 9 开始，已经不再对外提供，需要替换。&lt;/li&gt;
&lt;li&gt;从 Java 1.8 开始提供的标准库 java.util.Base64。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# UseJavaUtilBase64&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.UseJavaUtilBase64&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.exportDatatables=true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# ApacheBase64ToJavaBase64&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-apache:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.apache.commons.codec.ApacheBase64ToJavaBase64&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.exportDatatables=true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;另外更多关于 Apache 包升级、使用 Java 自带方法代替 Apache Utils 的方法，请参考 OpenRewrite 官网 &lt;code dir=&quot;auto&quot;&gt;apache&lt;/code&gt; 下的食谱。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;升级到-jakartaee&quot;&gt;升级到 JakartaEE&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;从 javax 到 jakarta，在 maven 项目父（根）模块下执行以下命令即可，注意命令可能随着版本变化，请去官网查看最新版本示例。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JakartaEE10&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drewrite.exportDatatables=true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;执行完成后可根据 git diff 查看修改效果。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;代码清理&quot;&gt;代码清理&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;先试一下 CodeCleanup 效果。&lt;/p&gt;
&lt;p&gt;pom.xml 文件中增加以下配置，然后执行 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:run&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.maven&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-maven-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.23.1&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.staticanalysis.CodeCleanup&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-static-analysis&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;1.3.1&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;我的某个项目执行效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除了一些多余的空格、空行、多括号。&lt;/li&gt;
&lt;li&gt;代码格式化，比如多个参数间缺少空格的加了空格。&lt;/li&gt;
&lt;li&gt;颠倒了一个 equals 比较： &lt;code dir=&quot;auto&quot;&gt;method.equals(&quot;CONNECT&quot;) -&gt; &quot;CONNECT&quot;.equals(method)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;长代码换行归整，三元运算格式化。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;静态分析查找重构&quot;&gt;静态分析、查找、重构&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;支持以下功能，不一一列举，请参考官方文档。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分析依赖关系。&lt;/li&gt;
&lt;li&gt;查找某个方法是否被使用。&lt;/li&gt;
&lt;li&gt;将某个方法替换为某个方法。&lt;/li&gt;
&lt;li&gt;批量修改方法名字，修改参数类型等。&lt;/li&gt;
&lt;li&gt;排序 imports ，删除未使用的 imports。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;通用代码格式化&quot;&gt;通用代码格式化&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;无用空行、空白符、前后空白符、方法参数前后空白符、Tab 转 space 等一些常用格式化方式。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;日志优化&quot;&gt;日志优化&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Parameterize logging statements：把字符串拼接自动转为参数类型。&lt;/li&gt;
&lt;li&gt;用 logger 代替 System.err、System.out、printStackTrace。&lt;/li&gt;
&lt;li&gt;把 JUL、log4j、logback 都转为 slf4j API。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-到-spring-boot&quot;&gt;Spring 到 Spring Boot&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;下面介绍下我们将 Spring 项目迁移到 Spring Boot 的过程，以及中间遇到的一些问题。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;升级项&lt;/th&gt;
&lt;th&gt;迁移前&lt;/th&gt;
&lt;th&gt;迁移后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Spring + Tomcat&lt;/td&gt;
&lt;td&gt;Spring Boot 3.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring&lt;/td&gt;
&lt;td&gt;Spring 4&lt;/td&gt;
&lt;td&gt;Spring 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junit&lt;/td&gt;
&lt;td&gt;Junit 4&lt;/td&gt;
&lt;td&gt;Junit 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;okhttp&lt;/td&gt;
&lt;td&gt;okhttp 3&lt;/td&gt;
&lt;td&gt;okhttp 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Java 8&lt;/td&gt;
&lt;td&gt;Java 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java EE&lt;/td&gt;
&lt;td&gt;Java EE&lt;/td&gt;
&lt;td&gt;Jakarta EE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;基本步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用 Spring Boot Migrator 将 Spring 项目转换成 Spring Boot，使用 Java 17 启动 spring-boot-migrator 程序并根据提示一步一步操作，如果某个步骤出错，根据 &lt;code dir=&quot;auto&quot;&gt;stacktrace&lt;/code&gt; 命令查看错误信息，修复后继续 &lt;code dir=&quot;auto&quot;&gt;apply &amp;#x3C;recipe-number&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 注意在执行 spring-boot-migrator 前，先在你要升级的项目中执行 mvn clean package install，拉取和安装依赖包，避免下面的 scan 步骤中下载依赖包失败&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 使用 Java 17 启动 spring-boot-migrator 程序&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ java -jar spring-boot-migrator.jar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Get Started:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Type&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;help&quot; - to display a list all of the available commands.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&quot;scan &amp;#x3C;dir&gt;&quot; - to scan an application&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;--------------------------------------------------------------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;migrator:&gt; scan my-spring-project-dir&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Found pom.xml.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] &apos;sbm.gitSupportEnabled&apos; is &apos;true&apos;, changes will be committed to branch [springboot] after each recipe.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Found required source dir &apos;src/main/java&apos;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Maven        100% │██████████████████████████████████│ 2/2 (0:00:00 / 0:00:00)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Applicable recipes:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1) remove-redundant-maven-compiler-plugin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Remove standard maven-compiler plugin for applications with boot parent.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2) initialize-spring-boot-migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Initialize an application as Spring Boot application.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3) spring-context-xml-import&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Import Spring Framework xml bean configuration into Java configuration without converting them.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4) migrate-spring-xml-to-java-config&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Spring Framework xml bean configuration to Java configuration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;5) cn-spring-cloud-config-server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Externalize properties to Spring Cloud Config Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;6) sbu30-set-java-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Set java version property in build file&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;7) sbu30-add-milestone-repositories&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Add milestone repository for dependencies and plugins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;8) sbu30-225-logging-date-format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Logging Date Format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Run command &apos;&gt; apply &amp;#x3C;recipe-number&gt;&apos; to apply a recipe.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ibss-site-dc:&gt; apply remove-redundant-maven-compiler-plugin&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Applying recipe &apos;remove-redundant-maven-compiler-plugin&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Clean up redundant properties for maven compiler plugin.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Clean up redundant maven compiler plugin.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;remove-redundant-maven-compiler-plugin successfully applied the following actions:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Clean up redundant properties for maven compiler plugin.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Clean up redundant maven compiler plugin.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Applicable recipes:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1) initialize-spring-boot-migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Initialize an application as Spring Boot application.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2) spring-context-xml-import&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Import Spring Framework xml bean configuration into Java configuration without converting them.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3) migrate-spring-xml-to-java-config&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Spring Framework xml bean configuration to Java configuration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4) cn-spring-cloud-config-server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Externalize properties to Spring Cloud Config Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;5) sbu30-set-java-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Set java version property in build file&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;6) sbu30-add-milestone-repositories&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Add milestone repository for dependencies and plugins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;7) sbu30-225-logging-date-format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Logging Date Format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Run command &apos;&gt; apply &amp;#x3C;recipe-number&gt;&apos; to apply a recipe.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ibss-site-dc:&gt; apply initialize-spring-boot-migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Applying recipe &apos;initialize-spring-boot-migration&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Add Spring Boot dependency management section to buildfile.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Add Spring Boot starter class.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Add initial unit test class to test Spring Boot Application Context startup.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[ok] Set packaging to &apos;jar&apos; type if different&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;initialize-spring-boot-migration successfully applied the following actions:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Add Spring Boot dependency management section to buildfile.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Add spring dependencies &apos;spring-boot-starter&apos; and &apos;spring-boot-starter-test&apos;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Delete dependencies to artifacts transitively managed by Spring Boot.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Add Spring Boot Maven plugin.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Add Spring Boot starter class.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Add initial unit test class to test Spring Boot Application Context startup.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;(x) Set packaging to &apos;jar&apos; type if different&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Applicable recipes:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1) spring-context-xml-import&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Import Spring Framework xml bean configuration into Java configuration without converting them.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2) migrate-spring-xml-to-java-config&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Migrate Spring Framework xml bean configuration to Java configuration.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3) cn-spring-cloud-config-server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Externalize properties to Spring Cloud Config Server&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4) boot-2.7-3.0-dependency-version-update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Bump spring-boot-starter-parent from 2.7.x to 3.0.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;5) boot-2.7-3.0-upgrade-report&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create a report for Spring Boot Upgrade from 2.7.x to 3.0.0-M3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;6) sbu30-report&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Create a report for Spring Boot Upgrade from 2.7.x to 3.0.x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;7) sbu30-upgrade-dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Upgrade dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;8) sbu30-set-java-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Set java version property in build file&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;9) sbu30-add-milestone-repositories&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Add milestone repository for dependencies and plugins&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;10) sbu30-remove-construtor-binding&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Remove redundant @ConstructorBinding annotations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;11) sbu30-225-logging-date-format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Logging Date Format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12) sbu30-upgrade-spring-cloud-dependency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Upgrade Spring Cloud Dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;13) sbu30-upgrade-boot-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&gt; Spring boot 3.0 Upgrade - Upgrade Spring Boot version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring Boot Migrator 目前还并不成熟，以上步骤中很多 &lt;code dir=&quot;auto&quot;&gt;recipe&lt;/code&gt; 可能出错，所以我只用 Spring Boot Migrator 做一些基础初始化，其他任务使用 rewrite-maven-plugin，交给 openrewrite 执行。在 maven pom.xml 中增加以下配置后，执行 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:run&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.maven&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-maven-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.19.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.java.testing.junit5.JUnit4to5Migration&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.okhttp.UpgradeOkHttp4&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.okhttp.ReorderRequestBodyCreateArguments&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.java.migrate.UpgradeToJava17&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-spring&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.2.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-testing-frameworks&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;2.2.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-okhttp&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;0.1.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-migrate-java&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;2.5.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Using active recipe(s) [org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7, org.openrewrite.java.testing.junit5.JUnit4to5Migration]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Using active styles(s) []&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Validating active recipes...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Project [ibss-site-dc] Resolving Poms...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Project [ibss-site-dc] Parsing source files&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] Running recipe(s)...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING] Changes have been made to pom.xml by:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]     org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]         org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]             org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                 org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                     org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                         org.openrewrite.java.testing.junit5.JUnit4to5Migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                             org.openrewrite.maven.ExcludeDependency: {groupId=junit, artifactId=junit}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]         org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=*, newVersion=2.7.x, overrideManagedVersion=false}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING] Changes have been made to src/main/java/com/fsde/ibss/dc/controller/ClientController.java by:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]     org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]         org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]             org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                 org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                     org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                         org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                             org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                 org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                     org.openrewrite.java.spring.boot2.SpringBoot2BestPractices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                         org.openrewrite.java.spring.NoRequestMappingAnnotation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                         org.openrewrite.java.spring.ImplicitWebAnnotationNames&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING] Changes have been made to src/test/java/com/fsde/ibss/dc/service/FileSaveTest.java by:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]     org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]         org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]             org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                 org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                     org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                         org.openrewrite.java.testing.junit5.JUnit4to5Migration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                             org.openrewrite.java.testing.junit5.UpdateTestAnnotation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                         org.openrewrite.java.spring.boot2.RemoveObsoleteSpringRunners&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                             org.openrewrite.java.testing.junit5.JUnit5BestPractices&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                 org.openrewrite.java.testing.cleanup.RemoveTestPrefix&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING]                                 org.openrewrite.java.testing.cleanup.TestsShouldNotBePublic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[WARNING] Please review and commit the results.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] ------------------------------------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] BUILD SUCCESS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[INFO] ------------------------------------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在上面步骤我是先升级到了 Spring Boot 2，然后再执行升级 Spring Boot 3 和 Jakarta EE。实际上直接升级到 Spring Boot 3 也是可以的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行完成后可从 maven 中删除 rewrite-maven-plugin，也可留着下次做其他变更。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;试用总结&quot;&gt;试用总结&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们项目配置都是通过远程配置中心管理的，所以升级过程中配置文件无法响应变更，解决办法是将配置从远端拷贝到本地 applicaiton.properties 中，执行完任务后根据变化再更新远程配置中心。&lt;/li&gt;
&lt;li&gt;执行过程中可能会报错，比如 maven 插件升级后某些属性可能变更或失效，需要手动解决错误后，再执行 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:run&lt;/code&gt;，&lt;/li&gt;
&lt;li&gt;可重复执行 &lt;code dir=&quot;auto&quot;&gt;mvn rewrite:run&lt;/code&gt; 命令，可随时加入新的 recipe 再执行，所以为了观察变更和保证执行成功，可分批次加入 recipe 多次执行。&lt;/li&gt;
&lt;li&gt;OpenRewrite 不解析依赖的第三方 jar 包内容，所以第三方 jar 的问题需要自行识别，比如从 Java EE 到 Jakarta EE，只是替换规则中知道的 jar 包版本和当前项目的源代码，不会自动把 jar 包内的 &lt;code dir=&quot;auto&quot;&gt;javax.&lt;/code&gt; 替换为 &lt;code dir=&quot;auto&quot;&gt;jakarta.&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><category>Java</category><category>OpenRewrite</category><category>Spring Boot Migrator</category><category>代码重构</category><category>自动化迁移</category></item><item><title>Spring Boot 3 到 4 迁移完全指南：新特性、废弃功能与实战踩坑经验</title><link>https://www.xlabs.club/blog/migration-spring-boot-3-to-4/</link><guid isPermaLink="true">https://www.xlabs.club/blog/migration-spring-boot-3-to-4/</guid><description>梳理 Spring Boot 4 关键特性、迁移步骤与常见问题，帮助团队从 3.x 平稳升级到 4.0 并完成验证。</description><pubDate>Tue, 25 Nov 2025 15:20:22 GMT</pubDate><content:encoded>&lt;p&gt;2025 年 11 月，Spring Boot 4.0 正式发布，这次升级被业界称为&lt;strong&gt;爆炸性升级&lt;/strong&gt;。同时对于云原生应用而言，这是一次&lt;strong&gt;不可错过的性能与效率跃迁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🐳 &lt;strong&gt;容器化友好&lt;/strong&gt;：模块化架构减少 30-50% 运行时占用，镜像更小、启动更快&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;启动速度提升 30-50%&lt;/strong&gt;：多线程异步启动机制，容器冷启动时间显著缩短，更适合 Kubernetes 快速扩缩容&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;2-3 倍吞吐量提升&lt;/strong&gt;：虚拟线程原生支持，I/O 密集型微服务资源利用率大幅提升，单实例可承载更高并发&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;云原生特性增强&lt;/strong&gt;：内置弹性功能（重试、限流）、HTTP 接口客户端自动配置，简化微服务间通信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这次升级也带来了 36 个废弃 API 移除、模块化架构重构、Tomcat 11 变更等挑战。本文基于 Spring Boot 官方文档和生产环境迁移经验，从原理到踩坑，提供&lt;strong&gt;完整、实用的迁移指南&lt;/strong&gt;，助你平稳完成云原生应用的升级。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;spring-boot-40-核心新特性&quot;&gt;Spring Boot 4.0 核心新特性&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;作为 Spring Boot 3.x 之后的首个大版本更新，Spring Boot 4.0 基于 Spring Framework 7.0、Jakarta EE 11 和 Java 17+（推荐 Java 21 或 25），带来了模块化架构重构、虚拟线程原生支持、HTTP 服务客户端自动配置等重大变化。&lt;/p&gt;
&lt;p&gt;Spring Boot 4.0 配套使用 &lt;strong&gt;Spring Cloud 2025.1.x&lt;/strong&gt;。虽然版本号看起来只是中号版本变更，但实际上这是一次&lt;strong&gt;大版本变更&lt;/strong&gt;，包含了大量破坏性变更和重要更新。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;模块化架构&quot;&gt;模块化架构&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4 将 &lt;code dir=&quot;auto&quot;&gt;spring-boot-autoconfigure&lt;/code&gt; 拆分为多个专注的模块，每个技术都有独立的 starter 和对应的测试 starter。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要变化：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模块命名：&lt;code dir=&quot;auto&quot;&gt;spring-boot-&amp;#x3C;technology&gt;&lt;/code&gt;，包路径：&lt;code dir=&quot;auto&quot;&gt;org.springframework.boot.&amp;#x3C;technology&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Starter 命名：&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-&amp;#x3C;technology&gt;&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-&amp;#x3C;technology&gt;-test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;之前没有 starter 的技术（如 Flyway、Liquibase）现在需要显式添加对应的 starter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;模块化的优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;减少 IDE 干扰&lt;/strong&gt;：IDE 自动完成只显示你实际使用的技术相关的类和配置属性，不再出现无关的代码提示。例如，如果你不使用 GraphQL，IDE 不会提示 GraphQL 相关的配置属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更小的运行时占用&lt;/strong&gt;：只引入实际使用的模块，减少类路径开销和启动扫描成本。Spring Boot 3.5 的 &lt;code dir=&quot;auto&quot;&gt;spring-boot-autoconfigure&lt;/code&gt; 是 2 MiB，模块化后只引入需要的模块，显著减少占用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免意外自动配置&lt;/strong&gt;：模块化后，Spring Boot 能更准确地判断你的意图。例如，如果只使用 &lt;code dir=&quot;auto&quot;&gt;WebClient&lt;/code&gt;（通过 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webclient&lt;/code&gt;），不会意外启用 Web 服务器自动配置，不再需要调用 &lt;code dir=&quot;auto&quot;&gt;SpringApplication.setWebApplicationType(WebApplicationType.NONE)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用新用例&lt;/strong&gt;：例如，现在可以独立使用 Micrometer 指标（&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-micrometer-metrics&lt;/code&gt;），而不需要完整的 Actuator 依赖链&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的维护性&lt;/strong&gt;：模块边界成为明确的契约，而不是软约定，便于团队协作和代码维护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;测试支持的模块化：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-test-autoconfigure&lt;/code&gt; 也被模块化了。测试相关的注解现在位于对应的测试模块中。例如，&lt;code dir=&quot;auto&quot;&gt;@AutoConfigureDataJdbc&lt;/code&gt; 注解现在位于 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jdbc-test&lt;/code&gt; 模块中，与 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jdbc&lt;/code&gt; 对齐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试 Starter 的传递性：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有 test starter 都会传递性地引入 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-test&lt;/code&gt;，因此不需要再单独声明 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-test&lt;/code&gt;。只需要列出被测试技术对应的 test starter 即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要废弃 Starter 和替代方案：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;废弃 Starter&lt;/th&gt;
&lt;th&gt;替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webmvc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-aop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-aspectj&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-oauth2-authorization-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-authorization-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-oauth2-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-client&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-oauth2-resource-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-resource-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-web-services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webservices&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;其他迁移要点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试依赖需要添加对应的 &lt;code dir=&quot;auto&quot;&gt;-test&lt;/code&gt; starter，如 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;之前没有 starter 的技术（如 Flyway、Liquibase）现在需要显式添加对应的 starter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;完整的 Starter 列表：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spring Boot 4.0 统一了 starter 的使用方式：大多数技术都有专门的 starter，每个 starter 都有对应的 test starter。以下是完整的列表（参考 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#starters&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;官方迁移指南&lt;/a&gt;）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Main Dependency&lt;/th&gt;
&lt;th&gt;Test Dependency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AspectJ&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-aspectj&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-aspectj-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Foundry Support&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cloudfoundry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cloudfoundry-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jakarta Validation&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-validation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-validation-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin Serialization&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-kotlin-serialization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-kotlin-serialization-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactor&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-reactor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-reactor-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Server Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jetty&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jetty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;none&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactor Netty&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-reactor-netty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;none&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-tomcat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;none&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Client Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring’s Imperative &lt;code dir=&quot;auto&quot;&gt;RestClient&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;RestTemplate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-restclient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-restclient-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring’s Reactive &lt;code dir=&quot;auto&quot;&gt;WebClient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webclient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webclient-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jersey&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jersey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jersey-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring GraphQL&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-graphql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-graphql-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring HATEOAS&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-hateoas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-hateoas-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Session Data Redis&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-session-data-redis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-session-data-redis-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Session JDBC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-session-jdbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-session-jdbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Web MVC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webmvc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webmvc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring WebFlux&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webflux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webflux-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Webservices&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webservices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-webservices-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cassandra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cassandra-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Couchbase&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-couchbase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-couchbase-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-elasticsearch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-elasticsearch-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flyway&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-flyway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-flyway-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JDBC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jdbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jdbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jOOQ&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jooq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jooq-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liquibase&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-liquibase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-liquibase-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LDAP&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-ldap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-ldap-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mongodb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mongodb-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neo4J&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-neo4j&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-neo4j-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R2DBC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-r2dbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-r2dbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Data Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data Cassandra&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-cassandra&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-cassandra-reactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-cassandra-test&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-cassandra-reactive-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data Couchbase&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-couchbase&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-couchbase-reactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-couchbase-test&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-couchbase-reactive-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data Elasticsearch&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-elasticsearch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-elasticsearch-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JDBC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jdbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jdbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JPA (using Hibernate)&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jpa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-jpa-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data LDAP&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-ldap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-ldap-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-mongodb&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-mongodb-reactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-mongodb-test&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-mongodb-reactive-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data Neo4J&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-neo4j&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-neo4j-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data R2DBC&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-r2dbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-r2dbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data Redis&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-redis&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-redis-reactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-redis-test&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-redis-reactive-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data REST&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-rest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-data-rest-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IO Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hazelcast&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-hazelcast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-hazelcast-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mail&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mail-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quartz&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-quartz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-quartz-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-sendgrid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-sendgrid-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Caching Support&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-cache-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Batch (with JDBC)&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-batch-jdbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-batch-jdbc-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Batch (without JDBC)&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-batch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-batch-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSON&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-gson&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-gson-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jackson&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jackson&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jackson-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSONB&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jsonb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jsonb-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Messaging Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ActiveMQ&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-activemq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-activemq-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artemis&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-artemis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-artemis-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JMS&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-jms-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSocket&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-rsocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-rsocket-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring AMQP&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-amqp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-amqp-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Integration&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-integration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-integration-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring for Apache Kafka&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-kafka-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring for Apache Pulsar&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-pulsar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-pulsar-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Websockets&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-websocket-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Security&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Security OAuth Authorization Server&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-authorization-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-authorization-server-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Security OAuth Client&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-client-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Security OAuth Resource Server&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-resource-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-oauth2-resource-server-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Security SAML&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-saml2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-security-saml2-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Templating Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freemarker&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-freemarker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-freemarker-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groovy Templates&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-groovy-templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-groovy-templates-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mustache&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mustache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-mustache-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thymeleaf&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-thymeleaf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-thymeleaf-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Production-Ready Starters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actuator&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-actuator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-actuator-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Micrometer Metrics&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-micrometer-metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-micrometer-metrics-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenTelemetry&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-opentelemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-opentelemetry-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zipkin&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-zipkin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-zipkin-test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; 所有 test starter 都会传递性地引入 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-test&lt;/code&gt;，因此不需要再单独声明 &lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-test&lt;/code&gt;。只需要列出被测试技术对应的 test starter 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Classic Starters（快速迁移方案）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果需要快速迁移，可以使用 Classic Starters，它们提供了类似 Spring Boot 3.x 的 classpath，让你可以快速恢复所有基础设施的可用性：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- 替代 spring-boot-starter --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-classic&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- 替代 spring-boot-starter-test --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-test-classic&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;&gt;test&amp;#x3C;/&lt;/span&gt;&lt;span&gt;scope&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;使用 Classic Starters 的好处：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速恢复应用功能，无需立即处理所有 starter 替换&lt;/li&gt;
&lt;li&gt;修复导入错误，验证应用能正常工作&lt;/li&gt;
&lt;li&gt;作为过渡方案，降低迁移风险&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;迁移建议（官方推荐的两步法）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一步&lt;/strong&gt;：使用 Classic Starters 快速迁移，修复编译错误，确保应用运行正常&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二步&lt;/strong&gt;：逐步移除 Classic Starters，根据更新后的导入语句识别缺失的 starter，添加对应的模块化 starter&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 官方建议最终还是要迁移到模块化的 starter，Classic Starters 只是过渡方案。完整的 starter 列表和对应关系请参考 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#starters&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;官方迁移指南&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;增强的配置属性元数据&quot;&gt;增强的配置属性元数据&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4 引入了新的注解 &lt;strong&gt;@ConfigurationPropertiesSource&lt;/strong&gt;，允许 Spring Boot 读取外部模块中定义的 &lt;code dir=&quot;auto&quot;&gt;@ConfigurationProperties&lt;/code&gt; 类型，这在之前是不可能的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ConfigurationPropertiesSource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;com.example.config&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ConfigurationProperties&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;prefix&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;app&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AppProperties&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; String name;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// getters and setters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更清晰的模块化设计&lt;/li&gt;
&lt;li&gt;改进工具支持（IDE 自动完成、配置提示等）&lt;/li&gt;
&lt;li&gt;支持跨模块的配置属性定义&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;任务调度增强&quot;&gt;任务调度增强&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4 现在支持配置多个 &lt;code dir=&quot;auto&quot;&gt;TaskDecorator&lt;/code&gt; Bean，允许你组合多个装饰器来增强任务执行行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用示例：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TaskConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; TaskDecorator &lt;/span&gt;&lt;span&gt;loggingTaskDecorator&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; runnable &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System.out.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Task started: &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; Thread.&lt;/span&gt;&lt;span&gt;currentThread&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;getName&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;runnable.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System.out.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Task completed&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; TaskDecorator &lt;/span&gt;&lt;span&gt;mdcTaskDecorator&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; runnable &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String traceId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; MDC.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;traceId&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MDC.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;traceId&quot;&lt;/span&gt;&lt;span&gt;, traceId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;runnable.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;MDC.&lt;/span&gt;&lt;span&gt;clear&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;多个 &lt;code dir=&quot;auto&quot;&gt;TaskDecorator&lt;/code&gt; 会按顺序应用，提供更灵活的任务执行增强能力。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;包结构变化&quot;&gt;包结构变化&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;模块化架构对包结构也有影响。每个模块现在都有独立的 &lt;code dir=&quot;auto&quot;&gt;org.springframework.boot.&amp;#x3C;module&gt;&lt;/code&gt; 包路径。根据模块的职责范围，可能包含 API、自动配置、Actuator 相关支持等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 3.x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 4.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;主要变化：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个技术模块都有独立的包路径：&lt;code dir=&quot;auto&quot;&gt;org.springframework.boot.&amp;#x3C;technology&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;测试模块包路径：&lt;code dir=&quot;auto&quot;&gt;org.springframework.boot.&amp;#x3C;technology&gt;.test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自动配置类、Actuator 支持等都位于对应的模块包下&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果你有自定义 starter，大概率都需要适配新的包路径&lt;/strong&gt;，尤其是使用 class 字符名字，而不是 class 类的。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;spring-framework-70-带来的增强&quot;&gt;Spring Framework 7.0 带来的增强&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4 基于 Spring Framework 7.0 构建，继承了以下特性。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;api-版本控制&quot;&gt;API 版本控制&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 原生支持 API 版本管理，这是&lt;strong&gt;首次在 Spring 框架中提供官方支持&lt;/strong&gt;。之前虽然可以通过 &lt;code dir=&quot;auto&quot;&gt;@RequestMapping&lt;/code&gt; 实现，但需要大量手动工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配置方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 配置：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WebConfiguration&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WebMvcConfigurer&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;configureApiVersioning&lt;/span&gt;&lt;span&gt;(ApiVersionConfigurer &lt;/span&gt;&lt;span&gt;configurer&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;configurer.&lt;/span&gt;&lt;span&gt;useRequestHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;API-Version&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 或使用其他策略：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// configurer.useQueryParameter(&quot;version&quot;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// configurer.usePathPattern(&quot;/api/{version}&quot;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// configurer.useMediaType(&quot;application/vnd.api.v1+json&quot;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot 配置属性：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;mvc&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;apiversion&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;header&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;API-Version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# 或使用：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# query-parameter: version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# path-pattern: /api/{version}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;使用示例：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RestController&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RequestMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserController&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;/users&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; List&amp;#x3C;&lt;/span&gt;&lt;span&gt;UserV1&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;getUsersV1&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; userService.&lt;/span&gt;&lt;span&gt;getUsersV1&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;/users&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;2&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; List&amp;#x3C;&lt;/span&gt;&lt;span&gt;UserV2&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;getUsersV2&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; userService.&lt;/span&gt;&lt;span&gt;getUsersV2&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;功能式端点支持：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;RouterFunction&amp;#x3C;&lt;/span&gt;&lt;span&gt;ServerResponse&lt;/span&gt;&lt;span&gt;&gt; route &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; RouterFunctions.&lt;/span&gt;&lt;span&gt;route&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/hello-world&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;1.2&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; ServerResponse.&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Hello World&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;支持的版本控制策略：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路径版本控制&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;/v1/users&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;/v2/users&lt;/code&gt;（需要在路径中声明 URI 变量）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求头版本控制&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;X-API-Version: 1&lt;/code&gt; 或自定义 header 名称&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询参数版本控制&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;/users?version=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;媒体类型版本控制&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;Accept: application/vnd.api.v1+json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;版本格式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;默认使用语义化版本（Semantic Versioning），支持 major.minor.patch 格式。如果未指定 minor 和 patch，默认为 0。版本解析器可以自定义，支持日期格式或其他格式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;废弃处理：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;API 版本控制支持 RFC 9745 标准的废弃处理，可以在响应中发送废弃提示。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;jspecify-空安全注解&quot;&gt;JSpecify 空安全注解&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 迁移到 JSpecify 注解，这是 Spring 生态系统中&lt;strong&gt;空安全支持的重大里程碑&lt;/strong&gt;。JSpecify 是一个由 OpenJDK、Broadcom、Google、JetBrains、Sonar 等组织共同参与的标准项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已支持 JSpecify 的 Spring 项目：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot 4.0&lt;/li&gt;
&lt;li&gt;Spring Framework 7.0&lt;/li&gt;
&lt;li&gt;Spring Data 4.0&lt;/li&gt;
&lt;li&gt;Spring Security 7.0&lt;/li&gt;
&lt;li&gt;Spring Batch 6.0&lt;/li&gt;
&lt;li&gt;Spring Kafka 4.0&lt;/li&gt;
&lt;li&gt;Spring Integration 7.0&lt;/li&gt;
&lt;li&gt;Spring GraphQL 2.0&lt;/li&gt;
&lt;li&gt;Spring Web Services 5.0&lt;/li&gt;
&lt;li&gt;Spring AMQP 4.0&lt;/li&gt;
&lt;li&gt;Spring Shell 4.0&lt;/li&gt;
&lt;li&gt;Spring Plugin 4.0&lt;/li&gt;
&lt;li&gt;Spring HATEOAS 3.0&lt;/li&gt;
&lt;li&gt;Spring Modulith 2.0&lt;/li&gt;
&lt;li&gt;Spring Vault 4.0&lt;/li&gt;
&lt;li&gt;Spring Cloud Commons 5.0&lt;/li&gt;
&lt;li&gt;Spring Cloud Gateway 5.0&lt;/li&gt;
&lt;li&gt;Micrometer 1.16&lt;/li&gt;
&lt;li&gt;Micrometer Tracing 1.6&lt;/li&gt;
&lt;li&gt;Context Propagation 1.2&lt;/li&gt;
&lt;li&gt;Reactor 2025.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用示例：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.jspecify.annotations.Nullable;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.jspecify.annotations.NonNull;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserService&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; @NonNull User &lt;/span&gt;&lt;span&gt;getUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;NonNull&lt;/span&gt;&lt;span&gt; String &lt;/span&gt;&lt;span&gt;userId&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; userRepository.&lt;/span&gt;&lt;span&gt;findById&lt;/span&gt;&lt;span&gt;(userId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orElseThrow&lt;/span&gt;&lt;span&gt;(() &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserNotFoundException&lt;/span&gt;&lt;span&gt;(userId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; @Nullable User &lt;/span&gt;&lt;span&gt;findUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;NonNull&lt;/span&gt;&lt;span&gt; String &lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; userRepository.&lt;/span&gt;&lt;span&gt;findByEmail&lt;/span&gt;&lt;span&gt;(email).&lt;/span&gt;&lt;span&gt;orElse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;IDE 支持：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IntelliJ IDEA 2025.3+&lt;/strong&gt;：提供完整的 JSpecify 支持，包括复杂的数据流分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kotlin 2.2&lt;/strong&gt;：自动将 JSpecify 注解转换为 Kotlin 空安全类型，告别平台类型（platform types）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NullAway&lt;/strong&gt;：构建时检查工具，需要 JDK 21+（JDK 17 支持可能在后续版本提供）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Kotlin 项目注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JSpecify 注解会明确标记可空性，Kotlin 编译器会自动识别并转换。如果 Java 接口使用 &lt;code dir=&quot;auto&quot;&gt;@Nullable&lt;/code&gt; 注解，Kotlin 实现必须返回可空类型；如果使用 &lt;code dir=&quot;auto&quot;&gt;@NonNull&lt;/code&gt;，则必须返回非空类型。IntelliJ IDEA 会提供相应的编译错误提示和修复建议。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;多线程异步启动&quot;&gt;多线程异步启动&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7.0 引入了多线程异步启动机制，可以并行初始化 ApplicationContext，显著加快应用启动速度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ApplicationContext 启动时，可以并行初始化多个 Bean&lt;/li&gt;
&lt;li&gt;利用多核 CPU 资源，将串行的 Bean 初始化改为并行执行&lt;/li&gt;
&lt;li&gt;自动处理 Bean 之间的依赖关系，确保初始化顺序正确&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;配置方式：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootApplication&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Application&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SpringApplication app &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SpringApplication&lt;/span&gt;&lt;span&gt;(Application.class);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 启用异步启动&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;setApplicationStartup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BufferingApplicationStartup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(args);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;或者通过配置属性：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;application&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;startup&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;性能提升：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大型应用启动时间可减少 30-50%&lt;/li&gt;
&lt;li&gt;特别适合 Bean 数量多、初始化耗时的应用&lt;/li&gt;
&lt;li&gt;充分利用多核 CPU，提升资源利用率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要确保 Bean 初始化是线程安全的&lt;/li&gt;
&lt;li&gt;某些单例 Bean 可能仍需要串行初始化&lt;/li&gt;
&lt;li&gt;建议在测试环境先验证，确保无并发问题&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;http-接口客户端&quot;&gt;HTTP 接口客户端&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 和 Spring Boot 4 大幅简化了 HTTP 接口客户端的配置。之前需要手动创建 &lt;code dir=&quot;auto&quot;&gt;HttpServiceProxyFactory&lt;/code&gt; 和配置每个客户端，现在可以通过声明式方式自动注册。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建议：&lt;/strong&gt; 虽然 &lt;code dir=&quot;auto&quot;&gt;RestTemplate&lt;/code&gt; 仍可使用，但推荐使用 &lt;code dir=&quot;auto&quot;&gt;RestClient&lt;/code&gt; 或 HTTP 接口客户端（&lt;code dir=&quot;auto&quot;&gt;@HttpExchange&lt;/code&gt;）作为替代方案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;旧方式（Spring Framework 6 / Spring Boot 3）：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;HttpExchange&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;/api/users&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserClient&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetExchange&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/{id}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;User &lt;/span&gt;&lt;span&gt;getUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;PathVariable&lt;/span&gt;&lt;span&gt; String &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostExchange&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;User &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; UserRequest &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 需要手动配置每个客户端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpClientConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; UserClient &lt;/span&gt;&lt;span&gt;userClient&lt;/span&gt;&lt;span&gt;(RestClient.Builder &lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;RestClient restClient &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; builder.&lt;/span&gt;&lt;span&gt;baseUrl&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://api.example.com&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;HttpServiceProxyFactory factory &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; HttpServiceProxyFactory&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;builderFor&lt;/span&gt;&lt;span&gt;(RestClientAdapter.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(restClient)).&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; factory.&lt;/span&gt;&lt;span&gt;createClient&lt;/span&gt;&lt;span&gt;(UserClient.class);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 如果有多个客户端，需要重复配置。..&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;新方式（Spring Framework 7 / Spring Boot 4）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code dir=&quot;auto&quot;&gt;@ImportHttpServices&lt;/code&gt; 注解：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;HttpExchange&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;/api/users&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserClient&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetExchange&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/{id}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;User &lt;/span&gt;&lt;span&gt;getUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;PathVariable&lt;/span&gt;&lt;span&gt; String &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostExchange&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;User &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; UserRequest &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ImportHttpServices&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;api&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;types&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {UserClient.class})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpClientConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;配置 &lt;code dir=&quot;auto&quot;&gt;application.yml&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;service&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;read-timeout&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2s&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;base-url&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;https://api.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;或使用包扫描自动发现：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ImportHttpServices&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;api&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;basePackages&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;com.example.client&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpClientConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;多组配置示例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果应用需要集成多个 REST API，可以为每个 API 配置不同的组：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ImportHttpServices&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;basePackages&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;com.example.client.github&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ImportHttpServices&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;stackoverflow&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;basePackages&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;com.example.client.stackoverflow&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpClientConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;对应的配置：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;service&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;github&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;base-url&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;https://api.github.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;read-timeout&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5s&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;stackoverflow&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;base-url&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;https://api.stackexchange.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;read-timeout&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10s&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;使用 WebClient（响应式）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;默认使用 &lt;code dir=&quot;auto&quot;&gt;RestClient&lt;/code&gt;，如需使用 &lt;code dir=&quot;auto&quot;&gt;WebClient&lt;/code&gt;，可通过配置切换：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ImportHttpServices&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;group&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;api&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;basePackages&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;com.example.client&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;clientType&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ClientType.WEB_CLIENT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpClientConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置简化&lt;/strong&gt;：不再需要为每个客户端手动创建 &lt;code dir=&quot;auto&quot;&gt;HttpServiceProxyFactory&lt;/code&gt; 和 Bean&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动注册&lt;/strong&gt;：所有 HTTP 接口自动注册为 Spring Bean，可直接注入使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一管理&lt;/strong&gt;：通过组（group）概念统一管理同一 API 的所有客户端&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活配置&lt;/strong&gt;：支持为不同组配置不同的超时、重试等策略&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样 &lt;code dir=&quot;auto&quot;&gt;com.example.client&lt;/code&gt; 包下所有带 &lt;code dir=&quot;auto&quot;&gt;@HttpExchange&lt;/code&gt; 的接口都会自动注册为 Spring Bean，可以直接注入使用：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserService&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; UserClient userClient;  &lt;/span&gt;&lt;span&gt;// 直接注入，无需手动配置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserService&lt;/span&gt;&lt;span&gt;(UserClient &lt;/span&gt;&lt;span&gt;userClient&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.userClient &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; userClient;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; User &lt;/span&gt;&lt;span&gt;getUser&lt;/span&gt;&lt;span&gt;(String &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; userClient.&lt;/span&gt;&lt;span&gt;getUser&lt;/span&gt;&lt;span&gt;(id);  &lt;/span&gt;&lt;span&gt;// 直接使用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;内置弹性功能resilience&quot;&gt;内置弹性功能（Resilience）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 引入了强大的弹性工具，直接集成到核心框架中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;@Retryable&lt;/strong&gt;：自动重试失败的方法调用，支持配置最大尝试次数、延迟、抖动和退避策略，也支持响应式返回类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;@ConcurrencyLimit&lt;/strong&gt;：限制并发方法调用数量，保护服务和资源（例如限制为单线程访问）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;配置方式：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableResilientMethods&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ApplicationConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaymentService&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Retryable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;maxAttempts&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;backoff&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; @&lt;/span&gt;&lt;span&gt;Retryable.Backoff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;delay&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;multiplier&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2.0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ConcurrencyLimit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)  &lt;/span&gt;&lt;span&gt;// 只允许 2 个并发调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;processPayment&lt;/span&gt;&lt;span&gt;(String &lt;/span&gt;&lt;span&gt;paymentId&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 处理支付逻辑&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无需引入第三方库（如 Resilience4j）&lt;/li&gt;
&lt;li&gt;与 Spring 生态深度集成&lt;/li&gt;
&lt;li&gt;支持响应式编程模型&lt;/li&gt;
&lt;li&gt;配置简单，开箱即用&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;流式-jms-客户端-apijmsclient&quot;&gt;流式 JMS 客户端 API（JmsClient）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 引入了 &lt;strong&gt;JmsClient&lt;/strong&gt;，类似于 &lt;code dir=&quot;auto&quot;&gt;JdbcClient&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;RestClient&lt;/code&gt;，提供流畅的构建器风格 API。这是对传统 JMS 模板的更优雅和可读的替代方案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用示例：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MessageService&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; JmsClient jmsClient;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MessageService&lt;/span&gt;&lt;span&gt;(JmsClient &lt;/span&gt;&lt;span&gt;jmsClient&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.jmsClient &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; jmsClient;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sendMessage&lt;/span&gt;&lt;span&gt;(String &lt;/span&gt;&lt;span&gt;destination&lt;/span&gt;&lt;span&gt;, String &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jmsClient.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;span&gt;(destination)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;(message)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; String &lt;/span&gt;&lt;span&gt;receiveMessage&lt;/span&gt;&lt;span&gt;(String &lt;/span&gt;&lt;span&gt;destination&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; jmsClient.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;queue&lt;/span&gt;&lt;span&gt;(destination)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;receive&lt;/span&gt;&lt;span&gt;(String.class);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流畅的 API 设计，代码更易读&lt;/li&gt;
&lt;li&gt;与 &lt;code dir=&quot;auto&quot;&gt;JdbcClient&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;RestClient&lt;/code&gt; 保持一致的风格&lt;/li&gt;
&lt;li&gt;Spring Boot 4 提供自动配置支持&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;统一消息转换unified-message-conversion&quot;&gt;统一消息转换（Unified Message Conversion）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 简化了消息转换，引入了新的 &lt;code dir=&quot;auto&quot;&gt;HttpMessageConverters&lt;/code&gt; 配置类。这种统一方法借鉴了响应式编解码器的设计，简化了 HTTP 消息的序列化和反序列化。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;jakarta-ee-11-升级&quot;&gt;Jakarta EE 11 升级&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 采用 Jakarta EE 11 规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Servlet 6.1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JPA 3.2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bean Validation 3.1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket 2.2&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;JPA 3.2 重要变化：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之前，&lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt; 只能通过 &lt;code dir=&quot;auto&quot;&gt;@PersistenceContext&lt;/code&gt; 注解注入。现在，&lt;code dir=&quot;auto&quot;&gt;EntityManagerFactory&lt;/code&gt; 和共享的 &lt;code dir=&quot;auto&quot;&gt;EntityManager&lt;/code&gt; 都可以使用 &lt;code dir=&quot;auto&quot;&gt;@Inject&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;@Autowired&lt;/code&gt; 注入，并支持使用限定符来选择特定的持久化单元（当配置了多个持久化单元时）。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 旧方式（仍然支持）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PersistenceContext&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; EntityManager entityManager;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 新方式（Spring Framework 7 / JPA 3.2）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; EntityManager entityManager;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 多个持久化单元时使用限定符&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Qualifier&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;primaryEntityManager&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; EntityManager primaryEntityManager;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里要提醒一下：&lt;code dir=&quot;auto&quot;&gt;javax.*&lt;/code&gt; 到 &lt;code dir=&quot;auto&quot;&gt;jakarta.*&lt;/code&gt; 的迁移应该在 Spring Boot 3.0 的时候就已经完成了。如果你还在用 Spring Boot 2.x，那得先升级到 3.x，然后再考虑 4.0。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;junit-4-和-jackson-2x-弃用&quot;&gt;JUnit 4 和 Jackson 2.x 弃用&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JUnit 4&lt;/strong&gt;：官方推荐迁移到 JUnit 5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jackson 2.x&lt;/strong&gt;：Spring Boot 4 使用 Jackson 3.x&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Jackson 3.x 的破坏性变更：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部分注解被移除&lt;/li&gt;
&lt;li&gt;更严格的类型处理&lt;/li&gt;
&lt;li&gt;模块整合&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;ObjectMapper&lt;/code&gt; 行为变更&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;迁移示例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Jackson 2.x&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;JsonInclude&lt;/span&gt;&lt;span&gt;(JsonInclude.Include.NON_NULL)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; String name;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Jackson 3.x （同样的语法，但内部行为可能不同）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;JsonInclude&lt;/span&gt;&lt;span&gt;(JsonInclude.Include.NON_NULL)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; String name;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;测试上下文暂停机制&quot;&gt;测试上下文暂停机制&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 引入了一个挺有意思的测试上下文管理机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试上下文在不使用的时候会自动暂停（stopped）&lt;/li&gt;
&lt;li&gt;下次用的时候会自动重启&lt;/li&gt;
&lt;li&gt;这样能显著降低测试套件的内存占用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该机制可显著降低测试套件的内存占用。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;spel-增强&quot;&gt;SpEL 增强&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;支持 &lt;code dir=&quot;auto&quot;&gt;Optional&lt;/code&gt; 类型和 Elvis 操作符：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#{userService.findUser(&apos;123&apos;)?.name ?: &apos;Unknown&apos;}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; String userName;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;程序化-bean-注册beanregistrar&quot;&gt;程序化 Bean 注册（BeanRegistrar）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Framework 7 引入了 &lt;code dir=&quot;auto&quot;&gt;BeanRegistrar&lt;/code&gt; 接口，允许在 &lt;code dir=&quot;auto&quot;&gt;@ConditionalOn...&lt;/code&gt; 注解族不够用时进行动态 Bean 注册。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 实现：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.springframework.context.bean.BeanRegistrar;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; org.springframework.context.bean.BeanRegistry;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;QuoteProviderRegistrar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BeanRegistrar&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(BeanRegistry &lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt;, Environment &lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;registry.&lt;/span&gt;&lt;span&gt;registerBean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;quoteProviderDb&quot;&lt;/span&gt;&lt;span&gt;, QuoteProviderDb.class);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;registry.&lt;/span&gt;&lt;span&gt;registerBean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;quoteProviderFallback&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;QuoteProviderFallback.class,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;spec &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;spec.&lt;/span&gt;&lt;span&gt;fallback&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;spec.&lt;/span&gt;&lt;span&gt;order&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Kotlin DSL 实现（更优雅）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Kotlin 2.2 提供了 &lt;code dir=&quot;auto&quot;&gt;BeanRegistrarDsl&lt;/code&gt;，让 Bean 注册更加简洁：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.context.bean.BeanRegistrarDsl&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;QuoteProviderRegistrar&lt;/span&gt;&lt;span&gt; : &lt;/span&gt;&lt;span&gt;BeanRegistrarDsl&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;registerBean&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;QuoteProviderDb&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;span&gt;&quot;quoteProviderDb&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;registerBean&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;QuoteProviderFallback&lt;/span&gt;&lt;span&gt;&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;quoteProviderFallback&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fallback &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;order &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据环境变量或配置动态注册 Bean&lt;/li&gt;
&lt;li&gt;实现复杂的条件逻辑，超出 &lt;code dir=&quot;auto&quot;&gt;@ConditionalOn...&lt;/code&gt; 注解的能力范围&lt;/li&gt;
&lt;li&gt;需要根据运行时信息决定注册哪些 Bean&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;tomcat-11-核心变更&quot;&gt;Tomcat 11 核心变更&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Tomcat 11 是 Apache Tomcat 项目第九个主要版本，标志着从传统 Java EE 向现代 Jakarta EE 框架的重要转变。本次升级支持 Jakarta EE 11，实现了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Servlet 6.1&lt;/strong&gt; - 改进异步请求处理，提升长运行任务的性能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSP 4.0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EL 6.0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket 2.2&lt;/strong&gt; - 性能和可扩展性显著提升，特别适合实时双向通信场景（如聊天应用、协作工具）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication 3.1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;虚拟线程支持java-21&quot;&gt;虚拟线程支持（Java 21+）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重要特性：&lt;/strong&gt; Tomcat 11 原生支持 Java 21 的 Project Loom 虚拟线程，这是本次升级的&lt;strong&gt;核心亮点之一&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能数据（基于实际测试）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据 Fast Thread 和 Java Code Geeks 的性能基准测试：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;平台线程&lt;/th&gt;
&lt;th&gt;虚拟线程&lt;/th&gt;
&lt;th&gt;性能提升&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;I/O 密集型应用&lt;/strong&gt;（数据库查询、HTTP 调用）&lt;/td&gt;
&lt;td&gt;基准&lt;/td&gt;
&lt;td&gt;2-3x 吞吐量&lt;/td&gt;
&lt;td&gt;+100-200%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;高并发 Web 请求&lt;/strong&gt;（1000+ 并发）&lt;/td&gt;
&lt;td&gt;基准&lt;/td&gt;
&lt;td&gt;2-3x 吞吐量&lt;/td&gt;
&lt;td&gt;+100-200%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU 密集型应用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基准&lt;/td&gt;
&lt;td&gt;无明显提升&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内存占用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基准&lt;/td&gt;
&lt;td&gt;-30-50%（线程栈）&lt;/td&gt;
&lt;td&gt;-30-50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;响应时间（P99）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基准&lt;/td&gt;
&lt;td&gt;-20-40%&lt;/td&gt;
&lt;td&gt;-20-40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;适用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;推荐使用&lt;/strong&gt;：REST API、数据库查询、HTTP 客户端调用、文件 I/O、消息队列消费&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;不推荐使用&lt;/strong&gt;：CPU 密集型计算、图像处理、加密解密、复杂算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实际案例：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个典型的 Spring Boot Web 应用（处理 REST 请求，调用数据库和外部 API）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用平台线程&lt;/strong&gt;：1000 并发请求，吞吐量约 500 req/s，P99 延迟 200ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用虚拟线程&lt;/strong&gt;：1000 并发请求，吞吐量约 1200-1500 req/s，P99 延迟 120ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能提升&lt;/strong&gt;：吞吐量提升 2-3 倍，延迟降低 40%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轻量级并发模型，简化线程管理&lt;/li&gt;
&lt;li&gt;相比传统线程模型，提供更好的可扩展性&lt;/li&gt;
&lt;li&gt;特别适合 I/O 密集型应用，可以处理大量并发连接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;配置方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Spring Boot 4 中，虚拟线程支持通过配置属性启用：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;threads&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;virtual&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;适用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web 请求处理&lt;/li&gt;
&lt;li&gt;WebSocket 连接管理&lt;/li&gt;
&lt;li&gt;高并发 I/O 操作&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;websocket-22-性能提升&quot;&gt;WebSocket 2.2 性能提升&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重要变化：&lt;/strong&gt; Tomcat 11 升级到 Jakarta WebSocket 2.2，带来显著的性能和可扩展性提升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要改进：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更高效的实时双向通信处理&lt;/li&gt;
&lt;li&gt;改进的流式数据传输能力&lt;/li&gt;
&lt;li&gt;更好的并发连接管理&lt;/li&gt;
&lt;li&gt;特别适合需要实时更新的应用（如聊天、协作工具、实时数据流）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;与 HTTP/2 和异步处理的协同：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tomcat 11 的 WebSocket 改进与 HTTP/2 支持和异步处理能力协同工作，确保更响应式和可扩展的 Web 应用。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;servlet-61-异步处理改进&quot;&gt;Servlet 6.1 异步处理改进&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重要变化：&lt;/strong&gt; Jakarta Servlet 6.1 改进了 Web 应用处理 HTTP 请求的方式，特别是异步请求处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要改进：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更高效的异步请求处理机制&lt;/li&gt;
&lt;li&gt;提升长运行任务的性能&lt;/li&gt;
&lt;li&gt;更好的资源利用率&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;安全增强&quot;&gt;安全增强&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重要变化：&lt;/strong&gt; Tomcat 11 引入了多项安全增强。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要改进：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更好的 TLS/SSL 默认配置&lt;/strong&gt; - 使管理员更容易建立安全的 HTTPS 连接，开箱即用&lt;/li&gt;
&lt;li&gt;改进的安全实践和默认设置&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;最低-java-版本要求&quot;&gt;最低 Java 版本要求&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Tomcat 11 要求至少 &lt;strong&gt;Java SE 17&lt;/strong&gt;，这个和 Spring Boot 4 的要求一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;影响：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保 Tomcat 能够利用最新的 Java 语言特性和性能改进&lt;/li&gt;
&lt;li&gt;包括增强的内存管理、Records、更高效的 switch 表达式等&lt;/li&gt;
&lt;li&gt;需要从旧版本 Java 升级的应用需要特别注意&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;openssl-支持java-22&quot;&gt;OpenSSL 支持（Java 22+）&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;如果你用的是 Java 22+，Tomcat 11 支持通过 FFM（Foreign Function &amp;#x26; Memory API）使用 OpenSSL，性能会更好。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;javax-到-jakarta-命名空间迁移&quot;&gt;javax. &lt;em&gt;到 jakarta.&lt;/em&gt; 命名空间迁移&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重要提醒：&lt;/strong&gt; 迁移到 Tomcat 11 的一个挑战性方面是需要重构应用以适应从 &lt;code dir=&quot;auto&quot;&gt;javax.*&lt;/code&gt; 到 &lt;code dir=&quot;auto&quot;&gt;jakarta.*&lt;/code&gt; 命名空间的切换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;影响：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这是从早期 Tomcat 版本迁移的基础性变更&lt;/li&gt;
&lt;li&gt;特别是使用 Java EE 的应用需要特别注意&lt;/li&gt;
&lt;li&gt;Tomcat 11 提供了 &lt;a href=&quot;https://github.com/apache/tomcat-jakartaee-migration/blob/main/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;迁移工具&lt;/a&gt; 来支持这一转换&lt;/li&gt;
&lt;li&gt;需要仔细测试以确保应用在新命名空间下继续正常工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 如果你还在使用 Spring Boot 2.x，需要先升级到 Spring Boot 3.x（已完成 &lt;code dir=&quot;auto&quot;&gt;javax.*&lt;/code&gt; 到 &lt;code dir=&quot;auto&quot;&gt;jakarta.*&lt;/code&gt; 迁移），然后再考虑升级到 Spring Boot 4.0。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;http-header-大小写处理变化&quot;&gt;HTTP Header 大小写处理变化&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Tomcat 11 使用 &lt;strong&gt;header name 的原始大小写&lt;/strong&gt;来存储 HTTP/1.1 request headers，而不是强制将其转换为小写。&lt;strong&gt;以前 Tomcat 会自动转小写，现在不会了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要影响：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;取 Header 时一定要关注大小写&lt;/li&gt;
&lt;li&gt;建议按照 RFC 7230 建议，全链路都统一使用小写&lt;/li&gt;
&lt;li&gt;如果代码中直接比较 header 名称字符串，可能因为大小写不匹配而失败&lt;/li&gt;
&lt;li&gt;Spring Framework 7.0 的 &lt;code dir=&quot;auto&quot;&gt;HttpHeaders&lt;/code&gt; API 也有相应变化&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;废弃功能与破坏性变更&quot;&gt;废弃功能与破坏性变更&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;移除的废弃-api&quot;&gt;移除的废弃 API&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4.0 这次移除了 36 个废弃类，大概占了所有废弃 API 的 88%。迁移的时候这些地方需要特别注意：&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;mockbean-和-spybean-移除&quot;&gt;MockBean 和 SpyBean 移除&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;影响：&lt;/strong&gt; 测试代码中广泛使用。旧写法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 3.x （已废弃，4.0 移除）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootTest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserServiceTest&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;MockBean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; UserRepository userRepository;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;新写法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 4.0 （使用 Spring 官方支持）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootTest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserServiceTest&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;MockitoBean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; UserRepository userRepository;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;mockitotestexecutionlistener-移除&quot;&gt;MockitoTestExecutionListener 移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4.0 移除了该监听器，使用 Spring 的原生 Mockito 支持。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;websecurityconfigureradapter-移除&quot;&gt;WebSecurityConfigurerAdapter 移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;该改动在 Spring Boot 3.x 时已废弃，4.0 正式移除。&lt;/p&gt;
&lt;p&gt;旧写法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 2.x/3.x （已废弃）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableWebSecurity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WebSecurityConfigurerAdapter&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;(HttpSecurity &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; Exception {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;http.&lt;/span&gt;&lt;span&gt;authorizeRequests&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;antMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/public/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;anyRequest&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;需要改成新的方式：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Spring Boot 4.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableWebSecurity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; SecurityFilterChain &lt;/span&gt;&lt;span&gt;filterChain&lt;/span&gt;&lt;span&gt;(HttpSecurity &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; Exception {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authorizeHttpRequests&lt;/span&gt;&lt;span&gt;(auth &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;span&gt; auth&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/public/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;anyRequest&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;该改动在 Spring Boot 3.x 时已废弃，4.0 正式移除。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;undertow-支持移除&quot;&gt;Undertow 支持移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Undertow 目前不兼容 Servlet 6.1，Spring Boot 4.0 移除了对它的支持：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- Spring Boot 4.0 不再支持 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-undertow&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;替代方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切换到 Tomcat 11（推荐）&lt;/li&gt;
&lt;li&gt;使用 Jetty 12.1+&lt;/li&gt;
&lt;li&gt;等待 Undertow 支持 Servlet 6.1&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-batch-默认行为变更&quot;&gt;Spring Batch 默认行为变更&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Batch 现在默认在内存中存储元数据，不再使用数据库。这可能导致 Job 执行历史丢失：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- 如果需要持久化元数据，显式添加 JDBC 支持 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-batch-jdbc&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;reactive-pulsar-支持移除&quot;&gt;Reactive Pulsar 支持移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Pulsar 移除了 Reactor 支持，所以 Spring Boot 4.0 也相应移除了响应式 Pulsar 客户端的自动配置。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;embedded-executable-uber-jar-launch-scripts-移除&quot;&gt;Embedded Executable Uber Jar Launch Scripts 移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 4.0 移除了用于创建”完全可执行” jar 文件的嵌入式启动脚本支持。该功能仅适用于类 Unix 系统，且与 &lt;a href=&quot;https://docs.spring.io/spring-boot/4.0-SNAPSHOT/reference/packaging/efficient.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;高效部署建议&lt;/a&gt; 冲突。&lt;/p&gt;
&lt;p&gt;如果需要类似功能，可以使用 Gradle 的 application plugin 等替代方案。你仍然可以使用 Spring Boot 的构建插件创建 uber jar，并通过 &lt;code dir=&quot;auto&quot;&gt;java -jar&lt;/code&gt; 运行。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-session-hazelcastmongodb-移除&quot;&gt;Spring Session Hazelcast/MongoDB 移除&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Session Hazelcast 和 Spring Session MongoDB 现在分别由 Hazelcast 团队和 MongoDB 团队维护，Spring Boot 4.0 移除了对它们的直接支持。如需使用，请直接使用对应的 starter。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;常见问题与解决方案&quot;&gt;常见问题与解决方案&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;http-header-大小写问题&quot;&gt;HTTP Header 大小写问题&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：Header 访问失败或行为不一致&lt;/p&gt;
&lt;p&gt;升级到 Tomcat 11.0.12+ 和 Spring Framework 7.0 后，某些 header 访问可能出现问题。这是 Spring Boot 4 迁移中&lt;strong&gt;必须关注的坑&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tomcat 11.0.12+&lt;/strong&gt;：HTTP 请求头名称会保留原始大小写，而不是强制转换为小写（&lt;strong&gt;以前 Tomcat 会自动转小写，现在不会了&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;Spring Framework 7.0：&lt;code dir=&quot;auto&quot;&gt;HttpHeaders&lt;/code&gt; API 不再继承 &lt;code dir=&quot;auto&quot;&gt;MultiValueMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Header 名称大小写不敏感，但某些代码可能依赖特定的大小写格式或直接比较 header 名称&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;建议：&lt;/strong&gt; 按照 RFC 7230 建议，全链路都统一使用小写 header 名称。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新 &lt;code dir=&quot;auto&quot;&gt;HttpHeaders&lt;/code&gt; 的使用方式：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 旧代码（Spring Framework 6.x）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HttpHeaders headers &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpHeaders&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;String contentType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; headers.&lt;/span&gt;&lt;span&gt;getFirst&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 新代码（Spring Framework 7.0）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HttpHeaders headers &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HttpHeaders&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;String contentType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; headers.&lt;/span&gt;&lt;span&gt;getFirst&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 仍然可用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 或使用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; hasContentType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; headers.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 大小写不敏感&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;统一使用标准格式的 header 名称：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 推荐：使用标准格式&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;Bearer token&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;X-Custom-Header&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;更新 header 名称比较的代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 问题代码：直接字符串比较可能失败&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Enumeration&amp;#x3C;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&gt; headerNames &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; request.&lt;/span&gt;&lt;span&gt;getHeaderNames&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; (headerNames.&lt;/span&gt;&lt;span&gt;hasMoreElements&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; headerNames.&lt;/span&gt;&lt;span&gt;nextElement&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (name.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span&gt;)) { &lt;/span&gt;&lt;span&gt;// 可能失败，name 可能是 &quot;Content-Type&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 推荐：使用大小写不敏感比较&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (name.&lt;/span&gt;&lt;span&gt;equalsIgnoreCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span&gt;)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;更新测试代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// MockHttpServletRequest 的行为已更新&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MockHttpServletRequest request &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MockHttpServletRequest&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;request.&lt;/span&gt;&lt;span&gt;addHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 以下都能正常工作（大小写不敏感）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;assertThat&lt;/span&gt;&lt;span&gt;(request.&lt;/span&gt;&lt;span&gt;getHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;isNotNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;assertThat&lt;/span&gt;&lt;span&gt;(request.&lt;/span&gt;&lt;span&gt;getHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;isNotNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;assertThat&lt;/span&gt;&lt;span&gt;(request.&lt;/span&gt;&lt;span&gt;getHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;CONTENT-TYPE&quot;&lt;/span&gt;&lt;span&gt;)).&lt;/span&gt;&lt;span&gt;isNotNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 但遍历时需要注意原始大小写&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Enumeration&amp;#x3C;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&gt; names &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; request.&lt;/span&gt;&lt;span&gt;getHeaderNames&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; (names.&lt;/span&gt;&lt;span&gt;hasMoreElements&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String name &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; names.&lt;/span&gt;&lt;span&gt;nextElement&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// name 可能是 &quot;Content-Type&quot; 而不是 &quot;content-type&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;assertThat&lt;/span&gt;&lt;span&gt;(name).&lt;/span&gt;&lt;span&gt;isEqualTo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;// 使用原始大小写&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;编译时-nullability-错误&quot;&gt;编译时 Nullability 错误&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：JSpecify 注解导致 Kotlin 或空值检查工具报错&lt;/p&gt;
&lt;p&gt;Kotlin 项目可能遇到以下错误：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;error: [nullness] incompatible types in return.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;required: @NonNull String&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;found:    @Nullable String&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Kotlin 中处理可空类型&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; user: &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;? &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; userService.&lt;/span&gt;&lt;span&gt;findUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;123&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt; name: &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; user?.name ?: &lt;/span&gt;&lt;span&gt;&quot;Unknown&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;或者临时禁用严格空值检查：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.apache.maven.plugins&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;maven-compiler-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;compilerArgs&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;&gt;-Xlint:-nullness&amp;#x3C;/&lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;compilerArgs&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;jackson-3x-序列化问题&quot;&gt;Jackson 3.x 序列化问题&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：JSON 序列化/反序列化行为变更&lt;/p&gt;
&lt;p&gt;Jackson 3.x 的行为和 2.x 有些不一样，可能会导致一些序列化问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JacksonConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; ObjectMapper &lt;/span&gt;&lt;span&gt;objectMapper&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ObjectMapper mapper &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ObjectMapper&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 恢复 Jackson 2.x 的兼容性行为&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mapper.&lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mapper.&lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;mapper.&lt;/span&gt;&lt;span&gt;registerModule&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JavaTimeModule&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; mapper;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;第三方库不兼容&quot;&gt;第三方库不兼容&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：某些库依赖旧版本的 Spring 或 Jakarta API&lt;/p&gt;
&lt;p&gt;部分第三方库可能尚未适配 Spring Boot 4.0。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查库的最新版本&lt;/li&gt;
&lt;li&gt;使用依赖排除和强制版本：&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.example&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;legacy-library&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;1.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;exclusions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;exclusion&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;*&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;exclusion&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;exclusions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;考虑替换为兼容的库&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-batch-元数据丢失&quot;&gt;Spring Batch 元数据丢失&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：升级后找不到 Job 执行历史&lt;/p&gt;
&lt;p&gt;升级后 Job 执行历史丢失，原因是 Spring Batch 默认行为变更。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-batch-jdbc&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;batch&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;jdbc&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;initialize-schema&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;undertow-无法使用&quot;&gt;Undertow 无法使用&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;spring-boot-starter-undertow&lt;/code&gt; 不再可用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：切换到 Tomcat：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-web&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 默认使用 Tomcat，无需额外配置 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果需要 Jetty：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-web&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;exclusions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;exclusion&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-tomcat&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;exclusion&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;exclusions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;spring-boot-starter-jetty&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;虚拟线程性能未达预期&quot;&gt;虚拟线程性能未达预期&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：启用虚拟线程后性能无明显提升&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可能原因：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用是 CPU 密集型的（虚拟线程对这种情况没用）&lt;/li&gt;
&lt;li&gt;数据库连接池配置不当&lt;/li&gt;
&lt;li&gt;用了同步的阻塞代码（比如 &lt;code dir=&quot;auto&quot;&gt;synchronized&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 优化数据库连接池&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;datasource&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;hikari&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;maximum-pool-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# 虚拟线程环境可适当增加&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;minimum-idle&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 确保启用虚拟线程&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;threads&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;virtual&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;使用 JFR (Java Flight Recorder) 分析：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-XX:StartFlightRecording=filename=recording.jfr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;app.jar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;测试上下文缓存问题&quot;&gt;测试上下文缓存问题&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：测试上下文自动暂停导致测试变慢&lt;/p&gt;
&lt;p&gt;Spring Framework 7 的测试上下文暂停机制可能导致测试变慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;调整测试上下文缓存配置：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootTest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TestPropertySource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;spring.test.context.cache.maxSize=10&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyTest&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;升级顺序与验收门禁&quot;&gt;升级顺序与验收门禁&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;不要把“依赖能解析”当成迁移完成。更稳妥的顺序是：先在 Spring Boot 3.x 升到当前维护线并清理废弃 API，再切换 Boot 4；这样能把问题拆成两批，回滚也更简单。具体版本以 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;官方迁移指南&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4 发布说明&lt;/a&gt; 为准。&lt;/p&gt;
&lt;p&gt;建议把下面几项设为合并门禁：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;依赖收敛&lt;/strong&gt;：检查 &lt;code dir=&quot;auto&quot;&gt;spring-*&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;jakarta.*&lt;/code&gt;、Servlet、Jackson、数据库驱动是否被第三方库拉入旧版本；Maven 使用 &lt;code dir=&quot;auto&quot;&gt;dependency:tree&lt;/code&gt;，Gradle 使用 &lt;code dir=&quot;auto&quot;&gt;dependencies&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;dependencyInsight&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先 dry run 再改代码&lt;/strong&gt;：OpenRewrite 只负责机械变更，运行后必须人工检查安全配置、序列化配置和自定义自动配置，不能把自动修复当成测试替代品。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试分层执行&lt;/strong&gt;：先编译和单元测试，再跑 &lt;code dir=&quot;auto&quot;&gt;@SpringBootTest&lt;/code&gt;、MockMvc/WebTestClient、数据库迁移、消息消费和真实容器集成测试；重点覆盖鉴权、异常响应、Header、日期/数字序列化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上线前可回滚&lt;/strong&gt;：保留 Boot 3 构建产物和数据库回滚方案，灰度期间同时观察启动失败率、5xx、P95 延迟、线程/连接池和 GC；不要在同一个发布窗口同时升级 JDK、数据库和中间件。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;判断标准&lt;/strong&gt;：构建通过只是起点；依赖树无冲突、关键链路测试通过、指标没有回归，并且能切回旧产物，才算迁移完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;自动化迁移工具&quot;&gt;自动化迁移工具&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;openrewrite&quot;&gt;OpenRewrite&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 是开源的自动化代码重构工具，支持 Spring Boot 迁移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maven 集成：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.maven&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-maven-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.40.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;recipe&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;activeRecipes&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.openrewrite.recipe&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rewrite-spring&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.22.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Gradle 集成：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;plugins {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;id &lt;/span&gt;&lt;span&gt;&apos;org.openrewrite.rewrite&apos;&lt;/span&gt;&lt;span&gt; version &lt;/span&gt;&lt;span&gt;&apos;6.25.0&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;rewrite {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;activeRecipe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;dependencies {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rewrite &lt;/span&gt;&lt;span&gt;&apos;org.openrewrite.recipe:rewrite-spring:5.22.0&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;运行迁移：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Maven: 预览变更&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rewrite:dryRun&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Maven: 应用变更&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rewrite:run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Gradle: 预览变更&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./gradlew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rewriteDryRun&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Gradle: 应用变更&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./gradlew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rewriteRun&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;主要功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动升级 Spring Boot 版本号&lt;/li&gt;
&lt;li&gt;替换废弃的 API（如 &lt;code dir=&quot;auto&quot;&gt;@MockBean&lt;/code&gt; → &lt;code dir=&quot;auto&quot;&gt;@MockitoBean&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;更新 import 语句&lt;/li&gt;
&lt;li&gt;修复依赖冲突&lt;/li&gt;
&lt;li&gt;调整配置文件格式&lt;/li&gt;
&lt;li&gt;重构安全配置（移除 &lt;code dir=&quot;auto&quot;&gt;WebSecurityConfigurerAdapter&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h4 id=&quot;示例迁移安全配置&quot;&gt;示例：迁移安全配置&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 会自动将：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WebSecurityConfigurerAdapter&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;(HttpSecurity &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; Exception {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;http.&lt;/span&gt;&lt;span&gt;authorizeRequests&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;anyRequest&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;重构为：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; SecurityFilterChain &lt;/span&gt;&lt;span&gt;filterChain&lt;/span&gt;&lt;span&gt;(HttpSecurity &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; Exception {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; http.&lt;/span&gt;&lt;span&gt;authorizeHttpRequests&lt;/span&gt;&lt;span&gt;(auth &lt;/span&gt;&lt;span&gt;-&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;auth.&lt;/span&gt;&lt;span&gt;anyRequest&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-boot-migrator&quot;&gt;Spring Boot Migrator&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects-experimental/spring-boot-migrator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot Migrator&lt;/a&gt; 是 Spring 官方的实验性工具，基于 OpenRewrite，专门用于将传统 Spring 应用迁移到 Spring Boot，同时提供 Spring Boot 版本升级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将传统 Spring 项目初始化为 Spring Boot 应用&lt;/li&gt;
&lt;li&gt;将 Spring XML 配置转换为 Java 配置&lt;/li&gt;
&lt;li&gt;Spring Boot 版本升级&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 安装&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clone&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://github.com/spring-projects-experimental/spring-boot-migrator.git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;spring-boot-migrator&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./mvnw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 扫描项目&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;applications/cli/target/spring-boot-migrator.jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scan&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 应用迁移&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;applications/cli/target/spring-boot-migrator.jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;migrate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/project&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;windup-red-hat&quot;&gt;Windup (Red Hat)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/windup/windup&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Windup&lt;/a&gt; 是 Red Hat 提供的 Java 应用现代化和迁移工具，支持大规模 Java 应用迁移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码分析和迁移评估&lt;/li&gt;
&lt;li&gt;支持多种迁移场景（Java 版本升级、框架迁移、云迁移等）&lt;/li&gt;
&lt;li&gt;生成迁移报告和评估工作量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;适用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大型企业级应用迁移&lt;/li&gt;
&lt;li&gt;多项目批量迁移&lt;/li&gt;
&lt;li&gt;云原生应用迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;intellij-idea-迁移助手&quot;&gt;IntelliJ IDEA 迁移助手&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;IntelliJ IDEA 2025.3+ 内置了 Spring Boot 4 迁移支持：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开项目&lt;/li&gt;
&lt;li&gt;右键点击 &lt;code dir=&quot;auto&quot;&gt;pom.xml&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;选择 Refactor → Migrate to Spring Boot 4.0&lt;/li&gt;
&lt;li&gt;按照向导完成迁移&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;eclipse-transformer&quot;&gt;Eclipse Transformer&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;从 Spring Boot 2.x 迁移时，可使用 Eclipse Transformer 批量转换 &lt;code dir=&quot;auto&quot;&gt;javax.*&lt;/code&gt; 到 &lt;code dir=&quot;auto&quot;&gt;jakarta.*&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;transformer.jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/project&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/path/to/output&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;官方文档&quot;&gt;官方文档&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog/2025/11/20/spring-boot-4-0-0-available-now/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4.0.0 Official Release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4.0 Release Notes&lt;/a&gt; - GitHub Wiki&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4.0 Migration Guide&lt;/a&gt; - GitHub Wiki，&lt;strong&gt;必读&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-7.0-Release-Notes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Framework 7.0 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog/2025/11/13/spring-framework-7-0-general-availability/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Framework 7.0 General Availability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;技术文章&quot;&gt;技术文章&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog/2025/10/28/modularizing-spring-boot/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Modularizing Spring Boot&lt;/a&gt; - Spring 官方对模块化改造的详细说明&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://loiane.com/2025/08/spring-boot-4-spring-framework-7-key-features/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4 and Spring Framework 7: Key Features and Changes&lt;/a&gt; - Loiane Groner 的详细解读&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-boot-4-spring-framework-7&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4 &amp;#x26; Spring Framework 7 – What’s New | Baeldung&lt;/a&gt; - Baeldung 权威教程&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.jetbrains.com/idea/2025/11/spring-boot-4/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4: Leaner, Safer Apps and a New Kotlin Baseline&lt;/a&gt; - JetBrains 官方对 Spring Boot 4 的分析&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/news/2025/11/spring-7-spring-boot-4/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Framework 7 and Spring Boot 4 Deliver API Versioning, Resilience, and Null-Safe Annotations&lt;/a&gt; - InfoQ 深度报道&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://foojay.io/today/preparing-for-spring-framework-7-and-spring-boot-4/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Preparing for Spring Framework 7 and Spring Boot 4&lt;/a&gt; - Foojay.io 对 Spring Framework 7 和 Spring Boot 4 新特性的全面介绍&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;迁移经验&quot;&gt;迁移经验&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.moderne.ai/blog/spring-boot-4x-migration-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4 Migration Guide: Faster, Safer, at Scale&lt;/a&gt; - Moderne.ai 的企业级迁移指南&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@kanishks772/the-spring-boot-4-migration-hack-every-developer-needs-to-know-in-2025-7c8ae3529cff&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Spring Boot 4 Migration Hack Every Developer Needs to Know in 2025&lt;/a&gt; - 实战技巧分享&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@javedmj786/migration-to-spring-boot-4-0-a-performance-enhancements-guide-aee2a90c303e&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Migration to Spring Boot 4.0: A Performance Enhancements Guide&lt;/a&gt; - 性能优化经验&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@vikrantdheer/migrating-to-spring-boot-3-7-subtle-pitfalls-and-how-to-fix-them-1ac457e6b098&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Migrating to Spring Boot 3+: 7 Subtle Pitfalls and How to Fix Them&lt;/a&gt; - 常见问题解决&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.xlabs.club/blog/migrating-java8-to-java25/&quot;&gt;Java 25 完整升级指南：从 Java 8 到 Java 25&lt;/a&gt; - Java 版本升级完整指南，包含新特性、性能优化、迁移实战和工具推荐&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;虚拟线程&quot;&gt;虚拟线程&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.fastthread.io/virtual-threads-performance-in-spring-boot/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Virtual Threads Performance in Spring Boot&lt;/a&gt; - Fast Thread 的性能基准测试&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.javacodegeeks.com/2025/04/spring-boot-performance-with-java-virtual-threads.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot Performance with Java Virtual Threads&lt;/a&gt; - Java Code Geeks 的性能分析&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-6-virtual-threads&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Working with Virtual Threads in Spring | Baeldung&lt;/a&gt; - Baeldung 虚拟线程教程&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@oleksandr.dendeberia/spring-boot-4-and-virtual-threads-a-practical-high-impact-upgrade-for-modern-java-development-10631f4f427b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot 4 and Virtual Threads: A Practical, High-Impact Upgrade&lt;/a&gt; - 虚拟线程实战&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;迁移工具&quot;&gt;迁移工具&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.openrewrite.org/recipes/java/spring/boot4/upgradespringboot_4_0-community-edition&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenRewrite: Migrate to Spring Boot 4.0&lt;/a&gt; - OpenRewrite 官方文档&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects-experimental/spring-boot-migrator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spring Boot Migrator&lt;/a&gt; - Spring 官方迁移工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://windup.github.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Windup&lt;/a&gt; - Red Hat 应用现代化工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/adoptium/emt4j&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;EMT4J&lt;/a&gt; - Eclipse Java 版本迁移工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.moderne.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Moderne.ai&lt;/a&gt; - 企业级 OpenRewrite 托管服务&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>Spring Boot</category><category>Spring Boot 4</category><category>Spring Framework 7</category><category>Jakarta EE</category><category>Tomcat 11</category><category>Virtual Threads</category><category>迁移指南</category><category>OpenRewrite</category></item><item><title>Awesome Alternatives Bitnami：寻找 Bitnami Charts 的替代方案</title><link>https://www.xlabs.club/blog/awesome-alternatives-bitnami/</link><guid isPermaLink="true">https://www.xlabs.club/blog/awesome-alternatives-bitnami/</guid><description>Bitnami 停止更新后，汇总常用的替代 Helm Charts 和容器镜像方案，帮助团队平滑迁移。</description><pubDate>Mon, 03 Nov 2025 14:03:42 GMT</pubDate><content:encoded>&lt;p&gt;Bitnami 曾是最受欢迎的 Helm Charts 和容器镜像提供商之一，但其维护策略调整后，很多 Charts 已停止更新或更新严重滞后。本文整理了常用中间件的替代 Helm Charts 和镜像来源，供团队迁移参考。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么要从-bitnami-迁移&quot;&gt;为什么要从 Bitnami 迁移&lt;/h2&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新滞后&lt;/strong&gt;：部分 Charts 长期未更新，Kubernetes 新版 API 不兼容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;镜像安全&lt;/strong&gt;：老旧镜像可能存在已知 CVE 漏洞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社区活跃度&lt;/strong&gt;：官方维护的资源更有保障。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;替代方案汇总&quot;&gt;替代方案汇总&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;数据库&quot;&gt;数据库&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;中间件&lt;/th&gt;
&lt;th&gt;Bitnami Chart&lt;/th&gt;
&lt;th&gt;推荐替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/postgresql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/CrunchyData/postgres-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CrunchyData Postgres Operator&lt;/a&gt; 或 &lt;a href=&quot;https://cloudnative-pg.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CloudNativePG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/mysql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/mysql/mysql-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mysql-operator&lt;/a&gt; 或官方 Docker 镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/redis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/ot-container-kit/redis-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;redis-operator&lt;/a&gt; 或直接使用官方 Helm Chart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/mongodb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/mongodb/mongodb-kubernetes-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MongoDB Community Operator&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClickHouse&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/clickhouse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/Altinity/clickhouse-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;clickhouse-operator&lt;/a&gt;（Altinity 维护）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;消息队列&quot;&gt;消息队列&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;中间件&lt;/th&gt;
&lt;th&gt;Bitnami Chart&lt;/th&gt;
&lt;th&gt;推荐替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://strimzi.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Strimzi&lt;/a&gt; Operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/rabbitmq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/rabbitmq/cluster-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RabbitMQ Cluster Operator&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NATS&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;官方 &lt;a href=&quot;https://github.com/nats-io/k8s&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;nats-io/k8s&lt;/a&gt; Helm Chart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;基础组件&quot;&gt;基础组件&lt;/h3&gt;&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;中间件&lt;/th&gt;
&lt;th&gt;Bitnami Chart&lt;/th&gt;
&lt;th&gt;推荐替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;官方 Docker 镜像 + 自编 Helm Chart，或用 &lt;a href=&quot;https://github.com/kubernetes/ingress-nginx&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ingress-nginx&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cert-manager&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;官方 &lt;a href=&quot;https://cert-manager.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;jetstack/cert-manager&lt;/a&gt; Helm Chart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/keycloak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;官方 &lt;a href=&quot;https://github.com/keycloak/keycloak&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;keycloak/keycloak&lt;/a&gt; Helm Chart（Quarkus 版）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harbor&lt;/td&gt;
&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;bitnami/harbor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;官方 &lt;a href=&quot;https://github.com/goharbor/harbor-helm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;goharbor/harbor-helm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;容器镜像替代来源&quot;&gt;容器镜像替代来源&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;如果仅使用 Bitnami 的容器镜像（而非完整 Chart），可考虑的替代来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker Official Images&lt;/strong&gt;：&lt;code dir=&quot;auto&quot;&gt;docker.io/library/*&lt;/code&gt;，最权威可靠的基础镜像。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chainguard Images&lt;/strong&gt;：专注于安全的最小化、无发行版镜像，CVE 极低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;linuxserver.io&lt;/strong&gt;：维护了大量流行应用的容器镜像，社区活跃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;官方项目镜像&lt;/strong&gt;：如 &lt;code dir=&quot;auto&quot;&gt;docker.io/envoyproxy/envoy&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;quay.io/prometheus/prometheus&lt;/code&gt; 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;迁移建议&quot;&gt;迁移建议&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;优先级排序&quot;&gt;优先级排序&lt;/h3&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安全优先&lt;/strong&gt;：先迁移存在已知 CVE 且无法更新的组件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖关系&lt;/strong&gt;：从底层依赖（数据库、消息队列）开始，再到上层应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度迁移&lt;/strong&gt;：在新环境（如新 namespace）部署替代方案，DNS/Service 切换后再清理旧资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;迁移检查清单&quot;&gt;迁移检查清单&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 确认替代方案的功能覆盖度（监控指标、备份恢复、持久化存储等）&lt;/li&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 导出并验证数据备份&lt;/li&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 在测试环境完整验证一次迁移流程&lt;/li&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 准备回滚方案（保留原有 PV 和配置，必要时可快速切换回去）&lt;/li&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 更新监控和告警规则（指标名称可能有变化）&lt;/li&gt;
&lt;li&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 更新 CI/CD 中的 Helm 仓库引用&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;自编-helm-chart-模板&quot;&gt;自编 Helm Chart 模板&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;对于简单的无状态服务，自编一个简洁的 Helm Chart 替代 Bitnami Chart 往往更可控：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;templates/deployment.yaml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apiVersion&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;apps/v1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;kind&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Deployment&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Release.Name&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;replicas&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Values.replicaCount&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;selector&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;matchLabels&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Release.Name&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;metadata&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;labels&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Release.Name&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;spec&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;containers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Release.Name&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;{{ .Values.image.repository }}:{{ .Values.image.tag }}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;containerPort&lt;/span&gt;&lt;span&gt;: {{ &lt;/span&gt;&lt;span&gt;.Values.service.port&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{{- &lt;/span&gt;&lt;span&gt;with .Values.resources&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;resources&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{{- &lt;/span&gt;&lt;span&gt;toYaml . | nindent 12&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{{- &lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;span&gt; }}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Bitnami Charts 的替代并非一刀切——对于仍正常维护的 Chart 可继续使用。重点关注已停止更新或存在安全漏洞的组件，优先迁移到官方 Operator 或活跃社区维护的方案。&lt;/p&gt;</content:encoded><category>K8S</category><category>Helm</category><category>Bitnami</category><category>容器化</category></item><item><title>Javax 和 Jakarta 过渡期兼容方案</title><link>https://www.xlabs.club/blog/javax-jakarta-compatibility/</link><guid isPermaLink="true">https://www.xlabs.club/blog/javax-jakarta-compatibility/</guid><description>详细介绍从 Javax 到 Jakarta EE 的迁移兼容方案，包括背景、工具选择和实施策略，帮助企业平滑完成迁移</description><pubDate>Sun, 14 Sep 2025 06:33:23 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;什么是-jakarta-ee为什么要切换&quot;&gt;什么是 Jakarta EE，为什么要切换&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Jakarta EE 就是 Java EE 的新名词。这里的 EE 全称是 Enterprise Edition，它是专门为企业级 Java 应用定义的一套规范，与 Java SE (Java Platform, Standard Edition) 相对应。&lt;/p&gt;
&lt;p&gt;Java EE 是从 Java 1.2 版本开始推出的 Java 企业级开发平台，最初的名称是 J2EE (Java 2 Platform, Enterprise Edition)。随着 Java 的发展，它的名称于 Java 1.5 版本时更改为 Java EE (Java Platform, Enterprise Edition)。2009 年 Oracle 收购了 Sun，Java EE 开始由 Oracle 通过 JCP (Java Community Process) 开发和维护。&lt;/p&gt;
&lt;p&gt;直到 2017 年，Oracle 将 Java EE 提交给了 Eclipse 基金会，并命名为 Eclipse Enterprise for Java。然而，&lt;strong&gt;由于”Java”这个名字的商标归 Oracle 所有，Eclipse 基金会无法继续使用 &lt;code dir=&quot;auto&quot;&gt;javax.*&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;java.*&lt;/code&gt;，因此，项目名称改为 Jakarta EE&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Jakarta EE 包含了许多技术规范和 API，涵盖了 Web 应用、数据库访问、消息传递、事务处理、安全性等方面的功能，其中包括但不限于下列规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Servlet&lt;/strong&gt;：前身是 J2EE Servlet，定义了如何管理 HTTP 请求的规范。这应该是大部分 Java Web 开发者最熟悉的，同时也是许多其它规范的基础。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Server Page (JSP)&lt;/strong&gt;：服务端动态生成网页的技术，可以看作 Java 版本的 PHP 和 ASP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Websocket&lt;/strong&gt;：定义了一套 WebSocket 连接相关的 API，用于实现全双工通信。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta RESTful Web Services&lt;/strong&gt;：开发符合 REST 原则的 Web 服务的一套规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta JSON Binding&lt;/strong&gt;：Java 类和 JSON 字符串互相转换的规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta XML Binding&lt;/strong&gt;：Java 类和 XML 的映射规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Enterprise Beans (EJB)&lt;/strong&gt;：这个规范比较复杂，包括 EJB 容器，RMI（远程过程调用），并发控制，依赖注入等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Persistent (JPA)&lt;/strong&gt;：ORM 规范，定义了 Java 类和数据库表直接的映射规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Transactions (JTA)&lt;/strong&gt;：包含了事务相关的接口和注解类，也用于管理分布式事务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Jakarta Messaging (JMS)&lt;/strong&gt;：消息系统的规范，用于实现异步消息传递，比如 Apache 的 ActiveMQ 就实现了这套规范。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;对研发的影响&quot;&gt;对研发的影响&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;依赖包坐标和版本号变更&quot;&gt;依赖包坐标和版本号变更&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;比如常见的 servlet-api：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- 老版本 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;javax.servlet&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;javax.servlet-api&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;3.1.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;!-- 新版本 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;jakarta.servlet&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;jakarta.servlet-api&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;6.1.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;package-namespace-变更&quot;&gt;package namespace 变更&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 老版本&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; javax.servlet.Filter;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 新版本&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; jakarta.servlet.Filter;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;兼容性问题&quot;&gt;兼容性问题&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;如果不兼容会发生什么，比如：&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;servlet-运行期错误&quot;&gt;Servlet 运行期错误&lt;/h4&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;javax.servlet.ServletRequest can not cast to jakarta.servlet.ServletRequest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;数据库扫描失败映射错误&quot;&gt;数据库扫描失败映射错误&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;比如下面定义是 &lt;code dir=&quot;auto&quot;&gt;javax.persistence.Table&lt;/code&gt;，开源或者自定义的数据库中间件件一般习惯按注解 &lt;code dir=&quot;auto&quot;&gt;jakarta.persistence.Table&lt;/code&gt; 进行扫描：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; javax.persistence.Id;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; javax.persistence.Table;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Table&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;mt_field_extra&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FieldExtra&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String id;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scanTableName&lt;/span&gt;&lt;span&gt;(Class&lt;/span&gt;&lt;span&gt;&amp;#x3C;?&gt;&lt;/span&gt;&lt;span&gt; clazz, PersistMeta meta, &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; mapCamelCaseToUnderscore) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jakarta.persistence.Table table &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; clazz.&lt;/span&gt;&lt;span&gt;getAnnotation&lt;/span&gt;&lt;span&gt;(jakarta.persistence.Table.class);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String tableName;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; table) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tableName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; table.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;tableName &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nameConvert&lt;/span&gt;&lt;span&gt;(clazz.&lt;/span&gt;&lt;span&gt;getSimpleName&lt;/span&gt;&lt;span&gt;(), mapCamelCaseToUnderscore);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果项目能直接升级到最新版本当然更好，但是作为中间件团队提供的组件，往往需要在过渡期间提供一种兼容两种模式的方案，既能让原有老服务正常使用，又兼容 Spring Boot 3+ Jakarta EE 9+新服务。&lt;/p&gt;
&lt;p&gt;Jakarta EE 兼容是一个复杂的课题，当前只记录 &lt;code dir=&quot;auto&quot;&gt;package namespace&lt;/code&gt; 差异解决方法。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;maven-本身特性说明&quot;&gt;Maven 本身特性说明&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;父子-pom-依赖继承&quot;&gt;父子 POM 依赖继承&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;假设父 POM 使用 dependencyManagement 定义如下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencyManagement&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;1.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;2.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;3.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;jakarta&amp;#x3C;/&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;3.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;jakarta-java21&amp;#x3C;/&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;3.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;jakarta-java23&amp;#x3C;/&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencyManagement&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Maven 官方文档说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; In two of these dependency references, we had to specify the &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;type/&gt;&lt;/code&gt; element. This is because the minimal set of information for matching a dependency reference against a dependencyManagement section is actually &lt;strong&gt;{groupId, artifactId, type, classifier}&lt;/strong&gt;. In many cases, these dependencies will refer to jar artifacts with no classifier. This allows us to shorthand the identity set to &lt;strong&gt;{groupId, artifactId}&lt;/strong&gt;, since the default for the type field is jar, and the default classifier is null.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;基于以上说明，假设子模块，依次按以下方式填写 dependency，实际生效的是哪个：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 方式 1 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 方式 2 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 方式 3 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;jakarta&amp;#x3C;/&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 方式 4，同时引入 2 个 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;com.tester&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;rocketmq-support&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;jakarta-java23&amp;#x3C;/&lt;/span&gt;&lt;span&gt;classifier&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;profile-继承和传递&quot;&gt;Profile 继承和传递&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;假设有个第三方组件，比如 rocketmq-support，他定义了一个 profile 叫 jakarta，独立加了 dependencies，deploy 到中央仓库。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;jakarta&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;jakarta.servlet&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;jakarta.servlet-api&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;5.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;有另外一个组件 B 依赖了 rocketmq-support，并且也定义了一个 jakarta profile 激活。那么这个组件 B 会自动依赖 jakarta.servlet-api 吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不会&lt;/strong&gt;。profile 为自身编译期服务，deploy 后失效。&lt;/p&gt;
&lt;p&gt;如果 deploy 后还能依赖传递的话，那某些人定义写一些恶意代码和插件然后定义一堆通用的 profile，比如大家喜欢叫 dev，然后 deploy 就能变相投毒了。&lt;/p&gt;
&lt;p&gt;如果父 pom 定义了 profile，子模块直接引用，是会继承生效的。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;主流的辅助工具&quot;&gt;主流的辅助工具&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;tomcat-migration-tool&quot;&gt;Tomcat Migration Tool&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apache/tomcat-jakartaee-migration&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Tomcat Migration Tool&lt;/a&gt; 是 Apache Tomcat 提供的迁移工具。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;jakartaee-migration-&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;-shaded.jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;source&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;destination&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The source should be a path to a compressed archive, a folder or an individual file. The destination will be created at the specified path as a resource of the same type as the source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用途：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节码转换&lt;/li&gt;
&lt;li&gt;已有项目，甚至无源码项目，直接转换。只负责转换制品，要想给别人用需自己手动 deploy&lt;/li&gt;
&lt;li&gt;一个 javax war 转换后放到 Tomcat 10，理论上可运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决不了本地开发 Debug 期间编译、运行的问题，如果不兼容本地启动、Test 失败&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外 Tomcat 10 提供了一种自动转换的方法，参考文档：&lt;a href=&quot;https://tomcat.apache.org/migration-10.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://tomcat.apache.org/migration-10.html&lt;/a&gt; 将 war 放到 webapps-javaee 目录下，就会自动转换后放到 webapp 下运行。然而这种方案最大的问题在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;必须是基于外置 Tomcat，不支持 Spring Boot Jar 形式运行。&lt;/li&gt;
&lt;li&gt;转换需要时间，这是时间基本上都是 1~10 分钟之间，对应用重启、快速扩缩容都有很大的副作用。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;maven-shade-plugin&quot;&gt;maven-shade-plugin&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;用途：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节码转换&lt;/li&gt;
&lt;li&gt;需要配置详细的转换规则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决不了本地开发 Debug 期间编译、运行的问题，本地启动、Test 失败&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;maven-assembly-plugin&quot;&gt;maven-assembly-plugin&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;上面我们都是假设只有 javax 和 jakarta namespace 差异，只需要转换字节码。但是，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有某个 Java 方法不兼容需要重写一个版本怎么办&lt;/li&gt;
&lt;li&gt;配置文件比如 xml 有差异怎么办&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;assembly-plugin 就可以帮助我们通过 include、exclude 不同的源码、resource、依赖项，重组编译发布不同的包。&lt;/p&gt;
&lt;p&gt;这是适合复杂不兼容的代码，但是对于多模块项目，需要某个模块开发指定 assembly xml，会非常痛苦。&lt;/p&gt;
&lt;p&gt;目前其他几种工具只处理 javax 和 jakarta package 差异，后续肯定会遇到除了 package 不同，接口或使用方式也不兼容的情况，等遇到了再单独处理。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;openrewrite&quot;&gt;OpenRewrite&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;OpenRewrite 迁移菜谱 &lt;a href=&quot;https://docs.openrewrite.org/recipes/java/migrate/jakarta/javaxmigrationtojakarta&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Migrate to Jakarta EE 9&lt;/a&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 命令行运行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-U&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.openrewrite.maven:rewrite-maven-plugin:run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-Drewrite.exportDatatables=false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;用途：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码级别转换，直接自动改代码&lt;/li&gt;
&lt;li&gt;除了代码，还能自动改写 pom 依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多模块目前适配还不完善，对于多模块，需要每个模块处理一次&lt;/li&gt;
&lt;li&gt;复杂代码可能转换失败，失败后可能导致编译失败，无法一键编译，需要人工介入&lt;/li&gt;
&lt;li&gt;改 pom 时，他会自己强制指定版本号，实际上我们希望用父 pom 管理版本号&lt;/li&gt;
&lt;li&gt;速度稍慢，遍历并修改文件源码，然后编译&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;transformer-maven-plugin&quot;&gt;transformer-maven-plugin&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/eclipse-transformer/transformer&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;transformer-maven-plugin&lt;/a&gt; 是 eclipse 出的字节码和配置文件转换工具，默认内置了一些 jakarta 的替换规则。&lt;/p&gt;
&lt;p&gt;它能帮我们实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时打包（或发布）出 javax 和 jakarta 两个包，通过 classifier 区分，比如设置 javax 默认 classifier 为 null，转换后的版本设置 classifier 为 jakarta。&lt;/li&gt;
&lt;li&gt;只打包（或发布）javax 和 jakarta 任意一个版本。&lt;/li&gt;
&lt;li&gt;web.xml 或 web-fragment.xml 等相关 xml 定义转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 这个插件自动把 javax 转为 jakarta，打包出 javax 和 jakarta 两个包，通过 classifier 区分 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.eclipse.transformer&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;transformer-maven-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;1.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;extensions&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;extensions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- javax 替换成 jakarta --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;jakarta-jar&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;package&amp;#x3C;/&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;jar&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;rules&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;jakartaDefaults&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;jakartaDefaults&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;rules&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifact&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;${project.groupId}&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;${project.artifactId}&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifact&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;用途：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节码转换&lt;/li&gt;
&lt;li&gt;支持 web 相关 xml 定义自动替换，比如 web-fragment.xml &lt;code dir=&quot;auto&quot;&gt;xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;转换和 deploy 可以一步到位，速度快很方便&lt;/li&gt;
&lt;li&gt;如果是多模块，可以在根父模块声明一次，一键全部编译&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺陷：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不处理 pom 依赖关系，发布出的 Jakarta 版本依赖项都遵循原 pom 声明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此插件运行需要 Java 21 版本，发布业务包的时候，必须使用 Java 21 编译，编译目标 release 版本可以是 Java 8，在 pom 中增加以下设置：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;java.version&lt;/span&gt;&lt;span&gt;&gt;21&amp;#x3C;/&lt;/span&gt;&lt;span&gt;java.version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;maven.compiler.release&lt;/span&gt;&lt;span&gt;&gt;8&amp;#x3C;/&lt;/span&gt;&lt;span&gt;maven.compiler.release&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;web-fragmentxml-适配&quot;&gt;web-fragment.xml 适配&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;一般咱们典型的 web-fragment 定义有如下两个版本，3.0 和 3.1：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.0 版本：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;?&lt;/span&gt;&lt;span&gt;xml&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span&gt; encoding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span&gt;?&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns:xsi&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xmlns&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://java.sun.com/xml/ns/javaee&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xsi:schemaLocation&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://java.sun.com/xml/ns/javaee&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;3.0&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metadata-complete&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3.1 版本：&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;?&lt;/span&gt;&lt;span&gt;xml&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span&gt; encoding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span&gt;?&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns:xsi&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xmlns&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xsi:schemaLocation&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://xmlns.jcp.org/xml/ns/javaee&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;3.1&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metadata-complete&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;其中涉及 jakarta 修改的替换的是其中的 xmlns、xsi、version 字段。&lt;/p&gt;
&lt;p&gt;Tomcat 8 以后支持 3.1 版本并兼容 3.0 版本，Tomcat 7 对应 3.0 版本。&lt;/p&gt;
&lt;p&gt;transformer-maven-plugin 已经不支持 3.0 版本的替换，所以如果还在用 3.0 的需要咱们手动改成以上 3.1 版本的定义，transformer-maven-plugin 在转换的过程中才能自动升级到 jakarta，自动升级后，升级到 5.0 后的结果如下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;?&lt;/span&gt;&lt;span&gt;xml&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span&gt; encoding&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span&gt;?&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns:xsi&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xmlns&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://jakarta.ee/xml/ns/jakartaee&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;xsi:schemaLocation&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;https://jakarta.ee/xml/ns/jakartaee&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;https://jakarta.ee/xml/ns/jakartaee/web-fragment_5_0.xsd&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;5.0&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metadata-complete&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;web-fragment&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h4 id=&quot;跳过转换&quot;&gt;跳过转换&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;我们的 maven 项目大部分都是多模块，对于打包成 war 自运行不需要发布给别人用的项目，一般都不需要转换，可以设置 properties &lt;code dir=&quot;auto&quot;&gt;transform.skip=true&lt;/code&gt; 跳过。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 不发布 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;maven.deploy.skip&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;maven.deploy.skip&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 不转换 --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;transform.skip&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;transform.skip&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;实施方案&quot;&gt;实施方案&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;classifier-vs-version&quot;&gt;Classifier vs Version&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;业界主流方案就是用版本号区分或者用 classifier 区分这两种方案。&lt;/p&gt;
&lt;p&gt;classifier 最主要的问题是无法通过父 pom 继承，这样将导致各个业务方需要大量改 pom.xml 文件。&lt;/p&gt;
&lt;p&gt;假设一个项目依赖了 app-a，而 app-a 又依赖 b、c、d.&lt;/p&gt;
&lt;p&gt;首先因为父 pom 定义 classifier 并不会继承。那么，我是不是指定 app-a 的 classifier 就行了。&lt;/p&gt;
&lt;p&gt;不是，除非 app-a 的特定 classifier 自身 pom 里已经为给个模块指定了 classifier，否则 app-a 依赖的各个都要重新指定一遍，这就使用 pom 文件长得又长又臭。如果哪天 app-a 又新增依赖 e, 但是使用者漏掉了 e classifier 声明的，就可能带来问题，这是很危险的动作。&lt;/p&gt;
&lt;p&gt;所以我们最终选择用版本号来区分。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;使用特定版本&quot;&gt;使用特定版本&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;版本号规则，在原版本基础上增加固定前缀：&lt;code dir=&quot;auto&quot;&gt;9999.${project.version}&lt;/code&gt;，如原 &lt;code dir=&quot;auto&quot;&gt;9.6.0&lt;/code&gt; 对应的 jakarta 版本号是 &lt;code dir=&quot;auto&quot;&gt;9999.9.6.0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;父 pom 区分版本号，Spring Boot 3.x 版本依赖，重申版本号。&lt;/p&gt;
&lt;p&gt;这样各个使用者，只需要选择不同的父 pom，遵循父 pom 版本定义，dependency 可以原样不变。&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;如何打包发布&quot;&gt;如何打包发布&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;目前暂定选择 transformer-maven-plugin，他不完美，但是足够快，相对稳定。&lt;/p&gt;
&lt;p&gt;发布目标：两次发布，第一次发布常规版本，第二次发布自动转换后的版本，版本号增加前缀。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发布步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在公司级父 pom 增加如下 profile：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;profiles&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;jakarta-transformer&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;org.eclipse.transformer&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;transformer-maven-plugin&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;1.0.0&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;extensions&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;extensions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;                            &lt;/span&gt;&lt;span&gt;&amp;#x3C;!-- 此处配置 javax 替换成 jakarta，artifactId 不变，无 classifier --&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;jakarta-jar&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;package&amp;#x3C;/&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;jar&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;rules&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;jakartaDefaults&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;jakartaDefaults&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;rules&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifact&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;${project.groupId}&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;${project.artifactId}&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifact&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;profiles&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;发布 jakarta 版本时，先改版本号增加前缀，再指定 profile 发布：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 获取当前版本号&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;help:evaluate&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Dexpression=project.version&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-q&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DforceStdout&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 如果项目都是自己写版本号的，使用 version 插件设置新版本号，增加前缀&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;versions:set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DnewVersion=9999.9.6.0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DgenerateBackupPoms=false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 重新发布，指定 -Pjakarta profile，父 pom 里的插件自动转换 javax&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deploy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Pjakarta-transformer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 如果使用 flatten-maven-plugin 插件管理版本号的，设置 revision&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deploy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Drevision=9999.9.6.0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-Pjakarta-transformer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;</content:encoded><category>Java</category><category>Spring Boot</category><category>Jakarta EE</category><category>Javax</category><category>迁移</category></item><item><title>预防和消灭技术债：Maven CI 如何在编译时禁止调用某些特定 API</title><link>https://www.xlabs.club/blog/ci-maven-forbidden-api/</link><guid isPermaLink="true">https://www.xlabs.club/blog/ci-maven-forbidden-api/</guid><description>介绍 Maven CI 中禁止调用特定 API 的实现方式，借助 Forbidden APIs 在编译期拦截技术债。</description><pubDate>Tue, 09 Sep 2025 14:18:59 GMT</pubDate><content:encoded>&lt;p&gt;在日常开发中，我们经常遇到类似如下需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目组提供了 SDK，某个 API 已经被标记为废弃，但是大家迟迟不升级，项目组还需要花费时间维护已经废弃的 API。&lt;/li&gt;
&lt;li&gt;有些项目在使用 JDK 已经废弃的 API，这些废弃 API 在更高版本 JDK 中已经删除，导致推动升级 JDK 比较困难。&lt;/li&gt;
&lt;li&gt;有些项目会跨版本混合编译，比如用 Java 8 编译运行在 Java 21 上，常见的错误比如 &lt;code dir=&quot;auto&quot;&gt;javafx.util&lt;/code&gt; 在 Java 21 默认已经去掉了，就会出现编译成功却运行时失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们想提供一种方案，能不能在编译期就强制禁用某些 API，主动阻止让编译不通过，提前报错及早发现及早处理。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;实现方案&quot;&gt;实现方案&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;当前我们使用 maven &lt;a href=&quot;https://github.com/policeman-tools/forbidden-apis&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;forbiddenapis&lt;/a&gt; 插件，结合 CI 流程来实现。&lt;/p&gt;
&lt;p&gt;执行效果，如果有使用禁用的 API，能看到类似如下错误提示，编译失败并给出原因。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.647 [INFO]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.648 [INFO] --- forbiddenapis:3.9:check (check-forbidden-apis) @ app-biz ---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.657 [INFO] Scanning for classes to check...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.661 [INFO] Reading bundled API signatures: jdk-deprecated-1.8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.694 [INFO] Reading API signatures: /usr/share/maven/conf/forbidden-apis.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.694 [INFO] Loading classes to check...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.695 [INFO] Scanning classes for violations...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.849 [ERROR] Forbidden class/interface use: javafx.util.Pair [禁止使用 JavaFX 相关类]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.850 [ERROR]   in com.tester.HomeController (HomeController.java:11)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.865 [ERROR] Scanned 13 class file(s) for forbidden API invocations (in 0.21s), 1 error(s).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;12:40:30.872 [INFO] ------------------------------------------------------------------------&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;24784 [INFO] --- forbiddenapis:3.9:check (check-forbidden-apis) @ app-api ---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;24812 [INFO] Scanning for classes to check...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;24884 [INFO] Reading bundled API signatures: jdk-deprecated-1.8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;25016 [INFO] Reading API signatures: /usr/share/maven/conf/forbidden-apis.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;25017 [INFO] Loading classes to check...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;25106 [INFO] Scanning classes for violations...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;25984 [ERROR] Forbidden method invocation: java.net.URLEncoder#encode(java.lang.String) [Deprecated in Java 1.8]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;25984 [ERROR]   in com.tester.FormBody$FormBodyBuilder (FormBody.java:60)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;26110 [ERROR] Scanned 660 class file(s) for forbidden API invocations (in 1.31s), 2 error(s).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;26113 [INFO]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;maven-核心配置&quot;&gt;Maven 核心配置&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;首先，在父 pom 里增加了以下片段，此内容主要含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在存在 ${env.MAVEN_HOME}/conf/forbidden-apis.txt 文件时激活此 profile，以便确保缺少 txt 文件时不会报错。&lt;/li&gt;
&lt;li&gt;默认激活了 forbiddenapis 插件的 jdk-deprecated 规则。&lt;/li&gt;
&lt;li&gt;增加了我们定制的 forbidden-apis.txt 规则，此文件放在 maven 的基础镜像里。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;forbidden&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;activation&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;exists&lt;/span&gt;&lt;span&gt;&gt;${env.MAVEN_HOME}/conf/forbidden-apis.txt&amp;#x3C;/&lt;/span&gt;&lt;span&gt;exists&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;activation&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;de.thetaphi&amp;#x3C;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;forbiddenapis&amp;#x3C;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;3.9&amp;#x3C;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;detail&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;detail&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;failOnUnsupportedJava&lt;/span&gt;&lt;span&gt;&gt;false&amp;#x3C;/&lt;/span&gt;&lt;span&gt;failOnUnsupportedJava&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ignoreSignaturesOfMissingClasses&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;ignoreSignaturesOfMissingClasses&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;bundledSignatures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;bundledSignature&lt;/span&gt;&lt;span&gt;&gt;jdk-deprecated&amp;#x3C;/&lt;/span&gt;&lt;span&gt;bundledSignature&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;bundledSignatures&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;signaturesFiles&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;signaturesFile&lt;/span&gt;&lt;span&gt;&gt;${env.MAVEN_HOME}/conf/forbidden-apis.txt&amp;#x3C;/&lt;/span&gt;&lt;span&gt;signaturesFile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;signaturesFiles&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;check-forbidden-apis&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;process-classes&amp;#x3C;/&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;check&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;test-check-forbidden-apis&amp;#x3C;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;process-test-classes&amp;#x3C;/&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;testCheck&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goal&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;goals&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;execution&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;executions&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugin&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;profile&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;我的项目需要紧急发版，来不及改代码，如何禁用以上插件，在 pom.xml 中增加以下属性，或通过 mvn 命令行 &lt;code dir=&quot;auto&quot;&gt;-Dforbiddenapis.skip=true&lt;/code&gt; 传递此参数：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;forbiddenapis.skip&lt;/span&gt;&lt;span&gt;&gt;true&amp;#x3C;/&lt;/span&gt;&lt;span&gt;forbiddenapis.skip&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;forbiddenapis-规则详解&quot;&gt;forbiddenapis 规则详解&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;forbiddenapis 插件有内置规则和自定义规则，规则名字叫 signatures。&lt;/p&gt;
&lt;p&gt;forbiddenapis 插件默认已经内置了一些规则 signatures，参考：&lt;a href=&quot;https://jenkins.thetaphi.de/job/Forbidden-APIs/javadoc/bundled-signatures.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;bundled-signatures&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;主要默认规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-unsafe-*&lt;/code&gt;: Signatures of “unsafe” methods that use default charset, default locale, or default timezone. For server applications it is very stupid to call those methods, as the results will definitely not what the user wants (for Java* = 1.7, 1.8, 9,…, 24; Ant / Maven / Gradle automatically add the compile Java version).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-deprecated-*&lt;/code&gt;: This disallows all deprecated methods from the JDK (for Java*= 1.7, 1.8, 9,…, 24; Ant / Maven / Gradle automatically add the compile Java version).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-internal-*&lt;/code&gt;: Lists all internal packages of the JDK as of Security.getProperty(“package.access”). Calling those methods will always trigger security manager and is completely forbidden from Java 9 on (for Java*= 1.7, 1.8, 9,…, 24; Ant / Maven / Gradle automatically add the compile Java version, since forbiddenapis v2.1).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-non-portable&lt;/code&gt;: Signatures of all non-portable (like com.sun.management.HotSpotDiagnosticMXBean) or internal runtime APIs (like sun.misc.Unsafe). This is a superset of jdk-internal.
Internally this is implemented using heuristics: Any reference to an API that is part of the Java runtime (rt.jar, extensions, Java 9+ java.&lt;em&gt;/ jdk.&lt;/em&gt; core modules) and is not part of the Java SE specification packages (mainly java, javax, but also org.ietf.jgss, org.omg, org.w3c.dom, and org.xml.sax) is forbidden (any java version, no specific JDK version, since forbiddenapis v2.1).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-system-out&lt;/code&gt;: On server-side applications or libraries used by other programs, printing to System.out or System.err is discouraged and should be avoided (any java version, no specific JDK version).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jdk-reflection&lt;/code&gt;: Reflection usage to work around access flags fails with SecurityManagers and likely will not work anymore on runtime classes in Java 9 or later (any java version, no specific JDK version, since forbiddenapis v2.1).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;commons-io-unsafe-*&lt;/code&gt;: If your application uses the famous Apache Common-IO library, this adds signatures of all methods that depend on default charset (for versions* = 1.0, 1.1, 1.2, 1.3, 1.4, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8.0, 2.9.0, 2.10.0, 2.11.0, 2.12.0, 2.13.0, 2.14.0, 2.15.0, 2.15.1, 2.16.0, 2.16.1, 2.17.0, 2.18.0).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过 txt 文件自定义规则，规则语法：&lt;a href=&quot;https://jenkins.thetaphi.de/job/Forbidden-APIs/javadoc/signatures-syntax.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;signatures-syntax&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Class reference: A binary/fully-qualified class name (including package). You may use the output of Class.getName(). Be sure to use correct name for inner classes! Example: &lt;code dir=&quot;auto&quot;&gt;java.lang.String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A package/class glob pattern: To forbid all classes from a package, you may use glob patterns, like &lt;code dir=&quot;auto&quot;&gt;sun.misc.**&lt;/code&gt;(&lt;code dir=&quot;auto&quot;&gt;**&lt;/code&gt; matches against package boundaries).&lt;/li&gt;
&lt;li&gt;A field of a class: package.Class#fieldName&lt;/li&gt;
&lt;li&gt;A method signature: It consists of a binary class name, followed by # and a method name including method parameters: java.lang.String#concat(java.lang.String) – All method parameters need to use fully qualified class names! Instead of method parameters, the special wildcard string ** may be used to add all variants of a method, regardless of their parameter types. To refer to instance constructors, use the method name &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;init&gt;&lt;/code&gt;, e.g. &lt;code dir=&quot;auto&quot;&gt;java.lang.Integer#&amp;#x3C;init&gt;(int)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The error message displayed when the signature matches can be given at the end of each signature line using ”@” as separator:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java.lang.String @ You are crazy that you disallow strings&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;To not repeat the same message after each signature, you can prepend signatures with a default message. Use a line starting with “@defaultMessage”.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;@defaultMessage You are crazy that you disallow substrings&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java.lang.String#substring(int)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;java.lang.String#substring(int,int)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里着重需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按方法名定义时，一定要定义好参数类型，否则不生效。&lt;/li&gt;
&lt;li&gt;按包名定义时，一定要区分好一个 &lt;code dir=&quot;auto&quot;&gt;*&lt;/code&gt; 和两个 &lt;code dir=&quot;auto&quot;&gt;**&lt;/code&gt;，一个&lt;code dir=&quot;auto&quot;&gt;*&lt;/code&gt;只能匹配一级，两个&lt;code dir=&quot;auto&quot;&gt;**&lt;/code&gt;能匹配 N 级。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>CICD</category><category>Java</category><category>Maven</category><category>编译检查，技术债</category></item></channel></rss>