🎉 5.0.1.RELEASE 增强角色判定与角色分配归属校验,建立全 TS 工程化底座
Some checks failed
Publish Snapshot / publish (push) Has been cancelled
Java CI / build on java17 (push) Has been cancelled

This commit is contained in:
smallchill 2026-08-11 21:39:37 +08:00
parent 583fd83b88
commit df67adcf3e
6 changed files with 267 additions and 5 deletions

View File

@ -1,5 +1,5 @@
<p align="center">
<img src="https://img.shields.io/badge/Release-V5.0.0-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Release-V5.0.1-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/JDK-21+-green.svg" alt="Build Status">
<img src="https://img.shields.io/badge/license-LGPL%20v3-blue.svg" alt="Build Status">
<img src="https://img.shields.io/badge/Spring%20Cloud-2025.1-blue.svg" alt="Coverage Status">

View File

@ -94,7 +94,7 @@ mybatis-plus:
swagger:
title: SpringBlade 接口文档系统
description: SpringBlade 接口文档系统
version: 5.0.0
version: 5.0.1
license: Powered By SpringBlade
licenseUrl: https://bladex.vip
terms-of-service-url: https://bladex.vip

View File

@ -41,6 +41,16 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--
Jackson 3 库本体。Spring Cloud 与 Sentinel 的多个组件已按「Jackson 3 在场」编写条件装配,
缺席时的表现从静默不注册Sentinel 规则数据源转换器到容器启动失败Feign 分页模型扩展、
Sentinel 熔断规则刷新监听器)不等,且都不属于本工程可修的范围。此处只引入库,
不引入 spring-boot-jackson 自动配置模块,序列化管线仍由 Jackson 2 承载,机制见父 POM 的门禁注释。
-->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Admin -->
<dependency>
<groupId>de.codecentric</groupId>

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.loadbalancer.config;
import net.dreamlu.mica.auto.annotation.AutoEnvPostProcessor;
import org.jspecify.annotations.NonNull;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.DefaultPropertiesPropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.ClassUtils;
import java.util.Map;
/**
* Feign 分页模型 JSON 支持的装配兼容处理
*
* <p>
* Feign Spring Data 的分页与排序模型提供的两个 JSON 扩展仅有 Jackson 3 实现
* 而其装配条件本应校验的 Jackson 模块基类在 Jackson 3 中已改名上游条件退化为恒成立
* 于是只要分页与排序模型在类路径上就会装配Jackson 3 缺席时中断容器启动
* </p>
*
* <p>
* 故仅在 Jackson 3 缺席时给出关闭的默认值该默认值优先级最低下游可显式开启
* Jackson 3 在场时不再干预交由上游条件判定
* </p>
*
* @author Chill
*/
@AutoEnvPostProcessor
public class FeignJacksonEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
/**
* Feign 分页模型 JSON 支持开关
*/
private static final String FEIGN_JACKSON_KEY = "spring.cloud.openfeign.autoconfiguration.jackson.enabled";
/**
* Jackson 3 模块基类上述两个扩展的直接父类
*/
private static final String JACKSON_MODULE_CLASS = "tools.jackson.databind.JacksonModule";
@Override
public void postProcessEnvironment(@NonNull ConfigurableEnvironment environment, SpringApplication application) {
if (ClassUtils.isPresent(JACKSON_MODULE_CLASS, application.getClassLoader())) {
return;
}
Map<String, Object> defaults = Map.of(FEIGN_JACKSON_KEY, Boolean.FALSE.toString());
DefaultPropertiesPropertySource.addOrMerge(defaults, environment.getPropertySources());
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@ -348,7 +348,7 @@ public class JacksonConfiguration {
> ⚠️【务必看清,这是升级中最隐蔽的一个坑】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 转换器即可,其余转换器原样保留。
> 正解是重写 `extendMessageConverters`:它在默认列表构建完成之后回调,拿到的是**完整**的默认转换器,再原地替换 JSON(因 §5.5⑤ 已在托管层剥离 Jackson 3,默认 JSON 转换器为 `MappingJackson2HttpMessageConverter`,属 `AbstractJackson2HttpMessageConverter`)与 String 转换器即可,其余转换器原样保留。
```java
@Override
@ -387,7 +387,28 @@ public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
> 与 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 处理静默失效:
**⑤ 【关键】让 Boot 4 选中 Jackson 2**——Boot 的择一逻辑写在 `org.springframework.boot.http.converter.autoconfigure` 下,读源码可见是**两条并列通路,满足其一即可**(`Jackson2HttpMessageConvertersConfiguration.PreferJackson2OrJacksonUnavailableCondition` 是 `AnyNestedCondition`):
```java
// Jackson 3 分支——三个条件同时成立才注册(javap 实测 JacksonJsonHttpMessageConverterConfiguration)
@ConditionalOnClass(tools.jackson.databind.json.JsonMapper.class)
@ConditionalOnBean(tools.jackson.databind.json.JsonMapper.class) // ← 关键:要 Bean,不只是类
@ConditionalOnProperty(name = "spring.http.converters.preferred-json-mapper",
havingValue = "jackson", matchIfMissing = true)
// Jackson 2 分支——以下任一命中即生效
@ConditionalOnProperty(name = "...preferred-json-mapper", havingValue = "jackson2") // 通路 A
@ConditionalOnMissingBean(JacksonJsonHttpMessageConvertersCustomizer.class) // 通路 B
```
- **通路 B(主)**:Jackson 3 分支要的是**容器里有 `JsonMapper` Bean**,而该 Bean 只由 `spring-boot-jackson` 自动配置模块产出;托管层已把该模块摘掉(见 §5.5⑥),Bean 不存在 → Jackson 3 分支不命中 → Jackson 2 分支的 `@ConditionalOnMissingBean` 成立。这是**编译期事实**,不依赖启动路径。
- **通路 A(兜底)**:`LauncherService` 设 `preferred-json-mapper=jackson2`,覆盖下游未继承 blade 托管层、`spring-boot-jackson` 漏进 classpath 的场景。
> ⚠️ 别把「剥离 Jackson 3」理解成「classpath 上不能有 `tools.jackson`」——**决定管线的是 `JsonMapper` Bean 在不在,不是 Jackson 3 的类在不在**。Jackson 3 的库照常引入不会翻转管线,反而是必需的,理由见 §5.5⑧。
> 两条都留的原因:通路 A 仅在 `BladeApplication` 启动路径执行,`@SpringBootTest`、blade-gateway(不含 blade-core-tool)覆盖不到;且 Jackson 3 的 **XML 转换器分支**(`JacksonXmlHttpMessageConverterConfiguration`)只有 `@ConditionalOnClass` + `@ConditionalOnBean`,**不受 `preferred-json-mapper` 约束**,属性通路结构性覆盖不到。
>
> 关于失效程度需说清:因 §5.5③ 的 `if (!jsonReplaced) converters.add(0, bladeJson)` 兜底,即使两条通路全漏、Jackson 3 转换器占位,blade 转换器仍会插到队首优先命中,**MVC 服务端不会失效**;真正会静默回落到 Jackson 3 的是 `RestClient`/`RestTemplate` 客户端转换器与 WebFlux 编解码器。
新增 `blade-core-tool/.../config/JacksonLauncherServiceImpl.java`:
@ -408,6 +429,92 @@ public class JacksonLauncherServiceImpl implements LauncherService {
}
```
**⑥ 【主通路】托管层剥离 Jackson 3 的自动配置模块(只剥模块,不剥库)**——在父 POM `dependencyManagement``spring-boot-jackson` 从传递链上摘掉,容器里便不会出现 `JsonMapper` Bean,§5.5⑤ 的通路 B 恒成立。**剥离对象仅限这个自动配置模块,`tools.jackson` 系的库本体照常引入**(理由见 §5.5⑧)。**需要两个条目,缺一不可**:
```xml
<!-- 主路径:webmvc、restclient 等多个 starter 都经由 starter-jackson 传递 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
<version>${spring.boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- springdoc 走的是另一条路,webflux-ui / webmvc-ui 两个坐标都要单独排 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
```
> ⚠️ 为什么 springdoc 必须单独排:`springdoc-openapi-starter-common:3.0.3` **直接依赖** `org.springframework.boot:spring-boot-jackson`,而不是经 `spring-boot-starter-jackson` 传递,上面第一个条目的 `<exclusions>` 只约束它自己的子树,管不到 springdoc。可用 `mvn dependency:tree` 单独验证(注意:在完整工程里跑,Maven 的 nearest-wins 会把 springdoc 分支下的重复节点省略,须用只含 springdoc 的最小 POM 或加 `-Dverbose` 才看得见)。
>
> springdoc 内部仅 Spring Data REST 支持类引用 Jackson 3,且被 `@ConditionalOnClass(RepositoryRestConfiguration)` 守卫,排除无副作用。
>
> 子模块**不需要**再写一遍局部 `<exclusions>`:子模块继承父 `dependencyManagement`,下游工程经 `blade-core-bom` 展开同样继承,重复声明纯属冗余。
**⑦ 序列化管线门禁**——托管层排除是"当前依赖树"的快照,第三方库升级随时可能开出新的引入路径(springdoc 直连就是活例)。父 POM `<build><plugins>` 挂一条 enforcer 规则,把漏排从**运行期静默回落**提前成**构建期失败**,并顺带校验托管层排除是否随 `blade-core-bom` 正确传导:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven.enforcer.version}</version>
<executions>
<execution>
<id>ban-jackson3-autoconfigure</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>org.springframework.boot:spring-boot-jackson</exclude>
<exclude>org.springframework.boot:spring-boot-jackson-test</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
```
> **封禁对象只有自动配置模块**,`tools.jackson` 系构件不在范围内——门禁守的是"`JsonMapper` Bean 不许出现",不是"Jackson 3 的类不许出现";把库一起禁掉会连带打死一批按「Jackson 3 在场」编写的第三方组件(见 §5.5⑧)。`spring-boot-jackson` / `spring-boot-jackson-test` 均为精确坐标匹配,**不会**误伤 `spring-boot-jackson2`
>
> 该门禁 Boot 单体 / Cloud 微服务两个下游工程需各自挂一份——它们以 Boot BOM import 方式引用 blade-tool,并非以其为 parent,`<build><plugins>` 不会继承过去。
**⑧ 【必读】Jackson 3 的库本体必须留着(Cloud 侧尤其)**——⑥ 只剥自动配置模块,而 `tools.jackson` 的库是随该模块传递进来的,剥掉模块库也就跟着没了。若不补回来,凡是**编译期直接 `import tools.jackson.*`** 的第三方组件都会踩雷:轻则条件不命中静默降级,重则 `NoClassDefFoundError: tools/jackson/databind/ObjectMapper` 打断启动。Boot 单体不受影响(这些组件只存在于 Cloud 技术栈),Cloud 侧实测清单:
| 组件 | 触发点 | 缺席后果 |
|---|---|---|
| `spring-cloud-circuitbreaker-sentinel` | `CircuitBreakerRuleChangeListener#afterSingletonsInstantiated``SentinelFeignClientProperties#copy()``new tools.jackson.databind.ObjectMapper()` | **容器 refresh 末尾抛错,服务起不来**;所有单例都建完了才炸,日志看着像启动成功 |
| `spring-cloud-gateway-server-webflux` | `RemoveJsonAttributesResponseBodyGatewayFilterFactory` 的实例字段 `private ObjectMapper mapper = new ObjectMapper()`,该 Bean 由 `@ConditionalOnEnabledFilter` 默认开启 | **网关起不来** |
| `spring-cloud-openfeign-core` | `PageJacksonModule` / `SortJacksonModule`,装配条件校验的模块基类在 Jackson 3 改名后退化为恒成立 | **容器启动失败**;见 `FeignJacksonEnvPostProcessor` 的兜底 |
| `spring-boot-admin-client` | `RestClientRegistrationClientConfig#registrationClient` 形参 `ObjectProvider<tools.jackson.databind.json.JsonMapper>` | 配了 `spring.boot.admin.client.url` 才装配,装配即失败 |
| `spring-cloud-alibaba-sentinel-datasource` | `JsonConverter` / `XmlConverter` | 静默不注册,Sentinel 规则数据源转换能力消失 |
| `spring-cloud-starter-alibaba-sentinel` | `SentinelAutoConfiguration$SentinelConverterConfiguration` | 自带 `@ConditionalOnClass` 守卫,整块跳过,同上静默降级 |
这些都不属于本工程可修的范围,逐个关开关是打地鼠——上表还只是**当前版本**的快照,三方库一升级又会冒出新的。所以口径是**留库**:
- `blade-core-cloud``tools.jackson.core:jackson-databind`,覆盖全部微服务(经 `blade-core-boot` 传递)。
- `blade-gateway` 不含 `blade-core-cloud`,需在自己的 POM 里单独引一份,否则会被上表第二行打挂。
- 版本由 Boot BOM 托管(`spring-boot-dependencies` 已 import `tools.jackson:jackson-bom`),不必自设版本属性。
留库**不会**翻转管线:§5.5⑤ 已实测 Jackson 3 转换器分支要的是 `@ConditionalOnBean(JsonMapper)`,而该 Bean 只由被剥离的 `spring-boot-jackson` 产出。`FeignJacksonEnvPostProcessor` 也据此加了 `ClassUtils.isPresent("tools.jackson.databind.JacksonModule")` 守卫——Jackson 3 在场就不干预,交回上游条件判定,只在缺席时才给出关闭默认值。
> 排查手法(升级三方库后建议复跑):对依赖树里的 jar 逐个扫常量池,`tools/jackson` 命中即为硬引用嫌疑,再回源码看有没有条件守卫。用 Python 的 `zipfile` 扫,别用 `strings`——macOS 的 `strings` 会把 `.class``0xCAFEBABE` magic 误判成 Mach-O fat 二进制,扫出空结果。
### 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` 的默认容器。

75
pom.xml
View File

@ -36,7 +36,7 @@
<properties>
<!-- Blade-Tool Version -->
<revision>5.0.0</revision>
<revision>5.0.1</revision>
<module.name>org.springblade.blade.tool</module.name>
<!-- jdk Version -->
<java.version>21</java.version>
@ -60,6 +60,7 @@
<druid.version>1.2.28</druid.version>
<spring.retry.version>2.0.13</spring.retry.version>
<httpclient.version>4.5.14</httpclient.version>
<maven.enforcer.version>3.6.3</maven.enforcer.version>
<spring.boot.version>4.1.0</spring.boot.version>
<spring.boot.admin.version>4.1.1</spring.boot.admin.version>
@ -127,6 +128,23 @@
<version>${mica.auto.version}</version>
<scope>provided</scope>
</dependency>
<!--
业务序列化由 Jackson 2 承载,中间件按各自条件使用 Jackson 3。
消息转换器改用哪一代,取决于容器中是否存在 JsonMapper Bean该 Bean 只由
spring-boot-jackson 产出故此处只剥离该模块Jackson 3 的库照常引入。
webmvc、restclient 等多个 starter 均经 spring-boot-starter-jackson 传递,在此一并收口。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
<version>${spring.boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- blade tool -->
<dependency>
<groupId>org.springblade</groupId>
@ -380,15 +398,28 @@
<artifactId>swagger-annotations-jakarta</artifactId>
<version>2.2.52</version>
</dependency>
<!-- springdoc 直连 spring-boot-jackson不受上方 starter 条目的排除约束,须单独剥离 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>3.0.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- protostuff -->
<dependency>
@ -420,6 +451,12 @@
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.64</version>
</dependency>
<!-- fastexcel -->
<dependency>
<groupId>cn.idev.excel</groupId>
@ -532,6 +569,42 @@
</resource>
</resources>
<plugins>
<!--
序列化管线门禁:把托管层排除的漏排转为构建期失败。
封禁对象仅为 Jackson 3 的自动配置模块,其库不在范围内;
spring-boot-jackson2 为精确坐标匹配,同样不受影响。
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven.enforcer.version}</version>
<executions>
<execution>
<id>ban-jackson3-autoconfigure</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>org.springframework.boot:spring-boot-jackson</exclude>
<exclude>org.springframework.boot:spring-boot-jackson-test</exclude>
</excludes>
<message>
检测到 spring-boot-jackson 进入依赖树。该模块会注册 JsonMapper Bean
使 Boot 4 改用 Jackson 3 的消息转换器,
@BladeView 视图过滤、大数转字符串、日期格式等序列化约定将静默失效。
请执行 mvn dependency:tree -Dincludes='org.springframework.boot:spring-boot-jackson'
定位引入路径,并在父 POM dependencyManagement 对应条目补充排除。
注意tools.jackson 系构件本身不在封禁范围,可以正常引入。
</message>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>