Introdução

Existe um conceito na computação que nos diz o seguinte:

Em qualquer parte da construção ou execução de um programa podemos resgatar informações sobre sua estrutura.

Esse conceito é o que chamamos de Reflexão Computacional. Ao compilarmos um código fonte estamos gerando códigos de baixo nível que são executados pelo computador, nesses códigos perdemos algumas informações sobre a estrutura do programa, mas se o nosso sistema suporta o paradigma de programação reflexiva, esses dados que usualmente são perdidos podem ser mantidos através de um tipo de dados, chamados metadados.

Nesse artigo discutimos a funcionalidade das Anotações, disponível desde Java 1.5 essa ferramenta corresponde aos metadados citados acima. Agora vamos apresentar um Problema-Exemplo e como podemos utilizar as anotações para que seja solucionado.

Um Problema-Exemplo

Vamos implementar um sistema simples de livraria no qual não haverá compras, reservas ou nada que deixará nosso sistema complexo pois podemos fugir demais do nosso foco que é aprender a usar anotações. Consideraremos em nosso mini-mundo apenas as instâncias livro e livraria, cujo código segue abaixo:

Listagem 01. Código da classe Livro.

public class Livro implements Processavel {

  private String titulo;

  private String autor;

  private double valorEmReais;

  public Livro(String titulo, String autor, double valor) {

    this.titulo = titulo;

    this.autor = autor;

    this.valorEmReais = valor;

  } //fim do construtor

  public String toString() {

    String retorno = "titulo = " + titulo +

      "\nautor = " + autor +

      "\nvalor em reais = " + valorEmReais + "\n\n";

    return retorno;

  } //fim da reescrita do metodo toString

  /* implementar metodos get e set */

} //fim da Classe Livro
Listagem 02. Código da classe Livraria.

public class Livraria implements Processavel {

  private String nome;

  private String endereco;

  private List < Livro > livros;

  public Livraria() {

    livros = new ArrayList < Livro > ();

  } //fim do construtor padrao

  public Livraria(String nome, String endereco) {

    this.nome = nome;

    this.endereco = endereco;

    livros = new ArrayList < Livro > ();

  } //fim do construtor

  public String toString() {

    String retorno = "Nome: " + nome +

      "\nEndereco " + endereco +

      "\n\nLista de livros eh : \n";

    for (Livro l: livros)

      retorno += l.toString();

    return retorno;

  } //fim da reescrita do metodo toString

  public void adicionaLivro(Livro livro) {

    livros.add(livro);

  } //fim do adicionaLivro

  public void removeLivro(int i) {

    if (i >= 0 && i < livros.size())

      livros.remove(i);

  } //fim do removeLivro

  public int procuraLivro(String nomeDoLivro) {

    int index = -1;

    for (int i = 0; i < livros.size(); i++)

      if (livros.get(i).getTitulo().equals(nomeDoLivro))

        index = i;

    return index;

  } //fim do procuraLivro

  /* implementar metodos get e set */

} //fim da Classe Livraria

Pronto, apresentado as classes livro e livraria você deve estar se perguntando por que implementar essa interface Processável?. A resposta será dada no decorrer do nosso artigo, a princípio só precisamos saber que essa interface é simples e que não possui nada em seu corpo. Logo, seu código se resume a:

Listagem 03. Código da Interface Processavel

public interface Processavel {


}//fim do Processavel

Agora que sabemos como está implementado nosso sistema de livraria, podemos descrever melhor nosso problema-exemplo. Queremos passar todas essas informações do nosso sistema para um banco de dados, de modo que o diagrama Entidade-Relacionamento seja da seguinte forma:

Esquema ER proposto para o problema
Figura 01. Esquema ER proposto para o problema.

Vamos supor que o implementador das classes livro e livraria também codifique toda a camada de persistência do sistema. Será que é possível sabermos, em tempo de execução, como que essas classes (livro e livraria) foram mapeadas para o banco de dados? A resposta é sim!

Um pouco de Anotações e uma solução para o Problema-Exemplo

Como foi dito no início do nosso artigo, a solução usará Anotações para resolver o problema. Começaremos explicando um pouco sobre Anotações e depois apresentaremos a solução.

Anotações

Na prática, podemos utilizar anotações para diversas situações, entre elas registro de log, geração de códigos, suporte para classes, métodos, parâmetros, etc. Assim, como qualquer outra tecnologia, é necessário termos bom senso na hora de utilizarmos Anotações em nosso código, ou seja, devemos anotar somente o que é necessário.

Podemos dividir nosso processo de Anotar em 3 etapas:

  • Escrever nossa Anotação
  • Aplicar nossa Anotação
  • Resgatar nossa Anotação

Escrever nossa Anotação consiste basicamente na sintaxe que devemos obedecer na hora da construção, um exemplo disso é:

Listagem 04. Código de um exemplo de anotação.

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface MinhaAnotacao {

  boolean valor() default true;

  String nome();

} //fim da anotacao MinhaAnotacao

