DEV Community

Renata Fraga
Renata Fraga

Posted on

Java Annotations no Spring: Crie restrições em suas DTOs

Vibrante por encontrar pessoas que querem criar annotations comigo!

Série Seinfeld: Elenco vibrando

Olá queridos devs!
Hoje neste tutorial irei explicar o que são e como se comportam as famosas annotations, um mecanismo muito poderoso e nem tanto explorado pelos desenvolvedores no universo Java. Além da contextualização, irei apresentar dois exemplos muito simples que podem ser um bom norteador a você que está iniciando esta caminhada.

-1. Alinhando expectativas

Neste tutorial você vai encontrar:

  • Conceitos sobre o que é uma anotação
  • Disponibilidade de anotações
  • Exemplos de anotações disponíveis em tempo de execução focadas em validar restrições

Neste tutorial você não vai encontrar:

  • Fundamentação teórica densa (deixarei bons materiais como referência)
  • Inclusão de atributos na anotação
  • Uso de reflections

1. O que é?

"As annotations são responsáveis por fornecer informações complementares sobre um programa." (GeeksForGeeks, 2020, tradução nossa)

Segundo Oracle ([20--]) "[...] uma forma de metadados, fornecem dados sobre um programa que não faz parte do próprio programa".

As annotations são responsáveis por embutir informações complementares no código fonte. (Groner, 2015)

As anotações surgiram a partir da versão 1.5 do Java e são precedidas por um arroba @. Provavelmente você já deve ter observado algumas dispersas em seu código, como por exemplo: @Override, @RequestBody, @PathVariable, @Getter, @Setter.

2. Qual a finalidade?

As anotações podem ser utilizadas em três finalidades distintas dentro do ciclo de vida do seu projeto (quem define esta configuração é a meta-annotation chamada @Retention, na qual, entrarei em detalhes nas próximas seções). Esta definição também é importante para indicar em qual momento as instruções da anotação devem estar disponíveis e serem descartadas (JAVA2NOVICE, 2020). As três finalidades são exemplificadas abaixo:

Instruções em tempo de compilação (CLASS): tem como objetivo gerar códigos fonte em tempo de compilação. Um exemplo famoso é o da biblioteca Mapstruct, que em tempo de compilação gera componentes que convertem objetos em outros, em cima de uma varredura em todas as interfaces que utilizam a anotação @Mapper (MAPSTRUCT, [201-]).
Instruções em tempo de construção (SOURCE): tem como objetivo gerar empacotamento de arquivos, arquivos XML em tempo de construção do projeto, ou seja, na etapa que está sendo gerado o arquivo .jar ou .war (JENKOV, 2019).
Instruções em tempo de execução(RUNTIME): tem como objetivo criar validações ou injetar informações em tempo de execução, como por exemplo, a anotação @NotNull que é utilizada para validar obrigatoriedade de um atributo de uma classe.

4. Como funciona: exemplo prático

Entendendo as três finalidades apresentadas na seção anterior e sabendo que queremos criar anotações que validem nossas requisições, necessitamos que estas estejam disponíveis em tempo de execução.

Como o foco deste tutorial são anotações disponíveis em tempo de execução, usaremos como exemplo explicativo a anotação @NotNull presente no projeto JakartaEE.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotNull[] value();
    }
}
Enter fullscreen mode Exit fullscreen mode

Abaixo é possível acompanhar as meta-annotations e atributos com seu nome e significado:

