JAVA 使用 Swagger3 显示异常?OpenAPI 配置与路径扫描机制剖析

JAVA 使用 Swagger3 显示异常?OpenAPI 配置与路径扫描机制剖析

大家好,今天我们来深入探讨一个在使用 Swagger3 (OpenAPI 3.0) 时经常遇到的问题:异常信息的显示问题。很多开发者在使用 Swagger 时,希望能够清晰地展示 API 接口可能抛出的异常,包括异常类型、状态码、以及可能包含的错误信息。但是,如果配置不当,Swagger 可能无法正确地显示这些异常信息,导致 API 文档不够完整和实用。

本次讲座将从以下几个方面展开:

  1. Swagger3 的基本配置与注解:回顾 Swagger3 的基本配置,包括依赖引入、配置类编写、以及常用注解的使用。
  2. 路径扫描机制:理解 Swagger 如何扫描并识别 API 接口,以及如何处理不同类型的Controller。
  3. 异常处理机制:分析 Java 应用的异常处理机制,包括全局异常处理和局部异常处理。
  4. Swagger 如何与异常处理集成:探讨如何将异常处理信息整合到 Swagger 文档中,包括使用 @ApiResponse 注解和自定义 OpenAPI 生成器。
  5. 常见问题与解决方案:列举在使用 Swagger3 显示异常时常见的错误配置,并提供相应的解决方案。
  6. 高级技巧与最佳实践:分享一些高级技巧和最佳实践,例如使用 OpenAPI 规范自定义异常响应结构。

1. Swagger3 的基本配置与注解

首先,我们来回顾一下 Swagger3 的基本配置。以 Spring Boot 项目为例,我们需要引入以下依赖:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.7.0</version>
</dependency>

接下来,我们需要创建一个 Swagger 配置类,用于配置 OpenAPI 信息:

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("My API")
                        .version("v1")
                        .description("My API Documentation"));
    }
}

这个配置类创建了一个 OpenAPI Bean,并设置了 API 的标题、版本和描述。

接下来,我们可以在 Controller 中使用 Swagger 注解来描述 API 接口:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    @Operation(summary = "Get user by ID")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "User found"),
            @ApiResponse(responseCode = "404", description = "User not found")
    })
    public User getUserById(@Parameter(description = "User ID") @PathVariable Long id) {
        // ... logic to retrieve user
        return new User();
    }

    @PostMapping
    @Operation(summary = "Create a new user")
    @ApiResponse(responseCode = "201", description = "User created")
    public User createUser(@RequestBody User user) {
        // ... logic to create user
        return new User();
    }
}

