1. 概述
在本教程中,我们将深入探讨如何使用 Problem Spring Web 库 来生成标准的 application/problem+json
响应。这个库可以帮助我们避免重复性的错误处理逻辑。
通过将 Problem Spring Web 集成到我们的 Spring Boot 项目中,我们可以简化异常处理流程,并统一返回格式化的错误响应。
2. Problem 库简介
Problem 是一个轻量级库,旨在标准化 Java 编写的 RESTful API 向客户端表达错误的方式。
一个 Problem
对象是对任意错误信息的抽象,它包含便于客户端理解的错误元数据。默认情况下,一个 Problem
的响应结构如下:
{
"title": "Not Found",
"status": 404
}
有时候仅靠状态码和标题还不足以说明问题,我们可以添加更详细的描述:
{
"title": "Service Unavailable",
"status": 503,
"detail": "Database not reachable"
}
我们也可以根据业务需求自定义 Problem
对象:
Problem.builder()
.withType(URI.create("https://example.org/out-of-stock"))
.withTitle("Out of Stock")
.withStatus(BAD_REQUEST)
.withDetail("Item B00027Y5QG is no longer available")
.with("product", "B00027Y5QG")
.build();
本文将重点介绍如何在 Spring Boot 项目中使用 Problem 库进行错误处理。
3. Problem Spring Web 依赖配置
这是一个基于 Maven 的项目,我们需要添加以下依赖项到 pom.xml
文件中:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>0.23.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.3.2</version>
</dependency>
✅ 注意:从 problem-spring-web
0.23.0 版本开始,必须引入 Spring Security 依赖。
4. 基础配置
首先,我们需要关闭 Spring Boot 默认的白名单错误页面,以便展示我们自定义的错误响应:
@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
接着,在 ObjectMapper
Bean 中注册必要的模块:
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().registerModules(
new ProblemModule(),
new ConstraintViolationProblemModule());
}
然后,在 application.properties
文件中添加如下配置:
spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
spring.http.encoding.force=true
最后,实现 ProblemHandling
接口:
@ControllerAdvice
public class ExceptionHandler implements ProblemHandling {}
5. 高级配置(安全集成)
除了基础配置之外,我们还可以让项目支持 Spring Security 相关的异常处理。
第一步是创建一个配置类,启用 Problem 与 Spring Security 的集成:
@Configuration
@EnableWebSecurity
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {
@Autowired
private SecurityProblemSupport problemSupport;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/"))
.permitAll())
.exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport))
.build();
}
}
第二步是创建一个用于处理安全相关异常的异常处理器:
@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {}
6. REST 控制器示例
配置完成后,我们就可以编写 REST 控制器了:
@RestController
@RequestMapping("/tasks")
public class ProblemDemoController {
private static final Map<Long, Task> MY_TASKS;
static {
MY_TASKS = new HashMap<>();
MY_TASKS.put(1L, new Task(1L, "My first task"));
MY_TASKS.put(2L, new Task(2L, "My second task"));
}
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public List<Task> getTasks() {
return new ArrayList<>(MY_TASKS.values());
}
@GetMapping(value = "/{id}",
produces = MediaType.APPLICATION_JSON_VALUE)
public Task getTasks(@PathVariable("id") Long taskId) {
if (MY_TASKS.containsKey(taskId)) {
return MY_TASKS.get(taskId);
} else {
throw new TaskNotFoundProblem(taskId);
}
}
@PutMapping("/{id}")
public void updateTask(@PathVariable("id") Long id) {
throw new UnsupportedOperationException();
}
@DeleteMapping("/{id}")
public void deleteTask(@PathVariable("id") Long id) {
throw new AccessDeniedException("You can't delete this task");
}
}
在这个控制器中,我们故意抛出了几个异常,这些异常会被自动转换为 Problem
对象,最终以 application/problem+json
格式返回给客户端。
接下来,我们将讨论内置的 Advice Trait 以及如何创建自定义的 Problem
实现。
7. 内置 Advice Trait
Advice Trait 是一组小型异常处理器,用来捕获特定类型的异常并返回对应的 Problem
响应。
例如,对于常见的未实现异常,我们可以直接抛出:
throw new UnsupportedOperationException();
此时会自动返回如下响应:
{
"title": "Not Implemented",
"status": 501
}
由于我们已集成 Spring Security,因此也可以抛出权限相关的异常:
throw new AccessDeniedException("You can't delete this task");
返回的响应如下:
{
"title": "Forbidden",
"status": 403,
"detail": "You can't delete this task"
}
8. 创建自定义 Problem
我们可以通过继承 AbstractThrowableProblem
类来创建自定义的 Problem
类型:
public class TaskNotFoundProblem extends AbstractThrowableProblem {
private static final URI TYPE
= URI.create("https://example.org/not-found");
public TaskNotFoundProblem(Long taskId) {
super(
TYPE,
"Not found",
Status.NOT_FOUND,
String.format("Task '%s' not found", taskId));
}
}
然后在控制器中使用:
if (MY_TASKS.containsKey(taskId)) {
return MY_TASKS.get(taskId);
} else {
throw new TaskNotFoundProblem(taskId);
}
这样就会返回如下 JSON:
{
"type": "https://example.org/not-found",
"title": "Not found",
"status": 404,
"detail": "Task '3' not found"
}
9. 响应中包含堆栈跟踪信息
如果我们希望在错误响应中包含堆栈跟踪信息,需要对 ProblemModule
进行额外配置:
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ProblemModule().withStackTraces());
默认情况下,因果链(causal chain)是禁用的,但我们可以通过覆盖方法来启用:
@ControllerAdvice
class ExceptionHandling implements ProblemHandling {
@Override
public boolean isCausalChainsEnabled() {
return true;
}
}
启用后,响应可能如下所示:
{
"title": "Internal Server Error",
"status": 500,
"detail": "Illegal State",
"stacktrace": [
"org.example.ExampleRestController
.newIllegalState(ExampleRestController.java:96)",
"org.example.ExampleRestController
.nestedThrowable(ExampleRestController.java:91)"
],
"cause": {
"title": "Internal Server Error",
"status": 500,
"detail": "Illegal Argument",
"stacktrace": [
"org.example.ExampleRestController
.newIllegalArgument(ExampleRestController.java:100)",
"org.example.ExampleRestController
.nestedThrowable(ExampleRestController.java:88)"
],
"cause": {
// ....
}
}
}
10. 总结
在本文中,我们学习了如何使用 Problem Spring Web 库构建符合 RFC 7807 规范的错误响应,即 application/problem+json
格式。我们还了解了如何在 Spring Boot 项目中进行配置,并创建了自定义的 Problem
实现。
本教程的完整代码可以在 GitHub 项目 中找到。这是一个基于 Maven 的项目,可以直接导入运行。