blade-tool/doc/guide/blade-tool-5.0升级适配指南.md

974 lines
68 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BladeX-Tool Spring Boot 4 升级适配指南
> 适用工程:`blade-tool`(SpringBlade 核心工具工程,开源扁平版布局)
> 升级区间:Spring Boot 3.5.13 → **4.1.0**,Spring Cloud 2025.0.2 → **2025.1.2**
> 文档定位:照此文档逐模块、逐文件操作即可完成升级;每个主题给出「为什么改 → 改哪个文件 → 改前/改后示例」
> 文档性质:升级指导 / 迁移规范。既供工程师直接阅读执行,也供 AI 依此拆解任务、逐文件推进迁移。
---
## 0. 怎么用这份文档
1. 先读 **第 1 节版本矩阵****第 2 节执行顺序**,建立全局认知。
2. **严格按第 2 节的顺序推进**:注解处理器(总闸门)→ 依赖坐标 → 底层模块 import → 运行期适配 → 第三方组件。顺序错了会连锁编译失败。
3. 每个主题按「改前 → 改后」示例照做,改完 `mvn clean install -DskipTests` 验证编译。
4. 编译通过即为本文档的交付边界;**应用启动与集成测试由你在真实环境执行**(见第 13 节运行期验证要点)。
---
## 1. 版本矩阵(升级前 → 升级后)
### 1.1 框架核心
| 组件 | 升级前 | 升级后 | 说明 |
|------|--------|--------|------|
| JDK | 17 | **21** | Boot 4 最低仍是 17,本次一并主动升到 21 LTS(见 §16.1);**构建需 JDK 21** |
| Spring Framework | 6.2.17 | 7.0.8 | 由 Boot BOM 传递,**不再单独 import `spring-framework-bom`** |
| Spring Boot | 3.5.13 | **4.1.0** | 总闸 |
| Spring Cloud | 2025.0.2 | **2025.1.2** | 与 Boot 4 强绑定 |
| Spring Cloud Alibaba | 2025.0.0.0 | **2025.1.0.0** | 对应 Cloud 2025.1 线 |
| Spring Boot Admin | 3.5.8 | **4.1.1** | SBA 大版本必须跟随 Boot 大.次版本 |
### 1.2 中间件与第三方
| 组件 | 升级前 | 升级后 | 处置 |
|------|--------|--------|------|
| mica-auto(注解处理器) | 3.1.6 | **4.0.1** | 换坐标版本(总闸门,见 §4) |
| MyBatis-Plus | 3.5.14 | **3.5.17** | 换坐标 + **`IService`/`ServiceImpl` 包迁移**(见 §8) |
| mybatis-spring | 3.0.5 | **4.0.0** | MP 3.5.17 的 boot4 装配要求,Spring 7 线 |
| Web 容器 | Undertow | **Tomcat 11** | Undertow 在 Boot 4 下线(见 §6) |
| JSON 栈 | Jackson 2(Boot 默认) | **Jackson 2.21.4 + `spring-boot-jackson2` 兼容** | Boot 4 默认 Jackson 3,BladeX 锁回 2(见 §5) |
| springdoc | 2.8.13 | **3.0.3** | v3 线才支持 Boot 4 |
| Knife4j | 4.5.0 | **移除** | 无 Boot 4 版,改用 springdoc 原生 UI(见 §9) |
| Druid starter | `druid-spring-boot-3-starter` | **`druid-spring-boot-4-starter:1.2.28`** | 坐标改名 |
| hibernate-validator | 9.0.1.Final(显式 pin) | **9.1.0.Final**(BOM 托管) | 移除过时 pin |
| spring-boot-starter-aop | 存在 | **`spring-boot-starter-aspectj`** | Boot 4 移除了 aop starter |
| spring-retry | BOM 托管 | **2.0.13**(自管) | Boot 4 BOM 不再托管 |
| Apache HttpClient 4 | 非 BOM 托管 | **4.5.14**(自管) | Boot BOM 仅托管 HttpClient 5;HttpClient 4 自 Boot 3.1 起已不在 BOM,JustAuth 需要故自管 |
### 1.3 删除 / 新增的坐标
| 动作 | 坐标 | 原因 |
|------|------|------|
| 删除 | `org.springframework:spring-framework-bom`(import) | 由 `spring-boot-dependencies` 统一托管 SF 7,避免版本漂移 |
| 删除 | `com.github.xiaoymin:knife4j-dependencies`(import)+ `knife4j-openapi3-jakarta-spring-boot-starter` | 无 Boot 4 版 |
| 新增 | `org.springframework.boot:spring-boot-jackson2`(+ `jackson-databind`) | Jackson 2 兼容降级 |
| 新增 | `org.springframework.boot:spring-boot-cache` | Boot 4 缓存自动配置类拆分至独立模块 |
| 新增 | `org.springframework.boot:spring-boot-restclient` | Boot 4 把 `RestTemplateBuilder` 拆到独立模块 |
| 新增 | `com.baomidou:mybatis-plus-extension` | 本模块直接使用 `MybatisPlusInterceptor`/分页/租户等 extension 类,按"直接依赖显式声明"补齐(经 `mybatis-plus-spring` 仍可传递获得,显式声明更稳健) |
| 改名 | `spring-boot-starter-web``spring-boot-starter-webmvc` | Undertow 退场,默认 Tomcat |
---
## 2. 升级执行顺序(强约束)
blade-tool 是核心工程,下游 Cloud / Boot 通过 `blade-core-bom` 继承整套版本。**升级必须 blade-tool 先行**,内部顺序不可打乱:
```
① 改父 POM 版本矩阵与坐标(§3)
② 升 mica-auto 到 4.0.1 —— 注解处理器是编译期总闸门(§4)
③ 底层模块 import 迁包:launch / cloud / tool / redis / log 等(§5.3 映射表)
④ 运行期适配:Jackson 2 降级(§5)、Web 容器(§6)、全局异常(§7)
⑤ 第三方组件:MyBatis-Plus 包迁移(§8)、Swagger(§9)、Boot4 移除坐标(§10)
⑥ 全量 mvn clean install(务必带 clean,重新生成 spring.factories / *.imports)
```
> **为什么 mica-auto 是总闸门**:BladeX 全工程的 `@AutoConfiguration`、`EnvironmentPostProcessor`、`LauncherService` 都靠 mica-auto 在编译期生成 `META-INF/spring/*.imports` 与 `META-INF/spring.factories`。mica-auto 3.1.6 把 `EnvironmentPostProcessor` 写成旧键 `org.springframework.boot.env.EnvironmentPostProcessor`,4.0.1 才写 Boot 4 的新键 `org.springframework.boot.EnvironmentPostProcessor`。
> ⚠️【勘误 · 别误判失效方式】Boot **4.1.0 仍向后兼容**:`SpringFactoriesEnvironmentPostProcessorsFactory#getEnvironmentPostProcessors()` 会**同时加载新键与旧键**(旧键实例经内部 `Adapter` 包装后照常执行),旧接口 `org.springframework.boot.env.EnvironmentPostProcessor` 标 `@Deprecated(since=4.0.0, forRemoval=true)`、要到 **Boot 4.2.0** 才移除。因此"不升 mica-auto 会**运行期静默失效**"的说法对 4.1.0 **不成立**——真实两种结果是:① 源码仍用旧接口 + mica 3.1.6 写旧键 → 旧键仍被加载执行(不失效);② 源码已改新接口但 mica 仍 3.1.6 写旧键 → 类型不匹配,启动**显式抛 `IllegalArgumentException`**(大声失败,非静默)。升 mica-auto 到 4.0.1 仍是**必须动作**(对齐新键、面向 4.2.0),只是失效机制并非"静默"。
---
## 3. 父 POM 版本矩阵与坐标(`pom.xml`)
### 3.1 `<properties>` 版本属性
```xml
<!-- 改前 -->
<spring.version>6.2.17</spring.version>
<spring.boot.version>3.5.13</spring.boot.version>
<spring.boot.admin.version>3.5.8</spring.boot.admin.version>
<spring.cloud.version>2025.0.2</spring.cloud.version>
<alibaba.cloud.version>2025.0.0.0</alibaba.cloud.version>
<mica.auto.version>3.1.6</mica.auto.version>
<mybatis.spring.version>3.0.5</mybatis.spring.version>
<mybatis.plus.version>3.5.14</mybatis.plus.version>
<knife4j.version>4.5.0</knife4j.version>
<!-- 改后 -->
<!-- spring.version 属性整行删除:SF 7 由 spring-boot-dependencies 托管 -->
<spring.boot.version>4.1.0</spring.boot.version>
<spring.boot.admin.version>4.1.1</spring.boot.admin.version>
<spring.cloud.version>2025.1.2</spring.cloud.version>
<alibaba.cloud.version>2025.1.0.0</alibaba.cloud.version>
<mica.auto.version>4.0.1</mica.auto.version>
<mybatis.spring.version>4.0.0</mybatis.spring.version>
<mybatis.plus.version>3.5.17</mybatis.plus.version>
<!-- knife4j.version 属性整行删除 -->
<!-- 新增:Boot 4 BOM 不再托管的坐标自管版本 -->
<spring.retry.version>2.0.13</spring.retry.version>
<httpclient.version>4.5.14</httpclient.version>
```
### 3.2 `<dependencyManagement>` 变更
**删除** `spring-framework-bom` import(让 Boot BOM 统一管 SF 7):
```xml
<!-- 删除整块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
```
**删除** `knife4j-dependencies` import:
```xml
<!-- 删除整块 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-dependencies</artifactId>
<version>${knife4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
```
**坐标改名**(boot3 → boot4)+ **springdoc 升 3.0.3**:
```xml
mybatis-plus-spring-boot3-starter → mybatis-plus-spring-boot4-starter
druid-spring-boot-3-starter → druid-spring-boot-4-starter
springdoc-openapi-starter-webflux-ui: 2.8.13 → 3.0.3
springdoc-openapi-starter-webmvc-ui : 2.8.13 → 3.0.3
```
**移除过时 pin**:删掉 `hibernate-validator``<version>9.0.1.Final</version>` 显式管理条目(Boot 4.1 BOM 托管为 9.1.0.Final)。
**新增自管坐标**(Boot 4 BOM 不再托管):
```xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${spring.retry.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
```
### 3.3 父 POM 公共 `<dependencies>`
```xml
<!-- 改前:Boot 4 已移除 aop starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 改后 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
```
---
## 4. mica-auto 注解处理器(总闸门)
**改动**:仅版本升级(§3.1 已改 `mica.auto.version` = 4.0.1)。mica-auto 4.0.1 的 `BootAutoType` 内置了 Boot 4 的新注册键,升级后重新编译即自动生成正确的 `spring.factories`
**配套改一处 import**——`blade-starter-loadbalancer` 唯一的 `EnvironmentPostProcessor`:
文件:`blade-starter-loadbalancer/.../rule/GrayscaleEnvPostProcessor.java`
```java
// 改前
import org.springframework.boot.env.EnvironmentPostProcessor;
// 改后(Boot 4 该接口上移根包)
import org.springframework.boot.EnvironmentPostProcessor;
```
**验证**:`mvn clean install` 后检查生成物,键应为新的根包键:
```properties
# blade-starter-loadbalancer/target/classes/META-INF/spring.factories
org.springframework.boot.EnvironmentPostProcessor=\
org.springblade.core.loadbalancer.rule.GrayscaleEnvPostProcessor
```
> 若这里仍是 `org.springframework.boot.env.EnvironmentPostProcessor`,说明 mica-auto 没升到 4.0.1。该键在 Boot 4.1.0 仍向后兼容加载执行(见 §2 勘误),但要到 **4.2.0** 旧接口移除后才彻底失效;应升 mica-auto 到 4.0.1 写入新键对齐,不要留旧键。
---
## 5. Boot 4 自动配置拆包 import 迁移 + Jackson 2 降级
### 5.1 背景:为什么会大面积编译断裂
Spring Boot 4.0 把巨型 `spring-boot-autoconfigure` 拆成按技术域划分的独立模块,包名从 `org.springframework.boot.autoconfigure.<tech>.*` 重排为 `org.springframework.boot.<tech>.autoconfigure.*`,部分类还顺带改名(如 `RedisAutoConfiguration``DataRedisAutoConfiguration`)。旧 import 直接编译不过。
### 5.2 【务必牢记】包名迁移映射表(已在 Boot 4.1.0 实测)
| 旧包 / 类 | 新包 / 类 | 所需 starter |
|-----------|-----------|--------------|
| `autoconfigure.web.ServerProperties` | `web.server.autoconfigure.ServerProperties` | spring-boot-web-server |
| `autoconfigure.web.servlet.WebMvcRegistrations` | `webmvc.autoconfigure.WebMvcRegistrations` | spring-boot-webmvc |
| `autoconfigure.web.servlet.WebMvcProperties` | `webmvc.autoconfigure.WebMvcProperties` | spring-boot-webmvc |
| `autoconfigure.web.servlet.error.BasicErrorController` | `webmvc.autoconfigure.error.BasicErrorController` | spring-boot-webmvc |
| `autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration` | `webmvc.autoconfigure.error.ErrorMvcAutoConfiguration` | spring-boot-webmvc |
| `web.servlet.error.DefaultErrorAttributes/ErrorAttributes/ErrorController` | `webmvc.error.*` | spring-boot-webmvc |
| `autoconfigure.cache.CacheProperties/CacheManagerCustomizer(s)` | `cache.autoconfigure.*` | spring-boot-cache |
| `autoconfigure.data.redis.RedisAutoConfiguration` | `data.redis.autoconfigure.**DataRedisAutoConfiguration**`(改名) | spring-boot-data-redis |
| `autoconfigure.jdbc.DataSourceProperties/DataSourceAutoConfiguration` | `jdbc.autoconfigure.*` | spring-boot-jdbc |
| `autoconfigure.jackson.JacksonAutoConfiguration` | `jackson.autoconfigure.JacksonAutoConfiguration` | spring-boot-jackson(Jackson 3) |
| `autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer` | `jackson2.autoconfigure.*`(deprecated-for-removal) | spring-boot-jackson2 |
| `web.context.WebServerInitializedEvent` | `web.server.context.WebServerInitializedEvent` | spring-boot-web-server |
| `web.client.RestTemplateBuilder` | `restclient.RestTemplateBuilder` | spring-boot-restclient |
| `boot.env.EnvironmentPostProcessor` | `boot.EnvironmentPostProcessor` | spring-boot |
> **保持不变、不要跟着乱改**:`autoconfigure.AutoConfiguration` 注解本体、`autoconfigure.AutoConfigureBefore/After`、`autoconfigure.condition.ConditionalOn*` / `SearchStrategy`、`autoconfigure.SpringBootApplication` / `EnableAutoConfiguration`、`autoconfigure.web.ErrorProperties`、`web.error.ErrorAttributeOptions`、`web.servlet.FilterRegistrationBean` / `ServletRegistrationBean` 均**未迁包**。
### 5.3 逐文件 import 迁移(blade-tool 实际受影响清单)
| 文件 | 改动 |
|------|------|
| `blade-core-launch/.../server/ServerInfo.java` | `ServerProperties``web.server.autoconfigure` |
| `blade-core-launch/.../StartEventListener.java` | `WebServerInitializedEvent``web.server.context` |
| `blade-core-cloud/.../version/BladeWebMvcRegistrations.java` | `WebMvcRegistrations``webmvc.autoconfigure` |
| `blade-core-cloud/.../version/VersionMappingAutoConfiguration.java` | `WebMvcRegistrations``webmvc.autoconfigure` |
| `blade-core-cloud/.../http/RestTemplateConfiguration.java` | `RestTemplateBuilder``restclient`;并把 `restTemplate()`/`lbRestTemplate()` 改为链式 `requestFactory(...).build()`(Boot 4 的 `RestTemplateBuilder` 不可变,旧的"先 `requestFactory(...)` 再单独 `build()`"两行式会丢弃携带 OkHttp3 工厂的 builder,顺带修掉该隐患) |
| `blade-core-tool/.../config/RedisConfiguration.java` | `RedisAutoConfiguration``DataRedisAutoConfiguration`(import + `@AutoConfigureBefore` 参数一起改) |
| `blade-starter-redis/.../config/RedisTemplateConfiguration.java` | 同上(import + `@AutoConfiguration(before=...)` 参数) |
| `blade-starter-redis/.../config/RedisCacheManagerConfig.java` | `CacheManagerCustomizer(s)``cache.autoconfigure` |
| `blade-starter-redis/.../config/BladeRedisCacheAutoConfiguration.java` | `CacheManagerCustomizers` / `CacheProperties``cache.autoconfigure` |
| `blade-starter-log/.../config/BladeErrorMvcAutoConfiguration.java` | 错误类拆两包 + `ServerProperties`(见 §7.2) |
| `blade-starter-log/.../error/BladeErrorController.java` | `BasicErrorController``webmvc.autoconfigure.error`;`ErrorAttributes` → `webmvc.error` |
| `blade-starter-log/.../error/BladeErrorAttributes.java` | `DefaultErrorAttributes``webmvc.error` |
| `blade-starter-transaction/.../annotation/SeataCloudApplication.java` | `DataSourceAutoConfiguration``jdbc.autoconfigure` |
| `blade-core-tool/.../config/JacksonConfiguration.java` | Jackson 2 降级重构(见 §5.5) |
**改名类(RedisAutoConfiguration → DataRedisAutoConfiguration)示例**——import 与注解参数必须一起改:
```java
// 改前
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
@AutoConfiguration(before = {RedisConfiguration.class, RedisAutoConfiguration.class})
// 改后
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
@AutoConfiguration(before = {RedisConfiguration.class, DataRedisAutoConfiguration.class})
```
### 5.4 拆包后各模块 POM 补齐 split-starter
新包里的类只有对应 starter 在 classpath 时才存在,以下为**硬性补齐**(不补则「找不到符号」):
| 模块 POM | 新增依赖 | 原因 |
|----------|----------|------|
| `blade-starter-redis/pom.xml` | `org.springframework.boot:spring-boot-cache` | 直接引用 `cache.autoconfigure.*`,而 `spring-boot-starter-data-redis` 不带 cache |
| `blade-core-cloud/pom.xml` | `org.springframework.boot:spring-boot-restclient` | `RestTemplateBuilder` 迁到此模块,webmvc starter 不含 |
| `blade-core-tool/pom.xml` | `spring-boot-jackson2` + `jackson-databind` | Jackson 2 降级(见 §5.5) |
版本一律由 BOM 托管,**不写 `<version>`**。
### 5.5 Jackson 2 降级与序列化管线重构
**背景**:Boot 4 默认底座是 Jackson 3(`tools.jackson.*`),BladeX 全链路(`JsonUtil`、`@BladeView` 视图过滤、大数转字符串、null 转空、读写分离转换器)重度依赖 Jackson 2(`com.fasterxml.jackson.*`)。方案是引 `spring-boot-jackson2` 兼容 starter 锁回 Jackson 2,并**不再自建主 `ObjectMapper` Bean 抢占框架 builder 装配管线**,改贡献 `Jackson2ObjectMapperBuilderCustomizer`
**`blade-core-tool/pom.xml` 新增依赖**:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
```
**`JacksonConfiguration.java` 重构**——从「自建主 ObjectMapper」改为「贡献 builder 定制器」:
```java
// 改前:自建主 ObjectMapper,抢占 builder 管线,且 import 的 JacksonAutoConfiguration 在 Boot4 已不存在
@AutoConfiguration(before = JacksonAutoConfiguration.class)
@ConditionalOnClass(ObjectMapper.class)
@EnableConfigurationProperties(BladeJacksonProperties.class)
public class JacksonConfiguration {
@Bean
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = JsonUtil.getInstance();
objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
objectMapper.findAndRegisterModules();
return objectMapper;
}
}
// 改后:贡献 Jackson2ObjectMapperBuilderCustomizer,由框架构建主 ObjectMapper
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@EnableConfigurationProperties(BladeJacksonProperties.class)
@SuppressWarnings({"deprecation", "removal"})
public class JacksonConfiguration {
@Bean
public Jackson2ObjectMapperBuilderCustomizer bladeJacksonCustomizer() {
return builder -> {
builder.featuresToEnable(
MapperFeature.DEFAULT_VIEW_INCLUSION,
// JsonReadFeature 不被 Jackson2ObjectMapperBuilder 接受,改用等价的 JsonParser.Feature
JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS,
JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER,
JsonParser.Feature.ALLOW_SINGLE_QUOTES,
DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
builder.featuresToDisable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
SerializationFeature.FAIL_ON_EMPTY_BEANS,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
builder.dateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA));
builder.modulesToInstall(new BladeJavaTimeModule());
builder.findModulesViaServiceLoader(true);
builder.annotationIntrospector(introspector -> new BladeViewAnnotationIntrospector());
};
}
}
```
**`MessageConfiguration.java`**——必须重写 `extendMessageConverters`(**不是** `configureMessageConverters`),在框架构建好的默认转换器列表上原地替换 JSON / String 转换器。
> ⚠️【务必看清,这是升级中最隐蔽的一个坑】Spring 7 的 `WebMvcConfigurationSupport.getMessageConverters()` 执行顺序是:先 `configureMessageConverters(list)` → 若 list 仍为空才 `addDefaultHttpMessageConverters(list)`(这一步才注册 ByteArray/String/Resource/ResourceRegion/Form 以及 JSON 转换器)→ 最后 `extendMessageConverters(list)`。
> 若重写 `configureMessageConverters` 并向 list 添加任何转换器,list 变为非空,`addDefaultHttpMessageConverters` 会被**整段跳过**,MVC 最终只剩下你加入的这一个转换器——文件下载、`byte[]`/图片/PDF、纯文本、表单请求体全部 406/`HttpMessageNotWritableException` 失败。**编译与纯 JSON 接口都正常,极难在冒烟测试中发现。**
> 正解是重写 `extendMessageConverters`:它在默认列表构建完成之后回调,拿到的是**完整**的默认转换器,再原地替换 JSON(因 §5.5⑤ 已强制 `preferred-json-mapper=jackson2`,默认 JSON 转换器为 `MappingJackson2HttpMessageConverter`,属 `AbstractJackson2HttpMessageConverter`)与 String 转换器即可,其余转换器原样保留。
```java
@Override
@SuppressWarnings("removal")
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingApiJackson2HttpMessageConverter bladeJson =
new MappingApiJackson2HttpMessageConverter(objectMapper, properties);
boolean jsonReplaced = false;
ListIterator<HttpMessageConverter<?>> iterator = converters.listIterator();
while (iterator.hasNext()) {
HttpMessageConverter<?> converter = iterator.next();
if (converter instanceof AbstractJackson2HttpMessageConverter) {
iterator.set(bladeJson);
jsonReplaced = true;
} else if (converter instanceof StringHttpMessageConverter) {
iterator.set(new StringHttpMessageConverter(Charsets.UTF_8));
}
}
// 兜底:未命中默认 JSON 转换器时插到队首,确保 blade 转换器优先生效
if (!jsonReplaced) {
converters.add(0, bladeJson);
}
}
```
**④ 抑制弃用 / 待删除告警**:Boot4 下 Jackson 2 的 Spring 集成类(`AbstractJackson2HttpMessageConverter`、`MappingJackson2HttpMessageConverter`、`MappingJacksonValue`、`Jackson2ObjectMapperBuilderCustomizer` 等)被标记为 deprecated-for-removal,部分 Jackson 2 `ObjectMapper` mutator 也被弃用,需逐类补 `@SuppressWarnings`。作用域按就近原则——弃用点在类声明(如 `extends` 待删除父类)或贯穿整类的放**类级**,只落在单个方法内的放**方法级**;`deprecation`(普通弃用)与 `removal`(待删除弃用)是不同 lint 类别,按该类实际触发的类别精确填写、不冗余:
| 类 / 方法 | 作用域 | 注解 | 触发点 |
|---|---|---|---|
| `jackson/AbstractReadWriteJackson2HttpMessageConverter` | 类级 | `@SuppressWarnings("removal")` | `extends AbstractJackson2HttpMessageConverter`(待删除),类声明即触发 |
| `jackson/MappingApiJackson2HttpMessageConverter` | 类级 | `@SuppressWarnings("deprecation")` | 覆盖 `initWriteObjectMapper` 里 Jackson 2 可变式定制 API(`setSerializerFactory` / `getSerializerProvider().setNullValueSerializer`);⚠️ **勘误**:在锁定的 jackson-databind 2.21.4 中这两个方法**尚未**标 `@Deprecated`(经 javap 核实),故此处属**前向/防御性**抑制,本类无 removal |
| `jackson/BladeViewResponseAdvice` | 类级 | `@SuppressWarnings("removal")` | 用 `MappingJacksonValue`(待删除)包装 `@BladeView` 视图响应 |
| `config/JacksonConfiguration` | 类级 | `@SuppressWarnings({"deprecation", "removal"})` | `JsonParser.Feature.*`(弃用)+ `Jackson2ObjectMapperBuilderCustomizer`(待删除) |
| `config/MessageConfiguration#extendMessageConverters` | 方法级 | `@SuppressWarnings("removal")` | 方法内引用 `AbstractJackson2HttpMessageConverter`(待删除) |
| `blade-core-cloud RestTemplateConfiguration#configMessageConverters` | 方法级 | `@SuppressWarnings("removal")` | 方法内引用 `MappingJackson2HttpMessageConverter`(待删除) |
> 与 Jackson 无关的 `org.springframework.lang.Nullable/NonNull` 在 Spring 7 也被弃用(官方推 JSpecify),那是一处独立、面广的迁移,不在本表范围,别混入 Jackson 的 `@SuppressWarnings`。
**⑤ 【关键】强制 preferred-json-mapper = jackson2**——新增一个 `LauncherService`,否则 Boot 4 默认注册 Jackson 3 转换器,§5.5③ 的 `instanceof AbstractJackson2HttpMessageConverter` 命不中,Blade 的 JSON 处理静默失效:
新增 `blade-core-tool/.../config/JacksonLauncherServiceImpl.java`:
```java
@AutoService(LauncherService.class)
public class JacksonLauncherServiceImpl implements LauncherService {
@Override
public void launcher(SpringApplicationBuilder builder, String appName, String profile) {
Properties props = System.getProperties();
// 命令式转换器与反应式编解码器分别指定首选实现,两处同锁 Jackson 2servlet / webflux 两侧序列化一致
props.setProperty("spring.http.converters.preferred-json-mapper", "jackson2");
props.setProperty("spring.http.codecs.preferred-json-mapper", "jackson2");
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
```
### 5.6 Redis pub/sub 容器与 Boot 4.1 新增 `@RedisListener` 自动配置的竞争
**背景**:Boot 4.1 的 `spring-boot-data-redis` 新增了注解驱动监听自动配置 `DataRedisAnnotationDrivenConfiguration`(由 `DataRedisAutoConfiguration` `@Import`,条件 `@ConditionalOnClass({org.springframework.data.redis.annotation.EnableRedisListeners, org.springframework.messaging.Message})`),用于支撑新的 `@RedisListener` 注解。它会注册一个 `@Bean(name = "redisMessageListenerContainer")` + `@ConditionalOnMissingBean` 的默认容器。
BladeX 自有的 Redis 发布订阅(`@RPubSubListener` + `RPubSubListenerDetector`)在 `blade-starter-redis``RedisPubSubConfiguration` 里**同样**注册一个同名 `redisMessageListenerContainer`(也是 `@ConditionalOnMissingBean`)。`spring-messaging`(经 spring-data-redis 传递)与 `EnableRedisListeners` 在 BladeX classpath 上均存在,故 Boot 这个自动配置**会激活**,与 BladeX 的容器**同名、同条件**竞争。
**行为分析**:两者都 `@ConditionalOnMissingBean` → 不会重复 Bean 报错,功能上任一容器都能用(BladeX 的 detector 只是 `getBean(RedisMessageListenerContainer.class)` 抓容器再挂监听);但**哪个容器胜出取决于自动配置处理顺序**——`RedisPubSubConfiguration` 原先未声明任何 `@AutoConfigureBefore/After`,顺序不确定,存在隐性非确定性。
**改动**(`blade-starter-redis/.../config/RedisPubSubConfiguration.java`)——让 BladeX 容器确定性胜出:
```java
// 改前
@AutoConfiguration
public class RedisPubSubConfiguration {
// 改后(新增 import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration)
@AutoConfiguration(before = DataRedisAutoConfiguration.class)
public class RedisPubSubConfiguration {
```
> 写法与同模块 `RedisTemplateConfiguration`(§5.3)一致:排在 `DataRedisAutoConfiguration` 之前,BladeX 的 `redisMessageListenerContainer` 先注册,Boot 的 `@ConditionalOnMissingBean` 随即退让;容器所需的 `RedisConnectionFactory` 由 bean 方法入参在实例化时解析,不受配置类排序影响。
> **与 §5.3 两处 Redis 改动的区别**:§5.3 里 `RedisConfiguration` / `RedisTemplateConfiguration` 调 `before=` 是因**类改名**(`RedisAutoConfiguration`→`DataRedisAutoConfiguration`)顺带修正既有 `before` 列表;本处 `RedisPubSubConfiguration` 原本**无** `before`、属**新增**,根因是 Boot 4.1 新引入的 `@RedisListener` 自动配置,是独立的适配点。
> **运行期验证**:启动后确认 `@RPubSubListener` 收发一次正常即可。
---
## 6. Web 容器:Undertow → Tomcat
**背景**:Spring Boot 4 把基线抬到 Servlet 6.1 / Jakarta EE 11(Tomcat 11、Jetty 12.1),Undertow 当时无对应 GA,Spring 遂移除了 Undertow 的 starter 与自动配置——`spring-boot-starter-undertow` 已无 4.x 正式版(仓库仅剩从未转正的 `4.0.0-M1` 里程碑),回归默认 Tomcat 11。
**`blade-core-launch/pom.xml`**——`web` + `undertow` 两个依赖合并为一个 `webmvc`:
```xml
<!-- 改前 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- 改后(webmvc 默认带 Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
```
**`blade-core-boot/src/main/resources/bootstrap.yml`**——`server.undertow.*` 换算为 `server.tomcat.*`:
```yaml
# 改前
server:
undertow:
buffer-size: 1024
direct-buffers: true
threads:
io: 16
worker: 400
# 改后(Tomcat 无独立 IO 线程 / buffer 概念:worker→threads.max、io→threads.min-spare,buffer 项废弃)
server:
tomcat:
# 线程配置
threads:
# 最大工作线程数(可同时处理请求的线程上限)
max: 400
# 最小空闲工作线程数(保持存活的常驻线程数)
min-spare: 16
```
**③(可选)`logback_dev.xml`**——Undertow 日志器换 Tomcat 命名空间:`io.undertow`→`org.apache.catalina`、`org.xnio.nio`→`org.apache.coyote`。
**`blade-core-boot/src/main/resources/bootstrap.yml`——Servlet 编码配置改前缀 `server.servlet.encoding``spring.servlet.encoding`**
Spring Boot 4.0 把 Servlet 编码属性从 `server.servlet.encoding.*` 迁到新的 `spring-boot-servlet` 模块下的 `spring.servlet.encoding.*`(绑定类 `org.springframework.boot.servlet.autoconfigure.ServletEncodingProperties`)。旧前缀在 Boot 4.1.0 的配置元数据里被标记为 `level: error`(非 warning)、`since: 4.0.0`,属**硬迁移**:旧键不再生效,只保留元数据供 IDE / 校验工具报错提示。判定依据是 Boot 自身随包发布的 `META-INF/spring-configuration-metadata.json`,比任何博客都权威。
```yaml
# 改前(server 命名空间下,Boot 4 起报错)
server:
servlet:
encoding:
charset: UTF-8
force: true
# 改后(并入既有 spring.servlet 块,与 multipart 平级)
spring:
servlet:
multipart:
max-file-size: 256MB
max-request-size: 1024MB
# 编码配置
encoding:
charset: UTF-8
force: true
```
> 键名一一对应,仅前缀 `server` → `spring`:`charset` / `force` / `enabled` / `force-request` / `force-response` 均照迁。`force: true` 是 BladeX 的刻意设置(Boot 默认 `force=false`),强制请求与响应都用 UTF-8,避免中文响应乱码,故保留而非删除。YAML 里 `spring:` 顶层键唯一,须把 `encoding` 并入已存在的 `spring.servlet` 块,不能新起一个 `spring:` 造成重复键。
> **下游连锁**:blade-core-launch 是全工程唯一声明 Web 容器 starter 的地方,下游所有 app(尤其未自带容器 starter 的 BladeX-Boot)会**静默从 Undertow 切到 Tomcat**,须回归测试。
---
## 7. 全局异常:SF 7 校验异常体系适配
**背景**:方法级 `@Validated` 校验自 Spring 6.1 起改抛 `HandlerMethodValidationException`(Boot 3.5 的 Spring 6.2 即具备,本次补齐其全局处理);Boot 4 另有错误处理链拆包、静态资源 404 的 `NoResourceFoundException`,以及 `ErrorProperties` 来源从 `ServerProperties` 迁到 `WebProperties`
### 7.1 `blade-starter-log/.../error/BladeRestExceptionTranslator.java` 新增/调整处理器
```java
// 去掉对 Hibernate 内部类 PathImpl 的依赖,改用标准 jakarta.validation.Path
// 改前
import org.hibernate.validator.internal.engine.path.PathImpl;
String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
// 改后
import jakarta.validation.Path;
String path = "";
for (Path.Node node : violation.getPropertyPath()) {
path = node.getName();
}
// 新增 SF7 方法级校验异常(注意:用 getParameterValidationResults(),SF7 无 getAllValidationResults())
@ExceptionHandler(HandlerMethodValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleError(HandlerMethodValidationException e) {
String message = e.getParameterValidationResults().stream()
.flatMap(result -> result.getResolvableErrors().stream()
.map(error -> String.format("%s:%s", result.getMethodParameter().getParameterName(), error.getDefaultMessage())))
.findFirst()
.orElse(ResultCode.PARAM_VALID_ERROR.getMessage());
return R.fail(ResultCode.PARAM_VALID_ERROR, message);
}
// 新增 静态资源 404(Spring 6.1+ 起 ResourceHttpRequestHandler 对缺失静态资源抛此异常,与已移除的 throw-exception-if-no-handler-found 无关)
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public R handleError(NoResourceFoundException e) {
log.warn("404资源不存在:{}", e.getResourcePath());
return R.fail(ResultCode.NOT_FOUND); // 只回 404、不把资源路径回显给客户端(避免信息泄露)
}
// 同时把既有 NoHandlerFoundException 处理器对齐:log.error→log.warn、去掉 e.getMessage() 返回参(只回 ResultCode.NOT_FOUND)
// 新增 上传超限 413
@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
public R handleError(MaxUploadSizeExceededException e) {
return R.fail(ResultCode.FAILURE, "上传文件大小超过系统限制");
}
```
新增 import:`org.springframework.web.method.annotation.HandlerMethodValidationException`、`org.springframework.web.multipart.MaxUploadSizeExceededException`、`org.springframework.web.servlet.resource.NoResourceFoundException`。
### 7.2 `blade-starter-log/.../config/BladeErrorMvcAutoConfiguration.java`——`ServerProperties.getError()` 已移除
Boot 4 把 `ErrorProperties` 的来源从 `ServerProperties` 挪到了 `WebProperties`:
```java
// 改前
import org.springframework.boot.autoconfigure.web.ServerProperties;
private final ServerProperties serverProperties;
return new BladeErrorController(errorAttributes, serverProperties.getError());
// 改后
import org.springframework.boot.autoconfigure.web.WebProperties;
@EnableConfigurationProperties({BladeLogProperties.class, WebProperties.class})
private final WebProperties webProperties;
return new BladeErrorController(errorAttributes, webProperties.getError());
```
---
## 8. MyBatis-Plus 3.5.17 包迁移(易漏)
**背景**:MyBatis-Plus 3.5.17 起把 `IService` / `ServiceImpl``com.baomidou.mybatisplus.extension.service.*` 迁到了 **`com.baomidou.mybatisplus.spring.service.*`**(位于 `mybatis-plus-spring` 模块)。
**`blade-starter-mybatis/pom.xml` 显式声明 `mybatis-plus-extension`**(本模块直接使用 `MybatisPlusInterceptor`、分页/租户插件等 extension 类;虽经 `mybatis-plus-spring` 仍可传递获得,按"直接使用即显式声明"补齐更稳健):
```xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
```
**② 源码 import 迁移**(blade-tool 内 5 处,业务工程凡用到 `IService`/`ServiceImpl` 同样要改):
```java
// 改前
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
// 改后
import com.baomidou.mybatisplus.spring.service.IService;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
```
受影响文件:`BaseService`、`BaseServiceImpl`(blade-starter-mybatis)、`TenantGuard`(blade-starter-tenant)、`IReportFileService`、`ReportFileServiceImpl`(blade-starter-report)。
> `com.baomidou.mybatisplus.extension.plugins.*`(拦截器、分页、租户)**未迁包**,不要改。
---
## 9. Swagger:移除 Knife4j,改用 springdoc 3.0.3 原生 UI
**背景**:Knife4j 最新 4.5.0 仅绑定 springdoc 2.x / Boot 2.2~3.x,无 Boot 4 版且已停更,Boot 4 下不可用。
**`blade-starter-swagger/pom.xml`**:
```xml
<!-- 改前 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- 改后 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
```
**② 删除** vendored 的 knife4j 影子类 `blade-starter-swagger/src/main/java/com/github/xiaoymin/knife4j/spring/extension/Knife4jOpenApiCustomizer.java`(整个 `com/github/xiaoymin` 目录树)。
**`SwaggerLauncherServiceImpl.java`**:删掉 `knife4j.enable` / `knife4j.production` 两处属性写入(prod/dev 两个分支都删),`springdoc.*` 开关已完整覆盖启停。同时清掉两处历史遗留:
- `springdoc.api-usage.enabled`——springdoc 并无此属性(宽松绑定下静默无效),两个分支都删。
- dev 分支的 `spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER`——这是 springdoc 2 / Boot 2.6 时代的旧 workaround,springdoc 3 + Boot4 原生走 `PathPatternParser`,且它只在非 prod 设置会导致 dev 用 AntPathMatcher、prod 用 PathPatternParser,两套路径匹配语义不一致(尾斜杠、`{*path}` 通配、矩阵变量),必须删除让各环境统一。
**`SwaggerWebConfiguration.java`**:删掉 `doc.html``/webjars/**` 资源映射(springdoc 自托管),连带去掉 `implements WebMvcConfigurer``addResourceHandlers` 方法;**保留** `@AutoConfiguration` + `@BladePropertySource("classpath:/blade-swagger.yml")`(它负责加载默认配置)。
**`blade-swagger.yml`**:删掉整段 `knife4j:` 配置;`springdoc.swagger-ui` 的 `tags-sorter` / `operations-sorter` **均不设**——接口与分组顺序改由 BladeX 自研的 `@ApiOrder` 控制(见 §9⑧),设 `alpha` / `method` 会在前端覆盖自定义顺序。
> `SwaggerAutoConfiguration.java` 的 springdoc 依赖(`io.swagger.v3.oas.models.*` 与 `org.springdoc.core.*`)在 3.0.3 下包路径一致、**无需迁移改动**;但⚠️ 它并非"零改动"——本次它正是 `@ApiOrder` 排序引擎的实现载体(见 §9⑧):新增 `GlobalOperationCustomizer`(写 `x-order`)、`GlobalOpenApiCustomizer`(按序重排 paths/tags)及 ASM 字节码行号读取等逻辑,是本节最大的一处代码新增。
> **注意**:springdoc 3.0.3 内部经 swagger-core 仍依赖 Jackson 2,须配合 §5 的 `spring-boot-jackson2` 在 classpath,否则 Swagger-UI 启动报 Jackson 版本错。
**⑥ 安全放行(springdoc 路径)**:springdoc UI 与规格端点需匿名可达。框架默认放行清单 `blade-core-secure``SecureRegistry.defaultExcludePatterns` 已含 `/v3/api-docs/**`、`/swagger-ui/**`、`/swagger-ui.html`(`/swagger-ui/**` 为 UI 静态资源,`/swagger-ui.html` 为跳转入口),下游 servlet 应用继承即可。若应用另有自建放行清单(Boot 单体的 `BladeConfiguration`、Cloud 网关的 `AuthProvider`),须同步补齐这三项,并去掉 `/doc.html`、`/swagger-resources/**` 等 knife4j / springfox 旧路径。
**⑦ 代码生成器模板**:`blade-starter-develop/src/main/resources/templates/controller.java.vm` 里的 `@ApiOperationSupport` import 与注解一并删除——它是 `.vm` 资源,下游按 `*.java` 批量清理的脚本覆盖不到,须单独处理,否则该生成器产出的 Controller 引用已移除的 knife4j 包、编译不过(排序改用 §9⑧ 的 `@ApiOrder`)。删干净后**还须在模板 `@Tag` 上方补一行无参 `@ApiOrder` + 其 import**(`org.springblade.core.swagger.annotation.ApiOrder`),让生成的 Controller 与手写控制器一致带排序(见下「落地约定」);下游 Boot(`src/main/resources/templates/`)、Cloud(`blade-ops/blade-develop/src/main/resources/templates/`)的同名模板副本一并补。`src/test/resources/templates/` 下的旧扁平路径 `.vm` 副本自模板重组为 `api/` 子目录后不再被加载,应直接删除,test 侧仅保留 `code.properties` 作为数据源配置覆盖点。
**⑧ 接口排序:`@ApiOrder`(替代 knife4j 的 `@ApiOperationSupport(order = n)`)**
springdoc / OpenAPI 3 无接口排序原生注解,blade-starter-swagger 新增 `org.springblade.core.swagger.annotation.ApiOrder`:
- **方法上** `@ApiOrder(n)`:该接口显式序号,值越小越靠前。
- **类上** `@ApiOrder(n)`:`n` 为该控制器分组(`@Tag`)的序号,类内接口按源码声明顺序排列;个别方法可再标方法级注解精确覆盖。
- 未标注:回落路径 / tag 名字母序。
实现在 `SwaggerAutoConfiguration`:`GlobalOperationCustomizer` 读注解写入 `x-order` 扩展(类级用字节码行号取声明序,反射不保证方法顺序)并记录「tag → 类级序号」;`GlobalOpenApiCustomizer` 按 `x-order` 重排 `paths`、按类级序号重排顶层 `tags`。swagger-ui 仅在两个 sorter 未设时保留 spec 顺序(§9⑤),并须保持 `springdoc.writer-with-order-by-keys` 默认 `false`
**落地约定(本次统一)**:三工程**每个带 `@Tag` 的控制器**均在 `@Tag` 上方补一行**无参 `@ApiOrder`**(值取默认 `Integer.MAX_VALUE`),方法体只保留 `@Operation`——效果是分组内接口按**源码声明顺序**展示、各 `@Tag` 分组之间因类级序号相等而回落 tag 名字母序;仅当个别接口要打破源码顺序时,才在该方法上补 `@ApiOrder(n)` 精确覆盖。删 knife4j 排序注解(§3.1 / §4.2)后**只删不补**会让接口在 springdoc UI 下按字母序、丢失原有顺序,故「删旧 + 补 `@ApiOrder`」是同一件事的两半。代码生成器模板 `controller.java.vm`(blade-tool `blade-starter-develop` 源模板 + 下游 Boot/Cloud `blade-develop` 的 main 副本,共 3 份)同步在 `@Tag` 上方生成 `@ApiOrder`,新生成的 Controller 开箱即带排序。
> 提示:类级 `@ApiOrder(n)` 的 `value` 用作 `@Tag` 分组之间的排序序号(`SwaggerAutoConfiguration#recordTagOrder` 写入、`GlobalOpenApiCustomizer` 据此重排顶层 `tags`);分组内各接口不受此值影响,按源码声明顺序排列。本次全量采用**无参**形式,`value` 恒为默认值、各分组落 tag 名字母序;如需固定分组顺序,给对应控制器标 `@ApiOrder(n)` 即可。
---
## 10. Boot 4 BOM 移除的坐标(自管版本)
| 坐标 | Boot 4 状况 | 处置 |
|------|------------|------|
| `spring-boot-starter-aop` | 已移除 | 换 `spring-boot-starter-aspectj`(§3.3) |
| `org.springframework.retry:spring-retry` | BOM 不再托管 | 自管 2.0.13(§3.2),`RetryConfiguration` 无源码改动 |
| `org.apache.httpcomponents:httpclient`(HttpClient 4) | 非 BOM 托管(Boot BOM 仅托管 HttpClient 5) | 自管 4.5.14(§3.2),`blade-starter-social` 的 JustAuth 依赖它 |
---
## 11. Druid / 数据源坐标
`blade-starter-mybatis/pom.xml``blade-core-boot/pom.xml`:
```
druid-spring-boot-3-starter → druid-spring-boot-4-starter
```
`blade-core-boot/pom.xml` 另需:`mybatis-plus-spring-boot3-starter → mybatis-plus-spring-boot4-starter`。
> Boot 4 把 JDBC 从 web starter 拆出;本工程用数据源的模块通过 `druid-spring-boot-4-starter` 已传递 `spring-boot-jdbc`,无需单独声明。
### 11.1 Nacos 客户端(Cloud 微服务)
`blade-core-cloud/pom.xml` 重新声明的 `com.alibaba.nacos:nacos-client` 上排除 `com.alibaba.nacos:logback-adapter`,并把 `alibaba.nacos.version` 升到 `3.2.2`。Boot4 托管的 logback 版本较新,Nacos 自带的 logback 适配器与之不匹配,不排除时启动期存在绑定失败风险:
```xml
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>logback-adapter</artifactId>
</exclusion>
</exclusions>
</dependency>
```
---
## 12. 编译验证
```bash
# 全量 clean install(务必带 clean,重新生成 spring.factories / *.imports)
mvn clean install -DskipTests -Dmaven.test.skip=true
```
通过标准:24 个模块全部 `SUCCESS`。验证生成物:
```bash
# 1. EnvironmentPostProcessor 注册键为新的根包键
cat blade-starter-loadbalancer/target/classes/META-INF/spring.factories
# 期望:org.springframework.boot.EnvironmentPostProcessor=...GrayscaleEnvPostProcessor
# 2. JacksonLauncherServiceImpl 已注册
cat blade-core-tool/target/classes/META-INF/services/org.springblade.core.launch.service.LauncherService
```
---
## 13. 运行期验证要点(交由真实环境执行)
编译通过不等于运行正确,升级后重点验证:
1. **JSON 序列化**:`@BladeView` 视图过滤、大数转字符串、null 转空、读写分离转换器是否生效(确认活跃转换器为 `MappingApiJackson2HttpMessageConverter`)。
2. **MyBatis-Plus + Druid** 在 Boot 4 下的自动装配与 SQL 执行。
3. **Web 容器** 已切 Tomcat,压测吞吐特征与 Undertow 时代不同。
4. **spring-retry 2.0.13 / Seata / springdoc UI** 在 Spring 7 运行期兼容。
5. **UReport 报表模块**(`blade-starter-report`)编译通过,运行期需验证。
---
## 14. 下游工程连锁影响(重要)
blade-tool 发版后,下游 SpringBlade Cloud / Boot 通过 `blade-core-bom` 自动获得整套 Boot 4 版本管理。但下游仍需自行处理:
- **MyBatis-Plus 包迁移(⚠️ 静默运行期风险)**:MyBatis-Plus 3.5.17 把 `IService`/`ServiceImpl` 从 `com.baomidou.mybatisplus.extension.service.*` 迁到了 `com.baomidou.mybatisplus.spring.service.*`。业务代码里凡 `import com.baomidou.mybatisplus.extension.service.IService/ServiceImpl` 都要改到 `spring.service`(§8)。**升级前务必全量 grep 下游二开工程**——任何仍编译在旧包上的二开模块或第三方库会在运行期抛 `NoClassDefFoundError`;若二开生态面广、一时改不完,可临时把 MyBatis-Plus 钉在 `3.5.16`(该版本仍保留旧 `extension.service` 包),推迟这次迁移。
- **Web 容器切 Tomcat**:静默继承,须回归。下游工程**自己的** yml 里若有 `server.undertow.*`(如 `application.yml`、`bootstrap.yml`、Nacos 配置模板)也要一并换成 `server.tomcat.*`,否则那段是被忽略的死配置、原线程调优静默失效;`logback` 里的 `io.undertow`/`org.xnio` 日志器可换成 `org.apache.catalina`/`org.apache.coyote`。
- **Knife4j 彻底移除**(无 Boot4 版):下游须一并删干净——`knife4j-openapi3-ui` / `knife4j-gateway-spring-boot-starter` 依赖改 springdoc 3.0.3、`doc.html` 入口去除、yml 里的 `knife4j:` 配置整段删除、业务控制器里的 `@ApiOperationSupport` / `@ApiSort` 注解与 import 全部删除(不保留 `knife4j-annotations` 兼容包)。接口排序改用 BladeX 的 `@ApiOrder`(见 §9⑧)。
- **各自直接依赖**:如 `dynamic-datasource-spring-boot3-starter`(Cloud)需换 boot4 版。
(下游 Cloud / Boot 的详细升级步骤见同目录另两份文档。)
---
## 15. Spring 7 空值注解迁移(JSpecify)
> 本次升级已执行该迁移(采用下文**方案一 · 机械替换**)。本节记录背景、映射、落地范围与验证结果。
### 背景
Spring Framework 7 把自带的空值注解 `org.springframework.lang.@Nullable / @NonNull / @NonNullApi / @NonNullFields` 全部标记为 `@Deprecated(since = "7.0")`(**仅弃用,未标 forRemoval**),官方转向业界标准 **JSpecify**(`org.jspecify.annotations.*`)。
**官方依据**(可直接查证):
- Spring 随源码发布的 `org.springframework.lang.Nullable` javadoc 明写 `@deprecated use {@link org.jspecify.annotations.Nullable} instead`,类上带 `@Deprecated(since = "7.0")`;且该包 `package-info.java` 自身已改用 `@org.jspecify.annotations.NullMarked`(即 Spring 官方源码内部也已切到 JSpecify)。
- Spring Framework 参考文档 · Null-safety:https://docs.spring.io/spring-framework/reference/core/null-safety.html
- Spring 官方博客《Null-safe applications with Spring Boot 4》(2025-11-12):https://spring.io/blog/2025/11/12/null-safe-applications-with-spring-boot-4/
- 立项 issue《Migrate to JSpecify annotations for nullability constraints》#28797:https://github.com/spring-projects/spring-framework/issues/28797
官方在上述博客中明确**建议整个生态**(含 Reactor、Micrometer 及依赖 Spring 的各库)统一采用 JSpecify——这正是本次把 Reactor / Jakarta / JetBrains 等其他库的空值注解也一并归一到 JSpecify 的依据。
### 依赖
JSpecify **1.0.0** 已由 Boot 4.1 BOM 托管,并经 `spring-core` 以 compile 作用域传递到 classpath——**迁移无需新增任何依赖**。
### 完成情况
本次分两步,把三工程**所有**空值注解统一到 JSpecify:
**第一步 · Spring 弃用注解迁移**(`org.springframework.lang.*` → `org.jspecify.annotations.*`)
- **blade-tool**:60 个文件、63 处 import(56 `@Nullable` + 7 `@NonNull`);无包级 `@NonNullApi/@NonNullFields`,故无需 `@NullMarked`
- **Cloud**:1 个文件(`blade-gateway/.../filter/GatewayFilter.java`)。
- **Boot**:无 `org.springframework.lang.*` 引用,无需改动。
- 落地方式:import 就地替换,最小 diff;14 处数组 / 可变参数入参 + 4 处数组返回值按 TYPE_USE 校正(见下)。
**第二步 · 其他库空值注解归一**(统一到 JSpecify,消除多套注解并存)
| 文件 | 原注解 | 改后 |
|---|---|---|
| `blade-starter-i18n`:`I18nLocaleResolver` / `I18nAutoConfiguration` | `jakarta.annotation.@Nonnull`(4 处) | `org.jspecify.annotations.@NonNull` |
| `blade-core-cloud`:`OkHttp3ClientHttpRequestFactory` | `org.jetbrains.annotations.@NotNull`(1 处) | `org.jspecify.annotations.@NonNull` |
| Cloud `blade-gateway`:`JwtCrypto` | `reactor.util.annotation.@Nullable`(3 处) | `org.jspecify.annotations.@Nullable` |
> 简单名变化需留意:`@Nonnull` / `@NotNull`(旧库表示"非空")在 JSpecify 中统一为 `@NonNull`,故这几处除换 import 外还需改注解名;`@Nullable` 简单名不变、仅换 import。这几处均为标量参数 / 返回值(无数组),不涉及 TYPE_USE 位移;import 均放到分组内的正确字母序位置。归一后三工程只剩 `org.jspecify.annotations.*` 一套空值注解——共 **68 处 import(57 `@Nullable` + 11 `@NonNull`)**。
### 迁移映射(Spring 弃用注解)
| 旧(`org.springframework.lang`) | 新(`org.jspecify.annotations`) |
|---|---|
| `@Nullable` | `@Nullable` |
| `@NonNull` | `@NonNull`(或在 `@NullMarked` 作用域下直接删除) |
| `@NonNullApi` / `@NonNullFields`(package-info) | `@NullMarked`(包 / 模块级) |
### ⚠️ 关键差异:声明级 → 类型用法级(TYPE_USE)
JSpecify 的注解是 **`@Target(TYPE_USE)`**,Spring 的是 `@Target({METHOD, PARAMETER, FIELD})` 声明级。类型用法注解必须**紧贴类型**,以下位置换 import 后需手工校正:
- **返回值**:`public @Nullable String get()`——注解须紧贴返回类型,不能放在 `public` 等修饰符之前。
- **数组**:`String @Nullable []`(数组本身可空)与 `@Nullable String[]`(元素可空)语义不同,TYPE_USE 能区分、声明级不能。
- **泛型**:`List<@Nullable String>` 只有 TYPE_USE 能标注类型参数。
简单的字段 / 参数 / 普通返回类型可直接换 import(视觉位置不变);上述三类位置需调整。换完**必须编译核验**,并把 IDE 的空值分析规则切到 JSpecify。
**本次实际处理**:14 处数组 / 可变参数入参(全在 `blade-core-tool``JsonUtil` / `Func` / `CollectionUtil` / `DigestUtil` / `StringUtil` 共 13 个方法,其中 `DigestUtil.slowEquals` 的两个 `byte[]` 入参各计一处),以及 4 处 `byte[]` 数组返回值(`AesUtil.decryptFormHex` / `decryptFormBase64`、`HexUtil.decode`、`RedisKeySerializer.serialize`),语义均为"该数组 / 可变参数 / 返回值可为 null",故按 TYPE_USE 规则把注解放在数组位:
```java
// 换 import 后仍需手工校正的数组位(否则语义从"数组可空"漂移成"元素可空")
public static <T> T readValue(byte @Nullable [] content, Class<T> valueType) // 入参:由 @Nullable byte[] content 调整
public static String format(@Nullable String message, Object @Nullable ... arguments) // 可变参数同理
public static byte @Nullable [] decryptFormHex(@Nullable String content, String aesTextKey) // 返回值:注解须在数组位,不能置于 public 修饰符前
```
> **能这么放心机械替换的根因**:Spring 的 `@Nullable/@NonNull` 是**声明级**(`@Target({METHOD, PARAMETER, FIELD})`),所以现存用法必然只落在方法 / 参数 / 字段声明上——这些位置 JSpecify 的 TYPE_USE 注解都能绑定到对应类型,编译等价;唯独数组 / 可变参数的"元素位 vs 数组位"才有语义差,单独校正即可。其余标量字段 / 参数 / 返回值仅换 import,位置不变。
### 采用方案:方案一 · 机械替换(本次已执行)
- **方案一 · 机械替换(已采用,低风险)**:逐 import 替换 `org.springframework.lang.Nullable → org.jspecify.annotations.Nullable`、`NonNull → org.jspecify.annotations.NonNull`,并按上文校正数组 / 可变参数的 TYPE_USE 位置。语义等价,脚本批量完成。
- **方案二 · JSpecify 惯用法(未采用)**:在包级 `package-info.java` 或模块级加 `@NullMarked`(默认一切非空),仅在可空处保留 `@Nullable`、删除显式 `@NonNull`。更贴合 JSpecify 设计,但"默认非空"会暴露隐含可空点、需逐一甄别,风险与工作量更高;可作为后续代码现代化的独立议题。
### 归一边界(哪些不并入 JSpecify)
本次已把三工程内所有**纯元数据**空值注解并入 JSpecify;唯一需排除的是 **`lombok.@NonNull`**——它在编译期**生成运行时判空代码**(非纯元数据),换成 JSpecify 会丢掉该运行时检查、属行为变更。经全量扫描,三工程**均不存在** `lombok.@NonNull`,故本次无此顾虑;后续若引入,不得机械替换。
### 验证
blade-tool(24 模块)、Cloud(23 模块)、Boot 均 `mvn clean install` `BUILD SUCCESS`。全量 grep 复核:三工程空值注解**仅存** `org.jspecify.annotations.*` 一套(68 处 import),零 `org.springframework.lang.*` / `jakarta.annotation` / `reactor.util.annotation` / `org.jetbrains.annotations` 残留,无 `@Nonnull` / `@NotNull` 遗留用法,无重复 import,无元素位数组注解。TYPE_USE 数组语法(`byte @Nullable []`)编译通过;IDE 空值分析规则可切到 JSpecify。
---
## 16. 升级收尾:JDK 21、依赖时效性与打包优化
> 核心迁移完成后同批落地的一批收尾优化。三工程均在 JDK 21 下 `mvn clean install` `BUILD SUCCESS`。
### 16.1 JDK 17 → 21
三工程 `<java.version>` 由 17 抬到 **21 LTS**(Boot 4 最低仍是 17,此为主动升级)。`maven-compiler-plugin` 以 `${java.version}` 驱动 `source`/`target`,改属性即全生效;三份 README 的 JDK 徽章与技术栈表同步改 21+。**升级后构建需 JDK 21**。
此外,运行期基础镜像须同步升级:Boot 单体 1 个、Cloud 微服务 9 个 `Dockerfile` 的基础镜像由 `bladex/alpine-java:openjdk17_cn_slim` 改为 `openjdk21_cn_slim`(注意注释里的阿里云备用镜像行 `registry.cn-hangzhou.aliyuncs.com/bladex-repo/alpine-java:openjdk17_cn_slim` 也要一并改成 `openjdk21_cn_slim`)——`java.version` 只决定编译目标,基础镜像才决定容器内实际运行的 JRE,两者版本号必须对齐,否则 JDK 21 编出的字节码在 openjdk17 镜像里会 `UnsupportedClassVersionError` 起不来。blade-tool 为纯库工程无 Dockerfile,不涉及。
### 16.2 依赖时效性刷新
对照 Boot 4.1.0 BOM 与 Maven Central 核过一轮第三方版本:
| 依赖 | 处置 | 说明 |
|---|---|---|
| lombok | **去 pin**,回落 BOM `1.18.46` | 本地原 pin `1.18.42` 反而把 BOM 降级了 |
| commons-lang3 | **去 pin** | 原 pin `3.20.0` 与 BOM 完全一致,属冗余自管 |
| guava | `33.5.0-jre``33.6.0-jre` | 非 BOM 托管,自管刷新 |
| maven-compiler-plugin | `3.14.1``3.15.0` | 对齐 Boot 4.1.0 自身构建/BOM 版本 |
| maven-jar-plugin | `3.4.2``3.5.0` | 对齐 Boot 4.1.0 BOM |
| disruptor | **删除孤儿属性** | 全仓零引用的死属性 |
| okhttp / protostuff | **保留** | okhttp 5.x 跨大版本、有硬阻断风险;protostuff 1.8.0 已是最新 GA(Java 21 下依赖 `sun.misc.Unsafe`,Redis 序列化路径建议运行期验证) |
### 16.3 清理未使用的 JAXB 依赖
`blade-core-launch` 原声明 `javax.xml.bind:jaxb-api` + `com.sun.xml.bind:jaxb-core/jaxb-impl`(旧 javax 坐标,当年为 JDK 11+ 补 JAXB)。四维度全量扫描确认三工程**零处使用** JAXB(无 `@Xml*` 注解、无 `JAXBContext`/`Marshaller`/`Unmarshaller`、无 Spring OXM/WebService),属历史死依赖,已删除。删后重复的 JAXB 运行时实现(RI)消除;现代 `jakarta.xml.bind` API 仍由框架传递引入,将来若需 XML 绑定,按 `jakarta.xml.bind:jakarta.xml.bind-api` + `org.glassfish.jaxb:jaxb-runtime` 引入即可(勿用回旧 javax 那套)。保留 `jakarta.activation-api`(现代 Activation 规范,非 JAXB)。此外,父 POM `<dependencyManagement>` 里与旧 JAXB 配套的孤儿托管条目(`javax.xml.bind:jaxb-api`、`com.sun.xml.bind:jaxb-core/jaxb-impl`、`javax.activation:activation`)已无任何模块引用,一并删除。
### 16.4 清理 Boot 4 已移除的死配置键
`blade-core-boot/bootstrap.yml` 删除 `spring.mvc.throw-exception-if-no-handler-found`——该键在 Boot 4 配置元数据里已 `level=error`、无替代(`since 3.2.0`;新默认即抛 `NoHandlerFoundException`,行为不变)。留着是被静默忽略的死键、会被 IDE / 配置校验 / `spring-boot-properties-migrator` 标红,且随核心 starter 传播下游。
### 16.5 javadoc 发布打包:规避模块路径报错
`-P release` 发布时 `maven-javadoc-plugin` 在 JDK 9+ 下走模块路径,会因**内嵌 Tomcat 与 servlet-api 分裂包**、以及**个别依赖自动模块名无法推导**(如 `nacos-client-3.2.2``META-INF/services` 声明了不在同 jar 的 provider 类)而报大量 javadoc 错误(原 `failOnError=false` 已让其非致命)。根治:release profile 的 javadoc 配置加 `<legacyMode>true</legacyMode>`(走类路径、不推导模块描述符),所有 javadoc 报错归零、javadoc jar 正常产出。此为构建期问题,与运行期无关。
### 16.6 `BladeErrorController` 补 `@SuppressWarnings("removal")`
`blade-starter-log/.../error/BladeErrorController#errorHtml` 用到 SF7 标记 `forRemoval``MappingJackson2JsonView`,与 §5.5④ 其余 6 处策略一致,补 `@SuppressWarnings("removal")` 消除告警。
### 16.7 启用 Boot 4.1 进程信息贡献器(可观测性收尾)
Boot 4.1 新增 `ProcessInfoContributor`,可让 `/actuator/info` 携带进程运行时长、启动时间、时区、工作目录等信息(便于 Spring Boot Admin 面板观测线上运行状态)。该贡献器默认关闭,需显式开启 `management.info.process.enabled=true`
BladeX 在 `blade-core-launch``BladeApplication.createSpringApplicationBuilder` 默认 props 块里(紧邻既有 `info.version` / `info.desc` 等 actuator info 默认)加一行:
```java
props.setProperty("management.info.process.enabled", "true");
```
- **放这里的原因**:`spring-boot-starter-actuator` 是父 POM 的全局公共依赖,全下游 app(Cloud + Boot)继承,一处默认惠及所有应用、零下游改动(契合同工程 `JacksonLauncherServiceImpl` / `SwaggerLauncherServiceImpl` 框架级设默认值的惯例)。
- **只开 `process`**:未开 `env`——`management.info.env.*` 会把已解析的配置属性值暴露到 `/actuator/info`,有泄露风险。
- **可覆盖**:命令行 `--management.info.process.enabled=false` 优先级高于 System 属性,可关闭。
- **无对外泄露**:BladeX 默认仅放行 `/actuator/health`(见 `SecureRegistry` / `BladeRequestFilter`),`/info` 端点受访问控制保护。
---
## 17. 方法注释规范统一 + 代码生成器模板同步
> 与 SB4 迁移同批推进的注释治理:按分层收敛方法注释风格,并让代码生成器产出的代码开箱即符合该规范。此节为「改造要求 + 改造记录」,后续新增 / 二开代码及生成器改动均以此为准对照着改。
### 17.1 分层注释规范(改造要求)
| 层 | 规范 | 为什么 |
|---|---|---|
| **Controller** | 方法 javadoc **只留摘要行**,不写 `@param`/`@return` | 接口文档由 springdoc 的 `@Operation`/`@Parameter` 承载,javadoc 标签与其重复且易残缺;摘要行已够 IDE 速览 |
| **Service / Mapper 接口** | 方法 javadoc **必须完整**:每个 `@param` 有意义描述、`@return` 有意义描述,不留空标签;`void` 方法不写 `@return` | 接口是调用方唯一契约来源(实现类 override 方法继承其注释),契约注释残缺会顺 IDE 悬浮提示传播到所有调用点 |
| **ServiceImpl** | public override 方法**不强制**注释(继承接口契约);**private 方法必须完整**(摘要 + `@param`/`@return`) | 私有方法无接口契约兜底,是实现内唯一注释来源 |
| **Entity / VO / DTO / Wrapper** | 不在本轮注释规范范围;字段用 OpenAPI 3 `@Schema`,Wrapper 的 override 方法同 ServiceImpl 处理 | — |
补充硬性要求:
- 描述从架构师视角写,准确、平实、专业;不写口水话、不逐行复述代码、不堆砌黑话。
- `@param` 名与方法签名**严格一致**;多个 `@param` 描述列对齐;**对齐同文件既有规范注释的措辞**,不另造风格。
- 空标签形态(须消除):`@param page`(参数名后为空)、`@return`(空)。完整形态(目标):`@param page 分页参数` / `@return 用户分页数据`
- 接口排序注解 `@ApiOrder`:每个带 `@Tag` 的 Controller 在 `@Tag` 上方加无参 `@ApiOrder`,详见 §9⑧。
### 17.2 改造记录(Boot + Cloud)
- **Controller**:含方法级标签的控制器 javadoc 精简为摘要行(Boot 6 文件 / Cloud 8 文件),删除空 `@param`/`@return`;类级 `@author`/`@since` 不动。
- **Service / Mapper 接口**:填全空 `@param`/`@return`(Boot 23 文件、Cloud 25 文件,约 195 个接口方法),对齐同文件既有措辞;并补齐 1 处**缺失**的 `@param`(Boot `MenuMapper#grantTreeByRole`)。
- **ServiceImpl private 方法**:补全 javadoc——两工程各 `RoleServiceImpl#grantRoleMenu/grantDataScope/grantApiScope`、`TenantServiceImpl#getTenantId`。
- **`void` 方法**:删除多余空 `@return`(如 `IUserService#importUser`,void 无返回值)。
- 复核口径:全改动**仅注释行**(零签名 / 代码 / import 变动),目标层空标签、缺失 `@param`、错配 `@param` 三项全部归零。
- 范围之外(已知待办):Cloud Feign 层 `*Client` 接口与 `ResponseProvider` 尚有空标签,不属本轮四层,后续如需同规格补;`RegionMapper#lazyList/lazyTree` 声明返回 `List<RoleVO>` 而 SQL 实为行政区划,疑历史类型笔误,因禁改签名仅按真实语义写 `@return`
### 17.3 代码生成器模板同步(`blade-starter-develop/src/main/resources/templates/`)
让生成的代码开箱符合 §17.1,并保持 SB4 / 最新 Swagger 风格:
- `controller.java.vm`:`@Tag` 上方 `@ApiOrder` + import(见 §9⑧⑦);方法 javadoc 本即摘要行,无 `@param`/`@return`。
- `service.java.vm` / `mapper.java.vm`:`select{Entity}Page` 空标签补为 `@param page 分页参数` / `@param {entityPath} {comment}查询条件` / `@return {comment}分页数据`
- `serviceImpl.java.vm`:仅 public override 方法、无 private 方法,按规范不加注释。
- 模板已是 SB4 风格:`jakarta.*`、OpenAPI 3(`io.swagger.v3.oas.annotations.*`,含实体 / VO 的 `@Schema`),无 `javax.*` / knife4j / springfox。
- 下游 Boot、Cloud 各自 `blade-develop` 内的 `controller.java.vm` 模板副本同步同一改动(见 §9⑧⑦)。
---
## 18. 代码生成器 `blade-starter-develop` 结构重构与生成器逻辑收敛
> 承接 §17 的模板治理,进一步把生成器模块的**模板目录**与**Java 逻辑**重构到最新形态,并修复 MyBatis-Plus Generator 的过期 API。此节为「改造要求 + 改造记录」,便于后续读取与检验。
### 18.1 模板目录结构(`blade-starter-develop/src/main/resources/templates/`)
- `api/`:9 个后端 `.vm`(controller / entity / entityDTO / entityVO / mapper / mapper.xml / service / serviceImpl / wrapper)。
- `saber/`:`api.js.vm` + `crud.vue.vm`(前端 **Vue3 Composition API + TypeScript** 现代化版)。
- `sql/`:`menu.sql.vm`(菜单初始化 SQL 模板)。
- `code.properties`:生成器参数配置。
### 18.2 生成器 Java 逻辑收敛
`develop` 包收敛后仅保留 `CodeGenerator`(入口)与 `support/BladeCodeGenerator`(配置):
- `BladeCodeGenerator`:后端模板路径指向 `/templates/api/*`、前端 `/templates/saber/{api.js,crud.vue}.vm`;移除按前端类型的多目录分流逻辑(生成器统一使用 `saber` 前端模板)与 `DevelopConstant` 依赖;菜单 SQL 经 `customFile`(`menu.sql`)+ `outputCustomFile` 接线,用 `IdWorker` 生成 5 个菜单 ID(主 / 增 / 改 / 删 / 查),输出到 `{outputDir}/sql/{entity}.menu.sql`。**注:`systemName` 字段予以保留**(默认值由 `DevelopConstant.SABER3_NAME` 改为字面量 `"Saber"`),因下游 Boot/Cloud 的 `CodeController` 与测试 `CodeGenerator` 仍调用 `setSystemName(...)`——保留该字段可让下游**零改动**编译通过,只是它不再参与前端模板分流。
- `CodeGenerator`:移除 `SYSTEM_NAME``setSystemName`
- `DevelopConstant.java`:整文件删除(全仓零引用)。
### 18.3 修复 MyBatis-Plus Generator 3.5.8 过期 API(`templateConfig` → builder 级 `*Template`)
- **过期点**:`TemplateConfig.Builder` 的 `entity/service/serviceImpl/mapper/xml/controller` 六个模板路径方法在 3.5.8 已 `@Deprecated`
- **改法**(参照 BladeX-Tool 商业版思路、以本工程 Velocity/`.vm` 自行适配,非照搬其 Beetl 源码):删除 `.templateConfig(builder -> builder.disable(TemplateType.ENTITY).entity(...) … .controller(...))`,改用 `strategyConfig` 各 builder 的非过期方法:
- `entityBuilder().javaTemplate("/templates/api/entity.java.vm")`
- `serviceBuilder().serviceTemplate(…service.java.vm).serviceImplTemplate(…serviceImpl.java.vm)`
- `mapperBuilder().mapperTemplate(…mapper.java.vm).mapperXmlTemplate(…mapper.xml.vm)`
- `controllerBuilder().template(…controller.java.vm)`
并移除随之无用的 `import …generator.config.TemplateType;`
- **语义等价依据**:生成引擎运行时直接读取 builder 级字段(`AbstractTemplateEngine` 用 `entity.getJavaTemplate()`、`mapper.getMapperTemplatePath()` 等);`VelocityTemplateEngine.templateFilePath` 对 `.vm` 结尾做条件判断(已带则不再追加),故沿用带 `.vm` 的完整路径安全;旧 `.disable(TemplateType.ENTITY)` 本被紧随的 `.entity(...)`(内部置 `disableEntity=false`)抵消,移除对生成行为无影响。
### 18.4 验证
- JDK 21 下 `mvn -pl blade-starter-develop compile` **成功**,`-Dmaven.compiler.showDeprecation=true` 对 `BladeCodeGenerator` **无 deprecation 告警**
- 代码引用的后端(`api/`)、前端(`saber/`)与菜单 SQL(`sql/`)模板路径与磁盘一一对应;无 `DevelopConstant` 残留。`systemName` 字段**按下游 API 兼容需要保留于 `BladeCodeGenerator`**(仅前端分流逻辑移除,见 §18.2),不属残留。
- 注:`saber/crud.vue.vm`(Vue3+TS)属采纳的现代化模板,运行期生成未连库实测;`beforeOutputFile` 里历史遗留的 `System.out.println` 与本次重构无关,按最小改动保留。
### 18.5 生成物 POJO 包结构(entity / dto / vo 归入 `pojo/`)
旧版生成的 `entity/`、`dto/`、`vo/` 平铺在模块根;5.0.0 起统一归入 `pojo/` 子包(`wrapper` / `mapper` / `controller` / `service`(含 `service/impl`)位置不变)。以 `notice` 为例:
```
notice/pojo/entity/Notice.java package …notice.pojo.entity
notice/pojo/dto/NoticeDTO.java package …notice.pojo.dto
notice/pojo/vo/NoticeVO.java package …notice.pojo.vo
notice/wrapper/NoticeWrapper.java package …notice.wrapper (不动)
notice/{mapper,controller,service,service/impl}/… (不动)
```
实现(`BladeCodeGenerator` + 2 个模板;entity/dto/vo 三模板不涉此项):
- `packageConfig` 的 entity 子包设为 **`.entity("pojo.entity")`**(与 `.serviceImpl("service.impl")→service/impl/` 同机制,MP 把点转成目录层级)。
- `outputCustomFile` 里 dto、vo 两处自定义输出路径**各含一层 `pojo/`**;wrapper 输出路径在模块根。
- dto/vo 的包由模板既有的 `$package.Entity.replace("entity","dto"/"vo")` 自动推导(entity 为 `pojo.entity` 时得 `pojo.dto`/`pojo.vo`),这三个模板无需为此改动。
- `wrapper.java.vm``controller.java.vm` 内 wrapper 包的推导用 **`$package.Entity.replace("pojo.entity","wrapper")`**,使 wrapper 落在模块根(而非 `pojo.wrapper`);其对 entity/vo 的 import 自动指向 `pojo.*`
**下游连锁**:Boot/Cloud 各带一套本地模板副本,在 classpath 上遮蔽 blade-tool jar 内模板 —— 下游实际用本地副本生成,故本节与 §18.1 的目录形态需在下游本地副本上一并对齐(目录 `api/`/`saber/`、wrapper 推导同上),否则要么命不中本地副本(回落 jar 内模板)、要么 wrapper 包路径与目录不符而编译失败。
**验证**:`blade-starter-develop` JDK21 `clean install` 通过,jar 内模板仅 `api/`(9)+`saber/`(2);notice 结构推演 dto→`pojo.dto`、vo→`pojo.vo`、wrapper→根 三处「包↔路径」一致。
> ⚠️ 维护提示:`.vm` 是 Velocity 模板,常被 IDE 当 Java 误格式化(如把 `package $!{package.Entity};` 拆行、`#foreach($pkg in $!{table.importPackages})` 打断而生成坏代码)。改动后务必核对指令配平(`#if`+`#foreach` 数 == `#end` 数)与实际产物,勿让编辑器自动重排 `.vm`。