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;
}
}
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
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());
}
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
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
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;
}
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
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;
}
}
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:
- 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. - 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 {
}
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();
}
}
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
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;
}
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)
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();
}
}
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