Guia para validadores REST de dados Spring

Guia para validadores REST de dados Spring

1. Visão geral

Este artigo aborda uma introdução básica aos Validadores REST do Spring Data. Se você precisa primeiro revisar os fundamentos do Spring Data REST, definitivamente visitethis article para revisar os fundamentos.

Simplificando, com o Spring Data REST, podemos simplesmente adicionar uma nova entrada ao banco de dados por meio da API REST, mas é claro que também precisamos garantir que os dados sejam válidos antes de realmente persistir.

Este artigo continua emexisting article e vamos reutilizar o projeto existente que configuramos lá.

 

E, se você está procurando primeiroget started with Spring Data REST - aqui está uma boa maneira de começar a correr:

[.iframe-fluido] ##

2. UsandoValidators

A partir do Spring 3, a estrutura apresenta a interfaceValidator - que pode ser usada para validar objetos.

2.1. Motivação

No artigo anterior, definimos nossa entidade com duas propriedades -nameeemail.

E assim, para criar um novo recurso, basta executar:

curl -i -X POST -H "Content-Type:application/json" -d
  '{ "name" : "Test", "email" : "[email protected]" }'
  http://localhost:8080/users

Essa solicitação POST salvará o objeto JSON fornecido em nosso banco de dados e a operação retornará:

{
  "name" : "Test",
  "email" : "[email protected]",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

Era esperado um resultado positivo, pois fornecemos dados válidos. Mas, o que acontecerá se removermos a propriedadename, ou apenas definirmos o valor para umString vazio?

Para testar o primeiro cenário, executaremos o comando modificado anterior, onde definiremos uma string vazia como um valor para a propriedadename:

curl -i -X POST -H "Content-Type:application/json" -d
  '{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users

Com esse comando, obteremos a seguinte resposta:

{
  "name" : "",
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

Para o segundo cenário, removeremos a propriedadename da solicitação:

curl -i -X POST -H "Content-Type:application/json" -d
  '{ "email" : "Baggins" }' http://localhost:8080/users

Para esse comando, obteremos esta resposta:

{
  "name" : null,
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/2"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/2"
    }
  }
}

Como podemos ver, ambas as solicitações estavam corretas e podemos confirmar isso com o código de status 201 e o link da API para nosso objeto.

Esse comportamento não é aceitável, pois queremos evitar a inserção de dados parciais em um banco de dados.

2.2. Eventos REST Spring Data

Durante cada chamada na API Spring Data REST, o exportador Spring Data REST gera vários eventos listados aqui:

  • BeforeCreateEvent

  • AfterCreateEvent

  • BeforeSaveEvent

  • AfterSaveEvent

  • BeforeLinkSaveEvent

  • AfterLinkSaveEvent

  • BeforeDeleteEvent

  • AfterDeleteEvent

Uma vez que todos os eventos são tratados de maneira semelhante, mostraremos apenas como tratarbeforeCreateEvent que é gerado antes de um novo objeto ser salvo no banco de dados.

2.3. Definindo umValidator

Para criar nosso próprio validador, precisamos implementar a interfaceorg.springframework.validation.Validator com os métodossupportsevalidate.

Supports verifica se o validador oferece suporte a solicitações fornecidas, enquanto o métodovalidate valida os dados fornecidos nas solicitações.

Vamos definir uma classeWebsiteUserValidator:

public class WebsiteUserValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
        return WebsiteUser.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        WebsiteUser user = (WebsiteUser) obj;
        if (checkInputString(user.getName())) {
            errors.rejectValue("name", "name.empty");
        }

        if (checkInputString(user.getEmail())) {
            errors.rejectValue("email", "email.empty");
        }
    }

    private boolean checkInputString(String input) {
        return (input == null || input.trim().length() == 0);
    }
}

O objetoErrors é uma classe especial projetada para conter todos os erros fornecidos no métodovalidate. Posteriormente neste artigo, mostraremos como você pode usar mensagens fornecidas contidas no objetoErrors. Para adicionar uma nova mensagem de erro, temos que chamarerrors.rejectValue(nameOfField, errorMessage).

Depois de definir o validador, precisamos mapeá-lo para um evento específico que é gerado após a solicitação ser aceita.

Por exemplo, em nosso caso,beforeCreateEvent é gerado porque queremos inserir um novo objeto em nosso banco de dados. Porém, como queremos validar o objeto em uma solicitação, precisamos primeiro definir nosso validador.

