sexta-feira, 23 de março de 2012

Java Custom Annotations

Olá povo,

Depois de um bom tempo sem postar, vou falar um pouquinho sobre anotações em Java. Quem já programa em Java, já deve ter visto algo como @Override, @Deprecated, @SupressWarnings, ou algo do tipo. Estas são anotações adicionadas ao código-fonte que podem ter funcionalidade meramente indicativa ou auxiliar o compilador Java. Mas um recurso bacana das anotações é a capacidade de utilizá-las em tempo de execução para ajudar na lógica do sistema, permitindo inclusive criar bibliotecas reutilizáveis.
Um framework muito popular que é construído baseado em anotações é o Hibernate (que implementa a especificação JPA, Java Persistence API). Ele permite a persistência de qualquer tipo de objeto, desde que as classes desses objetos estejam com as devidas anotações.

Para ilustrar o uso das anotações, vou criar um mini-mini-mini Hibertnate :) Nele definiremos duas anotações: uma para determinar qual tabela a classe vai persistir seus objetos; e a segunda mapeará um atributo da classe a um campo da tabela.

Vamos começar pelas anotações:
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DBTable {
  public String table();
}

A diferença básica de uma interface comum e uma annotation é o símbolo de @ (arroba) antes da palavra interface. Os "atributos" da anotação são métodos, que no exemplo acima é o nome da tabela no banco. As regras para os atributos/métodos é que eles só podem retornar: primitivos, Strings, enum, Class ou um array dos anteriores.

A anotação @Target indica para qual tipo de elemento java a anotação que estamos definindo irá tratar. Ele pode ser:
TYPE: para classes, interfaces, ou enums;
FIELD: para atributos;
METHOD: para métodos;
PARAMETER: para parâmetros de métodos;
CONSTRUCTOR: em construtores;
LOCAL_VARIABLE: em variáveis locais;
ANNOTATION_TYPE: em uma outra anotação;
PACKAGE: nos pacotes java.

Já a anotação @Retention indica por quanto tempo a anotação será retida. Pode assumir os valores:
RUNTIME: as anotações são armazenadas na classe e estão disponíveis em tempo de execução;
SOURCE: as anotações são armazenadas na classe, mas NÃO estão disponíveis em tempo de execução;
CLASS: as anotações NÃO são armazenadas na classe (default).

Por último, a anotação @Inherited indica que as subclasses herdarão essa anotação.

A anotação acima servirá para mapear uma classe em uma tabela do banco de dados.
Ela será utilizada apenas em classes, ficará disponível em tempo de execução e poderá ser herdada por subclasses que a usarem.

Vamos agora para a anotação que mapeará atributos em campos da tabela:
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBField {
  String colummn() default "";
}

A anotação acima, é apenas para atributos, terá sua informação disponível em tempo de execução e terá o atributo column. Definimos um valor default para a coluna, para que o nome do atributo seja igual ao nome do campo da tabela, não seja preciso preencher.
Vamos agora usar essas anotações em duas classes: Pessoa e Funcionário. Essa segunda herdando da primeira:
@DBTable(table="TBPessoa")
public class Pessoa {

  @DBField(colummn="str_nome")
  private String nome;

  @DBField(colummn="str_end")
  private String endereco;

  @DBField
  private int idade;

  public Pessoa(String nome,
    String endereco, int idade) {

    this.nome = nome;
    this.endereco = endereco;
    this.idade = idade;
  }

  public String getNome() {
    return nome;
  }
  public String getEndereco() {
    return endereco;
  }
  public int getIdade() {
    return idade;
  }
}

O mais importante dessa classe é o mapeamento que fizemos da classe com o nome da tabela e dos atributos com os campos. Tudo isso só usando nossas próprias anotações.
Agora a classe Funcionario:
@DBTable(table="TBFunc")
public class Funcionario extends Pessoa {

  @DBField(colummn="num_ctps")
  String ctps;

