|
|
Rafael Steil
Aprenda a usar Metadata, uma das novas funcionalidades do Java 1.5
Atenção
Todos os exemplos de código mostrados nesse artigo necessitam do Java 1.5 beta1 ou superior para funcionar. Para compilar, é necessários passar o parâmetro -source 1.5 ao javac, como mostrado abaixo:
javac -source 1.5 NomeDaClasse.java
|
Caso não seja explicitamente especificado o -source 1.5, o compilador irá tentar compilar usando a versão 1.4 e dará erros de compilação nas partes onde ele não reconhecer as coisas específicas do 1.5.
Aprendendo a usar Metadata no Java
As utilidades das Annotations, ou Metadata, que aqui no artigo chamaremos de "atributos", são as mais variadas, que vão desde adicionar suporte a transação aos métodos e classes, diretivas de configuração, controle de versão, validações, logging, unit tests e por ai vai.
Tais atributos, ou "metatags", são criadas pelo desenvolvedor, para qualquer coisa - QUALQUER COISA que achar necessário. Pense em XML: você cria um documento XML usando as tags que acha serem necessárias para a situação, como <nome>: para representar algum nome, <idade> para idade, e assim por diante. E após ter criado esse documento, ele automaticamente funciona da maneira que você deseja, como num passe de mágica? obviamente não. É necessário escrever umas linhas de código que processem esse arquivo XML, dando "vida" a cada tag.
O mesmo ocorre com metadata. Você cria uma tag da maneira que desejar, com o nome que desejar, para fazer o que bem entender. E depois escreve umas linhas de código para "processarem" tais tags, para darem sentiddo à sua existência. Se você criar uma tag para fazer logging de classes e métodos, vai também precisar fazer o código que buscará pelas entidades que tenham a sua recém criada tag e que faça o controle de log de alguma maneira. Como? isso é VOCÊ, o desenvolvedor, quem decide. Assim como em XML.
Claro que há casos onde usar atributos não faz muito sentido, ou casos onde seria melhor usar um arquivo de configuração à parte ao invés de atrelar tudo no seu código-fonte. Fazer uso dessa funcionalidade sabiamente pode não ser uma tarefa tão simples no começo, quando nossas mentes ainda não conseguem entender para o que de fato elas servem.
Exemplos, exemplos
No caso de ser usado em transações, imagine que você marque determinados métodos que necessitam de transação - ou a classe inteira, se for o caso -, e o container por sua vez consultaria o objeto, verificando se o método que está sendo chamado requer a funcionalidade. Ficaria algo como
1 ...
2 @RequiresTransaction
3 public void saveVendor(Vendor vendor) {
4 //
5 }
6 ...
|
simplemente fazendo isso, ao notar verificar que o método saveVendor necessita de transação, o container pode processar da seguinte maneira:
01 ...
02 beginTransaction();
03
04 saveVendor(vendor);
05
06 if (!transactionErrors) {
07 commit();
08 }
09 else {
10 rollback();
11 }
12 ...
|
Isso evita muita duplicação de código e tira a responsabilidade do desenvolvedor ter que explicitamente se preocupar com o controle transacional, nem a necessidade de configurar tudo usando arquivos XML. No exemplo que veremos mais adiante, usaremos atributos para realizar comparação dinâmica entre objetos. Mas antes de prosseguir, é necessário conhecer o básico sobre o funcionamento dos atributos.
Attributos podem não ter parâmetro algum, ou seja, voce usa eles apenas para "marcar" a classe, método ou campo da classe; podem receber tantos quantos parâmetros você desejar; e pode ter parâmetros com valores padrão. Além disso, pode ser configurado para estar disponível somente em tempo de compilação; disponível para a VM como um todo, ou disponível no byte-code apenas. Para processar os atributos dinâmicamente, via reflection, é necessário configurá-lo para estar disponível em RUNTIME. Veremos como fazer isso mais adiante.
Criamos atributos usando a palavra-chave @interface, como no exemplo abaixo:
1 public @interface MeuAtributo {
2 }
|
simples assim. Os atributos são interfaces como outras quaisquers, e o sinal de arroba ( @ ) é utilizado apenas para instruir o compilador sobre a necessidade de realiziar certas operações para "transformar" a interface em algo que seja reconhecido como uma anotação - Annotation, oficialmente. Para nós, desenvolvedores, é algo transparente, não necessitando de diretivas especiais para a compilação.
Para informar a visibilidade do atributo, usamos outro atributo chamado @Retention, cujo qual faz parte da linguagem. Os valores possíveis para um @Retention são os do tipo RetentionPolicy, encontrado no pacote java.lang.annotation. Há ainda a possibilidade de especificar onde o atributo poderá ser usado, sendo que algumas possiblidades são para somente construtores, somente métodos, package ou declaração de classe, enum ou interface. Os tipos possíveis estão declarados no enum ElementType, também presente em java.lang.annotation
Por exemplo, digamos que queremos que o nosso atributo somente possa ser usado em métodos, e deve estar disponível durante a execução do programa. Para isso, bastaria fazer
1 @Retention(RetentionPolicy.RUNTIME)
2 @Target(ElementType.METHOD)
3 public @interface MeuAtributo {
4 }
|
Assim, caso alguém tente usar o atribuo na declaração de alguma classe, irá obter um erro de compilação, pois MeuAtributo pode ser usado somente com métodos.
Usar o nosso atributo é igualmente simples, bastando colocar a chamada antes da declaração do método, precedido pela arroba. Veja o exemplo:
1 public class MinhaClasse {
2 @MeuAtributo
3 public void fazAlgumaCoisa() {
4 }
5 }
|
O atributo usado como exemplo não faz muita coisa além de "marcar" o método. Atributos sem parâmetros podem ser úteis nos casos onde você deseja informar que o "alvo" em questão é apto a fazer ou receber algo. Exemplo disso pode ser unit tests, que ao invés de usar uma determinada nomenclatura ao criar os métodos a serem testados, poderíamos fazer algo como
01 public class VendorTestCase extends TestCase {
02 @Test
03 public void criaVendor() {
04 }
05
06 @Test
07 public void atualizaVendor() {
08 }
09
10 private Vendor localizaVendor() {
11 // ...
12 }
13 }
|
Assim, os métodos marcados com o atributo @Test serão considerados como parte do test case.
Por outro lado, atributos com parâmetros são úteis quando precisamos passar algum tipo de informação, necessário para a sua execução. Para um controle de dependência, digamos que, antes de executar determinado método, você precisa se certificar que algum outro método, digamos que de configuração, tenha sido chamado, e que, ao finalizar a chamada, um outro método seja executado para enviar notificação para alguma entidade. Com base nisso, poderíamos pensar em fazer
01 public class ConfiguratorTabajara {
02 public Config prepareSystem() {
03 // faz qualquer coisa
04 }
05 }
06
07 public class ClasseDeAlgumaCoisa implements Observer {
08 @Depends(class = "ConfiguratorTabajara", method = "prepareSystem")
09 @AfterCall(execute = "notifyAll")
10 public void executaAlgumaFuncao() {
11 // ...
12 }
13
14 // Envia notificacoes para outras entidades
15 private void notifyAll(Object sender) {
16 }
17 }
|
Assim, antes do método executaAlgumaFuncao ser executado, o sistema verificaria se prepareSystem já foi chamado e, caso ainda não, realiza a execução do mesmo antes de prosseguir. Após a finalização da chamada do nosso método, o sistema automaticamente executaria o método notifyAll, passando a instância do objeto como parâmetro. Claro que isso tudo não acontece por mágica, como veremos no exemplo logo abaixo. Para esses passos todos poderem acontecer, é necessário que as chamadas aos métodos sejam feitas por alguma outra entidade, como um Container, Proxy ou Interceptador, ou explicitamente pelo próprio desenvolvedor.
Atributos que recebem parâmetros são declarador quase da mesma forma como declaramos interfaces, sendo que há uma pequena diferença possível. Veja o exemplo:
01 // Depends.java
02 @Retention(RetentionPolicy.RUNTIME)
03 @Target(ElementType.METHOD)
04 public @interface Depends {
05 String class();
06 String method();
07 }
08
09 // AfterCall.java
10 @Retention(RetentionPolicy.RUNTIME)
11 @Target(ElementType.METHOD)
12 public @interface AfterCall {
13 String execute();
14 }
|
como você pôde notar, os nomes dos parâmetros que usamos na chamada ao atributo são métodos comuns. A diferença, uma possibilidade adicional, é especifcar um valor padrão para as propriedades dos atributos. No caso do atributo @Test poderíamos fornecer uma propriedade para especificar se é para realizar o "test" ou não, sendo que o valor padrão seria true. A declaração ficaria como no exemplo abaixo:
1 // Test.java
2 @Retention(RetentionPolicy.RUNTIME)
3 @Target(ElementType.METHOD)
4 public @interface Test {
5 boolean value() default true;
6 }
|
Assim, poderíamos usar tanto
1 ...
2 @Test
3 public void criaVendor() {
4 }
5 ...
|
quanto
1 ...
2 @Test(false)
3 public void criaVendor() {
4 }
5 ...
|
Todas essas chamadas são válidas. Caso quiséssemos usar algum valor padrão do tipo String, bastaria colocar o valor entre aspas, como em qualquer outra declaração de String em Java.
Agora que já vimos vários exemplos de como criar e usar atributos, vamos ver o exemplo final, onde será mostrado como verificar, dinamicamente, quais métodos contem atributos, e realizar operações com base nisso. O exemplo mostrado será um programa que compara duas instâncias, retornando os nomes dos método cujo retorno não seja igual ao retorno da outra instância.
Primeiramente vamos criar o atributo e a classe que usaremos para comparar suas instâncias. Logo após veremos como consultar atributos dinamicamente e finalizaremos fazendo o código que faz as comparações em si.
Compare.java
01 import java.lang.annotation.Retention;
02 import java.lang.annotation.Target;
03 import static java.lang.annotation.RetentionPolicy.RUNTIME;
04 import static java.lang.annotation.ElementType.METHOD;
05
06 @Retention(RUNTIME)
07 @Target(METHOD)
08 public @interface Compare {
09 boolean value() default true;
10 }
|
Note que usamos uma outra funcionalidade do Java 1.5, que são os static imports. Prosseguindo, temos a nossa classe básica:
DummyObject.java
01 public class DummyObject {
02 private String name;
03 private String email;
04 private int age;
05
06 public DummyObject(String name, String email, int age) {
07 this.name = name;
08 this.email = email;
09 this.age = age;
10 }
11
12 @Compare
13 public String getName() {
14 return this.name;
15 }
16
17 @Compare(false)
18 public String getEmail() {
19 return this.email;
20 }
21
22 @Compare
23 public int getAge() {
24 return this.age;
25 }
26
27 public void something() {
28 }
29 }
|
Veja que o método getEmail recebeu false como parâmetro, e o método something não foi marcado como comparável. Ao executar o exemplo, isso será útil para mostrar a busca por métodos que contém o atributo em questão e como pegar o valor configurado para ele.
O próximo passo é fazer o método que irá receber as instâncias e buscar por métodos com atributos. Apesar de ser um pouco mais complexa que as classes vistas até agora, é bastante simples de entender mesmo assim.
ObjectComparator.java
01 import java.util.List;
02 import java.util.ArrayList;
03
04 import java.lang.reflect.AnnotatedElement;
05 import java.lang.reflect.Method;
06 import java.lang.annotation.Annotation;
07
08 public class ObjectComparator {
09 private List<Method> findAnnotatedMethods(Class clazz) {
10 // Pega todos os métodos da classe
11 Method[] methods = clazz.getDeclaredMethods();
12 List<Method> list = new ArrayList<Method>();
13
14 // Verifica cada método, procurando por Annotations
15 for (Method method : methods) {
16 // Verifica se o método atual contém o nosso atributo
17 if (method.isAnnotationPresent(Compare.class)) {
18 Annotation annotation = method.getAnnotation(Compare.class);
19
20 // Joga a annotation no console, para vermos o seu valor
21 System.out.printf("Método: %s - Annotation: %s\\n", method.getName(), annotation);
22 if (((Compare)annotation).value()) {
23 list.add(method);
24 }
25 }
26 }
27
28 return list;
29 }
30 }
|
Ao bater o olho, é possível ver outras duas novas funcionalidades do Java 1.5: Generics e foreach loop ( ou enhanced for loop, como também é chamado ). A declaração
1 List<Method> list = new ArrayList<Method>();
|
declarada uma lista que somente pode receber objetos do tipo Method, facilitando assim a manipulação, já que evita vários casts. Esse é um exemplo do uso mais simples que Generics nos proporcionam. Por sua vez, a linha
1 for (Method method : methods) {
|
é traduzida como "para cada elemento do tipo Method contido na variável methods, crie uma variável local chamada method". Essa linha é a mesma coisa que fazer
1 for (int i = 0; i < methods.lenth; i++) {
2 Method method = methods[i];
|
em versões do Java anteriores ao 1.5.
Voltando ao nosso exemplo, na linha
1 if (method.isAnnotationPresent(Compare.class)) {
|
verificamos se o método atual contém o atributo do tipo Compare. Como você percebeu, o método isAnnotationPresent recebe o "Class", ou "type", que queremos verificar. Por fim, em
1 if (((Compare)annotation).value()) {
|
chamamos o método needCompare, declarado no Objeto Comparer, para verificar se o método é para ser usado nas comparações.
A próxima e penúltima parte do nosso programa é executar a chamada ao método que consulta os atributos, e, via Reflection, comparar as instâncias. Adicione o seguinte método à classe ObjectComparator:
01 private List<String> compareInstances(Object original, Object toCompare) {
02 // Pega os métodos que contém o atributo de comparação
03 List<Method> annotatedMethods = this.findAnnotatedMethods(original.getClass());
04 List<String> differences = new ArrayList<String>();
05
06 try {
07 // Compara cada retorno de objeto
08 for (Method m : annotatedMethods) {
09 Object value = m.invoke(original, null);
10
11 Method toCompareMethod = toCompare.getClass().getMethod(m.getName());
12 if (!toCompareMethod.invoke(toCompare, null).equals(value)) {
13 differences.add(m.getName());
14 }
15 }
16 }
17 catch (Exception e) {
18 e.printStackTrace();
19 }
20
21 return differences;
22 }
|
Nesse método também não há muito segredo. Ele simplesmente pega os métodos a serem comparados, itera por cada um, executando uma chamada dinâmica para comparar o valor retornado pelo método do objeto "original" com o retorno do do mesmo método do objeto a ser comparado. Por fim, retorna uma lista contendo os possíveis nomes de métodos cujo retorno foi diferente.
Para finalizar, basta fazermos a classe main() para invocar o teste. Adicione o seguinte método na classe ObjectComparator:
01 public static void main(String[] args)
02 {
03 DummyObject d1 = new DummyObject("Nome 1", "nome@um", 13);
04 DummyObject d2 = new DummyObject("Nome 2", "nome@dois", 13);
05
06 List<String> diff = new ObjectComparator().compareInstances(d1, d2);
07 System.out.println("Objetos iguais? "+ (diff.size() == 0));
08
09 if (diff.size() > 0) {
10 System.out.println("Métodos cujo retorno não são iguais: ");
11
12 for (String methodName : diff) {
13 System.out.println(methodName);
14 }
15 }
16 }
|
Execute o programa e veja o resultado. Usando o exemplo acima, será mostrando que os objetos não são iguais, e que o método getName() é diferente entre as duas instâncias. Tente colocar valores iguais em ambos objetos, e veja o resultado.
Isso finaliza o artigo sobre Metadata / Annotations / Atributos. Há muita coisa útil e legal que é possível fazer com essa funcionalidade, uma das mais legais do Java 1.5. Usado com sabedoria, irá facilitar muito o dia-a-dia e aumentar a produtividade.
Para maiores informações e exemplos, consulte:
http://jcp.org/aboutJava/communityprocess/review/jsr175/index.html
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/annotation/package-summary.html
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/package-summary.html
E, claro, não deixe de discutir sobre o assunto no fórum do GUJ.
Até a próxima.
|
|
|