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 as teorias de ambos os conceitos e posteriormente veremos como aplicar 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.

Pense em 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? 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.

A resposta para esta pergunta é: Reflection, já que seu uso é comumente aplicado a muitos frameworks afim de tornar o 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 é a classe. Mas como isso é possível?

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


  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;
         }      
   
  }
Listagem 1. Classe Cliente

São três os recursos que o reflection possibilita que acessar em tempo de execução: Class, Field e Method. Com estes três você consegue fazer tudo que precisa 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. Observe a Listagem 2.


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

  }

Saída: Cliente
Listagem 2. Usando getClass()

O primeiro método é o getClass() e com ele nós 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 acima 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 a apresentada na Listagem 3.


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

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


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
Listagem 4. Capturando o nome dos atributos da classe Cliente

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.


   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
Listagem 5. Capturando o nome dos métodos da classe Cliente

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. Observe a Listagem 6.


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;
}
Listagem 6. Criando um método genérico para instanciar Classes com reflection

O método createNewInstance é 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 vazio. Após capturar este nós chamamos o método newInstance(), que retorna um objeto do tipo “clazz”. 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 6 com o código da Listagem 7.


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
Listagem 7. Usando o createnewinstance

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 uma linha de código sobre ela, repare que nada foi implementado especificamente para classe Cliente.

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.

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, assim entenderemos como funciona a criação de anotações. Para não confundir começaremos anotando nossos métodos com @Mostrar, como mostra a Listagem 8.


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;
     
     @Mostrar
     public String getNome() {
           return nome;
     }
     
     public void setNome(String nome) {
           this.nome = nome;
     }
     

     @Mostrar
     public int getCodigo() {
           return codigo;
     }
     
     public void setCodigo(int codigo) {
           this.codigo = codigo;
     }
     
     @Mostrar
     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;
     }
     

     @Mostrar
     public String getEnderecoCompleto() {
           return enderecoCompleto;
     }
     
     public void setEnderecoCompleto(String enderecoCompleto) {
           this.enderecoCompleto = enderecoCompleto;
     }      
 
}
Listagem 8. Anotando nossos métodos com @Mostrar

Inicialmente você verá o seguinte erro:”Mostrar cannot be resolved to a type”. Isso ocorre porque nossa anotação não foi criada ainda e para fazermos isso seguimos o código a seguir:


public @interface Mostrar {
  }

A partir do momento que a anotação Mostrar for criada o erro da Listagem 8 desaparecerá e você conseguirá compilar o código. O Java 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 Mostrar{}.

Agora 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 @Mostrar será usada apenas em tempo de execução, pois quando estivermos lendo os métodos da classe Cliente verificaremos se este foi anotado com @Mostrar, caso contrário, ignoraremos ele. Para isso usaremos: @Retention(RetentionPolicy.RUNTIME).

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


  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 Mostrar {
         
  }
Listagem 9. Anotação @Mostrar completa

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


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

O método mostrarValores() recebe o objeto que queremos manipular. Logo em seguida, capturamos a classe desse objeto. De posse da classe nós capturamos todos os métodos desse objeto, pois sabemos que a anotação @Mostrar 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 @Mostrar, e para fazer isso usamos o seguinte código:


m.isAnnotationPresent(Mostrar.class)

O isAnnotationPresent() verifica se aquele determinado método possui a anotação passada como parâmetro, no nosso caso, Mostrar.class. Se isso for verdade, podemos executar o método. Mas como fazemos isso?

Uma ação muito interessante da classe Method é o invoke(), que lhe possibilita chamar um método através do reflection, e é exatamente ele que usamos para chamar o getNome(), getCodigo() e etc., como mostra o código a seguir:


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.

Conseguiu entender o que irá acontecer após chamarmos o invoke()? Apenas os valores marcados com @Mostrar serão mostrados no console, assim como desejamos.

Vamos ver um exemplo prático disto na Listagem 11.


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

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 apresentada, se anotarmos um método set() com o @Mostrar teremos um erro, pois o método set() espera um parâmetro e nós não passamos nenhum parâmetro no invoke(), como mostra a Listagem 12.


  @Mostrar
     public void setNome(String nome) {
           this.nome = nome;
     }
Listagem 12. Anotando o método errado

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


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.mostrarValores(ReflectionApp.java:28)
   at ReflectionApp.main(ReflectionApp.java:19)
Listagem 13. Resultado do @Mostrar

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 acima. E como poderíamos resolver isso?

Uma das formas possíveis é checar no método mostrarValores() se o método que possui a anotação @Mostrar não recebe nenhum parâmetro, caso contrário, vamos mostrar uma mensagem no console e passar para o próximo. Dessa forma, mesmo que seja feita uma anotação errada não teremos muitos problemas, como mostra a Listagem 14.


// Mostra valores apenas com anotação @Mostrar
public static void mostrarValores(Object obj) {
     try {
        Class clazz = obj.getClass();
        for (Method m : clazz.getDeclaredMethods()) {
           if (m.isAnnotationPresent(Mostrar.class)){
              if (m.getParameterTypes().length > 0){
                  System.err.println(" "+m.getName()+" anotado com 
                  @Mostrar 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();
     }
}
Listagem 14. Ignorando métodos com parâmetros

Adicionamos 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 podemos 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