Validação para terminais funcionais no Spring 5
1. Visão geral
Muitas vezes é útil implementar a validação de entrada para nossas APIs para evitar erros inesperados posteriormente, quando estivermos processando os dados.
Infelizmente, no Spring 5 não há como executar validações automaticamente em endpoints funcionais como fazemos com base em anotações. Temos que gerenciá-los manualmente.
Ainda assim, podemos fazer uso de algumas ferramentas úteis fornecidas pela Spring para verificar com facilidade e limpeza se nossos recursos são válidos.
2. Usando validações de primavera
Vamos começar configurando nosso projeto com um endpoint funcional de trabalho antes de mergulhar nas validações reais.
Imagine que temos o seguinteRouterFunction:
@Bean
public RouterFunction functionalRoute(
FunctionalHandler handler) {
return RouterFunctions.route(
RequestPredicates.POST("/functional-endpoint"),
handler::handleRequest);
}
Este roteador usa a função de manipulador fornecida pela seguinte classe de controlador:
@Component
public class FunctionalHandler {
public Mono handleRequest(ServerRequest request) {
Mono responseBody = request
.bodyToMono(CustomRequestEntity.class)
.map(cre -> String.format(
"Hi, %s [%s]!", cre.getName(), cre.getCode()));
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(responseBody, String.class);
}
}
Como podemos ver, tudo o que estamos fazendo neste endpoint funcional é formatar e recuperar as informações que recebemos no corpo da solicitação, que é estruturado como um objetoCustomRequestEntity:
public class CustomRequestEntity {
private String name;
private String code;
// ... Constructors, Getters and Setters ...
}
Isso funciona bem, mas vamos imaginar que agora precisamos verificar se nossa entrada está em conformidade com algumas restrições fornecidas, por exemplo, se nenhum dos campos pode ser nulo e se o código deve ter mais de 6 dígitos.
Precisamos encontrar uma maneira de fazer essas afirmações de forma eficiente e, se possível, separadas da nossa lógica de negócios.
2.1. Implementando um validador
As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values:
public class CustomRequestEntityValidator
implements Validator {
@Override
public boolean supports(Class> clazz) {
return CustomRequestEntity.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "name", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "code", "field.required");
CustomRequestEntity request = (CustomRequestEntity) target;
if (request.getCode() != null && request.getCode().trim().length() < 6) {
errors.rejectValue(
"code",
"field.min.length",
new Object[] { Integer.valueOf(6) },
"The code must be at least [6] characters in length.");
}
}
}
Não entraremos em detalhes sobrehow como oValidator funciona. Basta saber que todos os erros são coletados na validação de um objeto -an empty error collection means that the object adheres to all our constraints.
Então, agora que temos nossoValidator no lugar, teremos que chamá-lo explicitamente devalidate antes de realmente executar nossa lógica de negócios.
2.2. Executando as validações
A princípio, podemos pensar que usar umHandlerFilterFunction seria adequado em nossa situação.
Mas devemos ter em mente que nesses filtros -mesmo que nos manipuladores- lidamos comasynchronous constructions -tais comoMonoeFlux.
Isso significa que teremos acesso aoPublisher (o objetoMono ouFlux), mas não aos dados que ele eventualmente fornecerá.
Portanto, a melhor coisa que podemos fazer é validar o corpo quando realmente o estamos processando na função de manipulador.
Vamos prosseguir e modificar nosso método de manipulador, incluindo a lógica de validação:
public Mono handleRequest(ServerRequest request) {
Validator validator = new CustomRequestEntityValidator();
Mono responseBody = request
.bodyToMono(CustomRequestEntity.class)
.map(body -> {
Errors errors = new BeanPropertyBindingResult(
body,
CustomRequestEntity.class.getName());
validator.validate(body, errors);
if (errors == null || errors.getAllErrors().isEmpty()) {
return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
} else {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
errors.getAllErrors().toString());
}
});
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(responseBody, String.class);
}
Resumindo, nosso serviço agora recuperará uma resposta ‘Bad Request’ se o corpo da solicitação não estiver em conformidade com nossas restrições.
Podemos dizer que alcançamos nosso objetivo? Bem, estamos quase lá. We’re running the validations, but there are many drawbacks in this approach.
Estamos misturando as validações com a lógica de negócios e, para piorar as coisas, teremos que repetir o código acima em qualquer manipulador para onde queremos carregar nossa validação de entrada.
Vamos tentar melhorar isso.
3. Trabalhando em uma abordagem DRY
To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request.
Todos os manipuladores que precisam de validação de entrada estenderão esta classe abstrata, de modo a reutilizar seu esquema principal e, portanto, seguindo o princípio DRY (não se repita).
Usaremos genéricos de modo a torná-lo flexível o suficiente para suportar qualquer tipo de corpo e seu respectivo validador:
public abstract class AbstractValidationHandler {
private final Class validationClass;
private final U validator;
protected AbstractValidationHandler(Class clazz, U validator) {
this.validationClass = clazz;
this.validator = validator;
}
public final Mono handleRequest(final ServerRequest request) {
// ...here we will validate and process the request...
}
}
Agora vamos codificar nosso métodohandleRequest com o procedimento padrão:
public Mono handleRequest(final ServerRequest request) {
return request.bodyToMono(this.validationClass)
.flatMap(body -> {
Errors errors = new BeanPropertyBindingResult(
body,
this.validationClass.getName());
this.validator.validate(body, errors);
if (errors == null || errors.getAllErrors().isEmpty()) {
return processBody(body, request);
} else {
return onValidationErrors(errors, body, request);
}
});
}
Como podemos ver, estamos usando dois métodos que ainda não criamos.
Vamos definir aquele que é invocado quando temos erros de validação primeiro:
protected Mono onValidationErrors(
Errors errors,
T invalidBody,
ServerRequest request) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
errors.getAllErrors().toString());
}
Essa é apenas uma implementação padrão, mas pode ser facilmente substituída pelas classes filho.
Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case:
abstract protected Mono processBody(
T validBody,
ServerRequest originalRequest);
Existem alguns aspectos a serem analisados nesta classe.
Em primeiro lugar, ao usar genéricos, as implementações filho terão que declarar explicitamente o tipo de conteúdo que estão esperando e o validador que será usado para avaliá-lo.
Isso também torna nossa estrutura robusta, pois limita as assinaturas de nossos métodos.
No tempo de execução, o construtor atribuirá o objeto validador real e a classe usada para converter o corpo da solicitação.
Podemos dar uma olhada na classe completahere.
Vamos agora ver como podemos nos beneficiar dessa estrutura.
3.1. Adaptando nosso manipulador
A primeira coisa que teremos que fazer, obviamente, é estender nosso manipulador desta classe abstrata.
Ao fazer isso,we’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method:
@Component
public class FunctionalHandler
extends AbstractValidationHandler {
private CustomRequestEntityValidationHandler() {
super(CustomRequestEntity.class, new CustomRequestEntityValidator());
}
@Override
protected Mono processBody(
CustomRequestEntity validBody,
ServerRequest originalRequest) {
String responseBody = String.format(
"Hi, %s [%s]!",
validBody.getName(),
validBody.getCode());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
}
Como podemos perceber, nosso manipulador filho agora é muito mais simples do que o obtido na seção anterior, já que evita bagunçar a validação real dos recursos.
4. Suporte para anotações da API de validação de bean
Com essa abordagem, também podemos aproveitar as vantagens do poderosoBean Validation’s annotations fornecido pelo pacotejavax.validation.
Por exemplo, vamos definir uma nova entidade com campos anotados:
public class AnnotatedRequestEntity {
@NotNull
private String user;
@NotNull
@Size(min = 4, max = 7)
private String password;
// ... Constructors, Getters and Setters ...
}
We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean:
public class AnnotatedRequestEntityValidationHandler
extends AbstractValidationHandler {
private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
super(AnnotatedRequestEntity.class, validator);
}
@Override
protected Mono processBody(
AnnotatedRequestEntity validBody,
ServerRequest originalRequest) {
// ...
}
}
Temos que ter em mente que, se houver outros beansValidator presentes no contexto, podemos ter que declarar explicitamente este com a anotação@Primary:
@Bean
@Primary
public Validator springValidator() {
return new LocalValidatorFactoryBean();
}
5. Conclusão
Para resumir, nesta postagem, aprendemos como validar dados de entrada em terminais funcionais do Spring 5.
Criamos uma abordagem agradável para lidar com validações graciosamente, evitando misturar sua lógica com a comercial.
Claro, a solução sugerida pode não ser adequada para qualquer cenário. Teremos que analisar nossa situação e provavelmente adaptar a estrutura às nossas necessidades.
Se quisermos ver todo o exemplo de trabalho, podemos encontrá-lo emour GitHub repo.