2016-02-03 13 views
5

Ho un'applicazione di jogging in esecuzione scritta in Java che utilizza Hibernate come implementazione JPA e che utilizza Guice per associare tutti i servizi.Hibernate context di persistenza basato sull'host del server con Jersey

Il mio caso di utilizzo consiste nell'avere un'istanza di app che serve più localizzazioni, disponibile in host diversi. Un semplice esempio potrebbe essere una versione inglese e una versione francese su application.com e application.fr. A seconda dell'host che viene attivato, avrei bisogno di cambiare l'app per utilizzare un database diverso.

Attualmente, ho configurato solo un singleton SessionFactory che viene utilizzato da tutti gli oggetti di accesso ai dati, fornendo accesso a un solo database.

Sto cercando di trovare il modo più semplice per passare le informazioni sul contesto del paese dalla risorsa (dove posso recuperarla dal contesto della richiesta) al DAO, che deve selezionare uno dei più SessionFactory s.

Potrei passare un parametro in ogni metodo di servizio, ma sembra molto noioso. Ho pensato di utilizzare un registro che avrebbe un'istanza ThreadLocal del parametro del paese corrente impostato da un filtro Jersey, ma i thread-locals interromperanno l'utilizzo di Executors ecc.

Esistono modi eleganti per raggiungere questo obiettivo?

+0

Quale versione di Jersey stai usando? –

+0

Usiamo Jersey 2.19, aggiornando i minori di volta in volta – vvondra

risposta

2

Non sono un utente Guice, quindi questa risposta utilizza il framework DI di Jersey, HK2. A un livello di configurazione di base, HK2 non è molto diverso dalla configurazione di Guice. Ad esempio con Guice hai lo AbstractModule, dove HK2 ha il AbstractBinder. Con entrambi i componenti, verrà utilizzata la sintassi bind(..).to(..).in(Scope) simile. Una differenza è che con Guice è bind(Contract).to(Impl), mentre con HK2 è bind(Impl).to(Contract).

HK2 ha anche Factory s, che consentono una creazione più complessa dei tuoi oggetti iniettabili. Con le tue fabbriche, dovresti usare la sintassi bindFactory(YourFactory.class).to(YourContract.class).