Meta-Annotation/Atributo Descrição
@Target Indica qual contexto esta anotação pode ser aplicada. Estes valores são indicados pelo enumerador ElementType. No exemplo acima, percebe-se que é possível aplicar esta anotação em níveis de: métodos, campos (atributos), classes, construtores e parâmetros
@Retention Indica se a anotação estará disponível em tempo de compilação, execução ou construção. Estes valores são indicados pelo enumerador RetentionPolicy. No exemplo acima, a retenção é do tipo RUNTIME, ou seja, a anotação estará disponível para uso em tempo de execução.
@Repeatable Indica possibilidade de utilizar mais de uma vez a anotação em uma classe. Observe que nesta meta-annotation é informado o tipo de anotação na qual deseja-se repetir. No final da implementação da mesma, é possível acompanhar uma espécie de "sub-annotation" que tem um array do tipo NotNull. Só é possível que a anotação utilize o mecanismo de repetição se for implementada esta estrutura com o array do mesmo tipo.
@Documented Indica que essa anotação será documentada via javadoc ou uma ferramenta similar.
@Constraint Indica que a anotação será marcada para uso de restrição de validação. O atributo validatedBy indica a classe que conterá essa validação de restrição (no exemplo acima, não foi elencada esta classe de validação, porém nos exemplos apresentados nas próximas seções irei apresentar o uso desse recurso). Com a adoção desta anotação, é necessário implementar os seguintes atributos: message, groups, payload.
message() Indica a mensagem de erro na qual será informada caso a restrição de validação seja violada. Por padrão, é necessário que informe uma mensagem de erro, que provavelmente estará no arquivo message.properties do seu projeto. No exemplo acima, esta mensagem está internamente em arquivos .properties da biblioteca JakartaEE.
groups() Este atributo é assunto que renderia um bom artigo. Mas resumidamente, este atributo serve para criar grupos de validação específicos. No exemplo, você percebe que não utilizamos este recurso. Ele apenas está ali como uma espécie de "contrato" necessário em detrimento do uso da anotação @Constraint.
payload() Este atributo é assunto que renderia um bom artigo. Segundo a documentação JBoss (2020), "pode ser usada [...] para atribuir objetos de payload personalizados a uma restrição". No exemplo, você percebe que não utilizamos este recurso. Ele apenas está ali como uma espécie de "contrato" necessário em detrimento do uso da anotação @Constraint.

5. Criando anotações

Nosso case: Temos uma API RESTful responsável por cadastrar pessoas físicas e jurídicas. Estas pessoas necessitam ter informações como: nome,documento e tipo de documento. Iremos utilizar duas abordagens para validar estes campos: uso de anotação em nível de classe e campo.

Primeiramente, é necessário que você adicione a anotação @Valid ao lado da @RequestBody. Só assim, é possível que as validações presentes nas anotações utilizadas pelo classe PersonRequest sejam aplicadas. A seguir você acompanha como fica o trecho do código onde se adiciona esta propriedade:

