2014-08-28 9 views
5

Breve storia: Sviluppiamo e gestiamo una libreria che può essere utilizzata in altri progetti utilizzando JavaEE7/CDI/JPA. Le applicazioni verranno eseguite con Glassfish-4.0 e utilizzeranno l'implementazione JPA di Hibernate per una persistenza PostgreSQL sottostante. Questo fa parte di uno sforzo di migrazione a lungo termine per riscrivere le vecchie applicazioni scritte in Spring/Struts/Hibernate nel nuovo mondo di JavaEE7/CDI/JTA.Come intercettare gli eventi delle transazioni JTA e ottenere un riferimento al EntityManager corrente associato alla transazione

Il problema: Per scopi di audit, la nostra biblioteca ha bisogno di intercettare tutte le transazioni di database e includono istruzioni SQL personalizzato prima vengono eseguite le istruzioni dell'utente. A questo punto, il nome utente e l'indirizzo IP correnti devono essere inseriti in una variabile di database temporanea (funzione specifica del fornitore) in modo che un trigger del database possa leggerli per creare la traccia di controllo per qualsiasi modifica di riga. This particular post was very helpful providing alternatives, e il nostro team è andato giù per la strada principale a causa di un patrimonio precedentemente stabilito.

TUTTAVIA: Siamo profondamente deluso di come JTA gestisce gli eventi di transazione. Esistono numerosi modi per intercettare le transazioni, ma questo caso particolare sembra essere assolutamente impossibile. Nella vecchia architettura, utilizzando il gestore delle transazioni di Spring, abbiamo semplicemente utilizzato un Hibernate Interceptor che implementa Interceptor.afterTransactionBegin (...). Leggendo il official JTA-1.2 spec, abbiamo trovato che ha il supporto per Synchronization.beforeCompletion e Synchronization.afterCompletion. Dopo diverse ore di sessioni di debug, abbiamo chiaramente notato che l'implementazione di JTA da parte di Hibernate utilizza queste strutture. Ma JTA sembra mancare di eventi come primaBin in arrivo e dopo il BUGin (che IMHO sembra essere una mancanza di buon senso). E poiché non ci sono strutture per intercettarle, Hibernate è completamente compatibile con JTA e semplicemente non lo farà. Periodo.

Non importa cosa facciamo, non riusciamo a trovare un modo. Abbiamo tentato, ad esempio, di intercettare le annotazioni @Transactional ed eseguire il nostro codice subito dopo che l'impl JTA del container ha fatto il suo lavoro per aprire la transazione. Ma ci manca la capacità di acquisire dinamicamente l'EntityManager associato a quella particolare transazione. Ricorda: questa è una libreria, non l'applicazione web stessa. Non può formulare ipotesi su quali Unità di Persistenza siano dichiarate e utilizzate dall'applicazione. E, per quanto possiamo dire, abbiamo bisogno di sapere quale specifico nome Persistent Unit per iniettarlo nel nostro codice. Stiamo cercando di fornire una struttura di controllo ad altri temi che sia il più trasparente possibile.

Quindi chiediamo umilmente aiuto. Se qualcuno là fuori ha una soluzione, una soluzione, qualsiasi opinione, saremo felici di sentirlo.

+1

Ignorare completamente JPA e aggiungere un intercettore sul lotto di connessione del database effettivo? So solo come farlo in Tomcat jdbc.pool, ma uno spera che Glassfish abbia un modo. – Affe

+0

Potrebbe essere il caso, ma a questo livello non penso che abbiamo accesso alla sessione Http per acquisire qualsiasi utente che abbia effettuato l'accesso o il suo indirizzo IP del client. – JulioHM

risposta

3

Questo è stato rapidamente risposto qui in questo post da me stesso, ma nascondendo il fatto che abbiamo trascorso oltre due settimane provando diverse strategie per superare questo. Quindi, ecco la nostra implementazione finale che abbiamo deciso di utilizzare.

idea di base: Creare la propria implementazione di javax.persistence.spi.PersistenceProvider estendendo quella data da Hibernate. Per tutti gli effetti, questo è l'unico punto in cui il codice verrà collegato a Hibernate oa qualsiasi altra implementazione specifica del fornitore.

public class MyHibernatePersistenceProvider extends org.hibernate.jpa.HibernatePersistenceProvider { 

    @Override 
    public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) { 
     return new EntityManagerFactoryWrapper(super.createContainerEntityManagerFactory(info, properties)); 
    } 

} 

L'idea è quella di avvolgere le versioni di Hibernate di EntityManagerFactory e EntityManager con una propria implementazione. Quindi è necessario creare classi che implementino queste interfacce e mantengano l'implementazione specifica del fornitore all'interno.

Questa è l'EntityManagerFactoryWrapper

public class EntityManagerFactoryWrapper implements EntityManagerFactory { 

    private EntityManagerFactory emf; 

