Como criar Anotações em Java

Veja neste artigo como criar Anotações em Java e usar reflection para utilizá-la de forma adequada.

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

Artigos relacionados