class User {
    private Long id;
    private String name;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

在上面的代码中,我们使用了 @Operation 注解来描述 API 接口,@Parameter 注解来描述请求参数,@ApiResponse 注解来描述响应状态码和描述。

2. 路径扫描机制

Swagger 使用特定的机制来扫描和识别 API 接口。默认情况下,它会扫描所有带有 @RestController@RequestMapping 注解的类。

Swagger 会扫描以下注解来确定 API 接口的信息:

注解 作用
@RestController 标识一个类为 REST 控制器
@RequestMapping 映射 HTTP 请求到特定的处理方法
@GetMapping 映射 HTTP GET 请求到特定的处理方法
@PostMapping 映射 HTTP POST 请求到特定的处理方法
@PutMapping 映射 HTTP PUT 请求到特定的处理方法
@DeleteMapping 映射 HTTP DELETE 请求到特定的处理方法
@Operation 描述 API 接口
@Parameter 描述请求参数
@ApiResponse 描述响应状态码和描述
@RequestBody 标识请求体参数
@PathVariable 标识路径变量参数
@RequestParam 标识请求参数

如果你的 Controller 没有使用这些注解,Swagger 将无法识别它。

此外,你还可以使用 springdoc.packagesToScanspringdoc.pathsToMatch 配置项来更精细地控制 Swagger 的扫描范围。例如:

springdoc:
  packagesToScan: com.example.controller
  pathsToMatch: /api/**

上面的配置表示 Swagger 只扫描 com.example.controller 包下的类,并且只匹配以 /api/ 开头的路径。

3. 异常处理机制

Java 应用通常使用以下两种方式来处理异常:

  • 全局异常处理:使用 @ControllerAdvice@ExceptionHandler 注解来集中处理所有 Controller 中抛出的异常。
  • 局部异常处理:在 Controller 方法中使用 try-catch 块来处理特定的异常。

全局异常处理示例:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error");
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

class ErrorResponse {
    private int statusCode;
    private String message;

    public ErrorResponse(int statusCode, String message) {
        this.statusCode = statusCode;
        this.message = message;
    }

    public int getStatusCode() { return statusCode; }
    public void setStatusCode(int statusCode) { this.statusCode = statusCode; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}

class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

局部异常处理示例:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/items")
public class ItemController {

    @GetMapping("/{id}")
    public ResponseEntity<Item> getItemById(@PathVariable Long id) {
        try {
            Item item = getItemFromDatabase(id);
            return ResponseEntity.ok(item);
        } catch (ResourceNotFoundException ex) {
            ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
            return new ResponseEntity(errorResponse, HttpStatus.NOT_FOUND);
        } catch (Exception ex) {
            ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error");
            return new ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private Item getItemFromDatabase(Long id) {
        // Simulate a database lookup
        if (id <= 0) {
            throw new ResourceNotFoundException("Item not found with id: " + id);
        }
        return new Item(id, "Item " + id);
    }
}

class Item {
    private Long id;
    private String name;

    public Item(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

4. Swagger 如何与异常处理集成

要将异常处理信息整合到 Swagger 文档中,主要有两种方法:

方法 1:使用 @ApiResponse 注解

可以在 Controller 方法上使用 @ApiResponse 注解来描述可能抛出的异常。例如:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    @Operation(summary = "Get product by ID")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Product found"),
            @ApiResponse(responseCode = "404", description = "Product not found", content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ErrorResponse.class))),
            @ApiResponse(responseCode = "500", description = "Internal server error", content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ErrorResponse.class)))
    })
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        try {
            Product product = getProductFromDatabase(id);
            return ResponseEntity.ok(product);
        } catch (ResourceNotFoundException ex) {
            ErrorResponse errorResponse = new ErrorResponse(404, ex.getMessage());
            return new ResponseEntity(errorResponse, HttpStatus.NOT_FOUND);
        } catch (Exception ex) {
            ErrorResponse errorResponse = new ErrorResponse(500, "Internal Server Error");
            return new ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private Product getProductFromDatabase(Long id) {
        // Simulate a database lookup
        if (id <= 0) {
            throw new ResourceNotFoundException("Product not found with id: " + id);
        }
        return new Product(id, "Product " + id);
    }
}

class Product {
    private Long id;
    private String name;

    public Product(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

在这个例子中,我们使用了 @ApiResponse 注解来描述 404500 状态码,并指定了 ErrorResponse 作为响应体的 Schema。 content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ErrorResponse.class)) 这部分代码,告诉Swagger,如果返回404或者500,响应体的内容是一个ErrorResponse对象。

方法 2:自定义 OpenAPI 生成器

可以自定义 OpenAPI 生成器来动态地添加异常信息到 Swagger 文档中。 这种方法比较复杂,但是更加灵活。 需要实现 OpenAPICustomiser 接口,并在 Spring 容器中注册。

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.stereotype.Component;

@Component
public class GlobalErrorResponseCustomiser implements OpenApiCustomiser {

    @Override
    public void customise(OpenAPI openAPI) {
        openAPI.getPaths().values().forEach(pathItem -> {
            pathItem.readOperations().forEach(operation -> {
                ApiResponses apiResponses = operation.getResponses();
                if (apiResponses.get("404") == null) {
                    apiResponses.addApiResponse("404", new ApiResponse().description("Resource not found"));
                }
                if (apiResponses.get("500") == null) {
                    apiResponses.addApiResponse("500", new ApiResponse().description("Internal server error"));
                }
            });
        });
    }
}

这个例子中,我们遍历了所有 API 接口,并为每个接口添加了 404500 状态码的描述。 注意,这个只是一个示例,实际使用中需要根据具体的异常处理逻辑进行调整。 这种方法的优点是可以集中管理所有API的异常信息,避免在每个Controller中重复编写 @ApiResponse 注解。

5. 常见问题与解决方案

以下是一些在使用 Swagger3 显示异常时常见的错误配置和相应的解决方案:

问题 解决方案
@ApiResponse 注解没有生效 确保 @ApiResponse 注解的位置正确,必须在 Controller 方法上。检查 Spring Boot 版本和 Swagger 依赖版本是否兼容。检查配置类是否正确配置,并且被 Spring 容器扫描到。
Swagger 没有显示全局异常处理的异常信息 默认情况下,Swagger 不会自动扫描全局异常处理类。需要使用 @ApiResponse 注解或者自定义 OpenAPI 生成器来手动添加异常信息。确保全局异常处理类被 Spring 容器扫描到,并且 @ControllerAdvice 注解生效。
Swagger 显示的异常信息不完整 (缺少字段) 检查 @ApiResponse 注解中指定的 Schema 是否包含了所有需要显示的字段。确保异常响应类的字段都有对应的 Getter 方法。检查全局异常处理返回的 ResponseEntity 中的 body 类型是否与 @ApiResponse 注解中指定的 Schema 一致。
Swagger UI 没有显示任何异常信息 检查 Swagger 的配置是否正确,包括依赖引入、配置类编写、以及注解使用。检查 Controller 是否使用了正确的 Swagger 注解。检查应用程序是否启动成功,并且 Swagger UI 可以正常访问。
状态码显示不正确 检查 @ApiResponse 注解中的 responseCode 属性是否与实际返回的状态码一致。确保全局异常处理方法返回的 ResponseEntity 的状态码正确。
如何处理不同 Content-Type 的异常响应 @ApiResponse 注解的 content 属性可以指定不同的 Content-Type 和对应的 Schema。例如,可以为 JSON 和 XML 格式的响应分别指定不同的 Schema。
如何在SwaggerUI中展示异常的详细信息,比如堆栈信息 通常不建议在SwaggerUI中展示异常的堆栈信息,因为这可能会暴露敏感信息。建议只展示必要的错误信息,例如错误码、错误消息等。如果需要查看详细的异常信息,可以在服务器端记录日志。

6. 高级技巧与最佳实践

以下是一些高级技巧和最佳实践,可以帮助你更好地使用 Swagger3 显示异常:

  • 使用 OpenAPI 规范自定义异常响应结构:可以定义一个通用的异常响应 Schema,并在 @ApiResponse 注解中引用它。这样可以保证所有 API 接口的异常响应结构一致。

    components:
      schemas:
        ErrorResponse:
          type: object
          properties:
            code:
              type: integer
              description: Error code
            message:
              type: string
              description: Error message
    @ApiResponse(responseCode = "400", description = "Bad Request", content = @io.swagger.v3.oas.annotations.media.Content(schema = @io.swagger.v3.oas.annotations.media.Schema(ref = "#/components/schemas/ErrorResponse")))
  • 使用枚举类定义错误码:可以使用枚举类来定义所有可能的错误码,并在异常响应中使用这些枚举值。这样可以提高代码的可读性和可维护性。

  • 使用 Spring AOP 拦截异常:可以使用 Spring AOP 来拦截所有 Controller 方法的异常,并在 Swagger 文档中自动添加异常信息。 这种方法可以减少手动编写 @ApiResponse 注解的工作量。

  • 编写单元测试来验证 Swagger 文档的正确性:可以编写单元测试来验证 Swagger 文档是否包含了所有必要的异常信息。 这可以帮助你尽早发现配置错误。

  • 保持 Swagger 文档与代码同步:在修改代码时,一定要及时更新 Swagger 文档。 可以使用 Swagger 的代码生成功能来自动生成 API 客户端代码,并确保客户端代码与 Swagger 文档保持同步。

代码演示总结

本次讲座我们探讨了如何在使用 Swagger3 时正确显示异常信息。通过理解 Swagger 的基本配置、路径扫描机制、以及异常处理机制,我们可以更好地将异常信息整合到 Swagger 文档中。 通过使用 @ApiResponse 注解、自定义 OpenAPI 生成器、以及一些高级技巧和最佳实践,我们可以创建更完整、更实用的 API 文档。

希望本次讲座对大家有所帮助。谢谢!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注