    public EntityManagerFactoryWrapper(EntityManagerFactory originalEMF) { 
     emf = originalEMF; 
    } 

    public EntityManager createEntityManager() { 
     return new EntityManagerWrapper(emf.createEntityManager()); 
    } 

    // Implement all other methods for the interface 
    // providing a callback to the original emf. 

L'EntityManagerWrapper è il nostro punto di intercettazione. Dovrai implementare tutti i metodi dall'interfaccia. In ogni metodo in cui è possibile modificare un'entità, includiamo una chiamata a una query personalizzata per impostare variabili locali nel database.

public class EntityManagerWrapper implements EntityManager { 

    private EntityManager em; 
    private Principal principal; 

    public EntityManagerWrapper(EntityManager originalEM) { 
     em = originalEM; 
    } 

    public void setAuditVariables() { 
     String userid = getUserId(); 
     String ipaddr = getUserAddr(); 
     String sql = "SET LOCAL application.userid='"+userid+"'; SET LOCAL application.ipaddr='"+ipaddr+"'"; 
     em.createNativeQuery(sql).executeUpdate(); 
    } 

    protected String getUserAddr() { 
     HttpServletRequest httprequest = CDIBeanUtils.getBean(HttpServletRequest.class); 
     String ipaddr = ""; 
     if (httprequest != null) { 
      ipaddr = httprequest.getRemoteAddr(); 
     } 
     return ipaddr; 
    } 

    protected String getUserId() { 
     String userid = ""; 
     // Try to look up a contextual reference 
     if (principal == null) { 
      principal = CDIBeanUtils.getBean(Principal.class); 
     } 

     // Try to assert it from CAS authentication 
     if (principal == null || "anonymous".equalsIgnoreCase(principal.getName())) { 
      if (AssertionHolder.getAssertion() != null) { 
       principal = AssertionHolder.getAssertion().getPrincipal(); 
      } 
     } 
     if (principal != null) { 
      userid = principal.getName(); 
     } 
     return userid; 
    } 

    @Override 
    public void persist(Object entity) { 
     if (em.isJoinedToTransaction()) { 
      setAuditVariables(); 
     } 
     em.persist(entity); 
    } 

    @Override 
    public <T> T merge(T entity) { 
     if (em.isJoinedToTransaction()) { 
      setAuditVariables(); 
     } 
     return em.merge(entity); 
    } 

    @Override 
    public void remove(Object entity) { 
     if (em.isJoinedToTransaction()) { 
      setAuditVariables(); 
     } 
     em.remove(entity); 
    } 

    // Keep implementing all methods that can change 
    // entities so you can setAuditVariables() before 
    // the changes are applied. 
    @Override 
    public void createNamedQuery(..... 

Downside: query di intercettazione (set locale) sarà probabilmente eseguito più volte all'interno di una singola transazione, specialmente se ci sono diverse dichiarazioni su una singola chiamata di servizio. Date le circostanze, abbiamo deciso di mantenerlo in questo modo perché è un semplice SET LOCAL in memoria che chiama PostgreSQL. Poiché non ci sono tavoli coinvolti, possiamo vivere con il successo della performance.

Ora basta sostituire provider di persistenza di Hibernate all'interno persistence.xml:

<?xml version="1.0" encoding="UTF-8"?> 
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" 
      version="2.1"> 
<persistence-unit name="petstore" transaction-type="JTA"> 
     <provider>my.package.HibernatePersistenceProvider</provider> 
     <jta-data-source>java:app/jdbc/exemplo</jta-data-source> 
     <properties> 
      <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform" /> 
      <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> 
     </properties> 
</persistence-unit> 

Come nota a margine, questo è il CDIBeanUtils dobbiamo aiutare con il gestore di fagioli in alcune occasioni speciali. In questo caso, lo stiamo usando per cercare un riferimento a HttpServletRequest e Principal.

public class CDIBeanUtils { 

    public static <T> T getBean(Class<T> beanClass) { 

     BeanManager bm = CDI.current().getBeanManager(); 

     Iterator<Bean<?>> ite = bm.getBeans(beanClass).iterator(); 
     if (!ite.hasNext()) { 
      return null; 
     } 
     final Bean<T> bean = (Bean<T>) ite.next(); 
     final CreationalContext<T> ctx = bm.createCreationalContext(bean); 
     final T t = (T) bm.getReference(bean, beanClass, ctx); 
     return t; 
    } 

} 

Per essere corretti, questo non intercetta esattamente gli eventi Transazioni. Ma siamo in grado di includere le query personalizzate di cui abbiamo bisogno all'interno della transazione.

Speriamo che questo possa aiutare gli altri a evitare il dolore che abbiamo attraversato.

+0

Sei riuscito a farlo funzionare? Ho seguito il tuo modello e ho delle eccezioni che escono dalla EntityManagerFactory delegata. Grazie in anticipo. –

Problemi correlati