Um guia para a biblioteca da Web Problem Spring
1. Visão geral
Neste tutorial, vamos explorarhow to produce application/problem+json responsesusing the Problem Spring Web library. Esta biblioteca nos ajuda a evitar tarefas repetitivas relacionadas ao tratamento de erros.
Ao integrar o Problem Spring Web em nosso aplicativo Spring Boot, podemossimplify the way we handle exceptions within our project and generate responses accordingly.
2. A Biblioteca de Problemas
Problem é uma pequena biblioteca com o propósito de padronizar a maneira como as APIs Rest baseadas em Java expressam erros para seus consumidores.
UmProblem é uma abstração de qualquer erro que desejamos informar. Ele contém informações úteis sobre o erro. Vamos ver a representação padrão de uma respostaProblem:
{
"title": "Not Found",
"status": 404
}
Nesse caso, o código de status e o título são suficientes para descrever o erro. No entanto, também podemos adicionar uma descrição detalhada dele:
{
"title": "Service Unavailable",
"status": 503,
"detail": "Database not reachable"
}
Também podemos criar objetosProblem personalizados que se adaptam às nossas necessidades:
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();
Neste tutorial, vamos nos concentrar na implementação da biblioteca de problemas para projetos Spring Boot.
3. Problema na instalação da Web do Spring
Como este é um projeto baseado em Maven, vamos adicionar a dependênciaproblem-spring-web aopom.xml:
org.zalando
problem-spring-web
0.23.0
org.springframework.boot
spring-boot-starter-web
2.12.RELEASE
org.springframework.boot
spring-boot-starter-security
2.12.RELEASE
Também precisamos das dependênciasspring-boot-starter-webespring-boot-starter-security. Spring Security é necessário a partir da versão 0.23.0 deproblem-spring-web.
4. Configuração básica
Em nossa primeira etapa, precisamos desativar a página de erro com etiqueta em branco para que possamos ver nossa representação de erro personalizada:
@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
Agora, vamos registrar alguns dos componentes necessários no beanObjectMapper:
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().registerModules(
new ProblemModule(),
new ConstraintViolationProblemModule());
}
Depois disso, precisamos adicionar as seguintes propriedades ao arquivoapplication.properties:
spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
spring.http.encoding.force=true
E, finalmente, precisamos implementar a interfaceProblemHandling:
@ControllerAdvice
public class ExceptionHandler implements ProblemHandling {}
5. Configuração avançada
Além da configuração básica, também podemos configurar nosso projeto para lidar com problemas relacionados à segurança. A primeira etapa é criar uma classe de configuração para habilitar a integração da biblioteca com o 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);
}
}
E, finalmente, precisamos criar um manipulador de exceções para exceções relacionadas à segurança:
@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {}
6. O controlador REST
Após configurar nosso aplicativo, estamos prontos para criar um controlador RESTful:
@RestController
@RequestMapping("/tasks")
public class ProblemDemoController {
private static final Map 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 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");
}
}
Neste controlador, estamos lançando intencionalmente algumas exceções. Essas exceções serão convertidas em objetosProblem automaticamente para produzir uma respostaapplication/problem+json com os detalhes da falha.
Agora, vamos falar sobre as características de conselho integradas e também como criar uma implementaçãoProblem personalizada.
7. Traços de aconselhamento incorporados
Uma característica de aconselhamento é um pequeno manipulador de exceções que captura exceções e retorna o objeto de problema apropriado.
Existem características de aconselhamento internas para exceções comuns. Portanto, podemos usá-los simplesmente lançando a exceção:
throw new UnsupportedOperationException();
Como resultado, obteremos a resposta:
{
"title": "Not Implemented",
"status": 501
}
Como também configuramos a integração com Spring Security, podemos lançar exceções relacionadas à segurança:
throw new AccessDeniedException("You can't delete this task");
E obtenha a resposta adequada:
{
"title": "Forbidden",
"status": 403,
"detail": "You can't delete this task"
}
8. Criando umProblem personalizado
É possível criar uma implementação personalizada de umProblem. Precisamos apenas estender a classeAbstractThrowableProblem:
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));
}
}
E podemos lançar nosso problema personalizado da seguinte maneira:
if (MY_TASKS.containsKey(taskId)) {
return MY_TASKS.get(taskId);
} else {
throw new TaskNotFoundProblem(taskId);
}
Como resultado de lançar o problemaTaskNotFoundProblem, obteremos:
{
"type": "https://example.org/not-found",
"title": "Not found",
"status": 404,
"detail": "Task '3' not found"
}
9. Lidando com traços de pilha
Se quisermos incluir rastreamentos de pilha na resposta, precisamos configurar nossoProblemModule adequadamente:
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ProblemModule().withStackTraces());
A cadeia causal de causas é desativada por padrão, mas podemos ativá-la facilmente substituindo o comportamento:
@ControllerAdvice
class ExceptionHandling implements ProblemHandling {
@Override
public boolean isCausalChainsEnabled() {
return true;
}
}
Depois de ativar os dois recursos, obteremos uma resposta semelhante a esta:
{
"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. Conclusão
Neste artigo, exploramos como usar a biblioteca Problem Spring Web para criar respostas com os detalhes dos erros usando uma respostaapplication/problem+json. Também aprendemos como configurar a biblioteca em nosso aplicativo Spring Boot e criar uma implementação personalizada de um objetoProblem.
A implementação deste guia pode ser encontrada emthe GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil importá-lo e executá-lo como está.