No exemplo acima, temos a declaração de uma anotação simples, o compilador Java é avisado que esse código é uma anotação pelo @ que precede a palavra chave 'interface'. Mas o que significa esse Retention e Target?

Ao escrever uma anotação devemos avisar o compilador onde essa anotação deverá ser resgatada. Para definirmos o que chamamos de 'visibilidade' da nossa Anotação usamos uma outra anotação disponível na biblioteca java.lang.annotation que é justamente a @Retention, passamos como parâmetro alguma constante da classe RetentionPolicy, no nosso exemplo usamos a constante RUNTIME, o que significa que todas anotações do tipo MinhaAnotacao estarão disponíveis em tempo de execução. É preciso dizer também ao nosso compilador quem será o 'alvo' da nossa anotação, ou seja, se serão declarações de variáveis, de tipos, de métodos e etc, para isso utilizamos outra anotação nativa que é @Target. No nosso exemplo utilizamos como parâmetro uma constante do tipo ElementType chamada METHOD, o que significa que todas anotações do tipo MinhaAnotacao deverá ser aplicada a somente métodos de classes.

Temos na anotação MinhaAnotacao os parâmetros valor e nome, esses valores são escritos como se fossem métodos simples e funcionam como uma 'espécie de atributo', sendo que podem adotar valores padrão utilizando a palavra chave default.

Escrita nossa Anotação, basta seguirmos para a próxima fase do nosso processo: Aplicar nossa Anotação. Consiste simplesmente em escrever nossa anotação nos locais definidos pelo @Target , no nosso caso precedendo uma declaração de método. Veja:

Listagem 05. Código exemplo de aplicação da anotação.

public class MinhaClasse {

  private int numero;

  @MinhaAnotacao(nome = "getNumero")

  public int getNumero() {

    return numero;

  } //fim do getNumero

} //fim da classe MinhaClasse

Basta seguirmos a restrição feita na hora da construção da Anotação que diz para usarmos somente em métodos e passarmos como parâmetros valores que queremos anotar.

Nota: Não foi preciso modificar o conteúdo do parâmetro valor pois este já assume um valor default, sendo opcional sua alteração.

Para concluirmos nossa explicação sobre Anotações, basta apresentarmos como Resgatar nossa Anotação. Até agora mostramos como é fácil escrever e aplicar nosso processo de anotar, porém a parte mais difícil ocorre na hora de resgatá-la porque a sintaxe pode parecer um pouco confusa De qualquer maneira vamos explicar passo a passo como fazer isso. Veja:

Listagem 06. Código de recuperação da anotação.

public class Programa {

  public static void main(String[] args) {

    MinhaClasse a = new MinhaClasse();

    MinhaAnotacao anot;

    for (Method metodo: a.getClass().getDeclaredMethods())

      for (Annotation anotacao: metodo.getAnnotations())

        if (anotacao instanceof MinhaAnotacao) {

          anot = (MinhaAnotacao) anotacao;

          System.out.println(anot.nome());

        } //fim do if

  } //fim da main

} //fim do programa

No código acima temos a construção de um objeto MinhaClasse, nesse momento o compilador já salvou nossos metadados no bytecode gerado pela compilação, tornando-os disponíveis em tempo de execução. Como nossa anotação age em um Target do tipo METHOD, temos primeiramente que resgatar todos os métodos do nosso objeto nomeado como a.

Nota: É um pouco curioso como programas implementados em Java conseguem essas informações sobre sua própria estrutura em tempo de execução não acha? Isso só é possível porque Java é uma linguagem que implementa o conceito de Reflexão Computacional que mencionamos no início do nosso artigo.

Assim, para cada método resgatado de nosso objeto, obtemos todas as suas anotações e verificamos se cada anotação é uma instância de MinhaAnotacao. Feito isso, precisamos fazer um cast explícito para que possamos utilizar os métodos da Anotação, no caso, nome(). Por fim, imprimimos na tela.

Será que é possível sabermos, em tempo de execução, como que essas classes(livro e livraria ) foram mapeadas para o banco de dados?

Agora que sabemos usar Anotações vamos a solução.

Solução para o problema-exemplo

Vamos utilizar nosso processo novamente.

  • Escrever nossa Anotação
Listagem 07. Código da nossa anotação Tabela.

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

public @interface Tabela {

  String[] colunas();

} //fim da anotacao Tabela

Escolhemos nossa visibilidade da anotação como RUNTIME para que nossas anotações estejam disponíveis em tempo de execução. Nosso @Target será do tipo ElementType.TYPE pois essa anotação só será aplicada em declarações de tipos, o que faz muito sentido para nossa solução já que queremos mostrar como cada classe será mapeada para o banco de dados.