@RestController
public class PersonController {

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Object> create(@Valid @RequestBody PersonRequest personRequest) {

        ...
Enter fullscreen mode Exit fullscreen mode

5.1 Nível de campo (Field Level)

Iremos criar uma anotação que seja capaz de verificar se o campo fullName tem mais de uma palavra. Caso este campo tenha apenas uma única palavra, uma exceção será lançada.
No trecho abaixo, é possível observar a classe PersonRequest com a anotação @GreaterThanOneWord acima do campo fullName:

public class PersonRequest {

    @GreaterThanOneWord
    @NotBlank
    private String fullName;

    @NotBlank
    private String document;

    @NotNull
    private DocumentType documentType;

Enter fullscreen mode Exit fullscreen mode

Colocamos uma anotação inexistente acima do atributo, porém é hora de pôr a mão na massa e cria-la!

Abaixo é possível acompanhar a implementação da anotação:

@Repeatable(GreaterThanOneWord.List.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = GreaterThanOneWorldValidation.class)
public @interface GreaterThanOneWord {

    String message() default "{error.business.greater_than_one_word}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    @interface List {
        GreaterThanOneWord[] value();
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe que no exemplo acima, utilizamos a meta-annotation @Repeatable para possibilitar o reuso desta anotação em outros atributos da mesma classe. Na @Target, informamos que a anotação só pode ser utilizada em nível de campo. Em @Retention, utilizamos o tipo RUNTIME em razão de sua utilização ser em tempo de execução. Em @Constraint, informamos no atributo validatedBy a classe que iremos aplicar a validação que indicará se ocorreu ou não violação da restrição.

No trecho a seguir, é possível acompanhar o arquivo messages_en_US.properties com a mensagem padrão que será apresentada caso haja violação da restrição:

error.business.greater_than_one_word=The field must be filled with more than one word

Enter fullscreen mode Exit fullscreen mode

Abaixo é possível acompanhar a implementação da validação das restrições:

public class GreaterThanOneWorldValidation implements ConstraintValidator<GreaterThanOneWord, String> {

    @Override
    public boolean isValid(String field, ConstraintValidatorContext constraintValidatorContext) {
        return field.split(" ").length > 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe no exemplo acima que para utilizar esta classe na propriedade validatedBy, é necessário que esta implemente a interface ConstraintValidator. Nos dois generics apresentados pela interface, você deve informar:

  • 1º: o tipo da anotação criada
  • 2º: tipo de objeto que será validado (caso queira trabalhar com atributos genéricos você deve informar Object)

Essa interface oferece dois métodos:

  • initilize(): pode ou não ser implementado. Normalmente utilizado quando a anotação recebe informações nos atributos que necessitam ser utilizados no método isValid.
  • isValid(): método booleano responsável por validar se há restrição violada ou não. Ali é aplicado a lógica desejada. Caso retorne true significa que nenhuma restrição foi violada, caso contrário uma exceção será lançada.

Acesse o LINK do repositório do projeto. Lá você encontra a implementação completa, além de tratamento de exceções, internacionalização e testes unitários.

5.2 Nível de classe (Type Level)

Agora, iremos criar uma anotação para verificar se a quantidade de caracteres inseridos no campo document corresponde ao documentType informado, ou seja:

  • se documentType for igual a CNPJ, o campo document deve ter 14 caracteres
  • se documentType for igual a CPF, o campo document deve ter 11 caracteres

Peço que não levem em consideração a veracidade desta validação na qual criei, pois sabemos que existem algumas variações acerca da quantidade de caracteres de um CNPJ. Mas para fins didáticos, esse exemplo será útil.

No trecho abaixo, é possível observar a anotação @IsValidDocumentFormat sobre a classe PersonRequest:

@IsValidDocumentFormat
public class PersonRequest {

    @NotBlank
    private String fullName;

    @NotBlank
    private String document;

    @NotNull
    private DocumentType documentType;
Enter fullscreen mode Exit fullscreen mode

Abaixo é possível acompanhar a implementação da anotação:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IsValidDocumentFormatValidation.class)
public @interface IsValidDocumentFormat {

    String message() default "{error.business.is_valid_document_format}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
Enter fullscreen mode Exit fullscreen mode

Como apresentado no exemplo acima, não utilizamos a meta-annotation @Repeatable, pois desejamos que esta seja utilizada apenas uma única vez na classe. Na @Target, informamos que a anotação só pode ser utilizada em nível de classe. As demais meta-annotations já foram esclarecidas na sub-seção anterior.

No trecho a seguir, é possível acompanhar o arquivo messages_en_US.properties com a mensagem padrão que será apresentada caso haja violação da restrição:

error.business.is_valid_document_format=Invalid document format for the type entered
Enter fullscreen mode Exit fullscreen mode

Abaixo é possível acompanhar a implementação da validação das restrições:

public class IsValidDocumentFormatValidation implements ConstraintValidator<IsValidDocumentFormat,
        PersonRequest> {

    @Override
    public boolean isValid(PersonRequest personRequest, ConstraintValidatorContext constraintValidatorContext) {
        final boolean isValidCpf = personRequest.getDocument().length() == 11 && personRequest.getDocumentType()
                .equals(DocumentType.CPF);

        final boolean isValidCnpj = personRequest.getDocument().length() == 14 && personRequest.getDocumentType()
                .equals(DocumentType.CNPJ);

        return isValidCpf || isValidCnpj;
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe que nos dois generics apresentados pela interface, você deve informar:

  • 1º: o tipo da anotação criada
  • 2º: tipo de classe que será validada (caso queira trabalhar com classes genéricas você deve informar Object)

Acesse o LINK do repositório do projeto. Lá você encontra a implementação completa, além de tratamento de exceções, internacionalização e testes unitários.

6. Conclusão e Próximos passos

O que podemos concluir acerca da adotação de anotações é que elas podem ser um ótimo mecanismo para realizar validações de entrada, reaproveitando códigos e abstraindo muitas estruturas.

É importante ressaltar que as anotações não são soluções mágicas que resolvem todos os seus problemas. Elas não podem ser utilizadas de forma demasiada, nem ter grande responsabilidade a ponto de sua camada de negócios tornar-se um mero coadjuvante dentro do seu serviço (acredito que isso seja pauta inclusive de maiores discussões).

Neste exemplo, não pude apresentar anotações em tempo de compilação e construção, tampouco a adoção de anotações genéricas que fazem uso de Reflections. Resolvi trazer neste artigo esta parte introdutória para que possamos ir aprofundando aos poucos. Caso você queira que eu continue essa série de artigos deixe nos comentários ou no meu LinkedIn.

Vou deixar abaixo as referências de minha pesquisa e recomendo fortemente que você as leia, principalmente na documentação oficial sobre as meta-annotations.

Referências:

DOLSZEWSKI. Spring Custom Validation by Example. Disponível em: http://dolszewski.com/spring/custom-validation-annotation-in-spring/. Acesso em: 26 Ago 2020.
GEEKS FOR GEEKS.Annotations In Java. Disponível em: https://www.geeksforgeeks.org/annotations-in-java/. Acesso em: 23 Ago 2020.
GRONER, Loiane.Vídeo: Curso de Java 65: Annotations (anotações). Disponível em: https://www.youtube.com/watch?v=6M8EE_oRwtM. Acesso em: 23 Ago 2020.
JAKARTAEE.Annotation Type NotNull. Disponível em: https://jakarta.ee/specifications/platform/8/apidocs/javax/validation/constraints/NotNull.html. Acesso em: 23 Ago 2020.
JAVA2NOVICE. What is Retention policy in java annotations?. Disponível em: https://www.java2novice.com/java-annotations/retention-policy/#:~:text=Annotation%20with%20retention%20policy%20SOURCE,to%20the%20JVM%20through%20runtime. Acesso em: 26 Ago 2020.
JBOSS. 6. Creating custom constraints. Disponível em: https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints. Acesso em: 26 Ago 2020.
JENKOV, Jakob. Java Annotations. Disponível em: http://tutorials.jenkov.com/java/annotations.html. Acesso em: 23 Ago 2020.
MAPSTRUCT. Mapper Annotation Type. Disponível em: https://mapstruct.org/documentation/dev/api/. Acesso em: 26 Ago 2020.
ORACLE. Annotation Type Constraint. Disponível em: https://jakarta.ee/specifications/platform/8/apidocs/javax/validation/Constraint.html. Acesso em: 26 Ago 2020.
ORACLE. Annotation Type Documented. Disponível em: https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Documented.html?is-external=true. Acesso em: 26 Ago 2020.
ORACLE. Annotation Type Repeatable. Disponível em: https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Repeatable.html?is-external=true. Acesso em: 26 Ago 2020.
ORACLE. Annotation Type Retention. Disponível em: https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Retention.html. Acesso em: 26 Ago 2020.
ORACLE. Annotation Type Target. Disponível em: https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Target.html. Acesso em: 26 Ago 2020.
ORACLE.Lesson: Annotations. Disponível em: https://docs.oracle.com/javase/tutorial/java/annotations/.
Acesso em: 23 Ago 2020.

Latest comments (0)