Neste artigo veremos dois assuntos muito importantes e interessantes no mundo Java: as anotações e o recurso de reflexão (reflection). Nosso intuito será criar uma anotação útil para determinada tarefa na codificação de nosso sistema, mas fazer isso não é possível sem que tenhamos o conhecimento de reflection.

Estudaremos teorias de ambos os conceitos e posteriormente veremos como aplicar na prática tais recursos.

Reflection

Ao desenvolver um software conseguimos definir diversos recursos de forma estática, como usar métodos de classes já criada, definir atributos também previamente criados e etc. Na maioria das vezes isso é suficiente para a construção de um projeto, mas há exceções e é exatamente elas que estudaremos.

Refletindo um pouco, pense como é possível o framework Hibernate saber que nossa classe Cliente tem um método setNome() se na codificação dele, com certeza, não há nenhuma referência a esta classe? A resposta para esta pergunta é Reflection. Lembre-se que os desenvolvedores do Hibernate, ou qualquer outro framework, jamais imaginaram que você precisaria usar a classe Cliente: eles desenvolveram tal recurso da forma mais genérica possível afim de adaptar-se a qualquer regra de negócio.

O uso do reflection é comumente aplicado a muitos frameworks para torná-lo mesmo plugável, tais como: Hibernate, Spring, CDI e etc. O pacote javax.reflection possibilita que sejam feitas chamadas a métodos, atributos e etc. em tempo de execução, ou seja, conseguimos instanciar uma classe sem saber qual é esta classe. Mas como isso é possível?

Primeiro vamos construir um Java Bean que será utilizado durante todo nosso artigo: uma classe simples chamada Cliente com seus atributos e métodos de acesso (getters e setters), como mostra a Listagem 1.

Listagem 1. Classe Cliente


import java.util.Date;
   
   
public class Cliente {
         
         private String nome;
         private int codigo;
         private Date dataNascimento;
         private String nomePai;
         private String nomeMae;
         private String enderecoCompleto;
         public String getNome() {
               return nome;
         }
         public void setNome(String nome) {
               this.nome = nome;
         }
         public int getCodigo() {
               return codigo;
         }
         public void setCodigo(int codigo) {
               this.codigo = codigo;
         }
         public Date getDataNascimento() {
               return dataNascimento;
         }
         public void setDataNascimento(Date dataNascimento) {
               this.dataNascimento = dataNascimento;
         }
         public String getNomePai() {
               return nomePai;
         }
         public void setNomePai(String nomePai) {
               this.nomePai = nomePai;
         }
         public String getNomeMae() {
               return nomeMae;
         }
         public void setNomeMae(String nomeMae) {
               this.nomeMae = nomeMae;
         }
         public String getEnderecoCompleto() {
               return enderecoCompleto;
         }
         public void setEnderecoCompleto(String enderecoCompleto) {
               this.enderecoCompleto = enderecoCompleto;
         }      
   
}

São três os recursos que o reflection possibilita o acesso em tempo de execução: Class, Field e Method, ou seja, Classe, atributos e métodos. Com estes três conseguimos fazer tudo que precisamos com qualquer tipo de classe. Primeiro vamos aprender um pouco sobre os métodos que o reflection fornece e depois usaremos como exemplo o bean criado na Listagem 1 para resolver problemas reais.

Listagem 2. Usando getClass()


public static void main(String[] args) {
    Cliente cliente = new Cliente();
    System.out.println(cliente.getClass().getName());
  
}
   
  Saída: Cliente

O primeiro método da Listagem 2 é o getClass(), com o qual capturamos a classe do objeto cliente, que no nosso caso é a classe Cliente. O getClass() retorna um objeto Class, que possui muitos métodos para manipularmos a classe Cliente, tais como: getName(), getModifiers, getConstructor e etc.

Usamos no código um objeto para retornar a classe dele, mas poderíamos usar a própria classe Cliente para obter essas informações, da mesma forma que apresentado na Listagem 3.

Listagem 3. Usando Cliente.class


public static void main(String[] args) {
    Cliente cliente = new Cliente();
    System.out.println(Cliente.class.getName());
}

Com Class em mãos podemos destrinchar os recursos contidos nela, como por exemplo: atributos, métodos, modificadores, construtores e etc.. Vamos ver o nome de todos os atributos na Listagem 4.

Listagem 4. Capturando o nome dos atributos da classe Cliente


