1/14
Hibernate e AspectJ
Gerenciando transações de forma 100% transparente
Aprenda como criar um módulo de controle transacional robusto, reutilizável e
completamente transparente à aplicação
PAULO CÉSAR COUTINHO
De que se trata o artigo:
Gerenciamento de transações no Hibernate utilizando AspectJ. Apresentamos como criar aspectos
capazes de tratarem tudo o que diz respeito ao gerenciamento de transações sem a necessidade de
alteração no código responsável pelas regras de negócio.
Para que serve:
Prover uma forma 100% transparente de gerenciar as transações em sistemas que utilizam o
Hibernate como framework de persistência. Para isso, criamos um conjunto de classes de controle
transacional reutilizáveis que podem ser facilmente adicionadas a qualquer sistema Java.
Em que situação o tema é útil:
As transações então presentes em grande parte dos sistemas, portanto, dispor de um componente
para gerenciamento de transações que seja facilmente reutilizável agiliza bastante o
desenvolvimento do software.
Hibernate e AspectJ – Resumo DevMan:
No desenvolvimento de sistemas, é bastante comum a necessidade de implementar algum tipo de
gerenciamento de transações. Esse gerenciamento pode ser feito de diversas formas, porém, grande
parte delas deixa o código que o utiliza acoplado, de alguma maneira, a esse serviço. Utilizando a
programação orientada a aspectos (AOP), por meio da linguagem AspectJ, isso pode ser feito de
maneira transparente. Com o uso de aspectos, todo o código relativo ao controle transacional fica
isolado. Só é preciso, então, informar que partes do código principal (o qual contém as regras de
negócios) devem ser interceptadas pelo código dos aspectos. Isso tudo é feito sem a necessidade de
alterar uma linha do código principal. Utilizando esse recurso juntamente com a API de anotações
de Java, é possível criar um componente reutilizável capaz de gerenciar transações em um sistema
qualquer.
2/14
Gerenciar transações de forma eficiente e não intrusiva é uma tarefa que ainda causa muita dor de
cabeça para o desenvolvedor. Neste artigo veremos como esse problema pode ser resolvido de
forma simples quando utilizamos os recursos providos pela programação orientada a aspectos.
É muito comum, no desenvolvimento de sistemas, nos depararmos com requisitos que exigem uma
seqüência de operações a serem realizadas de forma atômica, ou seja, onde apenas o resultado do
todo faz sentido. Nesse tipo de cenário, quando qualquer operação do conjunto falha, o conjunto
inteiro deve falhar. Um exemplo clássico é o da transferência entre contas de um banco, onde as
operações de débito na conta origem e crédito na conta destino devem ter sucesso por completo ou
falhar por completo.
Um dos benefícios1 do conceito de transação em banco de dados é exatamente o de prover essa
capacidade de restaurar o estado anterior dos dados, caso ocorra alguma falha dentro do contexto
transacional. Na realidade, isso é possível porque os dados não são persistidos, de fato, até que a
transação seja confirmada através do comando commit, ou seja, enquanto a transação não for
commitada esses dados ficam numa área de armazenamento temporária – o “log de transações” –
que é visível apenas à sessão corrente, mas não aos demais usuários do BD.
Para gerenciar transações no Hibernate, nos é fornecida a interface org.hibernate.Transaction com
métodos como beginTransaction(), commit() e rollback(). Com a ajuda desses métodos e do
mecanismo de tratamento de exceções de Java é possível demarcar o escopo transacional
facilmente. Vejamos na Listagem 1 como ficaria um pseudo-código onde vários métodos são
chamados num contexto transacional.
Listagem 1. Pseudo-código: métodos sendo chamados num contexto transacional
try {
transaction.beginTransaction();
metodo1();
metodo2();
...
metodoN();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
}
Contudo, veremos que implementar um controle transacional robusto e que não prejudique a
coesão dos módulos da aplicação nem sempre é uma tarefa fácil. Neste artigo aprenderemos a
implementar o gerenciamento de transações de uma forma simples, robusta, transparente e
reutilizável. Para isso utilizaremos recursos da programação orientada a aspectos, através da
linguagem AspectJ.
Aplicação Exemplo
Para que possamos praticar e validar nossa solução à medida que formos discutindo, este artigo
será baseado numa aplicação exemplo que tratará do caso clássico do sistema bancário com suporte
a transferência entre contas, uma vez que este deixa bem clara a importância de um controle
transacional bem definido. No nosso sistema será possível efetuar operações de crédito, débito e
transferência em contas bancárias. Vamos dar uma olhada no que seriam os principais requisitos da
nossa aplicação:
1. O sistema deve possuir operações para crédito e débito em contas bancárias;
2. Para a operação de débito, o saldo da conta deve ser maior ou igual ao valor que se deseja
debitar. Caso essa condição não seja atendida, a operação deve ser abortada e um erro deve ser
gerado;
1
Esses benefícios são, na realidade, requisitos impostos pelo modelo de transações em banco de dados, e são
conhecidos como ACID. (A)tomicidade, (C)onsistência, (I)solamento e (D)urabilidade.
3/14
3. O sistema deve prover a funcionalidade de transferência entre contas e esta funcionalidade
deve simplesmente encapsular as operações de crédito e débito;
4. Uma transferência só pode ser efetivada caso as operações de crédito na conta destino e débito
na conta origem tenham sido realizadas com sucesso. Qualquer falha numa dessas operações
deve cancelar a transferência.
Como podemos perceber, os itens 3 e 4 deixam clara a necessidade de um controle transacional.
Mas até mesmo o item 2 exige transações. Se analisarmos a fundo seria possível, por exemplo, o
sistema verificar que a conta tem um saldo, aprovando então o débito, mas na fração de segundo
entre esta decisão e a efetivação do débito, outro thread ou processo poderia realizar um débito
concorrente.
Entendidos os requisitos, vamos então definir nossas classes. A Figura 1 mostra o modelo de
classes que utilizaremos. Como podemos observar nosso modelo será bem simples, composto por
apenas três classes: a classe persistente Conta, a classe GerenciadorConta que encapsulará as regras
do negócio e, por fim, a classe ContaDao, responsável por persistir os dados usando o Hibernate.
Note também que, para simplificar, só definimos as operações necessárias para nosso estudo de
caso; por exemplo, a classe ContaDao não possui métodos para inserir ou listar já que não
precisaremos de tais métodos.
Figura 1. Modelo de classes da aplicação exemplo
A Listagem 2 mostra a definição da classe Conta. Note que estamos utilizando as annotations
@Entity e @Id da JPA para fazer o mapeamento objeto-relacional.
Listagem 2. Classe persistente Conta
// Declaração de pacote e imports omitidos ...
@Entity
public class Conta implements Serializable {
@Id private Integer numero;
private double saldo;
// Getters e setters...
}
Precisaremos também criar nossa base de dados que consistirá em apenas uma tabela "conta", com
os campos numero do tipo numérico e saldo do tipo ponto flutuante, ou seja, um reflexo da nossa
4/14
classe Conta. Um detalhe importante aqui é que os nomes da tabela e dos campos devem coincidir
com os da classe Conta e dos seus atributos, já que essa é a forma padrão2 de mapeamento da JPA.
Vamos criar também a classe HibernateUtil, que já é uma velha conhecida de quem trabalha com
Hibernate. O código pode ser visto na Listagem 3. Note que estamos configurando os mapeamentos
e propriedades da conexão programaticamente, porém isso é completamente opcional e foi feito
desta forma apenas para simplificar. Outro ponto é em relação ao banco de dados utilizado. No
nosso exemplo utilizamos o PostgreSQL, que também pode ser substituído por qualquer banco da
preferência do leitor.
Listagem 3. Classe HibernateUtil usada para configurar o Hibernate e obter a Session
// Declaração de pacote e imports omitidos.
public class HibernateUtil {
private static SessionFactory sessionFactory = new AnnotationConfiguration()
.addAnnotatedClass(Conta.class)
.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect")
.setProperty("hibernate.connection.driver_class", "org.postgresql.Driver")
.setProperty("hibernate.connection.url", "jdbc:postgresql://localhost:5432/bd_banco")
.setProperty("hibernate.connection.username", "postgres")
.setProperty("hibernate.connection.password", "senha")
.buildSessionFactory();
public static Session getSession() {
return sessionFactory.openSession();
}
}
Com nossa infra-estrutura básica criada, podemos partir para a criação das classes
GerenciadorConta e ContaDao, que conterão a lógica, propriamente dita, da nossa aplicação.
Começaremos pela classe ContaDao que ficará responsável pela persistência. Vamos então criar
uma primeira versão dessa classe que pode ser vista na Listagem 4. Perceba que os métodos
atualiza() e recupera() já se encarregam de gerenciar sessão e transação. Essa abordagem aparenta
funcionar sem problemas e, sendo assim, vamos seguir para a classe GerenciadorConta.
Listagem 4. Primeira versão da classe ContaDao
// Declaração de pacote e imports omitidos.
public class ContaDao {
public void atualiza(Conta conta) {
Session session = HibernateUtil.getSession();
session.beginTransaction();
HibernateUtil.getSession().update(conta);
session.getTransaction().commit();
session.close();
}
public Conta recupera(Integer numero) {
Session session = HibernateUtil.getSession();
Conta conta = (Conta) session.load(Conta.class, numero);
session.close();
return conta;
}
}
A Listagem 5 mostra uma primeira versão da classe GerenciadorConta. Note que o método
transfere() foi omitido, pois será discutido mais adiante. Os métodos credita() e debita() executam as
regras de negócios e em seguida atualizam os dados da conta utilizando o método atualiza() do
DAO. O método recupera() simplesmente delega a tarefa para o DAO. Até agora não temos
2
O nome e atributos da classe não precisam, necessariamente, coincidir com o nome e campos da tabela no banco de
dados, desde que sejam corretamente mapeados com a anotação @Column. Por motivos de simplificação, estamos
utilizando o mapeamento padrão.
5/14
"nenhum" problema com nossa abordagem, então vamos codificar o método transfere() de acordo
com nossos requisitos.
Listagem 5. Primeira versão da classe GerenciadorConta
// Declaração de pacote e imports omitidos.
public class GerenciadorConta {
private ContaDao dao = new ContaDao();
public void credita(Conta conta, double valor) {
conta.setSaldo(conta.getSaldo() + valor);
dao.atualiza(conta);
}
public void debita(Conta conta, double valor) {
if (conta.getSaldo() >= valor) {
conta.setSaldo(conta.getSaldo() - valor);
dao.atualiza(conta);
} else {
throw new IllegalStateException("Saldo insuficiente");
}
}
public Conta recupera(Integer numero) {
return dao.recupera(numero);
}
// Método transfere() omitido.
}
Sabemos que o ideal é que o método transfere() simplesmente utilize os métodos credita() e debita()
(ver Listagem 6). Mas se prestarmos atenção, veremos que tanto o método credita() quanto o
debita() utilizam o método atualiza() do DAO para persistir os dados, e que o método atualiza() cria
uma nova sessão e uma nova transação sempre que é chamado. Note também que o método
atualiza() faz o commit para confirmar a transação. Dessa forma não conseguiremos atender ao
requisito de número 4 - "Uma transferência só pode ser efetivada caso as operações de crédito na
conta destino e débito na conta origem tenham sido realizadas com sucesso. Uma falha em qualquer
uma dessas operações deve cancelar a transferência" - porque se método debita() falhar por algum
motivo (saldo insuficiente, por exemplo), o commit já terá sido efetuado no método credita() e não
teremos como desfazer essa operação. Aqui começam a surgir os problemas relativos ao
gerenciamento de transações.
Listagem 6. Implementação ideal para o método transfere() reutilizando os métodos já existentes
// Declaração de pacote e imports omitidos.
public class GerenciadorConta {
// Restante dos métodos e atributos omitidos.
public void transfere(Conta origem, Conta destino, double valor) {
credita(destino, valor);
debita(origem, valor);
}
}
Temos algumas possíveis soluções para nosso problema, como, por exemplo, trazer o código de
persistência e controle transacional para dentro do método transfere(), como mostrado na Listagem
7. Porém, com essa abordagem, além de estarmos duplicando o código, estamos prejudicando a
coesão e a divisão de responsabilidades entre as camadas do sistema. Outra alternativa seria
controlar a transação no método transfere() e passar a sessão como parâmetro para os métodos
credita() e debita(). Estes, por sua vez, passariam essa mesma sessão para o método atualiza(), o qual
também precisaria ter sua implementação alterada, de modo que uma única sessão e transação
fossem utilizadas por todo o conjunto de operações. Ainda assim não teríamos uma solução
satisfatória, já que seríamos obrigados a mudar a interface dos métodos de negócio.
6/14
Com isso, percebemos que apesar da simplicidade da API para controle transacional existe uma
questão bem menos simples, que é: “Em que ponto(s) do fluxo de execução da aplicação distribuir
esse controle?”.
Listagem 7. Método transfere() alterado para gerenciar a transação
// Declaração de pacote e imports omitidos.
public class GerenciadorConta {
// Restante dos métodos e atributos omitidos.
public void transfere(Conta origem, Conta destino, double valor) {
Session session = HibernateUtil.getSession();
session.beginTransaction();
destino.setSaldo(destino.getSaldo() + valor);
session.update(destino);
if (origem.getSaldo() >= valor) {
origem.setSaldo(origem.getSaldo() - valor);
} else { throw new IllegalStateException("Saldo insuficiente"); }
session.update(origem);
session.getTransaction().commit();
session.close();
}
}
Commit in Top-level Operation
Vimos que criar um método transacional isolado, como o método atualiza() do nosso DAO, é bem
simples. Mas o que fazer quando um método transacional precisa chamar ou ser chamado por outro
método transacional, como o método transfere() do nosso exemplo que precisa chamar o método
atualiza() do DAO através dos métodos credita() e debita()? Qual método deve iniciar a transação?
Qual deve efetuar o commit() ou rollback()?
O esperado nesses casos é que apenas o método transacional mais externo fique responsável pelo
gerenciamento da transação. Em outras palavras, não queremos que os métodos mais internos
efetuem o commit uma vez que estes já fazem parte de um contexto transacional que foi iniciado por
um método mais externo. Método este que deverá se responsabilizar por iniciar a transação e
realizar o commit ou rollback. Dessa forma, apenas a operação mais externa – no Topo da pilha de
chamadas – deve ficar responsável pelo controle transacional.
A Figura 2 mostra o funcionamento esperado. Note que no primeiro cenário, como o método
atualiza() é o primeiro método transacional a ser chamado, ele fica responsável por gerenciar a
transação. Já no segundo cenário, o mesmo método atualiza() é chamado pelo método transfere(),
que também é um método transacional. Nesse caso, apenas o método transfere() realiza o controle
transacional e não mais o método atualiza().
7/14
Figura 2. Gerenciamento da transação sendo realizado apenas pelo método transacional do topo da pilha
de chamadas
Podemos perceber que implementar a lógica de controle transacional de forma eficiente, robusta e
coesa não é uma tarefa muito fácil, nem mesmo com todos os design patterns do paradigma OO.
Porém, existe uma maneira de adicionarmos essas características de uma forma completamente
transparente à aplicação, utilizando a programação orientada a aspectos (AOP).
Veremos agora como criar um tipo de "classe" (aspecto) capaz de centralizar todo controle
transacional sem causar nenhum impacto à aplicação. Para isso utilizaremos a linguagem AspectJ e
o plugin (para Eclipse) AJDT que já inclui, dentre outros, o compilador do AspectJ. Mas,
primeiramente vamos dar uma pequena olhada nos conceitos básicos da AOP.
AOP e AspectJ
A programação orientada a aspectos (AOP) tem se tornado cada vez mais popular e já está
presente em diversos frameworks atuais, como Spring, JBossAOP, etc. Esse tipo de abordagem
permite separar, de forma eficiente, características comuns da aplicação, da lógica do negócio.
8/14
Essas características são o que chamamos de aspectos. Podemos dizer então que os aspectos de uma
aplicação são as características comuns a todos os módulos funcionais. Exemplos comuns de
aspectos são "Logging", "Autenticação e Autorização" e "Persistência". Com o uso da AOP
podemos separar completamente essas características da lógica de negócio da aplicação.
AspectJ é uma extensão da linguagem Java criada para suportar orientação a aspectos. Para tal
suporte são fornecidos um compilador específico (AspectJ Compiler), os jars com as classes de
infra-estrutura e também o plugin AJDT para eclipse. Mais informações podem ser obtidas na
página oficial (http://www.eclipse.org/aspectj).
Vejamos algumas definições simplificadas dos principais elementos da programação orientada a
aspectos.
• Joinpoints: Definem pontos específicos da execução de um programa que podem ser
interceptados3 pelos aspectos. Exemplos de joinpoints são: construção de uma classe, chamada
a um método, leitura/escrita num atributo da classe, etc.;
• Pointcuts: São responsáveis por capturar os joinpoints de acordo com um conjunto de
critérios. Os pointcuts também são capazes de expor o contexto dos joinpoints que capturam.
Por exemplo, um pointcut que captura a execução de um método pode expor o objeto no qual o
método foi invocado, bem como o valor dos parâmetros utilizados na chamada;
• Advices: São os trechos de código a serem executados "antes", "depois", ou "no lugar" da
execução de um ou mais joinpoints. Advices que são executados "no lugar" de joinpoints são
capazes de substituir o código a ser executado.
Para entendermos melhor esses conceitos, nada como um exemplo prático. Na Listagem 8 temos o
código de um aspecto responsável por logar a entrada e saída dos métodos capturados pelo pointcut
metodosLogaveis(). Essa técnica é conhecida como "tracing". Além disso, o log é feito de maneira
indentada, facilitando o acompanhamento do fluxo de execução e, conseqüentemente, ajudando a
encontrar erros mais facilmente. O quadro "Configurando o AspectJ no Eclipse" mostra como
preparar o ambiente de desenvolvimento.
Listagem 8. Aspecto simples para tracing
1 public aspect TracerAspect {
2
private pointcut metodosLogaveis() : call (* *.*(..)) && !within(TracerAspect+);
3
private int indentacao;
4
5
before() : metodosLogaveis() {
6
imprimeIndentado(indentacao++, "--> " + thisJoinPointStaticPart.getSignature());
7
}
8
9
after() : metodosLogaveis() {
10
imprimeIndentado(--indentacao, "<-- " + thisJoinPointStaticPart.getSignature());
11
}
12
13
private void imprimeIndentado(int nivel, String message) {
14
for (int i = 0; i < nivel; i++) System.out.print(" ");
15
System.out.println(message);
16
}
17 }
Vamos a uma breve explicação do código da Listagem 8.
• Na linha 1 definimos o aspecto da mesma maneira como definiríamos uma classe Java.
Aspectos possuem as mesmas opções de visibilidade que classes, podem ser declarados como
abstratos, podem estender de outro aspecto (não concreto) ou classe e implementar interfaces,
podem ser embutidos em classes assim como as Inner Classes, etc.;
3
Na realidade o que ocorre não é bem uma interceptação em tempo de execução, como no design pattern Intercept
Filter, como estamos acostumados. Com o AspectJ isso é feito no momento da "compilação". O compilador do AspectJ
combina o código dos aspectos com o das classes "interceptadas" através de um processo chamado weaving.
9/14
•
•
•
Na linha 2 temos a definição de um pointcut responsável por detectar os métodos da aplicação
que devem ser "logados". A instrução call (* *.*(..)) diz ao AspectJ que a chamada (call) a
qualquer método, de qualquer classe, com qualquer tipo de retorno e com quaisquer tipos de
argumentos; deve ser capturada por esse pointcut. A sintaxe utilizada nesse caso é call (<tipo de
retorno> <classe>.<metodo>(<argumentos>)). O trecho && !within(LoggerAspect+) serve para
excluir os métodos ou advices do próprio aspecto, caso contrário teríamos um loop infinito,
uma vez que o aspecto consegue capturar sua própria execução;
As linhas 5 e 9 definem os advices que serão executados antes (before) e depois (after) dos
joinpoints capturados pelo pointcut metodosLogaveis(). No nosso exemplo simplesmente
imprimimos a assinatura do método que está sendo interceptado;
Por último, na linha 13, temos apenas um método utilitário usado para ajudar na indentação
dos logs.
Com nosso primeiro aspecto criado, vamos criar uma classe de teste com um método main(), como
pode ser visto na Listagem 9. A Figura 3 mostra a saída gerada pelo programa. Perceba que a
lógica de tracing fica centralizada apenas no aspecto e não tem acoplamento algum com a classe de
negócio. Agora é só compilar e executar para ver um pouco do poder da AOP.
Listagem 9. Classe para testar o aspecto de tracing
public class TesteAspecto {
private static void imprime(String text) {
System.out.println(text);
}
public static void main(String[] args) {
imprime("Testando...");
}
}
Figura 3. Saída gerada pela classe TesteAspecto
Criando um aspecto reutilizável para gerenciar transações no Hibernate
Já vimos os conceitos básicos da AOP e o funcionamento básico de um aspecto. Agora vamos
criar uma solução baseada em AOP para o nosso problema de gerenciamento de transações na nossa
aplicação exemplo. Nosso aspecto terá a responsabilidade de capturar os métodos transacionais da
aplicação e fazer o gerenciamento da transação no método mais externo (como visto na seção
"Commit in Top-level Operation"). Para isso iremos criar, basicamente, um pointcut para capturar
os métodos que necessitarão do controle transacional (métodos transacionais) e um advice que será
responsável por chamar os métodos de gerenciamento de transação do Hibernate
(beginTransaction(), commit() e rollback()). O código do nosso aspecto pode ser visto na Listagem
10.
Listagem 10. Aspecto abstrato para gerenciamento de transações com Hibernate
// Declaração de pacote e imports omitidos ...
public abstract aspect AspectoAbstratoGerenciadorTransacoes {
public abstract static class ContextoTransacaoHibernate {
private Session sessao;
private Object valorRetorno;
public abstract void run();
// Getters e Setters omitidos...
}
protected abstract pointcut metodosTransacionais();
10/14
protected pointcut recuperaSessaoHibernate() :
call(org.hibernate.Session+ SessionFactory.*Session(..));
private pointcut dentroMetodoTransacional(ContextoTransacaoHibernate contexto) :
cflow(execution(* ContextoTransacaoHibernate.run()) && this(contexto));
Object around() : metodosTransacionais() &&
!dentroMetodoTransacional(ContextoTransacaoHibernate) {
ContextoTransacaoHibernate contexto = new ContextoTransacaoHibernate() {
public void run() {
try {
setValorRetorno(proceed());
if (getSessao() != null && getSessao().isOpen()) {
getSessao().getTransaction().commit();
}
} catch (Exception e) {
if (getSessao() != null && getSessao().isOpen()) {
getSessao().getTransaction().rollback();
}
throw new RuntimeException(e);
} finally {
if (getSessao() != null) {
getSessao().close();
}
}
}
};
contexto.run();
return contexto.getValorRetorno();
}
Object around(final ContextoTransacaoHibernate contexto) :
recuperaSessaoHibernate() && dentroMetodoTransacional(contexto) {
if (contexto.getSessao() == null || !contexto.getSessao().isOpen()) {
contexto.setSessao((Session) proceed(contexto));
contexto.getSessao().beginTransaction();
}
return contexto.getSessao();
}
protected pointcut gerenciamentoIlegalTransacao() : (call(* Session.*Transaction(..)) ||
call(* Transaction.*(..))) && !within(exemplo.aspecto.*+);
declare warning : gerenciamentoIlegalTransacao() :
"Não é permitido usar métodos de gerenciamento de transação diretamente.";
}
Agora vamos analisar o código do nosso aspecto:
• Primeiramente vejamos a classe interna que estamos utilizando no aspecto:
o ContextoTransacaoHibernate - Utilizamos essa classe para armazenar as informações que
precisamos compartilhar no contexto de uma transação. Além disso, também definimos um
método abstrato run(), o qual será responsável por envolver o conteúdo original do método
transacional com o código de gerenciamento da transação.
• Definimos então quatro pointcuts que serão utilizados no nosso aspecto:
o O primeiro pointcut, metodosTransacionais(), será o responsável por capturar os métodos da
aplicação que estarão sob o controle transacional. Como podemos perceber esse pointcut é
abstrato e, portanto, não tem uma implementação. Isso porque os métodos transacionais são
métodos específicos de uma determinada aplicação. Sendo assim, esse pointcut precisará de
uma implementação específica para cada aplicação. Note que da mesma forma que apenas
classes abstratas podem ter métodos abstratos, nosso aspecto tem que ser abstrato, uma vez
que possui um pointcut abstrato;
o O segundo pointcut, recuperaSessaoHibernate(), captura os métodos que são usados para a
obtenção da Sessão do Hibernate. Nossa implementação está detectando as chamadas a
qualquer método da classe org.hibernate.SessionFactory cujo nome acabe com "Session"
11/14
(Ex.: openSession(), getCurrentSession(), etc.) e possua um retorno do tipo
org.hibernate.Session;
o O terceiro pointcut, dentroMetodoTransacional(), é utilizado para implementar o "Commit in
Top-level operation". Ele simplesmente detecta a execução de qualquer método dentro do
fluxo de execução de um método transacional. Entenderemos melhor quando virmos sua
utilização no aspecto. Esse pointcut também expõe o Contexto Transacional para que esse
possa ser usado nos advices;
o O quarto e último pointcut, gerenciamentoIlegalTransacao(), captura chamadas ilegais4 aos
métodos de gerenciamento de transação feitas pela aplicação. Nossa implementação detecta
chamadas a métodos da interface org.hibernate.Transaction, bem como métodos para
obtenção/criação de transações a partir da interface org.hibernate.Session.
• Em seguida temos dois advices que serão executados "em torno" (around) dos joinpoints
capturados:
o O primeiro advice é responsável por realizar as chamadas aos métodos de controle de
transação do Hibernate nos momentos apropriados. A instrução Object around() :
metodosTransacionais() && !dentroMetodoTransacional(ContextoTransacaoHibernate) indica
que o advice deve ser executado para métodos transacionais que não estejam dentro do fluxo
de outro método transacional. Dessa forma apenas o método transacional mais externo será
substituído por esse advice. A chamada a proceed() invoca o conteúdo original do método
que está sendo substituído pelo advice e salva seu retorno no atributo valorRetorno do
contexto transacional. Logo em seguida verificamos se a sessão e transação estão válidas
para realizar o commit(). Note que se ao invocar o conteúdo original do método, através de
uma chamada a proceed() uma exceção for gerada, esta será capturada pelo bloco catch e
então a transação será cancelada através da chamada ao método rollback(); em seguida uma
RuntimeException5, encapsulando a exceção original, será lançada para que o código que
invocou o método transacional seja capaz de saber que algo deu errado na execução desse
método. Por fim, retornamos o valor de retorno armazenado no contexto transacional;
o Nosso segundo advice visa garantir que a mesma sessão será utilizada durante todo o
contexto transacional. Para isso substituímos o conteúdo dos métodos capturados pelo
pointcut recuperaSessaoHibernate() e que foram chamados dentro do fluxo de execução do
contexto transacional. Neste advice simplesmente verificamos se já existe uma sessão aberta
associada ao contexto transacional corrente, se houver ignoramos a execução do método
original e utilizamos esta sessão. Caso não exista, invocamos o conteúdo original do método
através de uma chamada a proceed() e guardamos a sessão retornada no contexto para que
possa ser utilizada em futuras chamadas;
• Por último temos então uma declaração declare warning para os joinpoints capturados pelo
pointcut gerenciamentoIlegalTransacao(). Isso irá gerar um warning de compilação para cada
uso ilegal de métodos de gerenciamento de transação. Também poderíamos gerar erros de
compilação usando declare error. Essa técnica é conhecida como Policy Enforcement e serve
para garantir que as políticas definidas na arquitetura serão seguidas pelos desenvolvedores.
4
Ilegais no sentido de que devemos centralizar o controle transacional no aspecto. Então qualquer chamada aos
métodos responsáveis por tal controle feita diretamente pela aplicação pode ser considerada ilegal.
5
Estamos utilizando RuntimeException aqui para simplificar, mas qualquer exceção customizada que estenda de
RuntimeException poderia ser utilizada, inclusive a própria exceção capturada no catch. A necessidade de ser subclasse
de RuntimeException se dá pelo fato que um advice around, o qual executa no lugar do método original não poder
lançar uma exceção checada que não esteja declarada nesse método. Como estamos capturando quaisquer métodos e
não sabemos que tipos de exceções os mesmos lançam, usamos então exceções não checadas.
12/14
Utilizando o aspecto na aplicação exemplo
Como vimos anteriormente, será necessário prover uma implementação para o pointcut abstrato
metodosTransacionais(). Para isso vamos criar um aspecto concreto que estenda nosso aspecto
genérico e forneça uma implementação para esse pointcut. Essa implementação deverá informar os
métodos específicos da nossa aplicação, que no nosso exemplo são: atualiza() e transfere(). A
Listagem 11 mostra o código do nosso aspecto concreto.
Listagem 11. Aspecto concreto fornecendo uma implementação para o pointcut metodosTransacionais()
// Declaração de pacote e imports omitidos...
public aspect AspectoConcretoGerenciadorTransacoes extends
AspectoAbstratoGerenciadorTransacoes {
protected pointcut metodosTransacionais() :
execution(void ContaDao.atualiza(Conta)) ||
execution(void GerenciadorConta.transfere(Conta, Conta, double));
}
Usando annotations para simplificar a identificação de métodos transacionais
Uma forma fácil de capturar os métodos transacionais da aplicação é decorá-los com uma
annotation que indique que o método deve possuir tal característica. Dessa forma, sempre que um
novo método transacional aparecer na aplicação, a única coisa que precisará ser feita é decorá-lo
com essa annotation. Com isso eliminamos a necessidade de alterar nossa implementação concreta
do aspecto sempre que um novo método transacional surgir. Veja o código da annotation:
@Target(ElementType.METHOD)
public @interface MetodoTransacional {}
Pode ser ainda que tenhamos classes nas quais a maioria dos métodos, talvez todos, sejam
transacionais. Um exemplo comum são os DAOs, onde operações como insert(), delete() e update()
precisam ser "commitadas" para que tenham efeito. Podemos considerar essas classes como "classes
transacionais" e para esse caso podemos criar uma annotation de classe que identifique uma "classe
transacional", ou seja, uma classe na qual todos os métodos são métodos transacionais. Veja o
código:
@Target(ElementType.TYPE)
public @interface ClasseTransacional {}
Contudo, outras classes podem conter alguns poucos métodos não transacionais, nos quais o
controle de transação seria apenas um overhead. A solução mais prática para esses casos é a criação
de uma terceira annotation para indicar que o método não é transacional, mesmo fazendo parte de
uma classe transacional. Veja o código:
@Target(ElementType.METHOD)
public @interface MetodoNaoTransacional {}
Tendo criado essas anotações, podemos então escrever nosso pointcut que captura os métodos
transacionais baseado nas mesmas. Veja o código:
protected pointcut metodosTransacionais() :
execution(@MetodoTransacional !@MetodoNaoTransacional * *.*(..)) ||
execution(!@MetodoNaoTransacional * (@ClasseTransacional *+).*(..));
Utilizando essa abordagem, podemos utilizar essa implementação diretamente no nosso aspecto
genérico, o qual não precisará mais ser abstrato e nem precisará de uma implementação para cada
aplicação. Dessa forma a única coisa que uma aplicação específica precisará fazer para utilizar o
13/14
aspecto será incluí-lo no seu classpath e anotar os métodos que estarão sob controle transacional
com as annotations que criamos.
As Listagens 12 e 13 mostram como ficam a classe ContaDAO e o método transfere() da classe
GenrenciadorConta utilizando as annotations. Note que todo o código de gerenciamento de
transação foi removido dos métodos. Dessa forma, conseguimos isolar completamente o controle
transacional dos módulos funcionais do nosso sistema, mantendo a coesão e divisão de
responsabilidades entre as camadas.
Listagem 12. Classe ContaDao utilizando a annotation @ClasseTransacional
// Declaração de pacote e imports omitidos.
@ClasseTransacional
public class ContaDao {
public void atualiza(Conta conta) {
HibernateUtil.getSession().update(conta);
}
public Conta recupera(Integer numero) {
return (Conta) HibernateUtil.getSession().load(Conta.class, numero);
}
}
Listagem 13. Implementação do método transfere() com a annotation @MetodoTransacional
// Declaração de pacote e imports omitidos...
public class GerenciadorConta {
// Restante dos métodos e atributos omitidos.
@MetodoTransacional
public void transfere(Conta origem, Conta destino, double valor) {
credita(destino, valor);
debita(origem, valor);
}
}
Open Session in View
Quem nunca recebeu uma LazyInitializationException usando Hibernate, principalmente numa
aplicação web? Isso acontece quando utilizamos os mecanismos de proxy e lazy loading do
Hibernate, que de fato devem ser utilizados por questões de desempenho. Nesses casos, quando
carregamos os dados da base de dados em nossos objetos (que na realidade são proxies criados
dinamicamente pelo Hibernate) e deixamos para ler o conteúdo desses objetos na camada de View,
onde geralmente a sessão do Hibernate já foi fechada, receberemos um erro, pois uma vez fechada a
sessão, não podemos mais carregar os dados via proxy.
Uma das formas mais conhecidas de contornar esse problema é o padrão "Open session in view",
que consiste em criar um interceptador (geralmente um filtro no caso de aplicações web) que
intercepta as requisições do usuário, abrindo a sessão do Hibernate antes de prosseguir com o
processamento feito pela aplicação (chamada ao método doFilter() do FilterChain) e fechando-a após
esse processamento. Mais detalhes sobre esse padrão podem ser encontrados no site do próprio
Hibernate (http://www.hibernate.org/43.html).
Pois bem, nosso aspecto pode ser facilmente aplicado em conjunto com esse padrão. Para isso,
basta criar um filtro e anotar o método doFilter() com nossa annotation para métodos transacionais.
Não precisamos nos preocupar com abrir ou fechar a sessão do Hibernate, fazer commit ou
rollback, porque o aspecto fará tudo para nós. Veja como fica o código do filtro na Listagem 14.
Com isso solucionamos facilmente o problema da Lazy Initialization em aplicações web.
Listagem 14. Filtro para implementação do Open Session in View
// Declaração de pacote e imports omitidos ...
public class FiltroGerenciadorTransacoes implements Filter {
@MetodoTransacional
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
14/14
chain.doFilter(request, response);
}
// Métodos init() e destroy() omitidos ...
}
Conclusões
Neste artigo tivemos uma breve introdução sobre gerenciamento de transações usando o Hibernate
e sobre a programação orientada a aspectos (AOP) usando o AspectJ. Vimos como construir um
aspecto simples para gerenciar transações de forma transparente à aplicação baseado em
annotations, que pode ser utilizado em vários sistemas sem precisar de nenhuma alteração em seu
código. Esse foi apenas um pequeno exemplo do que podemos fazer utilizando a AOP.
Configurando o AspectJ no Eclipse
Para utilizar o AspectJ no Eclipse baixe e instale o plugin AJDT a partir do site
http://www.eclipse.org/ajdt/downloads. Isso pode ser feito baixando o arquivo .zip e
descompactando-o na pasta do Eclipse ou através do Eclipse Update Manager
(Help|Software Updates > Find And Install...). É importante verificar qual versão do AJDT
é compatível com sua versão do Eclipse para que o plugin possa funcionar corretamente.
Uma vez instalado o plugin, já podemos criar projetos AspectJ e/ou converter projetos
existentes em projetos AspectJ.
• Para criar um Projeto AspectJ basta ir em File>New>Project... e escolher a opção
AspectJ Project.
• Para converter um projeto existente, clique com o botão direito do mouse sobre o
projeto e escolha a opção AspectJ Tools>Convert to AspectJ Project.
• Para criar um aspecto basta ir em File>New>Other... e escolher o item Aspect dentro
da pasta AspectJ.
Os projetos do tipo AspectJ Project já são compilados com o AspectJ Compiler
automaticamente. Com isso, seu ambiente está pronto para o desenvolvimento com
AspectJ.
Links
http://www.hibernate.org
Site oficial do Hibernate
http://eclipse.org/aspectj
Site oficial do AspectJ
http://en.wikipedia.org/wiki/Database_transaction
Mostra os conceitos básicos de transação em banco de dados.
Livros
AspectJ in Action: Practical Aspect-Oriented Programming, LADDAD, R., Manning Publications, 2003
Livro muito bom sobre AOP e AspectJ, possui vários exemplos de aplicações reais que facilitam o entendimento.
Paulo César M. N. A. Coutinho ([email protected]) é graduado em Tecnologia em Sistemas de Informação pelo Centro Federal
de Educação Tecnológica de Pernambuco (CEFETPE). Trabalha como Engenheiro de Sistemas no Centro de Estudos e Sistemas
Avançados do Recife (C.E.S.A.R) com desenvolvimento móvel em C/C++ e Java. Também vem trabalhando com Java para web em
projetos pessoais. Possui as certificações SCJP 5 e SCWCD 1.4.
Download

Paulo Cesar Coutinho - Hibernate e AspectJ