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. Контроллер 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");
}
}
В этом контроллере мы намеренно выбрасываем некоторые исключения. Эти исключения будут автоматически преобразованы в объекты Проблема
, чтобы создать ответ приложения/проблемы+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, поэтому его легко импортировать и запускать как есть.