1. 概述
本教程將探討如何使用 application/problem+json 響應的生成方法。我們將使用 Problem Spring Web 庫。該庫可以幫助我們避免與錯誤處理相關的重複性任務。
通過將 Problem Spring Web 集成到我們的 Spring Boot 應用程序中,我們可以簡化項目內異常的處理方式,並生成相應的響應。
2. 問題庫
Problem 是一個小型庫,旨在標準化 Java 風格的 Rest 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. Spring Web 問題配置
由於這是一個基於 Maven 的項目,請在 <pom.xml> 文件中添加 <em><a href="https://mvnrepository.com/artifact/org.zalando/problem-spring-web">problem-spring-web</a></em> 依賴:
<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>我們還需要 spring-boot-starter-web和spring-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. 高級配置
除了基本配置之外,我們還可以配置項目以處理與安全相關的難題。第一步是創建配置類,以便啓用庫與 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 控制器
在配置好我們的應用程序後,我們準備創建一個 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 響應。
現在,我們來討論一下內置的 Advice 特性和如何創建自定義 Problem 實現。
7. 內置建議特性
一個建議特性是一個小型的異常處理程序,用於捕獲異常並返回適當的問題對象。
有內置的建議特性用於常見的異常。因此,我們可以通過簡單地拋出異常來使用它們:
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. 創建自定義問題
可以創建自定義問題的實現。我們只需要擴展 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));
}
}我們也可以以以下方式拋出自定義問題:
if (MY_TASKS.containsKey(taskId)) {
return MY_TASKS.get(taskId);
} else {
throw new TaskNotFoundProblem(taskId);
}由於拋出 TaskNotFoundProblem 異常,我們將獲得:
{
"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());默認情況下,因果鏈已禁用,但我們可以通過覆蓋行為輕鬆啓用它:
@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 對象的實現。