Este texto é dedicado a várias abordagens à validação de dados: quais as armadilhas que um projeto pode tropeçar e quais métodos e tecnologias devem ser seguidos ao validar dados em aplicativos Java.

Muitas vezes vi projetos cujos criadores não se preocuparam em escolher uma abordagem para validação de dados. As equipes trabalharam no projeto sob uma pressão incrível na forma de prazos e requisitos vagos e, como resultado, eles simplesmente não tiveram tempo para uma validação precisa e consistente. Portanto, seu código de validação está espalhado por todo o lado: em trechos de Javascript, controladores de tela, em beans de lógica de negócios, entidades de domínio, gatilhos e restrições de banco de dados. Esse código estava cheio de instruções if-else, lançou várias exceções e tentou descobrir onde esse dado específico é validado lá ... Como resultado, à medida que o projeto se desenvolve, torna-se difícil e caro cumprir os requisitos (geralmente bastante confusos) e uniformidade de abordagens para validação de dados.
Existe alguma maneira simples e elegante de validar dados? Existe uma maneira de nos proteger do pecado da ilegibilidade, uma maneira de reunir toda a lógica da validação e que já foi criada para nós pelos desenvolvedores de estruturas Java populares?
Sim, existe esse caminho.
Para nós, desenvolvedores da plataforma CUBA , é muito importante que você possa usar as melhores práticas. Acreditamos que o código de validação deve:
- Seja reutilizável e siga o princípio DRY;
- Seja natural e compreensível;
- Colocado onde o desenvolvedor espera vê-lo;
- Poder verificar dados de diferentes fontes: interface do usuário, chamadas SOAP, REST, etc.
- Trabalhe em um ambiente multithread sem problemas;
- Chamado dentro do aplicativo automaticamente, sem a necessidade de executar verificações manualmente;
- Para fornecer ao usuário mensagens claras e localizadas em caixas de diálogo concisas;
- Siga os padrões.
Vamos ver como isso pode ser implementado usando um aplicativo de exemplo escrito usando a estrutura da plataforma CUBA. No entanto, como o CUBA é baseado no Spring e no EclipseLink, a maioria das técnicas usadas aqui funcionará em qualquer outra plataforma Java que suporte as especificações de Validação de JPA e Bean.
Validação usando restrições de banco de dados
Talvez a maneira mais comum e óbvia de validar dados seja usar restrições no nível do banco de dados, por exemplo, o sinalizador necessário (para campos cujo valor não pode estar vazio), comprimento da linha, índices exclusivos etc. Esse método é mais adequado para aplicativos corporativos, pois esse tipo de software geralmente é estritamente focado no processamento de dados. No entanto, mesmo aqui os desenvolvedores geralmente cometem erros ao definir limites separadamente para cada nível do aplicativo. Na maioria das vezes, o motivo está na distribuição de responsabilidades entre os desenvolvedores.
Considere um exemplo que muitos de nós sabemos, alguns até de nossa própria experiência ... Se a especificação indicar que deve haver 10 caracteres no campo número do passaporte, é muito provável que isso seja verificado por todos: um arquiteto de banco de dados no DDL, um desenvolvedor de back-end na Entidade correspondente e Serviços REST e, finalmente, o desenvolvedor da interface do usuário diretamente no lado do cliente. Em seguida, esse requisito muda e o campo aumenta para 15 caracteres. Os devops alteram os valores das restrições no banco de dados, mas nada muda para o usuário, porque no lado do cliente a restrição é a mesma ...
Qualquer desenvolvedor sabe como evitar esse problema - a validação deve ser centralizada! No CUBA, essa validação é encontrada nas anotações da entidade JPA. Com base nessas meta-informações, o CUBA Studio gerará o script DDL correto e aplicará os validadores apropriados do lado do cliente.

