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