Springfox 3 可以根据 OAS3 API 文档中的 JSR 303 @Min/@Max 生成整数边界吗?

Can Springfox 3 generate Integer boundaries based on JSR 303 @Min/@Max in OAS3 API docs?

我使用 RestController 创建了一个 Spring 引导应用程序,该应用程序根据 JSR 303 验证注释验证传递给 POST 方法的 DTO。 API 文档是使用 Springfox 生成的。

验证已正确应用并显示在 OAS2 API 文档中。然而,它们在 OAS3 API 文档中不完整 - 没有为整数字段生成 minimum/maximum 边界。

我正在使用 Spring Boot 2.5.2(这很重要,因为最新版本 2.6.2 与 Springfox 有问题)和 Springfox 3.0。 0.

由于我在文档中没有发现具体提示 + Springfox 问题跟踪和 JSR303 支持大部分都在工作,我认为这是 Springfox OAS3 支持中的错误或疏忽。与此同时,我找到了一个解决方法,我将 post 作为答案 - 如果我遗漏了什么或有更好的解决方案,我很乐意听到。

详情:

控制器

@Slf4j
@RestController
public class MyController {

    @PostMapping
    public void send(@Valid @RequestBody MyDTO dto) {
        log.info("{}", dto);
    }
}

DTO

@Value
public class MyDTO {
    @Size(max = 200)
    String text;

    @Max(2)
    @Min(1)
    Integer number;

    @Max(4)
    @Min(3)
    int number2;

    @Max(6)
    @Min(5)
    BigDecimal decimal;
}

OAS2 DTO 模式(从 http://localhost:8080/v2/api-docs 中提取)

{
  "MyDTO": {
    "type": "object",
    "properties": {
      "decimal": {
        "type": "number",
        "minimum": 5,
        "maximum": 6,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "number": {
        "type": "integer",
        "format": "int32",
        "minimum": 1,
        "maximum": 2,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "number2": {
        "type": "integer",
        "format": "int32",
        "minimum": 3,
        "maximum": 4,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      },
      "text": {
        "type": "string",
        "minLength": 0,
        "maxLength": 200
      }
    },
    "title": "MyDTO"
  }
}

OAS3 DTO 模式(从 http://localhost:8080/v3/api-docs 中提取)

{
  "schemas": {
    "MyDTO": {
      "title": "MyDTO",
      "type": "object",
      "properties": {
        "decimal": {
          "maximum": 6,
          "exclusiveMaximum": false,
          "minimum": 5,
          "exclusiveMinimum": false,
          "type": "number",
          "format": "bigdecimal"
        },
        "number": {
          "type": "integer",
          "format": "int32"
        },
        "number2": {
          "type": "integer",
          "format": "int32"
        },
        "text": {
          "maxLength": 200,
          "minLength": 0,
          "type": "string"
        }
      }
    }
  }
}

调试Springfox后,我了解到springfox-oas中的class springfox.documentation.oas.mappers.SchemaMapper将“通用模型”转换为OAS3格式。 在“通用模型”中,字段边界由“NumericElementFacet”表示。正在映射的特定 属性 是“Schema”的子class。

问题似乎发生在这里: https://github.com/springfox/springfox/blob/bc9d0cad83e5dfdb30ddb487594fbc33fc1ba28c/springfox-oas/src/main/java/springfox/documentation/oas/mappers/SchemaMapper.java#L385

由“NumberSchema”表示的属性得到正确处理(例如 BigDecimal),它们与“NumericElementFacet”的边界被应用。然而,整数字段(以及进一步的测试表明:还有短和长)由“IntegerSchema”表示,它没有在那里处理,因此边界不会应用于结果 API.

所以我做的解决方法是 subclassing SchemaMapper,post-处理 mapProperties 的结果并将 subclass 注册为 @Primary覆盖 springfox 组件:

import io.swagger.v3.oas.models.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.oas.mappers.*;
import springfox.documentation.schema.*;
import springfox.documentation.service.ModelNamesRegistry;

import java.util.*;

@Primary
@Component
@Slf4j
public class IntegerBoundarySupportingOasSchemaMapper extends SchemaMapper {
    @Override
    @SuppressWarnings("rawtypes")
    protected Map<String, Schema> mapProperties(
            Map<String, PropertySpecification> properties,
            ModelNamesRegistry modelNamesRegistry) {
        var result = super.mapProperties(properties, modelNamesRegistry);

        result.values()
                .stream()
                // "integer" seems to cover at least Java Short, Integer and Long.
                .filter(property -> "integer".equals(property.getType()))
                .forEach(property -> properties.get(property.getName())
                        .getFacets()
                        .stream()
                        .filter(NumericElementFacet.class::isInstance)
                        .map(NumericElementFacet.class::cast)
                        .findFirst()
                        .ifPresent(f -> {
                            log.trace("Adding boundaries to API field {} (min={}, max={})",
                                    property.getName(),
                                    f.getMinimum(),
                                    f.getMaximum());

                            property.setMaximum(f.getMaximum());
                            property.exclusiveMaximum(f.getExclusiveMaximum());
                            property.setMinimum(f.getMinimum());
                            property.exclusiveMinimum(f.getExclusiveMinimum());
                        }));

        return result;
    }
}

就我而言,这工作正常,所以也许它也能帮助其他人。

旁注:每次我检索 http://localhost:8080/v3/api-docs 时都会调用 SchemaMapper,在考虑其他耗时的模式修改时可能需要记住这一点。