Lazy Loading é o mecanismo utilizado pelos frameworks de persistência para carregar informaçöes sobre demanda. Esse mecanismo torna as entidades mais leves, pois suas associaçöes säo carregadas apenas no momento em que o método que disponibiliza o dado associativo é chamado. No JPA (Java Persistence API), todo atributo associativo de uma entidade é por padräo Lazy, podendo ser alterado para Eager através dos annotations @Basic, @ManyToOne, @OneToOne, @OneToMany, @ManyToMany. Quando definimos o fetch para Eager, estamos dizendo para o framework de persistência carregar além da entidade desejada, também o relacionamento que ela possui com o atributo annotado. Neste artigo, vamos tratar especificamente do carregamento Lazy quando a entidade é acessada fora do container, onde näo existe escopo transacional.
Para ficar claro porque é necessário um tratamento especial para o caso onde a entidade é acessada fora do escopo transacional, vamos fazer um acesso bem simples a uma entidade que possui um relacionamento com outra, e ver o que acontece quando fazemos acesso ao método “get” que retorna a entidade associativa.
Cenário
O cenário proposto é de criar um cadastro simples de endereço, onde o mesmo terá um relacionamento “muitos-para-um” com o tipo de logradouro.Quando um endereço for pesquisado, sua entidade só poderá carregar sua associaçäo com tipo de logradouro, quando o usuário acessar o método getTipoLogradouro.
Relacionamento muitos-para-um entre EnderecoBean e TipoLogradouroBean
Vamos criar as classes de persistência EnderecoBean e TipoLogradouroBean.
Näo é escopo deste artigo entrar em detalhes sobre os Annotations do JPA.
Para isso visite o site http://jcp.org/en/jsr/detail?id=220.
Classe TipoLogradouroBean.java
package com.artigo.apt.persistence;import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.TableGenerator;@Entity(name="TipoLogradouroBean")
@Table(name="TIPO_LOGRADOURO")
public class TipoLogradouroBean implements Serializable{
private Long id;
private String tipo;
@Id
@TableGenerator(
name="TABLE_SEQUENCE_GENERATOR",
table="TABLE_SEQUENCE_GENERATOR",
pkColumnName="GEN_KEY",
valueColumnName="GEN_VALUE",
pkColumnValue="tipo_logradouro_id",
allocationSize=1
)
@GeneratedValue(strategy=GenerationType.TABLE,
generator="TABLE_SEQUENCE_GENERATOR")
@Column(name="ID")
public Long getId() {
return id;
} public void setId(Long id) {
this.id = id;
} public void setTipo(String tipo){
this.tipo = tipo;
} public String getTipo(){
return tipo;
}
}
Classe EnderecoBean.java
package com.artigo.apt.persistence;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
@Entity(name="EnderecoBean")
@Table(name="ENDERECO")
public class EnderecoBean implements java.io.Serializable{
private Long id;
private String endereco;
private String complemento;
private TipoLogradouroBean tipoLogradouro;
@Id
@TableGenerator(
name="TABLE_SEQUENCE_GENERATOR",
table="TABLE_SEQUENCE_GENERATOR",
pkColumnName="GEN_KEY",
valueColumnName="GEN_VALUE",
pkColumnValue="endereco_id",
allocationSize=1
)
@GeneratedValue(strategy=GenerationType.TABLE,
generator="TABLE_SEQUENCE_GENERATOR")
@Column(name="ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEndereco() {
return endereco;
}
public void setEndereco(String endereco) {
this.endereco = endereco;
}
public String getComplemento(){
return complemento;
}
public void setComplemento(String complemento){
this.complemento = complemento;
}
@ManyToOne(optional=false)
@JoinColumn(name="TIPO_LOGRADOURO_ID", referencedColumnName="ID")
public TipoLogradouroBean getTipoLogradouro(){
return tipoLogradouro;
}
public void setTipoLogradouro(TipoLogradouroBean tipoLogradouro){
this.tipoLogradouro = tipoLogradouro;
}
}
Analisando o código EnderecoBean, vimos que a associaçäo no banco de dados
entre endereço e tipo logradouro será através da coluna TIPO_LOGRADOURO_ID
e que a associaçäo será do tipo muitos-para-um definido pela annotation
@ManyToOne. Por padräo, o framework de persistência já assume que o fetch
será Lazy por ser uma definiçäo do JPA, logo näo é necessário ter o
elemento fetch=FetchType.Lazy no annotation @ManyToOne.
Agora vamos criar os sessions beans que iräo popular as tabelas e fazer as
buscas nas mesmas.
O Session Bean EnderecoFacade será responsável pela inclusäo dos dados de
tipo de logradouro e endereço.
Interface IEnderecoFacade.java
package com.artigo.apt.interfaces;
import javax.ejb.Remote;
import com.artigo.apt.persistence.EnderecoBean;
import com.artigo.apt.persistence.TipoLogradouroBean;
@Remote
public interface IEnderecoFacade {
public EnderecoBean persist(EnderecoBean endereco);
public TipoLogradouroBean persist(TipoLogradouroBean logradouroBean);
}
Classe EnderecoFacade.java
package com.artigo.apt.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.artigo.apt.interfaces.IEnderecoFacade;
import com.artigo.apt.persistence.EnderecoBean;
import com.artigo.apt.persistence.TipoLogradouroBean;
public class EnderecoFacade implements IEnderecoFacade {
@PersistenceContext(unitName="Artigo")
EntityManager em;
public EnderecoBean persist(EnderecoBean endereco){
em.persist(endereco);
return endereco;
}
public TipoLogradouroBean persist(TipoLogradouroBean logradouro){
em.persist(logradouro);
return logradouro;
}
}
O Session Bean EnderecoQueryFacade será responsável pelas buscas feitas no
componente Endereço.
Classe IEnderecoQueryFacade.java
package com.artigo.apt.interfaces;
import javax.ejb.Remote;
import com.artigo.apt.persistence.EnderecoBean;
@Remote
public interface IEnderecoQueryFacade {
public EnderecoBean findEnderecoById(Long id);
}
Classe EnderecoQueryFacade.java
package com.artigo.apt.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.artigo.apt.interfaces.IEnderecoQueryFacade;
import com.artigo.apt.persistence.EnderecoBean;
@Stateless(name="EnderecoQueryFacade")
public class EnderecoQueryFacade implements IEnderecoQueryFacade{
@PersistenceContext(unitName="Artigo")
EntityManager em;
public EnderecoBean findEnderecoById(Long id) {
return em.find(EnderecoBean.class, id);
}
}
Build/Deploy
Agora que já temos nosso componente criado, precisamos apenas fazer o
build e deploy no servidor. Neste estaremos usando o jboss-4.0.4.GA no
fedora 4, por consequência disso, estou usando os jars :
–
/usr/jboss-4.0.4.GA/server/default/lib/ejb3-persistence.jar–
/usr/jboss-4.0.4.GA/server/default/deploy/ejb3.deployer/jboss-ejb3x.jar–
/usr/jboss-4.0.4.GA/server/default/lib/jboss-j2ee.jarpara fazer a compilaçäo das classes. Para o ciclo de build/deploy, nada
melhor que recorrer ao ant para execuçäo desta tarefa. O build.xml e os
artefatos necessários para o build encontram-se disponíveis para download.
Depois de rodar o build.xml o diretório com os pacotes fica assim:
Agora é só copiar o arquivo artigo-apt-core.ear para o diretório de deploy
do jboss /usr/jboss-4.0.4.GA/server/default/deploy e subir o servidor.
Agora vamos criar nossa classe cliente.
A classe cliente será responsável por inserir um tipo de logradouro e um
endereço, associando um ao outro. Depois de feita a inclusäo, vamos
recuperar o endereço cadastrado e verificar o retorno do tipo de logradouro
que associamos no cadastro.
Classe APTClient.java
package com.artigo.apt.client;
import javax.naming.InitialContext;
import com.artigo.apt.interfaces.IEnderecoFacade;
import com.artigo.apt.interfaces.IEnderecoQueryFacade;
import com.artigo.apt.persistence.EnderecoBean;
import com.artigo.apt.persistence.TipoLogradouroBean;
public class APTClient {
public TipoLogradouroBean persistLogradouro(String tipo)
throws Exception{
TipoLogradouroBean tipoLogradouroBean = new TipoLogradouroBean();
tipoLogradouroBean.setTipo(tipo);
IEnderecoFacade ief = getEnderecoFacade();
tipoLogradouroBean = ief.persist(tipoLogradouroBean);
return tipoLogradouroBean;
}
public EnderecoBean persistEndereco(TipoLogradouroBean tipoLogradouro, String
endereco, String complemento)
throws Exception{
EnderecoBean enderecoBean = new EnderecoBean();
enderecoBean.setEndereco(endereco);
enderecoBean.setComplemento(complemento);
enderecoBean.setTipoLogradouro(tipoLogradouro);
IEnderecoFacade ief = getEnderecoFacade();
enderecoBean = ief.persist(enderecoBean);
return enderecoBean;
}
public void test() throws Exception{
TipoLogradouroBean logradouroCadastrado = persistLogradouro("Rua");
persistEndereco(logradouroCadastrado, "Mao Tse Tung", "Apto 12E");
}
public void testGetLazy() throws Exception{
IEnderecoQueryFacade ieqf = getEnderecoQueryFacade();
EnderecoBean enderecoCadastrado = ieqf.findEnderecoById(new Long(1));
System.out.println(enderecoCadastrado.getTipoLogradouro().getTipo());
}
public IEnderecoQueryFacade getEnderecoQueryFacade(){
try{
InitialContext ctx = new InitialContext();
Object ref = ctx.lookup("artigo-aptcore/EnderecoQueryFacade/remote");
IEnderecoQueryFacade facade = IEnderecoQueryFacade.class.cast(ref);
return facade;
}catch(Exception e){
e.printStackTrace();
}
return null;
}
public IEnderecoFacade getEnderecoFacade(){
try{
InitialContext ctx = new InitialContext();
Object ref = ctx.lookup("artigo-apt-core/EnderecoFacade/remote");
IEnderecoFacade facade = IEnderecoFacade.class.cast(ref);
return facade;
}catch(Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String argv[]) throws Exception{
APTClient apt = new APTClient();
apt.test();
apt.testGetLazy();
}
}
Para rodar o cliente, precisamos de alguns pacotes extras do container
para poder ter acesso ao servidor e nos comunicarmos com o componente
Endereço. Usaremos o ant para gerar o client.jar que contém todas as
interfaces e classes que precisamos para ter o cliente pronto pra rodar. O
build-client.xml também se encontra disponível para download.
Depois que o build-client.xml é rodado o diretório de pacotes fica assim:
Agora é só rodar a target run-client do build-client.xml para executar a
aplicaçäo, repare que a target faz referência a pacotes que säo do
container para ser executado.
Build-client.xml
.....
......
Quando rodamos a aplicaçäo, vimos que uma exceçäo é lancada pelo client
quando o método testGetLazy é executado. Mas por que isso acontece?
run-client
:[
java] Sep 8, 2006 9:08:40 AM org.hibernate.LazyInitializationException[
java] SEVERE: could not initialize proxy - no Session[
java] org.hibernate.LazyInitializationException: could not initialize proxy -no Session
[
java] atorg.hibernate.proxy.AbstractLazyInitializer.initialize(
AbstractLazyInitializer.java:57
)[
java] atorg.hibernate.proxy.AbstractLazyInitializer.getImplementation(
AbstractLazyInitializer.java:111
)[
java] atorg.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(
JavassistLazyInitializer.java:196
)[
java] atcom.artigo.apt.persistence.TipoLogradouroBean_$$_javassist_0.getTipo(TipoLogradouro
Bean_$$_javassist_0.java)
[
java] at com.artigo.apt.client.APTClient.testGetLazy(APTClient.java:49)[
java] at com.artigo.apt.client.APTClient.main(APTClient.java:79)
Quando executamos o método testGetLazy, recuperamos uma entidade
EnderecoBean leve, ou seja, sem a materializaçäo do Tipo de Logradouro, já
que o padräo do framework é que o carregamento seja Lazy. Porém a entidade
que recebemos do container é uma entidade detachada, ou seja, näo existe
vínculo nenhum com o container e consequentemente näo existe sessäo
transacional aberta, o que impossibilita o framework de fazer qualquer
consulta ao banco de dados a partir do cliente. Mas aí fica a duvída: quer
dizer que o Lazy só poderá ser usado se estivermos fazendo operaçöes dentro
do container, onde existe escopo transacional? A resposta é sim, a näo ser
que... :).
Solucäo:
O intuito deste artigo é fazer um Lazy Loading Remoto, utilizando os
Annotations e o APT para isso. Vamos analisar como será feito toda essa
mágica.
O que é Annotation?
Os annotations (JSR-175) säo um tipo especial de modificador e que podem
ser usados em qualquer lugar onde qualquer outro modificador possa ser usado.
Quando criamos um annotation, indicamos onde o mesmo se encaixa dentro da
estrutura do código Java. Por convençäo os annotations precedem outros modificadores,
e consistem de um sinal “@” seguido pelo tipo do annotation e uma lista de
pares chave-valor quando houver. Os annotations foram criados para “marcar” o
código, essas marcaçöes serviräo como fonte de informaçäo para um comportamento
diferente quando a marcaçäo colocada for lida.
Estaremos usando a palavra “marcar” para nos referirmos aos annotations em
certos pontos do artigo.
O que é APT?
O APT (Annotation Processing Tool) é uma ferramenta incorporada no Java SE
5.0 para processar os annotations inseridos nos códigos Java. Sua API
possui novas funcionalidades reflections para analisar o código criado a
procura dos annotations desejado. No nosso exemplo, iremos usar o APT para
verificar alguns annotations criados por nós para podermos gerar um Proxy.
Mais a frente diremos porque esse Proxy será importante.
O APT é acionado por linha de comando que executa um Processador de
Annotations que é instanciado e retornado através de uma Factory. Sua
principal finalidade é analisar códigos marcados com annotations e gerar
através das informaçöes fornecidas pelos mesmos arquivos de suporte à
aplicaçäo, como novos códigos fontes, classes, arquivos de configuraçäo
etc. O mecanismo do APT funciona da seguinte forma:
1.
O APT verifica quais annotations estäo presente nos códigos fonte queseräo processados.
2.
O APT olha para a Factory de Processador de Annotations.3.
O APT pergunta para a Factory quais annotations seräo processados.4.
O APT solicita ao Factory a instância do Processador de Annotationpara processar os annotations existentes nos códigos fonte.
5.
O Processador de Annotation é executado para processar o annotation.Se o processador gerar novos códigos fontes, o APT vai repetir este
processo até que nenhum código novo seja gerado.
Criando nossos annotations
Pensando friamente sobre o problema, vimos que o desejado é que no momento
que o método getTipoLogradouro for acionado, uma chamada a facade
EnderecoQueryFacade seja feita, e execute uma consulta no banco de dados
retornando o Tipo de Logradouro associado ao Endereco que estou
manipulando.
Agora vamos analisar que tipos de marcaçöes precisamos ter, para que tudo
funcione transparentemente.
1
– Precisamos de uma marcaçäo que nos diga qual classe precisa de umproxy. Isso é importante para que o Processador de Annotations leia essa
marcaçäo e gere a nova classe Proxy.
Annotation Proxied.java
package com.artigo.apt.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation responsavel por marcar as classes que serao proxiadas
* Estando este annotation presente na classe, faz com que o Gerador de
* Proxy gere um proxy para a classe.
* @author rdias
*
*/
@Documented
@Retention(value=RetentionPolicy.RUNTIME)
@Target(value={ElementType.TYPE})
@Inherited
public @interface Proxied {
}
2
– Precisamos de uma marcaçäo que nos diga que o método é Lazy, e queforneça informaçöes de como recuperar essa associaçäo.
Annotation ProxiedLazy.java
package com.artigo.apt.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
@Inherited
public @interface ProxiedLazy {
String mappedBy() default "";
String jndiName() default "";
String factoryClass() default "";
String factoryMethod() default "";
String[] parameters();
}
3
– Precisamos de uma marcaçäo que informe que a partir daquele ponto daaplicaçäo o retorno do objeto deverá ser um objeto proxiado.
Annotation ProxiedReturn.java
package com.artigo.apt.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation responsavel por marcar os metodos cujo retorno devera
* ser convertido para um tipo Proxied.
*
* @author rdias
*
*/
@Documented
@Retention(value=RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
@Inherited
public @interface ProxiedReturn {
}
4
– Precisamos de uma marcaçäo que indique que o método find em questäo éum método que pode ser utilizado para recuperar informaçöes associativas.
Annotation ProxiedFind.java
package com.artigo.apt.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation responsavel por marcar os metodos que recuperam informacoes
associativas
* O key eh importante pois ele coloca unicidade em metodos de mesmo nome.
* @author rdias
*
*/
@Documented
@Retention(value=RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface ProxiedFind {
String key() default "";
}
5
– Precisamos de uma marcaçäo para ser colocada no proxy gerado, pois ocliente poderá manipular o proxy achando que está trabalhando com o objeto
real “smart-reference”, e quando esse proxy for passado de volta pra o
container o mesmo deverá saber que se trata de um proxy e que uma açäo deve
ser tomada para que o objeto real seja recuperado do proxy.
Annotation ProxiedClass.java
package com.artigo.apt.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***
* Annotation responsavel por marcar a classe proxiada
* Estando este annotation presente na classe proxiada, indicara
* ao engineer de injection que o que ele esta vendo na verdade
* eh um proxy de uma classe e nao a classe em si, toda a classe
* que possui este annotation devera ter um metodo getUnProxied() que
* retorna a classe real.
*
* @author rdias
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProxiedClass {
}
Atualizando as classes para que usem as marcaçöes
A primeira atualizaçäo que vamos fazer, será marcar a classe EnderecoBean
como sendo uma que deverá ter um proxy, e indicar que o método
getTipoLogradouro terá um comportamento Lazy Loading.
Atualizando a classe EnderecoBean.java
import com.artigo.apt.annotations.Proxied;
import com.artigo.apt.annotations.ProxiedLazy;
...
@
Proxied@Entity(name="EnderecoBean")
@Table(name="ENDERECO")
public class EnderecoBean implements java.io.Serializable{
...
@ProxiedLazy
(mappedBy="findLogradouroByEnderecoId",jndiName="artigo-apt-core/EnderecoQueryFacade/remote",
parameters={"id"})
@ManyToOne(optional=false)
@JoinColumn(name="TIPO_LOGRADOURO_ID", referencedColumnName="ID")
public TipoLogradouroBean getTipoLogradouro(){
return tipoLogradouro;
}
...
}
Agora vamos colocar na interface IEnderecoQueryFacade a marcaçäo que
indica que o retorno do método no caso o findEnderecoById, deverá na
verdade retornar um proxy do EnderecoBean e näo o próprio. Vamos também
inserir o método que irá retornar o tipo do logradouro pelo id do endereço.
Atualizando a interface IEnderecoQueryFacade.java
import com.artigo.apt.annotations.ProxiedReturn;
import com.artigo.apt.annotations.ProxiedFind;
...
@Remote
public interface IEnderecoQueryFacade {
@ProxiedReturn
public EnderecoBean findEnderecoById(Long id);
@ProxiedFind(key=” findLogradouroByEnderecoId”)
public TipoLogradouroBean findLogradouroByEnderecoId(Long id);
}
Com isso, já temos quatro marcaçöes nas nossas classes, das cinco que
precisamos para poder fechar a soluçäo. Mas até agora a única coisa que
fizemos foi colocar as marcaçöes; em momento algum criamos mecanismos que
iräo ler essas marcaçöes, entäo... vamos a isso.
Primeiramente vamos criar o Processador de Annotations que irá olhar
especificamente para o Annotation @Proxied.
Classe APTFactory.java
package com.artigo.apt.processor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.AnnotationProcessorFactory;
import com.sun.mirror.apt.AnnotationProcessors;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
public class APTFactory implements AnnotationProcessorFactory
{
/**
* Retorna o processador para os annotations do artigo
* @return Um annotation processor para o note annotations se requisitado,
* senao, retorna NO_OP annotation processor.
*
*/
public AnnotationProcessor getProcessorFor(
Set declarations,
AnnotationProcessorEnvironment env){
AnnotationProcessor result;
if (declarations.isEmpty()){
result = AnnotationProcessors.NO_OP;
}else{
result = new APTAnnotationProcessor(env);
}
return result;
}
public Collection supportedAnnotationTypes(){
Collection list = new ArrayList();
list.add("
com.artigo.apt.annotations.Proxied");return list;
}
public Collection supportedOptions(){
return Collections.emptyList();
}
}
A classe acima possui a responsabilidade de informar ao APT quais
annotations o processador vai analisar. Posterior a isso, o APT solicita à
factory a instância da classe processor, para ser acionada pelo APT através
do método process().
Criando a classe que irá processar o annotation.
Classe APTAnnotationProcessor.java
package com.artigo.apt.processor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.AnnotationMirror;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
import com.sun.mirror.declaration.Declaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.util.DeclarationVisitor;
import com.sun.mirror.util.DeclarationVisitors;
public class APTAnnotationProcessor implements AnnotationProcessor{
private AnnotationProcessorEnvironment environment;
private TypeDeclaration proxiedDeclaration;
private Collection declarations = null;
public APTAnnotationProcessor(AnnotationProcessorEnvironment env){
environment = env;
proxiedDeclaration =
environment.getTypeDeclaration("com.artigo.apt.annotations.Proxied");
declarations =
environment.getDeclarationsAnnotatedWith((AnnotationTypeDeclaration)
proxiedDeclaration);
}
public void process(){
for( Declaration declaration : declarations ){
selectVisitor(declaration);
}
}
private void selectVisitor(Declaration declaration){
Collection annotations =
declaration.getAnnotationMirrors();
for(AnnotationMirror mirror : annotations){
DeclarationVisitor scanner;
if (mirror.getAnnotationType().getDeclaration().equals
(proxiedDeclaration)){
scanner = DeclarationVisitors.getDeclarationScanner(
new ProxiedVisitor(environment),
DeclarationVisitors.NO_OP);
declaration.accept(scanner);
}
}
}
}
No momento que o método process() é acionado pelo APT, o mesmo percorre
todas as declaraçöes que possuem o annotation @Proxied. O objeto
Declaration armazena internamente a estrutura da classe que possui o
annotation. Quando passamos este objeto para o ProxiedVisitor, o APT se
responsabiliza por acionar o método visitor designado para ler a declaraçäo
específica que possui o annotation. No nosso caso, o @Proxied foi colocado
para marcar uma classe, entäo o APT vai acionar o método
visitClassDeclaration passando um objeto ClassDeclaration. Caso tivessemos
colocado nossa annotation na declaraçäo de um método, o APT iria acionar o
visitMethodDeclaration da nossa classe ProxiedVisitor e assim por diante.
Para maiores detalhes veja o Pattern Visitor.
Classe ProxiedVisitor.java
package com.artigo.apt.processor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.util.SimpleDeclarationVisitor;
public class ProxiedVisitor extends SimpleDeclarationVisitor{
private AnnotationProcessorEnvironment env = null;
public ProxiedVisitor(AnnotationProcessorEnvironment env){
this.env = env;
}
@Override
public void visitClassDeclaration(ClassDeclaration cd) {
GeneratorProxiedClass gpc = new GeneratorProxiedClass(cd, env);
gpc.accept();
}
}
Como já dissemos anteriormente, o fato de termos colocado o annotation
@Proxied na declaraçäo da classe EnderecoBean, faz com que o APT acione o
método visitClassDeclaration(..) passando a classe que foi anotada. Tendo
em mäos essa classe podemos começar a criar a nova classe proxy a partir
dessa. :)
Criando o gerador de código Proxy
Neste momento temos um objeto do tipo ClassDeclaration, que contém todas
as informaçöes pertinentes a classe que possui o annotation @Proxied. O que
temos que fazer agora, é analisar a classe e gerar um proxy dela mesma. Uma
informaçäo importante é que o APT tem poderes para criar qualquer arquivo
seja “.java” ou “.class” porém näo possui poderes para alterá-las. O que
quero dizer é que o ClassDeclaration por exemplo näo possui métodos do tipo
addNewMethod para inserir um novo método na classe que ele está analisando,
ou seja, toda classe analisada pelo APT é read-only. Mas o nosso intuito
näo é alterar a classe que possui o annotation @Proxied, o que queremos é
fazer um proxy dela. Bom.. poderíamos fazer algo bem simples, do tipo olhar
a classe e começar a criar a outra, mas vamos fazer diferente, vamos criar
um template do que será nossa nova classe proxy e gerar um parser para
poder moldá-la do jeito que quisermos. Isso se tornará útíl caso seja
necessário fazer uma atualizaçäo no proxy, como por exemplo, criar um cache
das associaçöes já retornadas, ou analisar um novo annotation contido no
objeto retornada. Enfim, a idéia do template é agilizar o processo de
atualizaçäo do novo código (Proxy), sem que aja a necessidade de abrir o
código do gerador para tal.
Arquivo Proxied.template (O template completo esta disponível para download)
...
@ProxiedClass
public class Proxied extends {
...
//Neste metodo vamos analisar a presença do annotation
//@ProxiedLazy e @ProxiedFind
public Object getProxiedValue(String nameMethod){
Class c = this.getClass().getSuperclass();
Method[] methods = c.getMethods();
for(Method m : methods ){
if (m.getName().equals(nameMethod)){
ProxiedLazy lazyRemote = m.getAnnotation(ProxiedLazy.class);
if (lazyRemote != null){
try{
String factoryClass = lazyRemote.factoryClass();
String jndiName = lazyRemote.jndiName();
String mappedBy = lazyRemote.mappedBy();
String[] parameters = lazyRemote.parameters();
Object objectWithLazyProxiedFind = null;
if (!factoryClass.equals("")){
Object factoryObject =
Class.forName(factoryClass, true,
Thread.currentThread().getContextClassLoader()).newInstance();
Method factoryMethod =
factoryObject.getClass().getMethod(
lazyRemote.factoryMethod(), new Class[]{});
objectWithLazyProxiedFind = factoryMethod.invoke
(factoryObject, new Object[]{});
}else if(!jndiName.equals("")){
try {
Context ct = new InitialContext();
objectWithLazyProxiedFind = ct.lookup(jndiName);
}catch (Exception e){
e.printStackTrace();
}
}
Method objectWithLazyFindMethod = null;
Class[] arrClazz = objectWithLazyProxiedFind.getClass().getInterfaces();
for(Class clazz : arrClazz ){
Method[] methodsClazz = clazz.getMethods();
for(Method methodClazz : methodsClazz){
ProxiedFind ProxiedFind = methodClazz.getAnnotation
(ProxiedFind.class);
if (ProxiedFind != null && ProxiedFind.key().equals(mappedBy)){
objectWithLazyFindMethod =
objectWithLazyProxiedFind.getClass().getMethod(
methodClazz.getName(), methodClazz.getParameterTypes() );
break;
}
}
if (objectWithLazyFindMethod != null)
break;
}
System.out.println("Executando o metodo proxiado:
" + objectWithLazyFindMethod.getName() +
" com " + (parameters.length + 1) + " parametros.");
return objectWithLazyFindMethod.invoke
(objectWithLazyProxiedFind, toObject(parameters) );
}catch(Exception ee){
ee.printStackTrace();
}
}
}
}
return null;
}
...
}
Vamos criar uma classe abstrata SimpleTemplateVisitor.java que irá percorrer
o template linha a linha atrás das diretivas que incluimos no template.
Sempre que ele achar uma diretiva irá direcionar a linha do arquivo para um
determinado método que trata aquela diretiva em questäo. Recorreremos ao
Pattern Visitor já que o APT o utiliza por padräo.
Classe SimpleTemplateVisitor.java
(O código completo está disponível para download)
...
public abstract class SimpleTemplateVisitor {
AnnotationProcessorEnvironment env;
Map mapMethods = new HashMap(){{
put("package.qualifiedName", "visitorPackage");
put("class.simpleName", "visitorNameClass");
put("class.type", "visitorTypeClass");
put("class.simpleName.field", "visitorField");
put("class.methods", "visitorMethods");
}};
public SimpleTemplateVisitor(AnnotationProcessorEnvironment env){
this.env = env;
}
...
public abstract String visitorPackage(String line);
public abstract String visitorNameClass(String line);
public abstract String visitorTypeClass(String line);
public abstract String visitorField(String line);
public abstract String visitorMethods(String line);
public abstract URL getTemplate();
public void endTemplate(){
}
...
}
Agora vamos criar a classe que irá utilizar o template para poder gerar o
nosso código Proxy. Repare no template, que o nome da nova classe será o
mesmo nome da classe marcada com @Proxied seguido de Proxied:
Classe GeneratorProxiedClass.java
(O código completo está disponível para download)
...
public class GeneratorProxiedClass extends SimpleTemplateVisitor {
final String PATH_TEMPLATE =
"/home/rdias/projects/TigerSistafe/Artigos/resources/";
...
public void endTemplate(){
System.out.println(".........Gerando Codigo....................");
System.out.println(newPackage + ".proxied." + newClass+"Proxied");
try{
PrintWriter writer = env.getFiler().createSourceFile(newPackage +
".proxied." + newClass+"Proxied");
writer.append(newSource.toString());
System.out.println(".........Codigo Gerado.................");
}catch(Exception e){
e.printStackTrace();
}
}
public String visitorPackage(String line){
String packageName = classDeclaration.getPackage().getQualifiedName();
this.newPackage = packageName;
return replace(line, "", packageName);
}
public String visitorNameClass(String line){
String nameClass = classDeclaration.getSimpleName();
this.newClass = nameClass;
return replace(line, "", nameClass);
}
public String visitorTypeClass(String line){
return "";
}
public String visitorField(String line){
String nameField = "my"+classDeclaration.getSimpleName();
return replace(line, "", nameField);
}
public String visitorMethods(String line){
StringBuffer sourceString = new StringBuffer("");
Collection methods = classDeclaration.getMethods();
System.out.println("Tamanho de methods:" + methods.size() );
setHierarquicalMethods( classDeclaration, methods );
System.out.println("Tamanho de methods:" + methods.size() );
for(MethodDeclaration m : methods){
if (!isMethodAbstractOrPrivate(m)){
//verifica os anotations presente no metodo
Collection annotations =
m.getAnnotationMirrors();
boolean processLazy = false;
for(AnnotationMirror mirror : annotations){
if(mirror.getAnnotationType().getDeclaration()
.getSimpleName().equals("ProxiedLazy")){
sourceString.append(
generatorMethodBody(m, mirror, line));
processLazy = true;
}
}
if (!processLazy){
sourceString.append(generatorMethodBody(m));
}
}
}
return sourceString.toString();
}
...
}
A classe acima simplesmente recupera as diretivas e altera as mesmas por
nomes retirados da classe original.
Vamos executar o APT pra ver se ele gera nossa classe Proxy.
Para rodar via linha de comando, vá para o diretório onde estäo suas
entidades. Isso é necessário para que näo seja preciso colocar o caminho
completo de todas as entidades marcadas com o annotation @Proxied lá
embaixo onde está o “*.java ”:
->
/home/rdias/projects/TigerSistafe/Atigos/scr/com/artigo/apt/persistence
-> Digite:
-> apt -cp /home/rdias/projects/TigerSistafe/Artigos/build/:
/usr/jboss-4.0.4.GA/server/default/lib/ejb3-persistence.jar/
-s /home/rdias/projects/TigerSistafe/Artigos/src -nocompile
-factory com.artigo.apt.processor.APTFactory *.java
•
A opcäo -s informa onde estäo os fontes•
A opcäo -cp informa onde estäo as classes•
A opcäo -nocompile näo compila as novas classes geradas.•
A opcäo -factory diz quem é a factory que irá processar os Annotations.Outra informacäo importante é que a API com.sun.mirror* utilizada pelas
nossas classes para criar o Processador, se encontra dentro do pacote
tools.jar no diretório jdk1.5.0_05/lib/tools.jar.
Uma observaçäo interessante, é que depois que a classe EnderecoBeanProxied
foi gerada o APT verificou que nela existe o annotation @ProxiedClass que
näo está sendo processado por ninguém, o que reforça o passo 5 do ciclo do
APT. Veja que foi criado no seu diretório source um novo pacote:
com.artigo.apt.persistence.proxied
e nele está contido a classe EnderecoBeanProxied.java que possui a mesma
estrutura definida no seu template. :).
No build existe uma target chamada aptProxied que executa o APT via ant,
sem a necessidade de abrir o console e fazer isso por linha de comando. O
incoveniente é que neste caso toda classe que possui o annotation @Proxied
deverá ser colocada como argumento para execuçäo do APT.
Build.xml
...
...
Revisando um pouco, afinal já se foram muitas linhas de código.
Inicialmente criamos 5 annotations, depois colocamos esses annotations em
algumas classes. Agora estamos analisando essas marcaçöes e gerando
resultados em cima delas. Por enquanto só analisamos os annotation @Proxied
pelo gerador. O @ProxiedLazy e @ProxiedFind estäo sendo analisados pela
classe proxy gerada, faltando agora o @ProxiedReturn e @ProxiedClass. O
annotation @ProxiedReturn deve ser verificado quando o usuário solicitar ao
componente EnderecoQueryFacade o endereço pelo Id, para isso vamos fazer o
seguinte, criaremos um proxy dinâmico que encapsula o componente
EnderecoQueryFacade, e retorna pro cliente esse proxy dinâmico. No momento
que o cliente acessar o método findEnderecoById, nosso proxy dinâmico irá
analisar se o método acionado pelo cliente tem o annotation @ProxiedReturn.
Caso sim, o proxy dinâmico gera uma instância do proxy gerado por nós,
passando como argumento o EnderecoBean retornando o proxy para o cliente,
calma que olhando o código fica mais simples:
Classe ProxiedInvocationHandler.java
package com.artigo.apt.processor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import com.artigo.apt.annotations.ProxiedReturn;
public class ProxiedInvocationHandler implements InvocationHandler{
private Object delegate;
private Class delegateClass;
public ProxiedInvocationHandler(Object delegate, Class interfaceClass)
{
this.delegate = delegate;
this.delegateClass = delegate.getClass();
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception{
try {
Method methodForInvoke =
delegateClass.getMethod(method.getName(), method
.getParameterTypes());
Object result = methodForInvoke.invoke(delegate, args);
if (method.isAnnotationPresent(ProxiedReturn.class)){
if (result instanceof Collection){
proxiedCollection( (Collection)result );
}else if (result instanceof Map){
//nao implementado ainda
}else{
return proxied(result);
}
}
return result;
}catch (Throwable e){
e.printStackTrace();
throw new Exception("Erro:" + e.getMessage() + " Causado por: " +
e.getCause());
}
}
private Object proxied(Object result) throws Exception{
Class resultClass = result.getClass();
String packageName = resultClass.getPackage().getName()+".proxied.";
String nameClassProxied = packageName +
resultClass.getSimpleName()+"Proxied";
Class c = Class.forName(nameClassProxied, false,
Thread.currentThread().getContextClassLoader());
return c.getConstructor(new Class[]{resultClass}).newInstance(result);
}
private void proxiedCollection(Collection resultCollection) throws Exception{
for(Iterator it = resultCollection.iterator(); it.hasNext();){
Object o = it.next();
resultCollection.remove( o );
resultCollection.add( proxied(o) );
}
}
private void proxiedMap(Map resultMap){
//TODO: Nao implementar por enquanto
}
}
Vamos agora criar uma EnderecoFacadeFactory que faz uso do
InvocationHandler criado acima. Essa Facade será responsavél por retornar
os objetos EnderecoFacade e EnderecoQueryFacade para o cliente.
Classe EnderecoFacadeFactory.java
package com.artigo.apt;
import java.lang.reflect.Proxy;
import javax.naming.Context;
import javax.naming.InitialContext;
import com.artigo.apt.interfaces.IEnderecoFacade;
import com.artigo.apt.interfaces.IEnderecoQueryFacade;
import com.artigo.apt.processor.ProxiedInvocationHandler;
public class EnderecoFacadeFactory{
static final String ENDERECO_FACADE_JNDI = "EnderecoFacade";
static final String ENDERECO_QUERY_FACADE_JNDI = "EnderecoQueryFacade";
static final String ENDERECO_COMPONENT = "artigo-apt-core";
public static IEnderecoFacade getEnderecoFacade() throws Exception{
return (IEnderecoFacade)getProxiedFacade(
ENDERECO_COMPONENT,
ENDERECO_FACADE_JNDI,
"remote",
IEnderecoFacade.class);
}
public static IEnderecoQueryFacade getEnderecoQueryFacade() throws Exception{
return (IEnderecoQueryFacade)getProxiedFacade(
ENDERECO_COMPONENT,
ENDERECO_QUERY_FACADE_JNDI,
"remote",
IEnderecoQueryFacade.class);
}
private static Object getProxiedFacade(
String jndiContext,String jndiName,
String typeAccess,Class interfaceClass) throws Exception{
try{
Context ct = new InitialContext();
Object ref =
ct.lookup(jndiContext+"/"+jndiName+"/"+typeAccess);
return Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{interfaceClass},
new ProxiedInvocationHandler(interfaceClass.cast(ref),
interfaceClass));
}catch (Exception e){
throw e;
}
}
}
Esta classe deverá estar disponível para o cliente, pois será através dela
que o cliente irá ter acesso aos componentes EnderecoFacade (para açöes de
persistência) e EnderecoQueryFacade (para acöes de consulta).
Alterar a classe
APTClient.java:
....
public IEnderecoQueryFacade getEnderecoQueryFacade(){
try{
r
eturn EnderecoFacadeFactory.getEnderecoQueryFacade();}catch(Exception e){
e.printStackTrace();
}
return null;
}
public IEnderecoFacade getEnderecoFacade(){
try{
return EnderecoFacadeFactory.getEnderecoFacade();
}catch(Exception e){
e.printStackTrace();
}
return null;
}
...
Agora é só fazer o build-deploy do componente endereço e rodar o buildclient
para que o client.jar sejá gerado novamente, rode a target runclient
para executar:
Buildfile:
/home/rdias/projects/TigerSistafe/Artigos/build-client.xmlrun-client
:[
java] Executando o metodo proxiado:findLogradouroByEnderecoId com 1parametro(s).
[
java] RuaBUILD SUCCESSFUL
Total time: 1 second
Pronto!
Agora o acesso ao tipo logradouro foi feito via nosso proxy. Caso näo
deseje que o proxy gerado seja usado, simplesmente retire o annotation
@ProxiedReturn e faça redeploy da aplicaçäo, e geraçäo do client.jar. No
momento que o cliente for executado o erro original irá voltar a acontecer,
pelos mesmo motivos já explicado no inicio do artigo.
Conclusäo
Lazy Loading é um recurso poderoso na obtenção de relacionamentos entre
entidades, pois nos dá a oportunidade de trabalharmos com entidades leves
tornando as consultas mais rapidas e enxutas. Essas caracteristicas
justificam sua adoção como padrão para os fetches no JPA, porém o fato de só
termos esse recurso enquanto tivermos no container, deixa uma limitação quanto
ao seu uso. Neste artigo resolvemos parte desse problema com o uso do APT e
dos annotations.