Java: Anotações, ClassLoader e Reflexão
Anotações
Como mostrado em outro post, as anotações foram a evolução natural da linguagem para colocar "dentro" das classes Java, os elementos de configurações que ficavam ficheiros XML. Os containers e frameworks existentes dependiam de ficheiros auxiliares de configuirações (XML), para indicar e definir como elementos (classes) deveriam ser "carregados". Porém estas configurações ficavam "escondida" do profissional que codifica as classes. A cada dia, estas configurações assumiram um perfil mais proximo da programação. Permitindo, por exemplo, influenciar o fluxo de execução do programa.
Para codificar uma classe, o programador precisava saber e entender o contexto no qual esta classe seria utilizada. Com isto, surgiu a necessidade de colocar na própria classe, os elementos de configurações. Desta forma a classe fica mais transparente para quem estiver a programar. Entretanto o papel do programador foi expandido, tendo este também que incorporar um perfil de "deployer".
Mas o que são as anotações, como cria-las e utiliza-las?
As anotações se assemelham muito as interfaces do Java, porém são elementos processados quando nossas classes são compiladas. Desta forma podemos incorporar informações adicionais as classes. Posteriormente, em execução e através de reflexão, podemos verificar estes valores incorporados e utilizá-los em nosso código.
Para criar uma anotação basta utilizarmos a palavra @interface na definição de nosso elemento.
Ex.:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MinhaAnotacao {
String attr1() default "";
String attr2() default "";
}
Observe no exemplo que estamos a criar a anotação "@MinhaAnotacao" e que esta possui dois atributos "attr1" e "attr2" que nos permitem associar valores a esta anotação. Ex.: @MinaAnotacao(attr1="atributo 1", attr2="atributo 2"). Ao utilizarmos a anotação como mostrado, estaremos a guardar os textos, nos respetivos atributos da anotação. Mais adiante veremos como consultar estes valores, junto a anotação.
Além dos atributos, podemos verificar no exemplo, dois outros elementos: @Target e @Retention.
O elemento @Target nos permite definir a qual parte da classe nossa anotação é "destinada". Entre as opções estão: ANNOTATION_TYPE, trata-se de uma anotação a ser utilizada na criação de outras anotações; CONSTRUCTOR, associadas a construtores de classes; FIELD, associadas a declaração de atributos (variáveis globais da classe) e inclui "enum"; LOCAL_VARIABLE, agregado a variáveis locais de um método; METHOD, associada a métodos da classe; PACKAGE, associadas ao package; PARAMETER, agregado a parametros de métodos; TYPE, associado a declaração da classe.
Ex.:
@TypeAnnotation
(TYPE)
public class MyAnnotatedClass {
@FieldAnnotation
(FIELD)
private String foo;
@ConstructorAnnotation
(CONSTRUCTOR)
public MyAnnotatedClass() {
... }
@MethodAnnotation
(METHOD)
public String bar(@ParameterAnnotation (PARAMETER) String str) {
@LocalVariableAnnotation (LOCAL_VARIABLE)
String asdf = "asdf";
return asdf + str;
}
}
@AnnotationTypeAnnotation
(ANNOTATION_TYPE)
public @interface SomeAnnotation {
..
}
@PackageLevelAnnotation
(PACKAGE)
package com.some.package;
import com.some.package.annotation.PackageLevelAnnotation;
...
O elemento @Retention é responsável por definir o comportamento de nossa anotação junto ao compilador e a máquina virtual. Entre as opções disponíveis estão: CLASS, opção padrão caso nenhuma seja informada - indica ao compilador que as informações relativas a anotação devem persistir junto a classe gerada, porém não devem ser disponibilizadas em tempo de execução; RUNTIME, indica ao compilador que as informações devem ser persistidas junto a classe gerada e também disponibilizadas em tempo de execução; e SOURCE, indica que as informações devem ser descartadas pelo compilador.
Para concluir o que esta a ser apresentado até aqui, segue um exemplo de utilização de nossa anotação exemplo "@MinhaAnotacao".
Ex.:
package pt.exemplos;
@MinhaAnotacao(attr1="Teste do atributo 1", attr2="Teste do atributo 2")
public class MinhaClasse {
....
}
Ou seja, com a declaração acima, estamos a dizer ao compilador que a nossa classe MinhaClasse deve ser "iniciada" e disponibilizada na JVM com a anotação "@MinhaAnotacao" e os valores "Teste do atributo 1" e "Teste do atributo 2"
para os atributos attr1
e attr.
ClassLoader/Classpath
Vamos fazer uma "pause" aqui para expandir um pouco o nosso conhecimento. Antes de prosseguir para a construção de uma arquitetura simples a utilizar as anotações, precisamos entender como a JVM executa nossa aplicação. Em qual contexto? Quais classes estão disponíveis? O nível de dependência entre contextos etc.
No Java, quando corremos uma aplicação, esta é executada em um contexto próprio. Chamo de contexto, o conjunto de recursos alocados e disponibilizados para a aplicação. Neste ponto, nos interessa observar dentro do contexto, as classes e bibliotecas disponíveis.
Observe que o comando "java", executado para correr qualquer aplicação, nos permite indicar um "classpath". Este classpath corresponde a referencia ao conjunto de elementos (bibliotecas e classes) que a aplicação a ser executada possui dependência. Ao elencarmos os elementos, a JVM irá disponibilizá-los no contexto da aplicação que estiver a ser executada. Então, todas as classes existentes, seja em bibliotecas, ou classes diretamente indicadas no classpath, serão "inicializadas" e prontas para uso.
Quando estamos em um servidor de aplicações, ou servidor web, nossas aplicações são executadas dentro de um contexto maior (o contexto do servidor). Então podemos imaginar a "confusão" que seria este ambiente. Imagine se a cada nova aplicação, novas classes fossem adicionadas ao um mesmo contexto? Primeiro, poderíamos ter conflito de classes, ou seja, classes com mesmo nome em aplicações distintas. Segundo, uma aplicação poderia utilizar classes de outra aplicação.
Então, para resolver esta bagunça, os servidores criam contexto individuais para cada aplicação disponibilizada em seu interior. Entretanto, os elementos disponibilizados pelo contexto, são propagados para os sub contextos. Ou seja, as classes disponíveis no contexto do servidor são propagadas para todos os contextos específicos das aplicações. Entretanto, as classes existentes no contexto específico de uma aplicação, não estão disponíveis no contexto "raiz" (o do servidor), nem nos contextos "irmãos" (contexto de outras aplicações).
Se quisermos disponibilizar classes a serem compartilhadas por todas as aplicações, devemos adicionar estas classes, ou bibliotecas, no contexto raiz.
Entretanto o Java nos permite gerenciar e consultar dinamicamente o contexto de execução. Podemos agregar em tempo de execução, novas bibliotecas de classe, ou apenas classes individuais ao contexto.
Ex.:
URL[1] allLocations = new URL("jar:file:/minhabiblioteca.jar!/");
ClassLoader loader = java.net.URLClassLoader.newInstance(allLocations, this.getClass().getClassLoader());
Veja que no exemplo montamos uma lista de URL contendo os ficheiros ".jar". Em seguida chamamos o método "newInstance" (do objeto ClassLoader) utilizando esta listagem e o "classloader" da classe corrente. Isto fará com que todas as classes existentes nas bibliotecas ".jar" sejam agregadas ao "classloader" da classe corrente. Estas novas classes passarão a estar disponíveis enquanto a aplicação estiver ativa na JVM. Sendo necessário refazer o processo sempre que a aplicação for reiniciada.
Reflexão
A reflexão é uma ferramenta poderosa do Java que permite "inspecionar" uma classe, bem como manipula-la dinamicamente. Como por exemplo instanciar objetos e executar métodos, verificar a estrutura de herança, identificar as interfaces utilizadas, varrer todas as anotações existentes etc.
No escopo deste post, este recurso nos permite descobrir dentro da classe, todas as suas anotações. Sejam elas de classe, de método, de atributos etc.
No exemplo a seguir será possível vermos uma parte desta funcionalidade em ação.
Anotação + Reflexão em Ação
Bom, neste exemplo vamos construir uma classe "base" para ser utilizada dentro da camada onde tratamos as requisições Http. Desta forma, iremos automatizar o preenchimento de objetos a partir de parâmetros existentes no pedido HTTP.
Vamos começar por criar a anotação @Inspect que será utilizada para marcar métodos a serem executados em um processo durante o processamento de auto preenchimento.
Ex.:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Inspect {
public String attributeName() default "";
}
No exemplo acima a anotação "Inspect" possui um atributo chamado "attributeName" que será utilizado para indicar o elemento a ser pesquisado, para ser considerado no momento da utilização do método ao qual a notação estiver associada. Veremos isto mais adiante.
Como já foi explicado, as anotações possuem escopo e deve ser explicitado o nível de disponibilidade. Para isto utilizamos os elementos "@Target" e o "@Retention".
O próximo passo será criar a classe "base" com a lógica necessária para fazer a carga automática, utilizando eventuais anotações existentes.
Ex.:
public class ParamsInspect {
public boolean autoLoad (HttpServletRequest request) {
String chave =
null;
Annotation anno =
null;
Class<?> clazz = this.getClass();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Inspect.class) //Possui a anotação Inspect
&& method..getParameters().size()==1 //Possui apenas 1 argumento
&& (method..getParameter(0) instanceof String
)) { //O argumento é String
anno = method.getAnnotation(Inspect
.class);
chave = anno.attributeName();
if (request.getParameter(chave)!=null) {
method.invoke(request.getParameter(chave));
} else if (request.getAttribute(chave)!=null
) {
method.invoke(request.getAttributer(chave));
}//if-else
}//if
}//for
}
}
A nossa classe base "ParamsInspect" possui o método "autoLoad" que irá fazer uma auto avaliação em busca de métodos que estejam anotados com a anotação "@Inspect" e que possuam apenas 1 argumento do tipo "String".
Em seguida será verificado no elemento "request", a presença de algum valor associado ao identificado "attributeName", que for definido junto a anotação do método.
E sempre que estas condições forem encontradas, o método em questão será executado, buscando nos parâmetros ou atributos do elemento "request" e passando-o como argumento na chamada do método.
Para que este procedimento fique claro, vamos agora construir uma classe derivada de nossa classe base "ParamsInspect.
Ex.:
public class Bicicleta extends ParamsInspect
{
private String marca;
private String modelo;
private int nroMudancas;
//Setter methods
@Inspect(attributeName="marcaBike")
public void setMarca (String marca) {
this.marca = marca;
}
@Inspect(attributeName="modeloIndicado")
public void setModelo (String modelo) {
this.modelo = modelo;
}
@Inspect(attributeName="mudanca")
public void setNroMudancas (String nro) {
this.nroMudancas
= Integer.parseInt(nro!=null?nro:0);
}
public void setNroMudancas (int nro) {
this.nroMudancas
= nro;
}
@Inspect(attributeName="qqCoisa")
public void outroMetodoQualquer (String valor) {
System.out.println(valor);
}
//Getter methods
}
//Dentro do método "service" da "Servlet" temos o seguinte código...
public void service (HttpServletRequest request, HttpServletResponse response) {
...
Bicicleta bicicleta = new Bicicleta();
bicicleta.autoLoad(request);
...
}
A analisar o exemplo anterior existe o pressuposto de que o Request HTTP deva possuir os parâmetros: marcaBike, modeloIndicado, mudanca, qqCoisa.
Então no método "service" estamos a instanciar um objeto do tipo "Bicicleta" e utilizamos o modelo que criamos, o "autoLoad", para preencher o objeto com os valores existentes no elemento "request".
Observe que o atributo "attributeName" que criamos para a nossa anotação "@Inspect" nos permite a flexibilidade de buscar qualquer valor dentro do "request". Ou seja, não precisamos padronizar de forma ao elemento do "request" ter o mesmo nome do atributo da classe. Assim ficamos com maior flexibilidade.
Outro ponto importante do exemplo é o método "outroMetodoQualquer", este foi adicionado para mostrar que não estamos restritos a métodos "setters". Podemos criar métodos e utilizá-los de forma dinâmica, desde que atendam a assinatura padronizada em nossa nova funcionalidade. Ou seja, possua apenas 1 argumento do tipo "String".
O atributo "nroMudancas" foi adicionado do tipo "int" para mostrar que, nestes casos, será necessário criar uma "versão" do "setter" com argumento "String" que seja capaz de resolver e guardar o valor no respetivo atributo da classe.
Então, repare que após o código "bicicleta.autoLoad(request)" ser processado, o objeto "bicicleta" já possuirá valores em seu interior. Os valore do fornecidos no "request". Com isto criamos uma funcionalidade que nos permite reduzir bastante o número de linhas de código necessárias sempre que estamos a fazer este "parse" de "Elementos Web" para objetos Java.
Poderíamos aqui imaginar inúmeras novas funcionalidades genéricas a serem implementadas em classes base para automatizar rotinas corriqueiras na programação. Podemos adicionar um método a nossa classe base (parseToEntity(Object obj) ) para converter o nosso objeto em uma entidade Hibernate. Adotando a mesma estratégia; criar uma anotação para ser utilizada nas classes "Entities" que serão utilizadas no argumento "obj", do método "parseToEntity".
Ou seja, após realizar o "parse" já poderíamos guardar as informações em base de dados, através do próprio Hibernate "save()".
Busca de Anotações no Contexto
Vejam que fazer uma busca completa no contexto pode ser algo muito custoso em termos de processamento e tempo. Afinal o contexto é algo carregado sobre demanda e só para exemplificar o pacote "rt.jar" do Java possui algo próximo de 48Mb. Então, não existem "enumerations" que nos permitem fazer tal varredura.
Então o que é aconselhado fazer, em caso de necessidade de busca sobre classes e pacotes do projeto, é criar um ficheiro de configuração onde serão especificados os elementos a serem avaliados. Sejam estes elementos, pacotes Java, ou diretorias físicas.
Segue um modelo de código que faz a varredura de classes em local pré determinado:
Ex.:
public class ElementScanner {
public void scanElements(){
try {
String className =
null;
int index = 0;
String classNamePath = null;
Class<?> repoClass;
Annotation[] annotations =
null;
String packageName = readConfig();
ClassLoader classLoader = getClass().getClassLoader();
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);
File folder = new File(urls.getPath());
File[] classes = folder.listFiles();
int size = classes.length;
List<Class<?>> classList = new ArrayList<Class<?>>();
for(int i=0;i<size;i++){
index = classes[i].getName().indexOf(".");
className = classes[i].getName().substring(0, index);
classNamePath = packageName+"."+className;
repoClass = Class.forName(classNamePath);
annotations = repoClass.getAnnotations();
for(int j =0;j<annotations.length;j++){
System.out.println("Annotation in class "+repoClass.getName()+ " is
"+annotations[j].annotationType().getName());
}
classList.add(repoClass);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public String readConfig(){
try{
URL url = getClass().getClassLoader().getResource("WEB-INF/config.xml");
JAXBContext jContext = JAXBContext.newInstance(RepositoryConfig.class);
Unmarshaller um = jContext.createUnmarshaller();
RepositoryConfig rc = (RepositoryConfig) um.unmarshal(new File(url.getFile()));
return rc.getRepository().getPackageName();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Observe no exemplo que estamos a utilizar os recursos de reflexão para obter a classe e as suas respetivas anotações. Ao invés de fazer um "print" destas, poderíamos fazer qualquer tipo de processamento. Por exemplo, se estivéssemos a construir um framework, poderíamos guardar esta informações em memória para serem consideradas em qualquer parte dentro do framework.