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 的项目,可以直接导入运行。


原始标题:A Guide to the Problem Spring Web Library | Baeldung

« 上一篇: Java周报,268期