public static void main(String[] args) {
     Cliente cliente = new Cliente();
     Class<Cliente> clazz = (Class<Cliente>) cliente.getClass();
     for(Field f : clazz.getDeclaredFields()){
          System.out.println(f.getName());
     }
   
}
   
  Saída:
  nome
  codigo
  dataNascimento
  nomePai
  nomeMae
  enderecoCompleto

O método getDeclaredFields() retorna um array de Field, onde Field é a classe utilizada para manipular os atributos presentes na classe que estamos trabalhando. Podemos aplicar a mesma lógica para os métodos, como mostra a Listagem 5.

Listagem 5. Capturando o nome dos métodos da classe Cliente


public static void main(String[] args) {
     Cliente cliente = new Cliente();
     Class<Cliente> clazz = (Class<Cliente>) cliente.getClass();
     for(Method m : clazz.getDeclaredMethods()){
           System.out.println(m.getName());
     }
   
}
   
  Saída:
  getNome
  setNome
  getCodigo
  setCodigo
  getDataNascimento
  setDataNascimento
  getNomePai
  setNomePai
  getNomeMae
  setNomeMae
  getEnderecoCompleto
  setEnderecoCompleto

Vamos ver um exemplo real. Como poderíamos criar um método genérico para instanciar/criar todo tipo de objeto independente da sua classe? Imagine que nós não saibamos qual a classe que deve-se instanciar, então não podemos usar a palavra reservada “new MinhaClass()”, precisamos apenas disponibilizar um método onde seja passada a classe, através do MinhaClasse.class, e neste método seja feita a instanciação e o retorno seja o objeto desejado.

Listagem 6. Criando um método genérico para instanciar Classes com reflection


private static Object createNewInstance(Class clazz) {
     Constructor<?> ctor;
     try {
              ctor = clazz.getConstructors()[0];
              Object object = ctor.newInstance();
              return object;
     } catch (SecurityException
         | InstantiationException | IllegalAccessException
         | IllegalArgumentException | InvocationTargetException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
  }
               
  return null;
}

O método createNewInstance da Listagem 6 é responsável por retornar uma instância de Class, independente do seu tipo. O que fazemos é utilizar o “clazz” para capturar o primeiro construtor encontrado, que é o construtor vazio. Após capturar este construtor nós chamamos o método newInstance() que retorna um objeto do tipo “clazz”, que até então não sabemos qual é, e não nos importa saber. Algumas exceções são obrigatórias por isso colocamos o bloco try-catch e adicionamos cinco exceções requeridas, ou checked exceptions.

Vejamos como utilizar o método disposto na listagem anterior usando o código da Listagem 7.

Listagem 7. Usando o createnewinstance


public static void main(String[] args) {
   Cliente cliente = (Cliente) createNewInstance(Cliente.class);
   
    if (cliente == null) {
       System.err.println("Ops, não foi possível criar o objeto cliente");
    } else {
        System.out.println("Objeto cliente criado = " + cliente.toString());
    }
   
}
Saída:
   
Objeto cliente criado = Cliente@5f67198e

Observe que em nenhum momento usamos “new Cliente()”, pois o método createnewinstance nem sabe que a classe Cliente existe, ele só saberá disso em tempo de execução. Agora fica mais claro entender como os frameworks atuais conseguem “ler” a sua classe sem ter digitado 1 linha de código sobre ela, nada foi implementado especificamente para classe Cliente, Endereco, Pessoa ou qualquer que seja.

Anotações

Anotações são metadados disponibilizados a partir do Java 5 para “configurar” determinados recursos que antes deveriam ser feitos em arquivos separados, como por exemplo no XML. Vejamos, você diariamente deve usar diversas anotações tais como: @Override, @Deprecated, @Entity, @Table, @Column e etc. Se você tenta usar o @Override em um método que não tem um semelhante na classe pai, então você verá um erro em tempo de design, e isso só é possível porque o Java usa o reflection para checar se existe um método com a mesma assinatura na classe pai, caso contrário o @Override não será aceito.

Nesta seção vamos criar nossa própria anotação, que terá por objetivo anotar os métodos que devem ser mostrados no console, algo bem simples, mas útil para entendermos em primeira instância como funciona a criação de anotações. Para não confundir começaremos anotando nossos métodos com @LogThis, como mostra a Listagem 8.

Listagem 8. Anotando nossos métodos com @LogThis


import java.util.Date;
   
   
public class Cliente {
         
         private String nome;
         private int codigo;
         private Date dataNascimento;
         private String nomePai;
         private String nomeMae;
         private String enderecoCompleto;
         
         @LogThis
         public String getNome() {
               return nome;
         }
         
