Um guia para a biblioteca da Web Problem Spring

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á.