Java SpringBoot OpenApi @ApiResponse 显示错误的返回对象

use*_*141 4 java spring swagger-ui openapi

我在 SpringBoot 项目中使用 OpenApi 3 来生成 Swagger html 页面。

POM.xml 中的依赖项:

    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-ui</artifactId>
        <version>1.5.12</version>
    </dependency>
Run Code Online (Sandbox Code Playgroud)

在控制器类中,我在方法上方定义了以下注释。

@Operation(
        summary = "Get a list of letters for a specific user",
        description = "Get a list of letters for a specific user",
        tags = {"letters"}
)
@ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "success", content = {@Content(
                                                                    mediaType = "application/json",
                                                                    array = @ArraySchema(schema = @Schema(implementation = LetterDTO.class)))}),
        @ApiResponse(responseCode = "400", description = "BAD REQUEST"),
        @ApiResponse(responseCode = "401", description = "UNAUTHORIZED"),
        @ApiResponse(responseCode = "403", description = "Forbidden"),
        @ApiResponse(responseCode = "404", description = "NOT_FOUND: Entity could not be found")}
)
@GetMapping(value = "letters/user/{userId}", produces = {"application/json"})
public List<LetterDTO> getLettersForUser(
    ...
)
Run Code Online (Sandbox Code Playgroud)

Swagger UI 的输出显示代码 200 的正确响应,它是 LetterDTO 对象的列表。

在此输入图像描述

但代码 401 的响应还显示 L​​etterDTO 对象的列表。不过,我没有为代码 401 定义任何响应对象。我期望 Swagger 生成与代码 400 相同的响应对象,它是包含错误代码和错误消息的默认返回对象。

在此输入图像描述

为什么 Swagger 采用与代码 200 定义的返回对象相同的返回对象?我期望 Swagger 会生成默认的返回对象。这是 Swagger 中的错误吗?

在此输入图像描述

ssc*_*mid 6

我通常这样配置 API 响应:

@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Invalid request", content = @Content)
Run Code Online (Sandbox Code Playgroud)

如果未content指定,则使用相应控制器方法的返回类型。content = @Content告诉 Swagger 响应中没有内容。

@ApiGetOne就是 Swagger 将显示的内容(屏幕截图来自不同的 DTO 类):

在此输入图像描述

为了简单性和可重用性,我通常将它们包装在可重用的帮助器注释中,这样我的端点就没有那么多注释,并且我不需要ResponseEntity在控制器中使用注释,例如:

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(method = RequestMethod.GET,
    produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Invalid request", content = @Content)
@ApiResponse(responseCode = "500", description = "Internal error", content = @Content)
public @interface ApiGet {

  @AliasFor(annotation = RequestMapping.class)
  String[] value() default {};

  @AliasFor(annotation = RequestMapping.class)
  String[] path() default {};

}
Run Code Online (Sandbox Code Playgroud)

您还可以使用更多 API 响应来扩展这些注释,例如,要为某些端点添加 404,请创建另一个注释@ApiGet

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ApiGet
@ApiResponse(responseCode = "404", description = "Not found", content = @Content)
public @interface ApiGetOne {

  @AliasFor(annotation = ApiGet.class)
  String[] value() default {};

  @AliasFor(annotation = ApiGet.class)
  String[] path() default {};

}
Run Code Online (Sandbox Code Playgroud)

最后,在任何端点上使用它们(使用 Java 17):

public record HelloWorldDto(String recipientName) {
  public String getMessage() {
    return "Hello, %s".formatted(recipientName);
  }
}
Run Code Online (Sandbox Code Playgroud)
public record ErrorDto(String message) {
}
Run Code Online (Sandbox Code Playgroud)
@RestController
@RequestMapping("api/test")
@Tag(name = "Demo", description = "Endpoints for testing")
public class DemoController {
  ...

  @ApiGet("/hello")
  public HelloWorldDto sayHello() {
    return new HelloWorldDto("stranger");
  }

  @ApiGetOne("/hello/{id}")
  public HelloWorldDto sayHelloWithParam(@PathVariable int id) {
    final var person = myPersonRepo.getById(id); // might throw a NotFoundException which is mapped to 404 status code
    return new HelloWorldDto(person.name());
  }
}
Run Code Online (Sandbox Code Playgroud)

将异常映射到自定义错误响应:

@ControllerAdvice
public class ErrorHandler {

  private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class);

  @ExceptionHandler
  public ResponseEntity<ErrorDto> handle(Exception exception) {
    log.error("Internal server error occurred", exception);

    return response(HttpStatus.INTERNAL_SERVER_ERROR, "Unknown error occurred.");
  }

  @ExceptionHandler
  public ResponseEntity<ErrorDto> handle(NotFoundException exception) {
    return response(HttpStatus.NOT_FOUND, exception.getMessage());
  }

  private ResponseEntity<ErrorDto> response(HttpStatus status, String message) {
    return ResponseEntity
        .status(status)
        .body(new ErrorDto(message));
  }
}
Run Code Online (Sandbox Code Playgroud)

我非常喜欢这个设置,因为

  • 我最终得到了一些足以满足典型 CRUD 端点的可重用注释
  • 我不需要构建ResponseEntity控制器方法
  • 作为@ControllerAdvice可重用错误处理的中心点
  • 所有这些都使我的控制器/端点保持干净和简单
  • 反过来,这又使测试变得简单

更新2022/04/20

只需修复一个错误,即我们有一个返回图像而不是 JSON 的端点。在这种情况下,为了防止HttpMessageNotWritableException: No converter for [class ErrorDto] with preset Content-Type 'image/jpeg',您需要Accept像这样检查请求的标头(使用标头作为后备):

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDto> handle(final Exception exception, final WebRequest webRequest) {
  return createResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Some error", webRequest);
}

protected ResponseEntity<ErrorDto> createResponse(final HttpStatus httpStatus,
                                                  final String message,
                                                  final WebRequest webRequest) {
  final var accepts = webRequest.getHeader(HttpHeaders.ACCEPT);
  if (!MediaType.APPLICATION_JSON_VALUE.equals(accepts)) {
    return ResponseEntity.status(httpStatus)
        .header("my-error", message)
        .build();
  }

  return ResponseEntity
      .status(status)
      .body(new ErrorDto(message));
}
Run Code Online (Sandbox Code Playgroud)