JAVA 使用 Swagger3 显示异常?OpenAPI 配置与路径扫描机制剖析
大家好,今天我们来深入探讨一个在使用 Swagger3 (OpenAPI 3.0) 时经常遇到的问题:异常信息的显示问题。很多开发者在使用 Swagger 时,希望能够清晰地展示 API 接口可能抛出的异常,包括异常类型、状态码、以及可能包含的错误信息。但是,如果配置不当,Swagger 可能无法正确地显示这些异常信息,导致 API 文档不够完整和实用。
本次讲座将从以下几个方面展开:
- Swagger3 的基本配置与注解:回顾 Swagger3 的基本配置,包括依赖引入、配置类编写、以及常用注解的使用。
- 路径扫描机制:理解 Swagger 如何扫描并识别 API 接口,以及如何处理不同类型的Controller。
- 异常处理机制:分析 Java 应用的异常处理机制,包括全局异常处理和局部异常处理。
- Swagger 如何与异常处理集成:探讨如何将异常处理信息整合到 Swagger 文档中,包括使用
@ApiResponse注解和自定义 OpenAPI 生成器。 - 常见问题与解决方案:列举在使用 Swagger3 显示异常时常见的错误配置,并提供相应的解决方案。
- 高级技巧与最佳实践:分享一些高级技巧和最佳实践,例如使用 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.packagesToScan 和 springdoc.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 注解来描述 404 和 500 状态码,并指定了 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 接口,并为每个接口添加了 404 和 500 状态码的描述。 注意,这个只是一个示例,实际使用中需要根据具体的异常处理逻辑进行调整。 这种方法的优点是可以集中管理所有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 文档。
希望本次讲座对大家有所帮助。谢谢!