Как да напишете съвместими с 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. По този начин не е необходимо да добавяме отделна библиотека към нашия проект.

В този урок ще ви покажа как да внедрите обработка на грешки с подробности за проблема в приложение за пролетно зареждане чрез изчерпателни примери.

Да започваме!

Демо проект

Ще създадем проста REST услуга, която връща продукти от база данни. Потребителите могат да търсят продукти по id. Услугата ще върне съобщение за грешка, ако идентификаторът не присъства в базата данни. Това е мястото, където ще използваме 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() позволява на потребителите да търсят продукти по id.

Първо, нека тестваме приложението без 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, може да харесате и моя свързан урок:



Благодаря за четенето и приятно кодиране!