Перейти к основному содержимому

Руководство по проблемной веб-библиотеке Spring

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

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, поэтому его легко импортировать и запускать как есть.