a r t i g o Em ambientes corporativos, a auditoria sobre as operações de banco de dados é importantíssima, quando não, indispensável. Essa importância surge a partir de um conjunto de necessidades, como: correção de inconsistências de dados, busca de responsáveis por consultas/alterações de informações indevidas, correção de informações geradas por código mal implementado, detecção de incidentes de segurança, dentre outras. Este artigo visa apresentar o caminho das pedras para implementar auditoria em persistência objeto relacional com JPA. José Yoshiriro Ajisaka Ramos (jyoshiriro@gmail.com): Bacharel em Sistema de Informação (IESAM). Mestrando em Ciência da Computação (UFPA). Instrutor na Equilibrium e na UAB. Engenheiro de software do Tribunal de Justiça do Pará. Possui as certificações SCJP, SCWCD e SCBCD. Trabalha com Java há 6 anos. Auditando persistência com JPA Aprenda como funcionam os mecanismos de escuta de eventos que permitem a implementação de auditoria de persistência no framework de mapeamento objeto relacional padrão do Java O termo auditoria é muito usado na contabilidade, em que auditar é como passar um pente fino nas contas de uma empresa na busca por operações incorretas ou indevidas. Nessas auditorias, todas as entradas e saídas de valores financeiros dentro do período a auditar são analisadas detalhadamente. Ao final, tem-se um relatório que pode fornecer conclusões, como: que as contas estão todas perfeitas; que houve desvio de dinheiro; que houve caixa 2 em certas operações; que é preciso gastar menos com material de escritório etc. Assim, será mais fácil tomar decisões, como: manter a disciplina do diaa-dia da empresa; tomar as medidas necessárias contra os responsáveis pelos desvios; normalizar as situações do caixa 2 ; equilibrar os gastos diários etc. Auditorias constantes ajudam fortemente na manutenção e até melhora da saúde financeira de uma empresa. Analogamente, auditoria em persistência visa analisar operações que envolvem o acesso a bancos de dados, visando à manutenção e melhora da saúde dos dados. Uma boa auditoria de persistência deve fornecer informações, como: da base de dados; Um fato importante é que o nível de detalhe das informações geradas pela auditoria deve ser suficiente para que seja possível desfazer operações quando necessário. 48 www.mundoj.com.br
É possível auditar as operações de acesso a dados com recursos próprios dos sistemas de gerenciamento de bancos e dados (SGBDs), como triggers e stored procedures. Possivelmente esse é o mecanismo mais usado até hoje, visto que recursos de auditoria em nível de aplicação, como os que serão explorados neste artigo, são relativamente novos. Este artigo não pretende difundir a ideia de que auditoria com recursos nativos de SGBDs deve ser totalmente descartada ou que é uma prática que faz parte do passado. Pretende-se mostrar uma alternativa que provoca menos processamento nos SGBDs, lançando mão das facilidades do mapeamento objeto relacional e da orientação a objetos tornando a auditoria portável entre diferentes bancos de dados. Auditoria com JPA A JPA (Java Persistence API) possui dois mecanismos nativos que permitem a introdução de auditoria que são os callbacks de ciclo de vida de classes de entidades e os Listeners. Estes mecanismos são portáveis entre os diferentes provedores de persistência (Toplink, Hibernate etc.) e podem ser feitos via anotações ou mapeamentos XML. Não é objetivo deste artigo detalhar o funcionamento da JPA, porém, será apresentado um breve resumo de como funcionam os mapeamentos. Mapeamentos com JPA Os mapeamentos objeto relacionais na JPA podem ser feitos com o uso de anotações e/ou arquivos XML. Não foram encontrados dados estatísticos sobre a proporção de uso de configurações JPA por anotações e por XML pelos desenvolvedores Java, porém é muito mais fácil encontrar em livros, fóruns, e-books e tutoriais, exemplos apenas com primeiro método. Todavia, o leitor deve atentar aos fatos que pode ter que dar manutenção em projetos nos quais foram adotados XML e que na prova para a certificação Sun Certified Business Component Developer for the Java Platform, Enterprise Edition 5 podem aparecer questões sobre JPA usando esse tipo de configuração. A organização dos arquivos XML na JPA é muito parecida com a do Hibernate Core. Se no Hibernate havia o hibernate.cfg.xml que poderia conter todos os mapeamentos ou possuir referências a vários outros XMLs menores, na JPA existe o orm.xml que segue praticamente a mesma ideia. A localização padrão do orm.xml é a pasta META-INF do projeto, a mesma onde fica o persistence.xml. Configurações em XML são predominantes sobre as feitas com anotações. A existência do orm.xml (que é opcional) não obriga que todas as entidades sejam mapeadas via XML. Tudo o que estiver no orm.xml ou em algum outro arquivo XML de mapeamento anexado a ele é considerado padrão ou mandatário no caso de conflitar com configurações realizadas com anotações. Por exemplo, se houver mapeamento XML para a classe Jogador (campos, chaves, Listeners etc.), as anotações de mapeamento dessa classe serão ignoradas. É possível que anotações existam em harmonia com XMLs. Se forem feitos mapeamentos XML, apenas para definir padrões como algum Listener padrão para todo o projeto, por exemplo, anotações específicas de entidades (campos, chave etc.) serão lidas e usadas pela JPA. Normalmente esses padrões são definidos apenas no arquivo orm.xml. Callbacks de ciclo de vida de classes de entidades Os callbacks são como gatilhos que informam quando operações CRUD são realizadas sobre as entidades mapeadas. Para que seja possível lançar mão deles, é preciso configurar escutas, o que pode ser feito em métodos das próprias classes das entidades ou em classes específicas da escuta, chamadas Listeners. As formas de configurar as escutas aos callbacks serão vistas nos próximos tópicos. Os callbacks da JPA estão listados na Tabela 1. pre-persist pre-update pre-remove post-persist post-update post-remove post-load quando Antes de uma persistida (inserida no banco de dados) Antes de uma atualizada Antes de uma excluída Após uma entidade ser persistida (inserida no banco de dados) Após uma atualizada Após uma excluída Após uma recuperada do banco de dados persistence. nager que o aciona * persist() merge() remove() persist() merge() remove() find() (todas do pacote javax.persistence) @PrePersist @PreUpdate @PreRemove @PostLoad Tabela 1. Callbacks de ciclo de vida de classes de entidades da JPA. Os callbacks pre são muito mais usados para criar regras de segurança do que para auditoria. Neles, o lançamento de uma exceção impede a continuidade da operação em vista. Fica mais fácil entender o funcionamento dos callbacks nos códigos onde estão sendo usados. Portanto, uma explicação gradual sobre eles será desenrolada nos tópicos a seguir. - não notificam os callbacks JPA. ** - A coluna Anotação informa qual anotação JPA deve ser usada para configurar um método para escutar um callback. Exemplos de uso dessas anotações estão nas Listagens 1 e 4. 49
Configurando callbacks nas próprias classes de entidades É possível configurar métodos de escuta dos callbacks nas próprias classes de entidade. Na Listagem 1 há um exemplo de como isso é feito via anotações e na Listagem 2 via XML. Listagem 1. Configurando callbacks em entidades via anotação. import javax.persistence.postpersist; import javax.persistence.postremove; import javax.persistence.table; import javax.persistence.entity; @Table(name = jogador ) // campos, construtores, getters e setters public void aposinserir() { // o que fazer após salvar um novo jogador no banco de dados? public void aposexcluir() { // o que fazer após excluir um jogador do banco de dados? // quantos outros métodos de escuta de callback achar necessário Listagem 2. Configurando callbacks em entidades via XML. //Classe de entidade: Jogador.java import javax.persistence.table; import javax.persistence.entity; @Table(name = jogador ) // campos, construtores, getters e setters public void aposinserir() { // o que fazer após salvar um novo jogador no banco de dados? public void aposexcluir() { // o que fazer após excluir um jogador do banco de dados? // quantos outros métodos de escuta de callback achar necessário //XML de mapeamento: orm.xml (ou um XML anexado a ele) <entity class= Jogador > <post-persist method-name= aposinserir /> <post-remove method-name= aposexcluir /> <!-- quantos outros métodos de escuta de callback achar necessário --> </entity> Em ambos os casos, os nomes dos métodos de escuta são de livre escolha do desenvolvedor. O que define que um método vai escutar um callback é uma anotação ou configuração XML como visto nas Listagens 1 e 2. É indispensável que métodos de escuta de callbacks configurados dentro das entidades não recebam nenhum parâmetro, senão ocorrerá uma exceção assim que algum recurso JPA for exigido na aplicação. Esta técnica lembra muito os triggers dos SGBDs e só deve ser usada quando se deseja comportamentos específicos para uma determinada entidade, para evitar replicação de código. Quando configurados nas próprias entidades, métodos de escuta de callback sempre devem possuir a seguinte assinatura: <qualquer nível de acesso> void <nome do método> () ; Listeners Como a tradução do nome sugere, Listeners funcionam como ouvintes das operações de acesso a banco de dados feita via JPA. Listeners podem ser configurados por entidade ou por projeto. Listeners por entidade Configurar Listeners por entidade acaba tendo o mesmo efeito do uso de callbacks em entidade. As únicas diferenças são que os métodos de escuta dos callbacks ficam localizados em outra classe (não dentro da entidade) e podem ser configurados mais de um Listener por entidade. A Listagem 3 contém uma entidade configurada via anotação para um Listener cujo código está na Listagem 4. É indispensável que métodos de escuta de callbacks em Listeners recebam um parâmetro do tipo da classe ou de uma superclasse da entidade da escuta, senão ocorrerá uma exceção assim que algum recurso JPA for exigido na aplicação. A anotação Listeners pode conter uma ou várias classes. Para configurar vários Listeners com ela, bastaria fazer como na Listagem 5. Listagem 3. Entidade configurada para usar um Listener. import javax.persistence.entity; import javax.persistence.entitylisteners; import javax.persistence.table; import mj.auditoria.listener.auditoriajogadorlistener; @Table(name = jogador ) Listeners(AuditoriaJogadorListener.class) /// campos, construtores, getters e setters Listagem 4. Listener usado pela entidade da Listagem 3. import javax.persistence.postpersist; import javax.persistence.postremove; public class AuditoriaJogadorListener { /// campos, construtores, getters e setters public void aposinserir(jogador jogador) { // o que fazer após salvar um novo jogador no banco de dados? public void aposexcluir(jogador jogador) { // o que fazer após excluir um jogador do banco de dados? 50 www.mundoj.com.br
Quando configurados em Listeners, métodos de escuta de callback sempre devem possuir a seguinte assinatura: <qualquer nível de acesso> void <nome do método> (<classe ou superclasse da entidade da escuta >) ; Listagem 5. Vários Listeners configurados para uma entidade. Listeners({AuditoriaJogadorListener.class, AuditoriaJogadorListener2. class) /// campos, construtores, getters e setters É possível ainda configurar os Listeners para uma entidade via XML. Nesse caso, ao invés da anotação Listeners, seria necessário um trecho como o da Listagem 6 no orm.xml ou em outro XML anexado a ele. Caso se deseje retirar as anotações de escuta dos callbacks dos Listeners, basta mapear os métodos ouvintes via XML, como na Listagem 7. Listagem 6. Configuração de Listeners por entidade via XML. <entity class= Jogador > <entity-listener class= mj.auditoria.listener. AuditoriaJogadorListener /> <!-- outros Listeners para Jogador --> </entity> Listagem 7. Configuração de Listeners e métodos de escuta de callbacks por entidade via XML. <entity class= Jogador > <entity-listener class= mj.auditoria.listener. AuditoriaJogadorListener > <post-persist method-name= aposinserir /> <post-remove method-name= aposexcluir /> </entity-listener> <!-- outros Listeners para Jogador --> </entity> Quando uma classe de entidade estende outra que possui Listeners configurados, ela também fica sendo escutada por eles. Caso seja necessário ignorar os Listeners de uma superclasse para usar os apenas os próprios ou nenhum, basta marcar a classe com a anotação @ javax.persistence.excludesuperclasslisteners. Listeners por projeto Boa parte da auditoria de persistência costuma ser compartilhada por várias ou até todas as entidades em um projeto. Isso ocorre porque auditoria é um típico exemplo daquilo que alguns autores chamam de responsabilidade transversal (crosscutting concern). Com JPA, é possível configurar um ou mais Listeners para atenderem a todas as entidades em um projeto. Esse tipo de configuração só é possível via XML (no orm. xml ), como consta na Listagem 8. Listagem 8. Definição de Listener(s) padrão(ões) por projeto. <persistence-unit-metadata> <persistence-unit-defaults> <entity-listener class= mj.auditoria.listener. AuditoriaFutebolAnnotationListener > <post-persist method-name= aposinserir /> <post-remove method-name= aposexcluir /> </entity-listener> <!-- outros Listeners padrão --> </persistence-unit-defaults> </persistence-unit-metadata> Com esse tipo de configuração, a princípio, os callbacks de todas das entidades do projeto são escutados por métodos dos Listeners mapeados. É possível configurar as entidades para que ignorem os Listeners padrão. Para isso, basta marcar a classe com a anotação @javax.persistence. ExcludeDefaultListeners. Assim, caso um Listener esteja mapeado para diretamente para essa entidade, ele será usado e os padrões não. Se nenhum Listener próprio estiver configurado para a entidade, ela não usará nenhum. Em caso de múltiplos mapeamentos de Listeners (orm.xml, outros XMLs de mapeamento, classes e superclasses), a ordem na qual o JPA os executa é: 1. classes definidas como Listeners padrão para o projeto no orm.xml, na ordem em que estão descritos; 2. classes definidas como Listeners por entidade no orm.xml ou em XMLs anexados a ele, na ordem em que estão descritos; 3. classes definidas em Listeners das superclasses da entidade, a partir da mais alta na hierarquia, na ordem em que estão no array da anotação em cada entidade; 4. classes definidas em @ EntityListeners da entidade, na ordem em que estão no array ;* 5. callbacks em métodos das superclasses da entidade, a partir da mais alta na hierarquia;* 6. callbacks em métodos da própria entidade. Ciclo de vida de listeners As classes listeners são instanciadas no momento em que um objeto do tipo javax.persistence.entitymanagerfactory é instanciado. A política de criação de Listeners é exemplificada no cenário na figura 1 e é a mesma tanto com mapeamentos via anotações quanto via XML, seja para mapeamentos por projeto seja por entidade. Se no cenário da figura 1 houvesse um segundo Listener mapeado para as entidades Jogador e Timefutebol, e esse possuísse três callbacks mapeados, seriam instanciados seis desses Listeners. Assim, seriam dez Listeners instanciados no total. As instâncias dos Listeners não são destruídas até que seja invocado o método close() da instância do EntityManagerFactory. * - Se houver mapeamentos de Listeners para uma mesma entidade tanto em anotação como no orm.xml ou em XMLs anexados a ele, os mapeamentos por entidade feitos com anotações serão ignorados. 51
Entidades Jogador TimeFutebol Configuradas para o 01 (um) Listener Entidades e mapeadas individualmente ou via padrão do projeto para o Listener post-persist post-update Instancia de um javax.persistence.entitymanagerfactory JPA cria de 04 (quatro) instâncias (Jogador post-persist) (Jogador post-update) (TimeFutebol post-persist) (TimeFutebol post-update) mesmo Listener, que possui dois métodos de escuta de callbacks, o que faz com que sejam instanciadas quatro instâncias do Listener. Dicas importantes sobre os callbacks JPA Exceções nos métodos de escuta post impedem a conclusão da operação Se um método de escuta de callback não tiver suas exceções devidamente tratadas, a operação acaba não ocorrendo. Por esse motivo, trate qualquer exceção em seus métodos de escuta de callbacks post para evitar que a não possibilidade de auditar impeça que a operação em si ocorra (a não ser que a auditoria seja obrigatória em uma aplicação, ou seja, se a ordem for se não der para auditar, não faça! ). Só pode haver um método de escuta por callback em Listeners Não pode haver mais de um método configurado (por anotação ou XML) com o mesmo callback no mesmo Listener. Por exemplo, não adianta criar dois métodos e anotá-los com e mudar os tipos dos parâmetros. Se fizer isso, ocorrerá uma exceção assim que algum recurso JPA for exigido na aplicação. Caso deseje configurar um método para escutar determinado evento de várias entidades diferentes, faça como no exemplo da Listagem 9. Listagem 9. Método para escuta de callback compatível com qualquer entidade. public void aposatualizar(object entidade) { if (entidade instanceof Jogador) { // o que fazer com o Jogador atualizado? if (entidade instanceof TimeFutebol) { // o que fazer com o Time atualizado? Só pode haver um método de escuta por callback em classes de Entidade Análogo ao item anterior, não pode haver mais de um método configurado (por anotação ou XML) com o mesmo callback na mesma entidade. Um método pode ser configurado para escutar vários callbacks Se desejar tratar mais de um evento de acesso a banco da mesma maneira na sua auditoria, é possível configurar um mesmo método para escutar vários callbacks. Há um exemplo desse tipo de configuração via anotação na Listagem 10 e via XML na Listagem 11. Listagem 10. Método de escuta de múltiplos callbacks configurado via anotação. public void apossalvar(jogador jogador) { // o que fazer após salvar um novo ou atualizar um jogador? Listagem 11. Configuração de múltiplos callbacks para o mesmo método via XML. <entity-listener class= mj.auditoria.listener. AuditoriaFutebolAnnotationListener > <post-persist method-name= apossalvar /> <post-update method-name= apossalvar /> </entity-listener> Os callbacks pre-update e post-update não são notificados sem atualizações reais É fácil deduzir que os callbacks de inserção, exclusão e consulta não são notificados em caso de erro nas respectivas operações que os notificam. Porém, não há como deduzir se qualquer chamada ao método merge() notifica os callbacks de atualização. Na verdade, se o EntityManager identificar que não houve mudança em nenhum campo da entidade, mesmo que o método merge() seja usado, os callbacks não são notificados. Essa regra vale tanto para instâncias de entidade gerenciadas como para não gerenciadas pelo EntityManager. Os callbacks pre-update, post-update e post-remove só são notificados no momento da sincronização com o banco de dados Esses callbacks só são notificados quando o EntityManager é sincronizado com o banco de dados. Segundo a especificação JPA, isso pode ocorrer em três momentos: 1) quando o EntityManager identificar alterações nos campos das entidades gerenciadas e se seu FlushMode estiver configurado como AUTO ; 2) quando for invocado o método flush() do EntityManager; 3) quando a transação é confirmada (commit). Os demais callbacks são notificados assim que seus respectivos métodos no EntityManager são invocados. Exemplo de implementação de auditoria com JPA Uma pequena aplicação visa o cadastro de Jogadores e Times de Futebol. Nesse sistema, as operações de acesso/atualização de dados devem ser auditadas. As entidades Jogador e TimeFutebol são as entidades que representam, respectivamente, esses cadastros. 52 www.mundoj.com.br
Essa entidades estão configuradas para um Listener padrão (Listagem 12), o (Listagem 13) que centraliza a auditoria do sistema. A classe controladora e o Listener compartilham a mesma instância de um EntityManager a partir de simples fábrica de EntityManagers (Listagem 14). O Listener persiste as informações de auditoria na tabela auditoria_futebol, mapeada para a classe AuditoriaFutebol (Listagem 15). Listagem 12. Configuração do arquivo orm.xml para uso de Listener padrão. <persistence-unit-metadata> <persistence-unit-defaults> <entity-listener class= /> </persistence-unit-defaults> </persistence-unit-metadata> Listagem 13..java. public class { // outros métodos private EntityManager getentitymanager() { EntityManager em = FabricaEMs.getEntityManager(); return em; public void aposexcluir(object entidade) throws Exception { AuditoriaFutebol auditoria = new AuditoriaFutebol(entidade, E,getUsuario()); getentitymanager().persist(auditoria); public void aposinserir(object entidade) throws Exception { AuditoriaFutebol auditoria = new AuditoriaFutebol(entidade, I,getUsuario()); getentitymanager().persist(auditoria); public void aposatualizar(object entidade) throws Exception { AuditoriaFutebol auditoria = new AuditoriaFutebol(entidade, A,getUsuario()); getentitymanager().persist(auditoria); Listagem 14. Fábrica de EntityManagers (FabricaEMs.java) que usa o padrão Local Thread. public class FabricaEMs { static ThreadLocal<EntityManager> ems = new ThreadLocal<EntityManager>(); static EntityManagerFactory emf = Persistence.createEntityManagerFactory( sisfutebol ); public static EntityManager getentitymanager() { EntityManager em = ems.get(); if (em!=null) @Table(name = auditoria_futebol ) @ExcludeDefaultListeners public class AuditoriaFutebol { return em; else return getnewentitymanager(); private static EntityManager getnewentitymanager() { EntityManager em = emf.createentitymanager(); ems.set(em); return em; Listagem 15. Classe AuditoriaFutebol.java, usada para persistir as informações de auditoria do sistema. Observe que ela está anotada com @ExcludeDefaultListeners para evitar que ela mesma seja auditada pelo listener padrão. Sem essa anotação, o sistema entraria em colapso porque existiriam infinitas chamadas recursivas. @Id @GeneratedValue(strategy = IDENTITY) @Column(name = idoperacao, unique = true, nullable = false) Integer idoperacao; @Temporal(TemporalType.TIMESTAMP) @Column(name= ocorridaem, nullable = false) Date ocorridaem; @Column(name= entidade, nullable = false) String entidade; @Column(name= operacao, nullable = false) String operacao; @Column(name= valores_campos, nullable = false) String valorescampos; @Column(name= usuario, nullable = false) String usuario; public AuditoriaFutebol(Object entidade, String operacao, String usuario) { super(); setentidade(entidade); setvalorescampos(entidade.tostring()); this.operacao = operacao; this.usuario = usuario; @PrePersist void configurarmomentoauditoria() { ocorridaem = new Date(); public void setentidade(object entidade) { this.entidade = entidade.getclass().getsimplename(); // outros métodos 53
Esse exemplo serve apenas para reforçar os conceitos abordados neste artigo e dar uma ideia de como se fazer auditoria de persistência com JPA. Ele não deve ser tomado como modelo para sistemas reais. O código completo desse exemplo está disponível para download no site da Mundoj. Pontos fracos da auditoria com JPA Os Listener no JPA são incapazes de interceptar operações feitas por objetos do tipo javax.persistence.query através dos métodos executeupdate(), get- SimgleResult() e getresultlist(). Exclusões como a da Listagem 16 não seria detectada pelo callback post remove e consultas como a da Listagem 17 não seria detectada pelo callback post-load. Listagem 16. Exemplo de operação de acesso, atualização do banco de dados que não será escutada pelo callback post-remove JPA. Query qexclusao = em.createquery( delete from Jogador j where j.idjogador=? ); qexclusao.setparameter(1, idjogador); qexclusao.executeupdate(); Listagem 17. Exemplo de operação de acesso consulta ao banco de dados que não será escutada pelo callback post-load JPA. Query qconsulta = em.createquery( select j from Jogador j where j.idjogador=? ); qconsulta.setparameter(1, idjogador); Jogador zebuduia = qconsulta.getsingleresult(); Outra desvantagem é que os callbacks nos fornecem apenas o estado atual do objeto associado. Em métodos post, não é possível saber como estavam as informações da entidade antes da inserção/atualização/ exclusão. Uma maneira de resolver essa questão seria usar os callbacks pre, como o pré update, por exemplo. Mas é realmente trabalhoso, visto que cada callback acaba ficando numa instancia diferente do Listener, conforme já foi abordado. Considerações finais Neste artigo foi abordado o conceito de auditoria de persistência e sua importância para a saúde dos dados de um sistema. Foram explicadas e exemplificadas todas as possibilidades de mapeamento de métodos de callback e Listeners, desde em entidade com em projeto. Não foi mostrado o que fazer dentro dos métodos mapeados para auditoria porque o objetivo deste é mostrar como construí-los e como chegar até eles. Foram também descritos os pontos fracos do sistema de callbacks e Listeners da JPA, para que o leitor avalie se vale a pena usar este mecanismo em sistemas legados ou em novos projetos. No site da Mundoj há dois projetos de exemplo disponíveis para download com muitos exemplos Referências 54 www.mundoj.com.br