Skip to content

响应包装定制器

GlobalResultOperationCustomizer 是 swagger 模块的核心:它把 Springdoc 生成的每个 Operation 的响应 schema 都包一层 Result,让文档与运行时返回结构对齐。

OperationCustomizer 契约

OperationCustomizer 是 Springdoc 提供的标准扩展点。Springdoc 在扫描完 Controller、生成 Operation 对象之后,会依次调用所有注册的 OperationCustomizer.customize(operation, handlerMethod),让外部代码有机会对每个接口的文档进行二次加工。

每个 customizer 都可以声明 Ordered.getOrder(),Springdoc 按 order 升序调用。本模块返回 Ordered.LOWEST_PRECEDENCE,确保自己最后执行——其它定制器(例如分页、权限注解扩展)先把 schema 处理完,再统一包一层 Result。

整体流程

@RawResponse 早退

java
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 这种通配状态码语义已经超出「响应实体」范畴
java
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 的提取与推断

java
Schema<?> originalSchema = extractOriginalSchema(apiResponse);
if (originalSchema == null || isVoidLike(originalSchema)) {
    originalSchema = inferSchemaFromHandlerReturnType(handlerMethod);
}

extractOriginalSchema 优先从 apiResponse.content 里找 application/json 媒体类型对应的 schema。找不到或拿到的是 Void、空 ObjectSchema 时,回退到 inferSchemaFromHandlerReturnType 用反射读取 Controller 方法的返回类型。

inferSchemaFromHandlerReturnType 有三层规则:

  1. 方法名特化映射SwaggerConstants.METHOD_SCHEMA_CONFIG 里硬编码了 searchUsersgetUserStats 等方法名对应的 schema 字段配置。这是给框架自身演示项目用的,业务方一般用不上
  2. List 泛型识别:如果返回类型是 List<T> 且能拿到泛型实参,构造 ArraySchema,items 用 $ref 指向 T
  3. 普通类型 $ref:非 void 的返回类型,统一用 #/components/schemas/SimpleName 引用

反射推断的局限

反射读取的是声明类型,对 ResponseEntity<UserDTO>CompletableFuture<UserDTO> 之类的包装类型会得到外层类。需要这些场景的业务方应该在 Controller 用 springdoc 的 @Schema(implementation = UserDTO.class) 显式声明,customizer 优先使用这种显式信息。

包装 schema

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

java
resultSchema.setName(String.format("Result«%s»", originalName));

使用 «»(Unicode 法语引号)而不是 <>,是因为 OpenAPI / Swagger UI 在处理尖括号时会与泛型解析、HTML 转义产生冲突。«» 是 swagger-codegen 等工具早期约定的安全替代字符,主流文档工具都能正确显示。

getOrder 的取舍

java
@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

为什么放最后:

  • 其它 customizer(例如 springdoc 自带的、业务方自定义的)可能修改 schema、添加示例
  • 等他们都跑完,我们拿到的就是最终 schema,包一层 Result 不会丢失上游的工作
  • 反之如果我们排在前面,后续 customizer 可能会把 Result schema 当成业务 schema 二次加工,破坏结构

示例数据

java
Object dataExample = null;
if (originalSchema != null) {
    dataExample = generateDataExample(originalSchema);
}
resultSchema.setExample(ExampleDataGenerator.generateCompleteResultExample(dataExample));

示例数据由 ExampleDataGenerator 生成,目前只覆盖 UserDTO,其它类型给一个空对象。详细规则见 Schema 构建

设计取舍

选项选择理由
拦截位置OperationCustomizer走 Springdoc 官方扩展点,与版本兼容性好
OrderOrdered.LOWEST_PRECEDENCE保证最后包装,不破坏上游工作
@RawResponse 判断顺序方法在前、类在后与 core 模块的 GlobalResponseAdvice 一致
已是 Result-like 的处理用空 ObjectSchema 重新包装防御性策略,简化逻辑;牺牲一些手填 schema 的精度
状态码过滤只处理纯数字跳过 default / 4XX 等通配状态码
schema 名Result«T»<> 与 OpenAPI 工具链冲突,«» 是业界惯例