Criando anotações no Java

(Last Updated On: 14 de fevereiro de 2018)

Temos a seguinte classe que representa um usuário no nosso sistema:


public class Usuario {

   private String nome;
   private String cpf;
   private LocalDate dataNascimento;
}

Para salvar um novo usuário, várias validações são feitas, como por exemplo: Ver se o nome só contém letras, o CPF só números e ver se o usuário possui no mínimo 18 anos. Veja o método que faz essa validação:


public boolean usuarioValido(Usuario usuario){
   if(!usuario.getNome().matches("[a-zA-Záàâãéèêíïóôõöúçñ\\s]+")){
      return false;
   }
   if(!usuario.getCpf().matches("[^0-9]+")){
      return false;
   }
   return Period.between(usuario.getDataNascimento(), LocalDate.now()).getYears() >= 18;
}

Suponha agora que eu tenha outra classe, a classe Produto, que contém um atributo nome e eu quero fazer a mesma validação que fiz para o nome do usuário: Ver se só contém letras. E aí? Vou criar outro método para fazer a mesma validação? Ou criar uma interface ou uma classe que tanto Usuario quanto Produto estendem? Não faz muito sentido né? Como resolver esse caso sem repetir código?

Anotações

No Java 5 um novo recurso foi introduzido à linguagem, as anotações. Elas permitem que metadados sejam escritos diretamente no código.

Metadados são, por definição, dados que fazem referência aos próprios dados.

Para nos ajudar a entender o conceito de metadados vou usar a definição feita pelo autor Eduardo Guerra no livro Componentes Reutilizáveis em Java com Reflexão e Anotações:

No contexto da orientação a objetos, os metadados são informações sobre os

elementos do código. Essas informações podem ser definidas em qualquer meio,

bastando que o software ou componente as recupere e as utilize para agregar novas

informações nos elementos do código.”

Perceba que, por si só, anotações não fazem nada. Elas precisam que a aplicação as recupere e as utilize, para que, só assim, elas consigam nos fornecer algo que possamos usar para realizar alguma tarefa.

Voltando ao nosso problema, vamos criar uma anotação para validar a idade mínina do usuário. Para isso, vamos anotar nossa classe:


public class Usuario {

    private String nome;
    private String cpf;
    @IdadeMinima
    private LocalDate dataNascimento;

Se olharmos nosso código perceberemos que ele não compila, pois falta implementarmos a anotação @IdadeMinina. Logo, precisamos criar uma nova classe com o nome IdadeMinima:


public class IdadeMinima {
}

Mas, pensando bem, estamos criando uma classe? Não estamos! Portanto, a nomenclatura é diferente para uma anotação. A forma correta seria:


public @interface IdadeMinima {
}

Estranho, né? Mas foi o jeito que o pessoal do Java fez para falar que esse arquivo se trata de uma anotação.

Agora temos que anotar nossa interface com algumas anotações obrigatórias para que o Java entenda onde e quando sua anotação pode ser utilizada, sendo elas:

  • @Retention – Aqui nós falaremos para a nossa aplicação até quando nossa anotação estará disponível.
  • @Target – Aqui passaremos os elementos que podem ser anotados com essa anotação.

Até onde nossa anotação estará disponível? Precisamos que ela seja executada quando o usuário enviar os seus dados, e isso acontece quando nossa aplicação está rodando, logo precisamos dela em tempo de execução, Runtime:


@Retention(RetentionPolicy.RUNTIME)
public @interface IdadeMinima {
}

E quem será anotado? Que elemento faz sentido ser anotado com uma anotação que verifica se o usuário tem idade suficiente? Um atributo, certo? Logo, um Field:


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdadeMinima {
}

Agora que já especificamos o contexto da nossa anotação, precisamos falar qual a idade mínima que a nossa anotação deve usar para validar a idade do usuário, para isso, vamos criar uma propriedade na nossa anotação chamada valor


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IdadeMinima {
    int valor();
}

Nossa anotação está completa, vamos agora anotar o atributo dataNascimento da classe Usuário com ela:


public class Usuario {

    private String nome;
    private String cpf;
    @IdadeMinima
    private LocalDate dataNascimento;
    //getters e setters
}

Fazendo apenas isso, receberemos um erro de compilação. Precisamos passar a idade mínima para a nossa anotação, logo:


public class Usuario {
    
    private String nome;
    private String cpf;
    @IdadeMin(valor=18)
    private LocalDate dataNascimento;
    //getters e setters
}

Para validar essa anotação vamos criar um usuário e passar para um método validador():


public static void main(String[] args) {
   Usuario usuario = new Usuario("Mário", "42198284863", LocalDate.of(1995, Month.MARCH, 14));
   System.out.println(validador(usuario));
}

Agora vamos criar o método validador() que retornará um boolean


public static boolean validador(Usuario usuario) {
}

O problema de criar nosso método dessa forma é que novamente estamos nos limitando a validar apenas usuários, só que nossa meta é validar qualquer objeto.

Para isso, podemos fazer uso de Generics que, no nosso caso, irá permitir receber um objeto de qualquer tipo:


public static <T> boolean validador(T objeto) {
}

Agora estamos falando que iremos receber um objeto de tipo genérico T. Mas fazer isso não é suficiente, precisamos validar esse objeto. E como validamos um objeto que não sabemos o tipo?

Precisamos descobrir, em tempo de execução, informações a respeito do objeto que irá chegar no nosso método, logo, podemos usar reflexão. Com reflexão podemos descobrir e operar dados da classe. Então, primeiramente, vamos pegar a classe desse objeto:


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
}

