«1. Обзор

В этом руководстве мы собираемся изучить, как создавать ответы application/problem+json с помощью веб-библиотеки Problem Spring. Эта библиотека помогает нам избежать повторяющихся задач, связанных с обработкой ошибок.

Интегрируя Problem Spring Web в наше приложение Spring Boot, мы можем упростить способ обработки исключений в нашем проекте и соответственно генерировать ответы.

2. Библиотека проблем

Проблема — это небольшая библиотека, предназначенная для стандартизации того, как API-интерфейсы Rest на основе Java сообщают об ошибках своим потребителям.

Проблема — это абстракция любой ошибки, о которой мы хотим сообщить. Он содержит удобную информацию об ошибке. Давайте посмотрим на представление ответа о проблеме по умолчанию:

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

В этом случае кода состояния и заголовка достаточно, чтобы описать ошибку. Тем не менее, мы также можем добавить его подробное описание:

{
  "title": "Service Unavailable",
  "status": 503,
  "detail": "Database not reachable"
}

Мы также можем создавать собственные объекты задач, которые адаптируются к нашим потребностям:

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.

3. Проблема Spring Web Setup

Поскольку это проект на основе 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 -web и зависимости spring-boot-starter-security. Spring Security требуется с версии 0.23.0 Problem-spring-web.

4. Базовая конфигурация

В качестве первого шага нам нужно отключить страницу с белой меткой ошибки, чтобы вместо нее мы могли видеть собственное представление ошибки:

@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)

Теперь давайте зарегистрируем некоторые из необходимые компоненты в bean-компоненте ObjectMapper:

@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 extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProblemSupport problemSupport;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Other security-related configuration
        http.exceptionHandling()
          .authenticationEntryPoint(problemSupport)
          .accessDeniedHandler(problemSupport);
    }
}

И, наконец, нам нужно создать обработчик исключений для исключений, связанных с безопасностью:

@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {}

6. ОСТАЛЬНАЯ Контроллер

После настройки нашего приложения мы готовы создать контроллер 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");
    }

}

В этом контроллере мы намеренно генерируем некоторые исключения. Эти исключения будут автоматически преобразованы в объекты Проблема, чтобы создать ответ приложения/проблемы+json с подробными сведениями об ошибке.

Теперь давайте поговорим о встроенных трейтах-советах, а также о том, как создать пользовательскую реализацию задачи.

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 для создания ответов с подробными сведениями об ошибках с использованием ответа application/problem+json. Мы также узнали, как настроить библиотеку в нашем приложении Spring Boot и создать пользовательскую реализацию объекта Problem.

Реализацию этого руководства можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.