Detto questo, è possibile implementare il caso d'uso con qualcosa di simile al seguente.

  1. Creare un Factory per gli inglesi SessionFactory

    public class EnglishSessionFactoryFactory implements Factory<SessionFactory> { 
        @Override 
        public SessionFactory provide() { 
         ... 
        } 
        @Override 
        public void dispose(SessionFactory t) {} 
    } 
    
  2. Creare un Factory per i francesi SessionFactory

    public class FrenchSessionFactoryFactory implements Factory<SessionFactory> { 
        @Override 
        public SessionFactory provide() { 
         ... 
        } 
        @Override 
        public void dispose(SessionFactory t) {}  
    } 
    

    Nota queste due SessionFactory s saranno binded portata Singleton e nome.

  3. Creare un altro Factory che sarà in un ambito di richiesta, che utilizzerà le informazioni sul contesto della richiesta. Questa fabbrica inietterà i due sopra SessionFactory s per nome (usando l'associazione dei nomi) e, in base alle informazioni di contesto richieste, restituirà l'appropriato SessionFactory. L'esempio che segue semplicemente utilizza un parametro di query

    public class SessionFactoryFactory 
         extends AbstractContainerRequestValueFactory<SessionFactory> { 
    
        @Inject 
        @Named("EnglishSessionFactory") 
        private SessionFactory englishSessionFactory; 
    
        @Inject 
        @Named("FrenchSessionFactory") 
        private SessionFactory frenchSessionFactory; 
    
        @Override 
        public SessionFactory provide() { 
         ContainerRequest request = getContainerRequest(); 
         String lang = request.getUriInfo().getQueryParameters().getFirst("lang"); 
         if (lang != null && "fr".equals(lang)) { 
          return frenchSessionFactory; 
         } 
         return englishSessionFactory; 
        } 
    } 
    
  4. Poi si può solo iniettare il SessionFactory (che daremo un nome diverso) nella vostra dao.

    public class IDaoImpl implements IDao { 
    
        private final SessionFactory sessionFactory; 
    
        @Inject 
        public IDaoImpl(@Named("SessionFactory") SessionFactory sessionFactory) { 
         this.sessionFactory = sessionFactory; 
        } 
    } 
    
  5. Per associare tutto insieme, si utilizzerà un AbstractBinder simile al seguente implementazione

    public class PersistenceBinder extends AbstractBinder { 
    
        @Override 
        protected void configure() { 
         bindFactory(EnglishSessionFactoryFactory.class).to(SessionFactory.class) 
           .named("EnglishSessionFactory").in(Singleton.class); 
         bindFactory(FrenchSessionFactoryFactory.class).to(SessionFactory.class) 
           .named("FrenchSessionFactory").in(Singleton.class); 
         bindFactory(SessionFactoryFactory.class) 
           .proxy(true) 
           .proxyForSameScope(false) 
           .to(SessionFactory.class) 
           .named("SessionFactory") 
           .in(RequestScoped.class); 
         bind(IDaoImpl.class).to(IDao.class).in(Singleton.class); 
        } 
    } 
    

    Qui ci sono alcune cose da notare circa il legante

    • I due lingue diverse specifiche SessionFactory s sono legati per nome. Quale viene utilizzato per l'iniezione @Named, come si può vedere al passaggio 3.
    • Alla richiesta di ambito che prende la decisione viene anche assegnato un nome.
    • Si noterà il proxy(true).proxyForSameScope(false). Questo è necessario, poiché presupponiamo che lo IDao sia un singleton e dal momento che lo "selezionato" SessionFactory sia incluso in un ambito di richiesta, non è possibile iniettare l'effettivo SessionFactory, poiché cambierà da richiesta a richiesta, quindi è necessario iniettare un proxy. Se lo IDao fosse ambito ambito, invece di un singleton, potremmo escludere queste due righe. Potrebbe essere meglio fare lo scope con la richiesta dao, ma volevo solo mostrare come dovrebbe essere fatto come un singleton.

      Vedere anche Injecting Request Scoped Objects into Singleton Scoped Object with HK2 and Jersey, per ulteriori approfondimenti su questo argomento.

  6. Poi basta registrare il AbstractBinder con Jersey. Per questo, è sufficiente utilizzare il metodo register(...) di ResourceConfig. See also, se è richiesta la configurazione web.xml.

Questo è tutto. Di seguito è riportato un test completo con Jersey Test Framework. Puoi eseguirlo come qualsiasi altro test JUnit. Il SessionFactory utilizzato è solo una classe fittizia, non l'attuale Hibernate SessionFactory. È solo per mantenere l'esempio il più breve possibile, ma basta sostituirlo con il normale codice di inizializzazione di Hibernate.

import java.util.logging.Logger; 
import javax.inject.Inject; 
import javax.inject.Named; 
import javax.inject.Singleton; 
import javax.ws.rs.GET; 
import javax.ws.rs.Path; 
import javax.ws.rs.core.Response; 
import javax.ws.rs.ext.ExceptionMapper; 

import org.glassfish.hk2.api.Factory; 
import org.glassfish.hk2.utilities.binding.AbstractBinder; 
import org.glassfish.jersey.filter.LoggingFilter; 
import org.glassfish.jersey.process.internal.RequestScoped; 
import org.glassfish.jersey.server.ContainerRequest; 
import org.glassfish.jersey.server.ResourceConfig; 
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; 
import org.glassfish.jersey.test.JerseyTest; 
import org.junit.Test; 

import static junit.framework.Assert.assertEquals; 

/** 
* Stack Overflow https://stackoverflow.com/q/35189278/2587435 
* 
* Run this like any other JUnit test. There is only one required dependency 
* 
* <dependency> 
*  <groupId>org.glassfish.jersey.test-framework.providers</groupId> 
*  <artifactId>jersey-test-framework-provider-inmemory</artifactId> 
*  <version>${jersey2.version}</version> 
*  <scope>test</scope> 
* </dependency> 
* 
* @author Paul Samsotha 
*/ 
public class SessionFactoryContextTest extends JerseyTest { 

    public static interface SessionFactory { 
     Session openSession(); 
    } 

    public static class Session { 
     private final String language; 
     public Session(String language) { 
      this.language = language; 
     } 
     public String get() { 
      return this.language; 
     } 
    } 

    public static class EnglishSessionFactoryFactory implements Factory<SessionFactory> { 
     @Override 
     public SessionFactory provide() { 
      return new SessionFactory() { 
       @Override 
       public Session openSession() { 
        return new Session("English"); 
       } 
      }; 
     } 

     @Override 
     public void dispose(SessionFactory t) {}  
    } 

    public static class FrenchSessionFactoryFactory implements Factory<SessionFactory> { 
     @Override 
     public SessionFactory provide() { 
      return new SessionFactory() { 
       @Override 
       public Session openSession() { 
        return new Session("French"); 
       } 
      }; 
     } 

     @Override 
     public void dispose(SessionFactory t) {}  
    } 

    public static class SessionFactoryFactory 
      extends AbstractContainerRequestValueFactory<SessionFactory> { 

     @Inject 
     @Named("EnglishSessionFactory") 
     private SessionFactory englishSessionFactory; 

     @Inject 
     @Named("FrenchSessionFactory") 
     private SessionFactory frenchSessionFactory; 

     @Override 
     public SessionFactory provide() { 
      ContainerRequest request = getContainerRequest(); 
      String lang = request.getUriInfo().getQueryParameters().getFirst("lang"); 
      if (lang != null && "fr".equals(lang)) { 
       return frenchSessionFactory; 
      } 
      return englishSessionFactory; 
     } 
    } 

    public static interface IDao { 
     public String get(); 
    } 

    public static class IDaoImpl implements IDao { 

     private final SessionFactory sessionFactory; 

     @Inject 
     public IDaoImpl(@Named("SessionFactory") SessionFactory sessionFactory) { 
      this.sessionFactory = sessionFactory; 
     } 

     @Override 
     public String get() { 
      return sessionFactory.openSession().get(); 
     } 
    } 

    public static class PersistenceBinder extends AbstractBinder { 

     @Override 
     protected void configure() { 
      bindFactory(EnglishSessionFactoryFactory.class).to(SessionFactory.class) 
        .named("EnglishSessionFactory").in(Singleton.class); 
      bindFactory(FrenchSessionFactoryFactory.class).to(SessionFactory.class) 
        .named("FrenchSessionFactory").in(Singleton.class); 
      bindFactory(SessionFactoryFactory.class) 
        .proxy(true) 
        .proxyForSameScope(false) 
        .to(SessionFactory.class) 
        .named("SessionFactory") 
        .in(RequestScoped.class); 
      bind(IDaoImpl.class).to(IDao.class).in(Singleton.class); 
     } 
    } 

    @Path("test") 
    public static class TestResource { 

     private final IDao dao; 

     @Inject 
     public TestResource(IDao dao) { 
      this.dao = dao; 
     } 

     @GET 
     public String get() { 
      return dao.get(); 
     } 
    } 

    private static class Mapper implements ExceptionMapper<Throwable> { 
     @Override 
     public Response toResponse(Throwable ex) { 
      ex.printStackTrace(System.err); 
      return Response.serverError().build(); 
     } 
    } 

    @Override 
    public ResourceConfig configure() { 
     return new ResourceConfig(TestResource.class) 
       .register(new PersistenceBinder()) 
       .register(new Mapper()) 
       .register(new LoggingFilter(Logger.getAnonymousLogger(), true)); 
    } 

    @Test 
    public void shouldReturnEnglish() { 
     final Response response = target("test").queryParam("lang", "en").request().get(); 
     assertEquals(200, response.getStatus()); 
     assertEquals("English", response.readEntity(String.class)); 
    } 

    @Test 
    public void shouldReturnFrench() { 
     final Response response = target("test").queryParam("lang", "fr").request().get(); 
     assertEquals(200, response.getStatus()); 
     assertEquals("French", response.readEntity(String.class)); 
    } 
} 

Un'altra cosa che si potrebbe anche prendere in considerazione è la chiusura delle SessionFactory s. Sebbene il Factory abbia un metodo dispose(), non viene chiamato in modo affidabile da Jersey. Potresti voler esaminare un ApplicationEventListener. È possibile iniettare i SessionFactory s e chiuderli all'evento di chiusura.

+0

grazie per il tempo, guardandolo ora! – vvondra

+0

fantastico! specialmente per indicare 'proxy (true) .proxyForSameScope (false)' – vvondra

+0

@wondra vedere anche http://stackoverflow.com/q/35994965/2587435. Con questo, non è necessario utilizzare il nome extra per la sessionfactory principale. Puoi iniettarlo senza il nome. Ho appena imparato qualcosa di nuovo :-) –

Problemi correlati