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

68 KiB
Raw Blame History

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-webspring-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 全工程的 @AutoConfigurationEnvironmentPostProcessorLauncherService 都靠 mica-auto 在编译期生成 META-INF/spring/*.importsMETA-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> 版本属性

<!-- 改前 -->
<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):

<!-- 删除整块 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-framework-bom</artifactId>
    <version>${spring.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

删除 knife4j-dependencies import:

<!-- 删除整块 -->
<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:

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 不再托管):

<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>

<!-- 改前: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

// 改前
import org.springframework.boot.env.EnvironmentPostProcessor;
// 改后(Boot 4 该接口上移根包)
import org.springframework.boot.EnvironmentPostProcessor;

验证:mvn clean install 后检查生成物,键应为新的根包键:

# 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.*,部分类还顺带改名(如 RedisAutoConfigurationDataRedisAutoConfiguration)。旧 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/Afterautoconfigure.condition.ConditionalOn* / SearchStrategyautoconfigure.SpringBootApplication / EnableAutoConfigurationautoconfigure.web.ErrorPropertiesweb.error.ErrorAttributeOptionsweb.servlet.FilterRegistrationBean / ServletRegistrationBean未迁包

5.3 逐文件 import 迁移(blade-tool 实际受影响清单)

文件 改动
blade-core-launch/.../server/ServerInfo.java ServerPropertiesweb.server.autoconfigure
blade-core-launch/.../StartEventListener.java WebServerInitializedEventweb.server.context
blade-core-cloud/.../version/BladeWebMvcRegistrations.java WebMvcRegistrationswebmvc.autoconfigure
blade-core-cloud/.../version/VersionMappingAutoConfiguration.java WebMvcRegistrationswebmvc.autoconfigure
blade-core-cloud/.../http/RestTemplateConfiguration.java RestTemplateBuilderrestclient;并把 restTemplate()/lbRestTemplate() 改为链式 requestFactory(...).build()(Boot 4 的 RestTemplateBuilder 不可变,旧的"先 requestFactory(...) 再单独 build()"两行式会丢弃携带 OkHttp3 工厂的 builder,顺带修掉该隐患)
blade-core-tool/.../config/RedisConfiguration.java RedisAutoConfigurationDataRedisAutoConfiguration(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 / CachePropertiescache.autoconfigure
blade-starter-log/.../config/BladeErrorMvcAutoConfiguration.java 错误类拆两包 + ServerProperties(见 §7.2)
blade-starter-log/.../error/BladeErrorController.java BasicErrorControllerwebmvc.autoconfigure.error;ErrorAttributeswebmvc.error
blade-starter-log/.../error/BladeErrorAttributes.java DefaultErrorAttributeswebmvc.error
blade-starter-transaction/.../annotation/SeataCloudApplication.java DataSourceAutoConfigurationjdbc.autoconfigure
blade-core-tool/.../config/JacksonConfiguration.java Jackson 2 降级重构(见 §5.5)

改名类(RedisAutoConfiguration → DataRedisAutoConfiguration)示例——import 与注解参数必须一起改:

// 改前
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 新增依赖:

<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 定制器」:

// 改前:自建主 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 转换器即可,其余转换器原样保留。

@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 集成类(AbstractJackson2HttpMessageConverterMappingJackson2HttpMessageConverterMappingJacksonValueJackson2ObjectMapperBuilderCustomizer 等)被标记为 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:

@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-redisRedisPubSubConfiguration同样注册一个同名 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 容器确定性胜出:

// 改前
@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 / RedisTemplateConfigurationbefore= 是因类改名(RedisAutoConfigurationDataRedisAutoConfiguration)顺带修正既有 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:

<!-- 改前 -->
<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.*:

# 改前
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.undertoworg.apache.catalinaorg.xnio.nioorg.apache.coyote

blade-core-boot/src/main/resources/bootstrap.yml——Servlet 编码配置改前缀 server.servlet.encodingspring.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,比任何博客都权威。

# 改前(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

键名一一对应,仅前缀 serverspring: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 新增/调整处理器

// 去掉对 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.HandlerMethodValidationExceptionorg.springframework.web.multipart.MaxUploadSizeExceededExceptionorg.springframework.web.servlet.resource.NoResourceFoundException

7.2 blade-starter-log/.../config/BladeErrorMvcAutoConfiguration.java——ServerProperties.getError() 已移除

Boot 4 把 ErrorProperties 的来源从 ServerProperties 挪到了 WebProperties:

// 改前
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 / ServiceImplcom.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 仍可传递获得,按"直接使用即显式声明"补齐更稳健):

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
</dependency>

② 源码 import 迁移(blade-tool 内 5 处,业务工程凡用到 IService/ServiceImpl 同样要改):

// 改前
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;

受影响文件:BaseServiceBaseServiceImpl(blade-starter-mybatis)、TenantGuard(blade-starter-tenant)、IReportFileServiceReportFileServiceImpl(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:

<!-- 改前 -->
<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 WebMvcConfigureraddResourceHandlers 方法;保留 @AutoConfiguration + @BladePropertySource("classpath:/blade-swagger.yml")(它负责加载默认配置)。

blade-swagger.yml:删掉整段 knife4j: 配置;springdoc.swagger-uitags-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-secureSecureRegistry.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 → 类级序号」;GlobalOpenApiCustomizerx-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.xmlblade-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 适配器与之不匹配,不排除时启动期存在绑定失败风险:

<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. 编译验证

# 全量 clean install(务必带 clean,重新生成 spring.factories / *.imports)
mvn clean install -DskipTests -Dmaven.test.skip=true

通过标准:24 个模块全部 SUCCESS。验证生成物:

# 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/ServiceImplcom.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.ymlbootstrap.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.*)。

官方依据(可直接查证):

官方在上述博客中明确建议整个生态(含 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-toolJsonUtil / Func / CollectionUtil / DigestUtil / StringUtil 共 13 个方法,其中 DigestUtil.slowEquals 的两个 byte[] 入参各计一处),以及 4 处 byte[] 数组返回值(AesUtil.decryptFormHex / decryptFormBase64HexUtil.decodeRedisKeySerializer.serialize),语义均为"该数组 / 可变参数 / 返回值可为 null",故按 TYPE_USE 规则把注解放在数组位:

// 换 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.NullableNonNull → 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-jre33.6.0-jre 非 BOM 托管,自管刷新
maven-compiler-plugin 3.14.13.15.0 对齐 Boot 4.1.0 自身构建/BOM 版本
maven-jar-plugin 3.4.23.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-apicom.sun.xml.bind:jaxb-core/jaxb-impljavax.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.2META-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 标记 forRemovalMappingJackson2JsonView,与 §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-launchBladeApplication.createSpringApplicationBuilder 默认 props 块里(紧邻既有 info.version / info.desc 等 actuator info 默认)加一行:

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/grantApiScopeTenantServiceImpl#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_NAMEsetSystemName
  • DevelopConstant.java:整文件删除(全仓零引用)。

18.3 修复 MyBatis-Plus Generator 3.5.8 过期 API(templateConfig → builder 级 *Template)

  • 过期点:TemplateConfig.Builderentity/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 级字段(AbstractTemplateEngineentity.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=trueBladeCodeGenerator 无 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.vmcontroller.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