  public Funcionario(String nome, String endereco,
    int idade, String ctps) {

    super(nome, endereco, idade);
    this.ctps = ctps;
  }
  public String getCtps() {
    return ctps;
  }
}

Agora vamos criar a classe que lerá objetos (de qualquer classe) e procurará nossas anotações para fazer uma inclusão (FAKE claro :) no banco de dados.
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Set;

public class Repositorio {

  public void insert(Object obj) throws Throwable {
    // Tenta obter nossa anotação na classe
    DBTable persistable =
      obj.getClass().getAnnotation(DBTable.class);

    // Se tiver a anotação...
    if (persistable != null){
      String tabela = persistable.table();
      // Map para montar o SQL campo/valor :)
      HashMap<String, String> keyAndValues =
        new HashMap<String, String>();

      // Obtém os atributos da classe via reflection  
      Field[] fields = getFields(obj.getClass());
      for (Field field : fields) {
        // como os atributos são private,
        // setamos ele como visible
        field.setAccessible(true);
        DBField coluna = 
          field.getAnnotation(DBField.class);

        // Se o atributo tem a anotação
        if (coluna != null){
          // Verifica se está vazio pra usar o
          // nome do próprio atributo
          String columnName = 
            coluna.colummn().equals("") ?
            field.getName() : coluna.colummn();
       
          // Adiciona campo/valor no map
          keyAndValues.put(
            columnName, field.get(obj).toString());
        }
      }
 
      // Varre o map para montar o SQL
      String values = "";
      Set<String> keys = keyAndValues.keySet();
      for (String campo: keys) {
        if (!values.equals("")) values += ",";
        String valor = keyAndValues.get(campo);
        values += campo +"='"+ valor +"'";
      }

      String sql = "INSERT INTO "+ tabela +
        " values("+ values +")";
      System.out.println("SQL---->"+ sql);
    }
  }

  // Método recursivo para obter os atributos da 
  // class e da superclasse
  public Field[] getFields(Class c){
    // Se tem superclasse
    if (c.getSuperclass() != null){
      // Chama o próprio método para pegar os 
      // atributos da superclasse
      Field[] superClassFields = 
        getFields(c.getSuperclass());
      // Pega os atributos da própria classe
      Field[] thisFields = c.getDeclaredFields();
 
      // array com todos os atributos
      Field[] allFields = new Field[
        superClassFields.length +
        thisFields.length];

      // Copia os atributos da superclasse 
      System.arraycopy(superClassFields, 0,
        allFields, 0, superClassFields.length);
      // Copia os atributos da classe atual
      System.arraycopy(thisFields, 0, allFields,
        superClassFields.length, thisFields.length);
   
      return allFields;

    // Se não tem superclasse, retorna os 
    // próprios atributos
    } else {
      return c.getDeclaredFields();
    }
  }
}

O código acima está todo comentado. Qualquer dúvida, deixem seus comentários... Ah mas ainda tem a classe que usa isso tudo :)
public class Main {

  public static void main(String[] args) {
    Repositorio repo = new Repositorio();

    Pessoa p = new Pessoa("Nelson", "Rua tal", 28);
    Funcionario f = new Funcionario(
      "Glauber", "Rua x", 18, "1234");

    try {
      repo.insert(p);
      repo.insert(f);
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

O resultado é apresentado abaixo:

SQL---->INSERT INTO TBPessoa values(str_nome='Nelson',idade='28',str_end='Rua tal')
SQL---->INSERT INTO TBFunc values(str_nome='Glauber',idade='18',str_end='Rua x',num_ctps='1234')

Como podemos observar, o mesmo repositório está persistindo duas classes distintas, baseando-se apenas nas notações que usamos nelas. Esse recurso pode trazer inúmeras vantagens para o desenvolvimento de aplicações facilitando a componentização da sua aplicação.

4br4ç05,
nglauber

PS.: Me baseei nesse post.

Nenhum comentário: