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.