Se as anotações forem alteradas, o CUBA atualizará os scripts DDL e gerará os scripts de migração. Portanto, na próxima vez que você implantar o projeto, novas restrições baseadas em JPA entrarão em vigor na interface e no banco de dados do aplicativo.
Apesar da simplicidade e implementação no nível do banco de dados, o que confere confiabilidade absoluta a esse método, o escopo das anotações JPA é limitado aos casos mais simples que podem ser expressos no padrão DDL e não incluem acionadores de banco de dados ou procedimentos armazenados. Portanto, restrições baseadas em JPA podem tornar um campo de entidade exclusivo ou obrigatório ou definir um tamanho máximo de coluna. Você pode até definir uma restrição exclusiva na combinação de colunas usando a anotação @UniqueConstraint
. Mas isso é provavelmente tudo.
Seja como for, nos casos que exigem lógica de validação mais complexa, como verificar um campo em busca de um valor mínimo / máximo, validar com uma expressão regular ou executar uma verificação personalizada específica apenas para o seu aplicativo, a abordagem conhecida como "Validação de Bean" é aplicada .
Validação de Bean
Todo mundo sabe que é uma boa prática seguir padrões que tenham um longo ciclo de vida, cuja eficácia foi comprovada em milhares de projetos. A validação do Java Bean é uma abordagem documentada nas JSR 380, 349 e 303 e seus aplicativos: Hibernate Validator e Apache BVal .
Embora essa abordagem seja familiar para muitos desenvolvedores, geralmente é subestimada. Essa é uma maneira fácil de incorporar a validação de dados mesmo em projetos herdados, o que permite criar validações de forma clara, simples, confiável e o mais próxima possível da lógica de negócios.
O uso da Validação de Bean oferece ao projeto muitas vantagens:
- A lógica de validação está localizada próxima à área de assunto: a definição de restrições para os campos e métodos do compartimento ocorre de maneira natural e verdadeiramente orientada a objetos.
- O padrão de Validação de Bean fornece dezenas de anotações de validação
@Size
para uso, por exemplo: @NotNull
, @Size
, @Min
, @Max
, @Pattern
, @Email
, @Past
, não muito padrão @URL
, @Length
, o mais poderoso @ScriptAssert
e muitos outros . - O padrão não nos limita a anotações prontas e nos permite criar as nossas. Também podemos criar uma nova anotação combinando várias outras ou defini-la usando uma classe Java separada como validador.
Por exemplo, no exemplo acima, podemos definir a anotação do nível de classe @ValidPassportNumber
para verificar se o número do passaporte corresponde ao formato, dependendo do valor do campo do country
. - As restrições podem ser definidas não apenas em campos ou classes, mas também em métodos e seus parâmetros. Essa abordagem é chamada de "validação por contrato" e será discutida um pouco mais tarde.
Quando o usuário envia as informações inseridas, a Plataforma CUBA (como algumas outras estruturas) inicia a Validação de Bean automaticamente, para exibir instantaneamente uma mensagem de erro se a validação falhar, e não precisamos executar os validadores de lixeira manualmente.
Vamos voltar ao exemplo com o número do passaporte, mas desta vez vamos complementá-lo com várias restrições da entidade Pessoa:
- O campo de
name
deve ter 2 ou mais caracteres e deve ser válido. (Como você pode ver, regexp não é simples, mas "Charles Ogier de Batz de Castelmore, Conde d'Artagnan" passa no teste, mas "R2D2" não); height
(altura) deve estar no seguinte intervalo: 0 < height <= 300
cm;- O campo de
email
deve conter uma sequência que corresponda ao formato do email correto.
Com todas essas verificações, a classe Person ficará assim:
@Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { private static final long serialVersionUID = -9150857881422152651L; @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") @Length(min = 2) @NotNull @Column(name = "NAME", nullable = false) protected String name; @Email(message = "Email address has invalid format: ${validatedValue}", regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") @Column(name = "EMAIL", length = 120) protected String email; @DecimalMax(message = "Person height can not exceed 300 centimeters", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) @Column(name = "HEIGHT") protected BigDecimal height; @NotNull @Column(name = "COUNTRY", nullable = false) protected Integer country; @NotNull @Column(name = "PASSPORT_NUMBER", nullable = false, length = 15) protected String passportNumber; ... }
Person.java
Acredito que o uso de anotações como @NotNull
, @DecimalMin
, @Length
, @Pattern
e similares é bastante óbvio e não requer comentários. Vamos dar uma olhada na implementação da anotação @ValidPassportNumber
.
Nosso novo @ValidPassportNumber
verifica se Person#passportNumber
corresponde ao padrão de regexp para cada país especificado pelo campo Person#country
.
Primeiro, vejamos a documentação (os manuais CUBA ou Hibernate estão bem), de acordo com ela, precisamos marcar nossa classe com esta nova anotação e passar o parâmetro groups
para ela, onde UiCrossFieldChecks.class
significa que essa validação deve ser executada no cruzamento validações - depois de verificar todos os campos individuais, e Default.class
salva a restrição no grupo de validação padrão.
A descrição da anotação fica assim:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = ValidPassportNumberValidator.class) public @interface ValidPassportNumber { String message() default "Passport number is not valid"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
ValidPassportNumber.java
Aqui, @Target(ElementType.TYPE)
diz que o objetivo desta anotação de tempo de execução é a classe, e @Constraint(validatedBy = … )
determina que a validação é executada pela classe ValidPassportNumberValidator
que implementa a interface ConstraintValidator<...>
. O código de validação em si está no método isValid(...)
, que executa a verificação real de uma maneira bastante direta:
public class ValidPassportNumberValidator implements ConstraintValidator<ValidPassportNumber, Person> { public void initialize(ValidPassportNumber constraint) { } public boolean isValid(Person person, ConstraintValidatorContext context) { if (person == null) return false; if (person.country == null || person.passportNumber == null) return false; return doPassportNumberFormatCheck(person.getCountry(), person.getPassportNumber()); } private boolean doPassportNumberFormatCheck(CountryCode country, String passportNumber) { ... } }
ValidPassportNumberValidator.java
Isso é tudo. Com a plataforma CUBA, não precisamos escrever nada além de uma linha de código que faça nossa validação personalizada funcionar e forneça mensagens de erro ao usuário.
Nada complicado, certo?
Agora vamos ver como tudo funciona. Aqui, o CUBA possui outro nishtyaki: não apenas mostra ao usuário uma mensagem de erro, mas também destaca os campos vermelhos que não passaram na validação do bean:

Não é uma solução elegante? Você obtém uma exibição adequada dos erros de validação na interface do usuário adicionando apenas algumas anotações Java às entidades da área de assunto.
Para resumir a seção, listemos brevemente as vantagens da Validação de Bean para entidades mais uma vez:
- É compreensível e legível;
- Permite definir restrições de valor diretamente nas classes de entidade;
- Pode ser personalizado e complementado;
- Integrado em ORMs populares, e as verificações são executadas automaticamente antes que as alterações sejam salvas no banco de dados;
- Algumas estruturas também executam a validação de bean automaticamente quando o usuário envia dados para a interface do usuário (e, caso contrário, é fácil chamar a interface do
Validator
manualmente); - A Validação de Bean é um padrão reconhecido e está cheio de documentação na Internet.
Mas e se você precisar definir uma restrição em um método, construtor ou endereço REST para validar dados provenientes de um sistema externo? Ou, se você precisar verificar declarativamente os valores dos parâmetros do método, sem escrever código chato com muitas condições if-else em cada método sendo testado?
A resposta é simples: a Validação de Bean também se aplica a métodos!
Validação por contrato
Às vezes, você precisa ir além da validação do estado do modelo de dados. Muitos métodos podem se beneficiar da validação automática de parâmetros e do valor de retorno. Isso pode ser necessário não apenas para verificar os dados que vão para endereços REST ou SOAP, mas também para os casos em que queremos anotar as pré-condições e pós-condições de chamadas de método para garantir que os dados inseridos foram verificados antes da execução do corpo do método ou que o valor de retorno está no intervalo esperado ou, por exemplo, apenas precisamos descrever declarativamente os intervalos dos valores dos parâmetros de entrada para melhorar a legibilidade do código.
Usando a validação de bean, restrições podem ser aplicadas aos parâmetros de entrada e retornam valores de métodos e construtores para verificar as pré-condições e pós-condições de suas chamadas em qualquer classe Java. Esse caminho possui várias vantagens sobre os métodos tradicionais de verificação da validade dos parâmetros e valores de retorno:
- Não é necessário executar verificações manualmente em um estilo imperativo (por exemplo, lançando
IllegalArgumentException
e similares). Você pode definir restrições declarativamente e tornar o código mais compreensível e expressivo; - As restrições podem ser configuradas, reutilizadas e configuradas: você não precisa escrever a lógica de validação para cada verificação. Menos código significa menos erros.
- Se a classe, o valor de retorno do método ou seu parâmetro estiver marcado com a anotação
@Validated
, as verificações serão executadas automaticamente pela plataforma a cada chamada de método. - Se o executável estiver marcado com a anotação
@Documented
, suas pré e pós-condições serão incluídas no JavaDoc gerado.
Usando 'validação de contrato' , obtemos um código claro, compacto e de fácil manutenção.
Por exemplo, vejamos a interface do controlador REST de um aplicativo CUBA. A interface PersonApiService
permite obter uma lista de pessoas do banco de dados usando o método getPersons()
e adicionar uma nova pessoa usando a addNewPerson(...)
.
E não esqueça que a validação de bean é herdada! Em outras palavras, se anotamos uma determinada classe, campo ou método, todas as classes que herdam essa classe ou implementam essa interface estarão sujeitas à mesma anotação de validação.
@Validated public interface PersonApiService { String NAME = "passportnumber_PersonApiService"; @NotNull @Valid @RequiredView("_local") List<Person> getPersons(); void addNewPerson( @NotNull @Length(min = 2, max = 255) @Pattern(message = "Bad formed person name: ${validatedValue}", regexp = "^[AZ][az]*(\\s(([az]{1,3})|(([az]+\\')?[AZ][az]*)))*$") String name, @DecimalMax(message = "Person height can not exceed 300 cm", value = "300") @DecimalMin(message = "Person height should be positive", value = "0", inclusive = false) BigDecimal height, @NotNull CountryCode country, @NotNull String passportNumber ); }
PersonApiService.java
Esse trecho de código é claro o suficiente?
_ (Exceto a anotação @RequiredView(“_local”)
, específica da Plataforma CUBA e verificando se o objeto Person
retornado contém todos os campos da tabela PASSPORTNUMBER_PERSON
) ._
A @Valid
define que cada objeto de coleção retornado pelo método getPersons()
também deve ser validado com relação às restrições da classe Person
.
Em um aplicativo CUBA, esses métodos estão disponíveis nos seguintes endereços:
- / app / rest / v2 / services / passportnumber_PersonApiService / getPersons
- / app / rest / v2 / services / passportnumber_PersonApiService / addNewPerson
Vamos abrir o aplicativo Postman e garantir que a validação funcione como deveria:

Como você deve ter notado, o número do passaporte não é validado no exemplo acima. Isso ocorre porque esse campo requer uma verificação cruzada dos parâmetros do método addNewPerson
, pois a escolha de um modelo de expressão regular para validar passportNumber
depende do valor do campo country
. Essa validação cruzada é um análogo completo das restrições de entidade no nível de classe!
A validação cruzada de parâmetros é suportada pelo JSR 349 e 380. Você pode ler a documentação do hibernate para aprender como implementar sua própria validação cruzada de métodos de classe / interface.
Validação de Bean Externo
Não há perfeição no mundo, portanto, a validação de bean tem suas desvantagens e limitações:
- Às vezes, precisamos apenas verificar o estado de um gráfico complexo de objetos antes de salvar as alterações no banco de dados. Por exemplo, você precisa garantir que todos os elementos de um pedido do cliente sejam colocados em um pacote. Essa é uma operação bastante difícil, e realizá-la sempre que o usuário adicionar novos itens ao pedido não é uma boa ideia. Portanto, essa verificação pode ser necessária apenas uma vez: antes de salvar o objeto
Order
e seus subobjetos OrderItem
no banco de dados. - Algumas verificações precisam ser feitas dentro de uma transação. Por exemplo, o sistema de armazenamento eletrônico deve verificar se há cópias suficientes das mercadorias para atender ao pedido antes de enviá-lo ao banco de dados. Essa verificação só pode ser realizada dentro de uma transação, porque O sistema é multiencadeado e a quantidade de mercadorias em estoque pode mudar a qualquer momento.
A Plataforma CUBA oferece dois mecanismos de validação de dados pré-confirmados chamados ouvintes de entidade e ouvintes de transação . Vamos considerá-los com mais detalhes.
Listemers de entidade
Os ouvintes de entidade no CUBA são muito semelhantes aos PreInsertEvent
, PreUpdateEvent
e PredDeleteEvent
que o JPA oferece ao desenvolvedor. Ambos os mecanismos permitem verificar objetos de entidade antes e depois de serem armazenados no banco de dados.
No CUBA, é fácil criar e conectar um ouvinte de entidade, para isso você precisa de duas coisas:
- Crie um bean gerenciado que implemente uma das interfaces do ouvinte da entidade. 3 interfaces são importantes para validação:
BeforeDeleteEntityListener<T>
,
BeforeInsertEntityListener<T>
,
BeforeUpdateEntityListener<T>
- Adicione a anotação
@Listeners
ao objeto de entidade que você planeja rastrear.
E isso é tudo.
Comparadas ao padrão JPA (JSR 338, Seção 3.5), as interfaces de ouvinte da Plataforma CUBA são digitadas, portanto, você não precisa converter um argumento do tipo Object
em um tipo de entidade para começar a trabalhar com ele. A plataforma CUBA adiciona entidades relacionadas ou chamadores do EntityManager a capacidade de carregar e modificar outras entidades. Todas essas alterações também chamarão o ouvinte da entidade correspondente.
A plataforma CUBA também suporta "exclusão reversa" , uma abordagem em que, em vez de excluir registros do banco de dados, eles são marcados apenas como excluídos e se tornam inacessíveis para uso normal. Portanto, para exclusão leve, a plataforma chama ouvintes BeforeDeleteEntityListener
/ BeforeDeleteEntityListener
, enquanto implementações padrão chamam PostUpdate
/ PostUpdate
.
Vejamos um exemplo. Aqui, o bean de ouvinte de evento se conecta à classe de entidade com apenas uma linha de código: a anotação @Listeners
, que leva o nome da classe de ouvinte:
@Listeners("passportnumber_PersonEntityListener") @NamePattern("%s|name") @Table(name = "PASSPORTNUMBER_PERSON") @Entity(name = "passportnumber$Person") @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @FraudDetectionFlag public class Person extends StandardEntity { ... }
Person.java
A implementação do ouvinte se parece com isso:
@Component("passportnumber_PersonEntityListener") public class PersonEntityListener implements BeforeDeleteEntityListener<Person>, BeforeInsertEntityListener<Person>, BeforeUpdateEntityListener<Person> { @Override public void onBeforeDelete(Person person, EntityManager entityManager) { if (!checkPassportIsUnique(person.getPassportNumber(), person.getCountry(), entityManager)) { throw new ValidationException( "Passport and country code combination isn't unique"); } } @Override public void onBeforeInsert(Person person, EntityManager entityManager) {
PersonEntityListener.java
Ouvintes de entidade são uma ótima opção se:
- É necessário verificar os dados dentro da transação antes que o objeto da entidade seja armazenado no banco de dados;
- É necessário verificar os dados no banco de dados durante o processo de validação, por exemplo, para verificar se há produto suficiente em estoque para aceitar o pedido;
- Você precisa olhar não apenas para o objeto da entidade, como
Order
, mas também para entidades relacionadas, por exemplo, OrderItems
para a entidade Order
; - Queremos rastrear as operações de inserção, atualização ou exclusão apenas para determinadas classes de entidade, por exemplo, apenas para
OrderItem
Order
e OrderItem
, e não precisamos verificar alterações em outras classes de entidade durante a transação.
Ouvintes de transação
Os ouvintes de transação CUBA também atuam no contexto de transações, mas, comparados aos ouvintes de entidade, eles são chamados para cada transação de banco de dados.
Isso lhes dá super poder:
- nada pode escapar da atenção deles.
Mas o mesmo é determinado por suas deficiências:
- eles são mais difíceis de escrever;
- eles podem reduzir significativamente o desempenho;
- Eles devem ser escritos com muito cuidado: um bug no ouvinte de transações pode interferir mesmo com o carregamento inicial do aplicativo.
Portanto, os ouvintes de transação são uma boa solução quando você precisa inspecionar diferentes tipos de entidades usando o mesmo algoritmo, por exemplo, verificando todos os dados em busca de fraudes cibernéticas com um único serviço que atende a todos os seus objetos de negócios.

Dê uma olhada em uma amostra que verifica se a entidade possui uma anotação @FraudDetectionFlag
e, se houver, inicia um detector de fraude. Repito: lembre-se de que esse método é chamado no sistema antes de confirmar cada transação do banco de dados ; portanto, o código deve tentar verificar o menor número possível de objetos.
@Component("passportnumber_ApplicationTransactionListener") public class ApplicationTransactionListener implements BeforeCommitTransactionListener { private Logger log = LoggerFactory.getLogger(ApplicationTransactionListener.class); @Override public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) { for (Entity entity : managedEntities) { if (entity instanceof StandardEntity && !((StandardEntity) entity).isDeleted() && entity.getClass().isAnnotationPresent(FraudDetectionFlag.class) && !fraudDetectorFeedAndFastCheck(entity)) { logFraudDetectionFailure(log, entity); String msg = String.format( "Fraud detection failure in '%s' with id = '%s'", entity.getClass().getSimpleName(), entity.getId()); throw new ValidationException(msg); } } } ... }
ApplicationTransactionListener.java
Para se tornar um ouvinte de transações, um bean gerenciado deve implementar a interface BeforeCommitTransactionListener
e o método beforeCommit
. Os ouvintes de transação são vinculados automaticamente quando o aplicativo é iniciado. CUBA registra todas as classes que implementam BeforeCommitTransactionListener
ou AfterCompleteTransactionListener
como ouvintes de transação.
Conclusão
A validação de bean (JPA 303, 349 e 980) é uma abordagem que pode servir como base confiável para 95% dos casos de validação de dados encontrados em um projeto corporativo. A principal vantagem dessa abordagem é que a maior parte da lógica de validação está concentrada diretamente nas classes de modelo de domínio. Portanto, é fácil de encontrar, fácil de ler e fácil de manter. Spring, CUBA e muitas outras bibliotecas suportam esses padrões e executam automaticamente verificações de validação ao receber dados na camada da interface do usuário, chamar métodos validados ou armazenar dados através do ORM, portanto, a validação do Bean geralmente parece mágica do ponto de vista do desenvolvedor.
Alguns desenvolvedores de software vêem a validação no nível de classes do modelo de assunto como antinatural e muito complexa, dizem que a validação de dados no nível da interface do usuário é uma estratégia bastante eficaz. No entanto, acredito que vários pontos de validação em componentes e controladores de interface do usuário não são a abordagem mais racional. , , , , , listener' .
, , :
- JPA , , DDL.
- Bean Validation — , , , . , .
- bean validation, . , , REST.
- Entity listeners: , Bean Validation, . , . Hibernate .
- Transaction listeners — , , . , , .
PS: , Java, , , .
Links úteis
Texto ocultoNormas e sua implementação
Bibliotecas