         public void setNome(String nome) {
               this.nome = nome;
         }
         
   
         @LogThis
         public int getCodigo() {
               return codigo;
         }
         
         public void setCodigo(int codigo) {
               this.codigo = codigo;
         }
         
         @LogThis
         public Date getDataNascimento() {
               return dataNascimento;
         }
         
         
         public void setDataNascimento(Date dataNascimento) {
               this.dataNascimento = dataNascimento;
         }
         
         public String getNomePai() {
               return nomePai;
         }
         
         public void setNomePai(String nomePai) {
               this.nomePai = nomePai;
         }
         
         public String getNomeMae() {
               return nomeMae;
         }
         
         public void setNomeMae(String nomeMae) {
               this.nomeMae = nomeMae;
         }
         
   
         @LogThis
         public String getEnderecoCompleto() {
               return enderecoCompleto;
         }
         
         public void setEnderecoCompleto(String enderecoCompleto) {
               this.enderecoCompleto = enderecoCompleto;
         }      
   
}

Inicialmente você verá o seguinte erro: ”LogThis cannot be resolved to a type”. Isso ocorre porque nossa anotação não foi criada ainda, e para fazermos isso seguimos a Listagem 9.

Listagem 9. Criando a anotação com @Interface


public @interface LogThis {
         
}

A partir do momento que a anotação LogThis for criada o erro desaparecerá da Listagem 8 e você conseguirá compilar o código. O Java 5 optou por usar @interface como recurso para anotações, pois os arquitetos da linguagem preferiram não criar uma nova palavra reservada apenas para anotação, algo como: public Annotation LogThis{}.

Mas ainda não terminamos, precisamos definir dois atributos importantes para nossa anotação:

  1. Que tipo de estrutura ela pode anotar? Métodos, Classes, atributos, construtores, pacotes e etc.?
    Para isso usamos @Target(ElementType.METHOD) quando desejamos especificar que nossa anotação servirá apenas para métodos, ou @Taget(ElementType.FIELD) para anotar atributos e assim por diante.
  2. A anotação serve apenas em tempo de compilação ou execução? Um exemplo disto é a anotação @Override que serve apenas em tempo de compilação, pois o Java irá checar se aquele método existe na classe pai, caso contrário o código nem chegará a ser compilado. Já a nossa anotação @LogThis será usada apenas em tempo de execução, pois quando estivermos lendo os métodos da classe Cliente verificaremos se este foi anotado com @LogThis caso contrário ignoraremos ele. Para isso usaremos: @Retention(RetentionPolicy.RUNTIME).

Veja como ficou nossa anotação final na Listagem 10.

Listagem 10. Anotação @LogThis completa


  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
   
   
  @Target(ElementType.METHOD)
  @Retention(RetentionPolicy.RUNTIME)
  public @interface LogThis {
         
  }

Agora sim estamos prontos para o último passo que é fazer uso da anotação @LogThis com o reflection. Nosso objeto será popular o objeto cliente com alguns dados e depois passá-lo para um método onde os valores anotados com @LogThis serão mostrados no console, como mostra a Listagem 11. Por exemplo, getNome() mostrará o nome do cliente e assim por diante.

Listagem 11. Mostrando método com anotações @LogThis


  // Mostra valores apenas com anotação @LogThis
  public static void verDetalhesDoObjeto(Object obj) {
         try {
               Class clazz = obj.getClass();
               for (Method m : clazz.getDeclaredMethods()) {
                      if (m.isAnnotationPresent(LogThis.class)){
                             System.out.println(m.getName()+": "+m.invoke(obj));
                      }
               }
         } catch (IllegalAccessException | IllegalArgumentException
               | InvocationTargetException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
         }
  }

O método verDetalhesDoObjeto() recebe o objeto que queremos manipular. Logo em seguida capturamos a classe desse objeto, como já explicamos logo no início. De posse da classe nós capturamos todos os métodos desse objeto, pois sabemos que a anotação @LogThis só pode ser usada em métodos.

Fazendo uma iteração nos métodos do objeto passado, nós precisamos verificar se aquele determinado método possui a anotação @LogThis, e para fazer isso usamos:

m.isAnnotationPresent(LogThis.class)

O isAnnotationPresent() verifica se aquele determinado método possui a anotação passada como parâmetro, em nosso caso LogThis.class. Se isso for verdade podemos chamar executar o métod, mas como fazemos isso? Uma ação muito interessante da classe Method é o invoke(), ele lhe possibilita chamar um método através do reflection e é exatamente ele que usamos para chamar o getNome(), getCodigo() e etc.:

