Spring Web库Problem指南

2023/05/12

1. 概述

在本教程中,我们将探讨如何使用Problem Spring Web库生成application/problem+json响应,这个库可以帮助我们避免与错误处理相关的重复性任务。

通过将Problem Spring Web集成到我们的Spring Boot应用程序中,我们可以简化我们在项目中处理异常并相应地生成响应的方式

2. Problem库

Problem是一个小型库,其目的是标准化基于Java的Rest API向其消费者表达错误的方式。

Problem是我们想要通知的任何错误的抽象,它包含有关错误的方便信息。让我们看看Problem响应的默认表示形式:

{
    "title": "Not Found",
    "status": 404
}

在这种情况下,status和title足以描述错误,但是,我们也可以添加对它的详细描述:

{
    "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的项目,因此我们将problem-spring-web依赖项添加到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>2.4.0</version> 
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.4.0</version>  
</dependency>

我们还需要spring-boot-starter-webspring-boot-starter-security依赖项,Spring Security从problem-spring-web的0.23.0版本开始是必需的。

4. 基本配置

作为我们的第一步,我们需要禁用白标签错误页面,以便我们能够看到我们的自定义错误表示:

@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. 高级配置

除了基本配置之外,我们还可以配置我们的项目来处理与安全相关的Problem,第一步是创建一个配置类以启用与Spring Security的库集成:

@Configuration
@EnableWebSecurity
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {

    @Autowired
    private SecurityProblemSupport problemSupport;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // Other security-related configuration
        http.exceptionHandling()
              .authenticationEntryPoint(problemSupport)
              .accessDeniedHandler(problemSupport);
        return http.build();
    }
}

最后,我们需要为与安全相关的异常创建一个异常处理程序:

@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {}

6. REST控制器

配置完我们的应用程序后,我们创建一个RESTful控制器:

@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响应。

现在,让我们谈谈内置的建议特征以及如何创建自定义Problem实现。

7. 内置建议特征

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

可以创建Problem的自定义实现,我们只需要扩展AbstractThrowableProblem类:

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));
    }
}

我们可以抛出我们的自定义Problem,如下所示:

if (MY_TASKS.containsKey(taskId)) {
    return MY_TASKS.get(taskId);
} else {
    throw new TaskNotFoundProblem(taskId);
}

作为抛出TaskNotFoundProblem Problem的结果,我们将得到:

{
    "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());

默认情况下禁用cause链,但我们可以通过覆盖行为来轻松启用它:

@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库使用application/problem+json响应创建包含错误详细信息的响应,并学习了如何在我们的Spring Boot应用程序中配置库并创建Problem对象的自定义实现。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章