Как писать ответы об ошибках в соответствии с RFC7807 в приложении Spring Boot

Что такое RFC7807

RFC7807, также известный как спецификация Сведения о проблемах для HTTP API, определяет стандартизированный формат для описания ошибок или проблем, возникающих в HTTP API. Он представляет собой сообщение JSON с предопределенными полями, такими как тип, заголовок, подробности и т. д. Преимущество заключается в том, что разработчикам API не нужно реализовывать свой подход к описанию деталей проблемы.

Spring Boot 3 и подробности проблемы

В версиях Spring Boot до 3 мы использовали библиотеку Zalando Problem’s, чтобы показать подробные ответы на наши ответы об ошибках HTTP. Теперь Spring Boot 3 из коробки поддерживает RFC7807 с помощью класса ProblemDetail. Таким образом, нам не нужно добавлять в наш проект отдельную библиотеку.

В этом руководстве я покажу вам, как реализовать обработку ошибок с помощью сведений о проблеме в приложении Spring Boot на подробных примерах.

Давайте начнем!

Демонстрационный проект

Мы создадим простую службу REST, которая возвращает товары из базы данных. Пользователи могут искать товары по идентификатору. Служба вернет сообщение об ошибке, если идентификатор отсутствует в базе данных. Здесь мы будем использовать ProblemDetail, чтобы предоставить более удобный ответ на ошибку.

Я использую Maven для этого проекта. Нам нужны следующие зависимости в pom.xml:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
  • spring-boot-starter-web используется для включения веб-возможностей. Стандарт ProblemDetail является частью этой зависимости.
  • Нам нужны spring-boot-starter-data-jpa и com.h2database для поиска сущностей в базе данных.

Обратите внимание, что Java 17 является обязательным условием для функций Spring Boot 3.

Во-первых, давайте создадим сущность Product, как показано ниже:

@Entity
@Table(name = "Product")
public class Product {

    @Id
    private Long id;

    @Column
    private String name;
}

Затем создайте репозиторий JPA, чтобы вернуть Product со следующим кодом:

public interface ProductRepository extends JpaRepository<Product, Long> {

    Optional<Product> findById(Long id);
}

Теперь давайте создадим DemoService для извлечения продуктов из базы данных:

@Service
public class DemoService {

    private final ProductRepository productRepository;

    public DemoService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }
}

Нам также нужен RestController для включения HTTP-вызовов. Вот как это сделать:

@RestController
@RequestMapping("/product")
public class DemoController {

    private final DemoService demoService;

    public DemoController(DemoService demoService) {
        this.demoService = demoService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        var result = demoService.getProductById(id);
        if (result.isEmpty()) {
            throw new ProductNotFoundException(id);
        }
        return ResponseEntity.ok(result.get());
    }
}
  • Метод getProduct() позволяет пользователям искать товары по идентификатору.

Во-первых, давайте протестируем приложение без ProblemDetail. Запустите приложение и попробуйте найти продукт:

curl http://localhost:8080/product/101010

Поскольку база данных пуста, продукты не будут найдены. Вы должны увидеть такой ответ:

{
  "timestamp": "2023-06-08T11:23:47.223+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/product/101010"
}

Как видите, ответ совсем не подробный и может быть недостаточным для пользователей.

Давайте добавим в наш код поддержку ProblemDetail. Создайте собственный обработчик исключений:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    ProblemDetail handleProductNotFoundException(ProductNotFoundException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    }
}
  • ResponseEntityExceptionHandler с ExceptionHandler обрабатывает встроенные исключения Spring MVC, такие как HttpRequestMethodNotSupportedException, MethodArgumentNotValidException, TypeMismatchException и т. д.

Класс ProductNotFoundException выглядит так:

public class ProductNotFoundException extends RuntimeException {

    public ProductNotFoundException(Long productId) {
        super("Product with id " + productId + " was not found!");
    }
}

Это просто стандартный класс исключений.

Попробуем снова выбрать несуществующий товар:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Product with id 101010 was not found!",
  "instance": "/product/101010"
}

Хороший! На этот раз объяснение более удобное и имеет подробное описание. ResponseEntityExceptionHandler вернул детали ошибки в формате RFC7807 в тексте ответа.

Вы можете настроить сообщение об ошибке, установив различные свойства. Давайте рассмотрим несколько примеров:

    @ExceptionHandler(ProductNotFoundException.class)
    ProblemDetail handleProductNotFoundException(ProductNotFoundException e) {
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
        problemDetail.setTitle("Product Not Found");
        problemDetail.setType(URI.create("https://myproduct.com/error"));
        problemDetail.setProperty("exception type", "This is a business exception");
        return problemDetail;
    }
  • Свойство title отображает пользовательский заголовок.
  • Свойство type позволяет отображать URI, описывающий проблему. Информация может быть более конкретной, чем сам код состояния HTTP.
  • Установив property, вы можете добавить любые настраиваемые свойства к динамической карте "ключ-значение".

Ответ

{
  "type": "https://myproduct.com/error",
  "title": "Product Not Found",
  "status": 404,
  "detail": "Product with id 101010 was not found!",
  "instance": "/product/101010",
  "exception type": "This is a business exception"
}

Заключение

В этой статье вы узнали, как использовать ProblemDetail для обеспечения точной и единообразной передачи ответов об ошибках HTTP.

Придерживаясь формата RFC7807, разработчики API могут гарантировать, что ответы на ошибки в различных реализациях API будут согласованными и понятными пользователям API.

Надеюсь, эта статья была вам полезна. Полный исходный код этой демонстрации можно найти в моем репозитории GitHub.

Если вам понравилась эта статья о Spring Boot 3, вам также может понравиться мой связанный учебник:



Спасибо за чтение и удачного кодирования!