System.out.println(m.getName()+": "+m.invoke(obj));

O invoke() retorna um Object, por ser a classe de mais alta hierarquia e podemos fazer cast para qualquer outra, isso significa que nosso método pode retornar um inteiro, double, string, char, list, set e etc., não importa.

Conseguiu entender o que irá acontecer após chamarmos o invoke()? Apenas os valores marcados com @LogThis serão mostrados no console, assim como desejamos. Vamos ver um exemplo prático disto na Listagem 12.

Listagem 12. Usando o método mostrarValores()


  public static void main(String[] args) {
    Cliente cliente = new Cliente();
    cliente.setCodigo(1010);
    cliente.setDataNascimento(new Date());
    cliente.setEnderecoCompleto("Rua QUALQUER, Bairro DESCONHECIDO nº 190");
    cliente.setNome("Antonio da Silva Nunes");
    cliente.setNomeMae("Maria da Silva Nunes");
    cliente.setNomePai("Joao da Silva Nunes");
               
    verDetalhesDoObjeto(cliente);
}   
  Saída:
   
  getNome: Antonio da Silva Nunes
  getCodigo: 1010
  getDataNascimento: Thu Mar 12 21:04:33 BRT 2015
  getEnderecoCompleto: Rua QUALQUER, Bairro DESCONHECIDO nº 190

Imagine o mundo de possibilidades que se abre quando aprendemos a usar o reflection, principalmente para quem quer trabalhar com reusabilidade em larga escala, construindo API's responsáveis por acoplar-se em qualquer projeto.

Vale atentar que dada nossa lógica acima se anotarmos um método set() com o @LogThis teremos um erro, pois o método set() espera um parâmetro e nós não passamos nenhum parâmetro no invoke().

Listagem 13. Anotando o método errado


  @LogThis
  public void setNome(String nome) {
         this.nome = nome;
  }

Anotamos na Listagem 13 o setNome() com o @LogThis, agora vamos executar novamente a Listagem 12 e ver o resultado:


java.lang.IllegalArgumentException: wrong number of arguments
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:606)
 at ReflectionApp.verDetalhesDoObjeto (ReflectionApp.java:28)
 at ReflectionApp.main(ReflectionApp.java:19)

Exatamente o que esperávamos: wrong number of arguments. Neste caso o invoke() precisa receber o parâmetro para o setNome() e como não está recebendo ele retorna o erro apresentado. Como poderíamos resolver isso? Uma das formas possíveis é checar no método verDetalhesDoObjeto() se o método que possui a anotação @LogThis não recebe nenhum parâmetro, caso contrário vamos mostrar um mensagem no console e passar para o próximo. Dessa forma, mesmo que seja feita uma anotação errada não teremos muitos problemas.

Listagem 14. Ignorando métodos com parâmetros


// Mostra valores apenas com anotação @LogThis
public static void verDetalhesDoObjeto(Object obj) {
  try {
     Class clazz = obj.getClass();
     for (Method m : clazz.getDeclaredMethods()) {
       if (m.isAnnotationPresent(LogThis.class)){
          if (m.getParameterTypes().length > 0){
             System.err.println(" "+m.getName()+" anotado com @LogThis de forma errada, ignorando ...");
              continue;
          }
       System.out.println(m.getName()+": "+m.invoke(obj));
     }
   }
  } catch (IllegalAccessException | IllegalArgumentException
     | InvocationTargetException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
  }
}

Adicionamos na Listagem 14 a linha “if (m.getParameterTypes().length > 0)” que irá verificar se existe pelo menos um parâmetro neste método, e caso isso seja verdade uma mensagem será mostrada e a iteração irá para o próximo passo, através do “continue”.

Este artigo teve como principal objetivo mostrar a criação de uma anotação simples para mostrar valores específicos de um objeto, mas que por necessidade foi necessário explicar todo o conceito de reflection até podermos chegar nas anotações, caso contrário isso não seria possível. O uso de anotações e consequentemente reflections está diretamente ligada a construção principalmente de frameworks que possamos trabalhar de forma mais genérica possível sem se preocupar especificamente com a regra de negócio do desenvolvedor, mas sim com a estrutura que ele precisará.

Com o log apenas de determinadas propriedades você pode evitar erros ao tentar mostrar valores de propriedades que não pode ser lida, ou seja, estão inacessíveis. Assim você torna o seu sistema com menor nível de acoplamento e maior reusabilidade.