Com isso conseguimos operar sob a classe referente ao tipo do objeto recebido. Primeiro, vamos descobrir qual atributo da nossa classe está anotado com o @IdadeMinima.

Para descobrir isso, vamos iterar sobre os atributos da classe usando o método getDeclaredFields():


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
   }
}

Agora estamos iterando por todos os atributos da classe do nosso objeto. O próximo passo é descobrir qual campo está anotado com a nossa anotação.

Vamos usar o método isAnnotationPresent(). Esse método verifica se o campo contém a anotação passada e retorna um boolean.


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
      if (field.isAnnotationPresent(IdadeMinima.class)) {
      }
   }
}

Caso entre no if saberemos que o campo possui a anotação IdadeMinima. Só falta compararmos a idade mínima que atribuímos na nossa anotação com a idade passada. Para fazer isso vamos pegar a nossa anotação.

Para conseguir pegar a nossa anotação usaremos o método getAnnotation() passando a nossa anotação:


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
      if (field.isAnnotationPresent(IdadeMinima.class)) {
         IdadeMinima idadeMinima = field.getAnnotation(IdadeMinima.class);
      }
   }
}

Temos um objeto do tipo da nossa anotação, com ele conseguimos pegar a idade mínima que setamos. Agora precisamos, também, da idade passada pelo usuário.

Para acessar o valor de um atributo private precisamos falar que esse atributo está acessível, desta forma:


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
      if (field.isAnnotationPresent(IdadeMinima.class)) {
         IdadeMinima idadeMinima = field.getAnnotation(IdadeMinima.class);
         field.setAccessible(true);
      }
   }
}

Mas perceba que não recebemos a idade do usuário, recebemos a data de nascimento dele. Precisamos pegar esta data e descobrir a idade do usuário. Para pegar o valor do atributo anotado com @IdadeMinima vamos usar o método get() da classe Field:


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
      if (field.isAnnotationPresent(IdadeMinima.class)) {
         IdadeMinima idadeMinima = field.getAnnotation(IdadeMinima.class);
         try{
            field.setAccessible(true);
            LocalDate dataNascimento = (LocalDate) field.get(objeto);
         } catch (IllegalAccessException e) {
              e.printStackTrace();
         }
      }
   }
}

Perceba que fizemos um cast para LocalDate, pois o método get() nos retorna um Object.

Para finalizar, vamos comparar para ver se o período entre dataNascimento e a data atual é maior ou igual ao valor que colocamos como idade mínima na nossa anotação:

Na comparação iremos usar o método between() que recebe como parâmetro duas datas para serem comparadas, o método now() para obtermos a data atual e o método getYears() para conseguirmos saber o valor do período em anos:


public static <T> boolean validador(T objeto) {
   Class<?> classe = objeto.getClass();
   for (Field field : classe.getDeclaredFields()) {
      if (field.isAnnotationPresent(IdadeMinima.class)) {
         IdadeMinima idadeMinima = field.getAnnotation(IdadeMinima.class);
         try{
            field.setAccessible(true);
            LocalDate dataNascimento = (LocalDate) field.get(objeto);
            return Period.between(dataNascimento, LocalDate.now()).getYears() >= idadeMinima.valor();
         } catch (IllegalAccessException e) {
              e.printStackTrace();
         }
      }
   }
   return false;
}

Perceba que pegamos o período em anos e comparamos para ver se o valor é maior ou igual a idadeMinina.valor() que nada mais é do que aquele valor que colocamos na nossa anotação. Além disso, no final retornamos falso, pois caso nenhum campo do objeto possua a anotação @IdadeMinima ele não pode ser validado.

Agora, para testar, vamos criar um usuário e printar o retorno do nosso método validador:


public static void main(String[] args) {
   Usuario usuario = new Usuario("Mário", "52902488033", LocalDate.of(2005, Month.JANUARY, 13));
   System.out.println(validador(usuario));
}

A data de nascimento do usuário criado é 13/01/2005, logo, comparado com a data atual resultará em false. Vamos ver:

Agora vamos testar com a data 10/01/2000:


public static void main(String[] args) {
   Usuario usuario = new Usuario("Mário", "52902488033", LocalDate.of(2000, Month.JANUARY, 10));
   System.out.println(validador(usuario));
}

Boa, conseguimos! Agora nosso validador() consegue validar qualquer classe que tenha nossa anotação.

Conclusão

Neste post conseguimos descobrir o poder da reflexão, realmente ela nos ajuda e muito quando precisamos operar sobre a classe dos objetos dinâmicamente.

Com anotações fomos capazes de marcar os elementos da nossa aplicação para que nosso método que usa reflexão consiga captar informações úteis para que fosse possível executar nossa lógica de validação.

Usar reflexão é muito útil quando queremos criar algo mais genérico, mas precisamos ter cautela pois com reflexão, operamos sobre os tipos dos objetos dinamicamente e isso faz com que algumas otimizações da máquina virtual não sejam executadas. Então temos que tomar cuidado para ver onde é realmente necessário o uso de reflexão.

Para saber mais a respeito de reflexão e metadados, a Alura possui o curso Curso Java Reflection: mágica e meta programação, onde é mostrado várias outras funcionalidades legais que o Java nos traz para trabalhar com reflexão.

FIQUE POR DENTRO

Próximo ArtigoMicrolearning: Conheça as principais vantagens para treinar a sua equipe