Isso pode ser feito de três maneiras:

  • Adicione a anotaçãoComponent com o nome “beforeCreateWebsiteUserValidator“. O Spring Boot reconhecerá o prefixobeforeCreate que determina o evento que queremos capturar e também reconhecerá a classeWebsiteUser do nomeComponent.

    @Component("beforeCreateWebsiteUserValidator")
    public class WebsiteUserValidator implements Validator {
        ...
    }
  • CrieBean no contexto do aplicativo com a anotação@Bean:

    @Bean
    public WebsiteUserValidator beforeCreateWebsiteUserValidator() {
        return new WebsiteUserValidator();
    }
  • Registro manual:

    @SpringBootApplication
    public class SpringDataRestApplication
      extends RepositoryRestConfigurerAdapter {
        public static void main(String[] args) {
            SpringApplication.run(SpringDataRestApplication.class, args);
        }
    
        @Override
        public void configureValidatingRepositoryEventListener(
          ValidatingRepositoryEventListener v) {
            v.addValidator("beforeCreate", new WebsiteUserValidator());
        }
    }
    • Para este caso, você não precisa de nenhuma anotação na classeWebsiteUserValidator.

2.4. Bug de descoberta de evento

No momento, abug exists in Spring Data REST - que afeta a descoberta de eventos.

Se chamarmos o pedido POST que gera o eventobeforeCreate, nosso aplicativo não chamará o validador porque o evento não será descoberto, devido a este bug.

Uma solução simples para esse problema é inserir todos os eventos na classe Spring Data RESTValidatingRepositoryEventListener:

@Configuration
public class ValidatorEventRegister implements InitializingBean {

    @Autowired
    ValidatingRepositoryEventListener validatingRepositoryEventListener;

    @Autowired
    private Map validators;

    @Override
    public void afterPropertiesSet() throws Exception {
        List events = Arrays.asList("beforeCreate");
        for (Map.Entry entry : validators.entrySet()) {
            events.stream()
              .filter(p -> entry.getKey().startsWith(p))
              .findFirst()
              .ifPresent(
                p -> validatingRepositoryEventListener
               .addValidator(p, entry.getValue()));
        }
    }
}

3. Teste

EmSection 2.1., mostramos que, sem um validador, podemos adicionar objetos sem propriedade de nome em nosso banco de dados, o que não é o comportamento desejado porque não verificamos a integridade dos dados.

Se quisermos adicionar o mesmo objeto sem a propriedadename, mas com o validador fornecido, obteremos este erro:

curl -i -X POST -H "Content-Type:application/json" -d
  '{ "email" : "[email protected]" }' http://localhost:8080/users
{
   "timestamp":1472510818701,
   "status":406,
   "error":"Not Acceptable",
   "exception":"org.springframework.data.rest.core.
    RepositoryConstraintViolationException",
   "message":"Validation failed",
   "path":"/users"
}

Como podemos ver, os dados ausentes da solicitação foram detectados e um objeto não foi salvo no banco de dados. Nossa solicitação foi retornada com código HTTP 500 e mensagem para um erro interno.

A mensagem de erro não diz nada sobre o problema em nossa solicitação. Se quisermos torná-lo mais informativo, teremos que modificar o objeto de resposta.

EmException Handling in Spring article, mostramos como lidar com as exceções geradas pelo framework, então essa é definitivamente uma boa leitura neste ponto.

Como nosso aplicativo gera uma exceçãoRepositoryConstraintViolationException, criaremos um manipulador para essa exceção em particular que modificará a mensagem de resposta.

Esta é a nossa classeRestResponseEntityExceptionHandler:

@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
  ResponseEntityExceptionHandler {

    @ExceptionHandler({ RepositoryConstraintViolationException.class })
    public ResponseEntity handleAccessDeniedException(
      Exception ex, WebRequest request) {
          RepositoryConstraintViolationException nevEx =
            (RepositoryConstraintViolationException) ex;

          String errors = nevEx.getErrors().getAllErrors().stream()
            .map(p -> p.toString()).collect(Collectors.joining("\n"));

          return new ResponseEntity(errors, new HttpHeaders(),
            HttpStatus.PARTIAL_CONTENT);
    }
}


Com esse manipulador personalizado, nosso objeto de retorno terá informações sobre todos os erros detectados.

4. Conclusão

Neste artigo, mostramos que os validadores são essenciais para cada API REST do Spring Data, que fornece uma camada extra de segurança para inserção de dados.

Também ilustramos como é simples criar um novo validador com anotações.

Como sempre, o código para este aplicativo pode ser encontrado emGitHub project.