Como cada classe pode possuir quantos atributos for necessário e para que nossa anotação seja a mais genérica possível, adotamos como solução utilizar um array de Strings como parâmetros da nossa anotação, nesse array estarão disponíveis os nomes das colunas correspondente ao mapeamento da classe.

  • Aplicar nossa Anotação

Agora é a hora de aplicarmos nossa anotação Tabela nas classes do nosso sistema de livraria, como nosso @Target é do tipo ElementType.TYPE devemos aplicá-las precedendo a declaração de cada classe. Veja:

Listagem 08. Aplicando nossa anotação Tabela na classe Livro.

@Tabela(colunas = {

  "IdLivraria",

  "IdLivro",

  "Titulo",

  "Autor",

  "Valor em Reais"

})

public class Livro implements Processavel {

  private String titulo;

  private String autor;

  private double valorEmReais;

  /* continuação da classe */

}
Listagem 09. Aplicando nossa anotação Tabela na classe Livraria.

@Tabela(colunas = {

  "IdLivraria",

  "Nome",

  "Endereco"

})

public class Livraria implements Processavel {

  private String nome;

  private String endereco;

  private List < Livro > livros;

  /* continuação da classe */

}

Como visto acima, cabe ao próprio autor das classes definir o nome das colunas para o mapeamento da sua classe para o BD.

  • Resgatar nossa Anotação

Agora para resgatar nossa anotações temos algumas diferenças em relação ao exemplo anterior. Nesse caso temos que resgatar as anotações da classe propriamente dita e não de cada método seu, confira no código abaixo:

Listagem 10. Código do programa principal.

public class Programa {

  public static String processaAnotacoes(Processavel objeto) {

    String valorDeRetorno = "";

    Tabela tabela;

    for (Annotation anotacao: objeto.getClass().getAnnotations())

      if (anotacao instanceof Tabela) {

        valorDeRetorno = "A classe " +

          objeto.getClass().getName() +

          " sera mapeada para a uma tabela com as " +

          "seguintes colunas:\n";

        tabela = (Tabela) anotacao; //necessario fazer cast

        for (String coluna: tabela.colunas())

          valorDeRetorno += "| " + coluna + " |";

        valorDeRetorno += "\n";

      } //fim do if

    return valorDeRetorno;

  } //fim do processaAnotacoes

  public static void main(String[] args) {

    Livro l1 = new Livro("O Primo Basilio", "Eça de Queiroz", 10.00);

    Livro l2 = new Livro("Dom Casmurro", "Machado de Assis", 11.00);

    Livraria livraria = new Livraria("Livraria do Japa", "Rua Milton Bandeira, 351");

    livraria.adicionaLivro(l1);

    livraria.adicionaLivro(l2);

    System.out.println(processaAnotacoes(l1));

    System.out.println(processaAnotacoes(livraria));

  } //fim da main

} //fim do Programa

Dessa vez para resgatarmos nossas anotações podemos pegar de uma vez no trecho objeto.getClass().getAnnotations(), que só é possível devido a reflexão computacional. Para cada anotação fazemos a verificação se ele é do tipo Tabela e logo após consultamos todos os seus nomes de colunas e adicionamos a um String valorDeRetorno.

Agora fica claro o por que implementar a interface Processavel?, porque assim fizemos somente um único método que atende todas as classes que implementam essa interface, se optarmos em não utilizar esse mecanismo teríamos que ter feito dois métodos processaAnotacoes, sendo um para a classe Livro e outro para a classe Livraria.

Ao executar o código acima devemos ter como saída:

Resultado esperado do Programa principal
Figura 02. Resultado esperado do Programa principal.

Que é exatamente o que queríamos! Ao executar esse programa nossa intenção era conseguir recuperar as anotações feitas pelo programador em tempo de execução e saber como essas objetos foram mapeados para o banco de dados. Preferimos imprimir na tela essas informações e não envolver a sintaxe das camadas de persistência justamente para não complicar demais nosso sistema e perder o foco na aprendizagem da ferramenta anotações.

Uma outra maneira de solucionar esse nosso problema-exemplo sem usar anotações seria colocar em cada classe um array de Strings como atributo privado, isso iria gerar a mesma saída mas provavelmente não seria a maneira mais elegante porque estaríamos atribuindo algo às classes que não são "necessárias". Essas informações podem ser melhor classificadas como "dados periféricos" nesse nosso exemplo e que são resolvidos facilmente com anotações.

Conclusão

Definimos como a reflexão computacional presente na linguagem Java pode nos auxiliar na solução de problemas como o proposto nesse artigo. Vimos que anotações é uma poderosa ferramenta que nos proporciona a possibilidade de anotar dados importantes e acessá-los em qualquer etapa na construção do nosso programa, isso foi possível porque utilizamos um processo simples, composto de 3 etapas, para anotar e vimos como isso serviu para solucionarmos nosso problema-exemplo.

De agora em diante você tem conhecimento de mais uma tecnologia Java, tomara que isso se torne mais uma opção para solução de seus problemas de programação.