2016-02-03 47 views
5

Mam uruchomioną aplikację Jersey napisaną w Javie, używając Hibernate jako implementacji JPA i używając Guice do wiązania wszystkich usług razem.Kontekst utrwalania hibernacji oparty na hoście serwera z Jersey

Mój przypadek użycia polega na posiadaniu jednej instancji aplikacji obsługującej wiele lokalizacji dostępnych pod różnymi hostami. Prostym przykładem może być wersja angielska i wersja francuska na application.com i application.fr. W zależności od tego, który host zostanie uruchomiony, będę musiał zmienić aplikację, aby używała innej bazy danych.

Obecnie mam tylko jeden konfigurator singleton SessionFactory, który jest używany przez wszystkie obiekty dostępu do danych, zapewniając dostęp tylko do jednej bazy danych.

Próbuję wymyślić najłatwiejszy sposób przekazania informacji o kontekście kraju z zasobu (gdzie mogę go pobrać z kontekstu żądania) do DAO, który musi wybrać jeden z wielokrotny SessionFactory s.

Mogę przekazać parametr w każdej metodzie serwisowej, ale wydaje się to bardzo uciążliwe. Pomyślałem o użyciu rejestru, który miałby instancję ThreadLocal bieżącego parametru kraju ustawionego przez filtr Jersey, ale użytkownicy nitek włamywali się za pomocą Executorów itp.

Czy istnieją jakieś eleganckie sposoby osiągnięcia tego?

+0

Którą wersję Jersey używasz? –

+0

Korzystamy z usługi Jersey 2.19, aktualizując nieletnich od czasu do czasu – vvondra

Odpowiedz

2

Nie jestem zbytnim użytkownikiem Guice, więc ta odpowiedź wykorzystuje strukturę DI dla Jersey, HK2. Na podstawowym poziomie konfiguracji, HK2 nie różni się zbytnio od konfiguracji Guice. Na przykład z Guice masz AbstractModule, gdzie HK2 ma AbstractBinder. W przypadku obu składników użyjesz podobnej składni bind(..).to(..).in(Scope). Jedna różnica polega na tym, że w przypadku Guice jest to bind(Contract).to(Impl), natomiast w przypadku HK2 jest to bind(Impl).to(Contract).

HK2 ma również Factory s, które pozwalają na bardziej złożone tworzenie obiektów wstrzykiwanych. W swoich fabrykach użyjesz składni bindFactory(YourFactory.class).to(YourContract.class).

Mimo to można zaimplementować swój przypadek użycia za pomocą następującego rozwiązania.

  1. Tworzenie Factory dla angielskiej SessionFactory

    public class EnglishSessionFactoryFactory implements Factory<SessionFactory> { 
        @Override 
        public SessionFactory provide() { 
         ... 
        } 
        @Override 
        public void dispose(SessionFactory t) {} 
    } 
    
  2. Tworzenie Factory dla Francuzów SessionFactory

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

    Uwaga Powyższe dwie SessionFactory s zostanie oprawiony w zakresie singleton i przez Nazwa.

  3. Utwórz kolejny Factory, który będzie w zasięgu żądania, który użyje informacji kontekstowych żądania. Ta fabryka wstrzyknie powyższe dwa SessionFactory s za pomocą nazwy (przy użyciu powiązania nazwy) i z jakiejkolwiek informacji kontekstowej żądania, zwróci odpowiednią SessionFactory. Poniższy przykład po prostu wykorzystuje parametr zapytania

    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. Następnie można po prostu wstrzyknąć SessionFactory (który podamy inną nazwę) do swojej dao.

    public class IDaoImpl implements IDao { 
    
        private final SessionFactory sessionFactory; 
    
        @Inject 
        public IDaoImpl(@Named("SessionFactory") SessionFactory sessionFactory) { 
         this.sessionFactory = sessionFactory; 
        } 
    } 
    
  5. Aby związać wszystko razem, można użyć AbstractBinder podobny do następującego wdrożenia

    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); 
        } 
    } 
    

    Oto kilka rzeczy, o których należy pamiętać o spoiwie

    • dwóch różnych języków specyficznych SessionFactory s są powiązane nazwą. Który jest używany do iniekcji @Named, jak widać w kroku 3.
    • Fabryka o żądanym zakresie, która podejmuje decyzję, otrzymuje również nazwę.
    • Zobaczysz proxy(true).proxyForSameScope(false). Jest to wymagane, ponieważ zakładamy, że IDao będzie singletonem, a ponieważ "wybrany" SessionFactory znajduje się w zasięgu żądania, nie możemy wprowadzić rzeczywistego SessionFactory, ponieważ zmieni się on z żądania na żądanie, więc potrzebujemy wstrzykiwać proxy. Jeśli IDao to żądanie o zasięgu, zamiast pojedynczego, możemy pominąć te dwie linie. Lepiej byłoby po prostu ustawić zakres żądania dao, ale chciałem tylko pokazać, jak powinno się to robić jako singleton.

      Zobacz także Injecting Request Scoped Objects into Singleton Scoped Object with HK2 and Jersey, aby uzyskać więcej informacji na ten temat.

  6. Potem wystarczy zarejestrować AbstractBinder z Jersey. W tym celu można po prostu użyć metody register(...) z ResourceConfig. See also, jeśli potrzebujesz konfiguracji web.xml.

To wszystko. Poniżej znajduje się pełny test z użyciem Jersey Test Framework. Możesz uruchomić go jak każdy inny test JUnit. Zastosowany SessionFactory to tylko fikcyjna klasa, a nie rzeczywisty Hibernacja SessionFactory. Należy jedynie zachować możliwie krótki przykład, ale zastąpić go zwykłym kodem inicjalizacji Hibernuj.

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)); 
    } 
} 

Kolejną rzeczą, którą warto wziąć pod uwagę, jest zamknięcie SessionFactory s. Chociaż Factory ma metodę dispose(), nie jest ona niezawodnie nazywana przez Jersey. Możesz zajrzeć do ApplicationEventListener. Możesz wstrzyknąć do niego SessionFactory s i zamknąć je na zamknięciu.

+0

dzięki za czas, patrząc na to teraz! – vvondra

+0

świetnie! szczególnie dla wskazania 'proxy (true) .proxyForSameScope (false)' – vvondra

+0

@wondra patrz także http://stackoverflow.com/q/35994965/2587435. Dzięki temu nie musisz używać dodatkowej nazwy dla głównej biblioteki sesji. Możesz wstrzyknąć bez nazwy. Właśnie nauczyłem się czegoś nowego :-) –