搜索 K
Appearance
Appearance
GlobalResultOperationCustomizer 是 swagger 模块的核心:它把 Springdoc 生成的每个 Operation 的响应 schema 都包一层 Result,让文档与运行时返回结构对齐。
OperationCustomizer 是 Springdoc 提供的标准扩展点。Springdoc 在扫描完 Controller、生成 Operation 对象之后,会依次调用所有注册的 OperationCustomizer.customize(operation, handlerMethod),让外部代码有机会对每个接口的文档进行二次加工。
每个 customizer 都可以声明 Ordered.getOrder(),Springdoc 按 order 升序调用。本模块返回 Ordered.LOWEST_PRECEDENCE,确保自己最后执行——其它定制器(例如分页、权限注解扩展)先把 schema 处理完,再统一包一层 Result。
if (isRawResponse(handlerMethod)) {
return operation;
}
private boolean isRawResponse(HandlerMethod handlerMethod) {
if (handlerMethod == null) return false;
if (handlerMethod.getMethod().isAnnotationPresent(RawResponse.class)) return true;
return handlerMethod.getBeanType().isAnnotationPresent(RawResponse.class);
}方法或类上有 @RawResponse 都视为跳过。判断顺序故意是「方法在前、类在后」,与 core 模块的 GlobalResponseAdvice.supports 保持一致的语义。
operation.responses 的 key 可能是 "200"、"4XX"、"default" 之类。本 customizer 只对纯数字状态码(200 / 201 / 400 ...)进行包装,原因:
default 是 OpenAPI 的兜底 schema,往往是模糊定义,包装它意义不大4XX / 5XX 这种通配状态码语义已经超出「响应实体」范畴private boolean isNumericStatusCode(String code) {
if (code == null || code.isEmpty()) return false;
for (int i = 0; i < code.length(); i++) {
if (!Character.isDigit(code.charAt(i))) return false;
}
return true;
}Schema<?> originalSchema = extractOriginalSchema(apiResponse);
if (originalSchema == null || isVoidLike(originalSchema)) {
originalSchema = inferSchemaFromHandlerReturnType(handlerMethod);
}extractOriginalSchema 优先从 apiResponse.content 里找 application/json 媒体类型对应的 schema。找不到或拿到的是 Void、空 ObjectSchema 时,回退到 inferSchemaFromHandlerReturnType 用反射读取 Controller 方法的返回类型。
inferSchemaFromHandlerReturnType 有三层规则:
SwaggerConstants.METHOD_SCHEMA_CONFIG 里硬编码了 searchUsers、getUserStats 等方法名对应的 schema 字段配置。这是给框架自身演示项目用的,业务方一般用不上List<T> 且能拿到泛型实参,构造 ArraySchema,items 用 $ref 指向 T#/components/schemas/SimpleName 引用反射推断的局限
反射读取的是声明类型,对 ResponseEntity<UserDTO>、CompletableFuture<UserDTO> 之类的包装类型会得到外层类。需要这些场景的业务方应该在 Controller 用 springdoc 的 @Schema(implementation = UserDTO.class) 显式声明,customizer 优先使用这种显式信息。
private ObjectSchema buildResultWrappedSchema(Schema<?> originalSchema) {
if (originalSchema == null || isAlreadyResultLike(originalSchema)) {
return SchemaBuilder.buildResultSchema(new ObjectSchema(), null);
}
String originalName = safeSchemaName(originalSchema);
return SchemaBuilder.buildResultSchema(originalSchema, originalName);
}isAlreadyResultLike(schema) 检查 schema 的属性集合是否同时包含 success / code / message / data。如果业务方手动返回 Result<T>,文档生成阶段拿到的 schema 已经长这样,这时再包一层会变成 Result«Result»。逻辑里做了防御:检测到已经是 Result-like,就用空 ObjectSchema 重做一次包装,重置 data 字段。
理想行为应该是「检测到已经是 Result,直接返回不变」。当前的实现是一个偏保守的选择,可能丢掉业务方手填的 data schema 细节。如有强需求可以提 PR 修改 buildResultWrappedSchema 的早退分支。
Result«T» resultSchema.setName(String.format("Result«%s»", originalName));使用 «»(Unicode 法语引号)而不是 <>,是因为 OpenAPI / Swagger UI 在处理尖括号时会与泛型解析、HTML 转义产生冲突。«» 是 swagger-codegen 等工具早期约定的安全替代字符,主流文档工具都能正确显示。
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}为什么放最后:
Object dataExample = null;
if (originalSchema != null) {
dataExample = generateDataExample(originalSchema);
}
resultSchema.setExample(ExampleDataGenerator.generateCompleteResultExample(dataExample));示例数据由 ExampleDataGenerator 生成,目前只覆盖 UserDTO,其它类型给一个空对象。详细规则见 Schema 构建。
| 选项 | 选择 | 理由 |
|---|---|---|
| 拦截位置 | OperationCustomizer | 走 Springdoc 官方扩展点,与版本兼容性好 |
| Order | Ordered.LOWEST_PRECEDENCE | 保证最后包装,不破坏上游工作 |
| @RawResponse 判断顺序 | 方法在前、类在后 | 与 core 模块的 GlobalResponseAdvice 一致 |
| 已是 Result-like 的处理 | 用空 ObjectSchema 重新包装 | 防御性策略,简化逻辑;牺牲一些手填 schema 的精度 |
| 状态码过滤 | 只处理纯数字 | 跳过 default / 4XX 等通配状态码 |
| schema 名 | Result«T» | <> 与 OpenAPI 工具链冲突,«» 是业界惯例 |