Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate persistence context based on server host with Jersey

I have a running Jersey application written in Java using Hibernate as JPA implementation and using Guice to bind all services together.

My use case lies in having one app instance serving multiple localizations, available under different hosts. Simple example would be an English version and a French version on application.com and application.fr. Depending on which host is triggered, I would need to switch the app to use a different database.

Currently, I only have one singleton SessionFactory configure which is used by all of the data access objects, providing access to only one database.

I'm trying to come up with the easiest way to pass the information about the country context all they way from the resource (where I can fetch it from the request context) to the DAO, which needs to select one of multiple SessionFactorys.

I could pass a parameter in every service method, but that seems very tedious. I thought of using a Registry which would have a ThreadLocal instance of the current country parameter set by a Jersey filter, but thread-locals would break on using Executors etc.

Are there any elegant ways to achieve this?

like image 535
vvondra Avatar asked Oct 30 '22 10:10

vvondra


1 Answers

I'm not much of a Guice user, so this answer uses Jersey's DI framework, HK2. At a basic configuration level, HK2 is not much different from Guice configuration. For example with Guice you have the AbstractModule, where HK2 has the AbstractBinder. With both components, you will use similar bind(..).to(..).in(Scope) syntax. One difference is that with Guice it's bind(Contract).to(Impl), while with HK2 it's bind(Impl).to(Contract).

HK2 also has Factorys, which allow for more complex creating of your injectable objects. With your factories, you would use the syntax bindFactory(YourFactory.class).to(YourContract.class).

That being said, you could implement your use case with something like the following.

  1. Create a Factory for the English SessionFactory

    public class EnglishSessionFactoryFactory implements Factory<SessionFactory> {
        @Override
        public SessionFactory provide() {
           ...
        }
        @Override
        public void dispose(SessionFactory t) {}
    }
    
  2. Create a Factory for the French SessionFactory

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

    Note the above two SessionFactorys will be binded in singleton scope and by name.

  3. Create another Factory that will be in a request scope, that will make use the request context information. This factory will inject the above two SessionFactorys by name (using name binding), and from whatever request context information, return the appropriate SessionFactory. The example below simply uses a query parameter

    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. Then you can just inject the SessionFactory (which we will give a different name) into your dao.

    public class IDaoImpl implements IDao {
    
        private final SessionFactory sessionFactory;
    
        @Inject
        public IDaoImpl(@Named("SessionFactory") SessionFactory sessionFactory) {
            this.sessionFactory = sessionFactory;
        }
    }
    
  5. To bind everything together, you will use an AbstractBinder similar to the following implementation

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

    Here are some things to note about the binder

    • The two different language specific SessionFactorys are bound by name. Which is used for @Named injection, as you can see in step 3.
    • The request scoped factory that makes the decision is also given a name.
    • You will notice the proxy(true).proxyForSameScope(false). This is required, as we assume that the IDao will be a singleton, and since the "chosen" SessionFactory we be in a request scope, we can't inject the actual SessionFactory, as it will change from request to request, so we need to inject a proxy. If the IDao were request scoped, instead of a singleton, then we could leave out those two lines. It might be better to just make the dao request scoped, but I just wanted to show how it should be done as a singleton.

      See also Injecting Request Scoped Objects into Singleton Scoped Object with HK2 and Jersey, for more examination on this topic.

  6. Then you just need to register the AbstractBinder with Jersey. For that, you can just use the register(...) method of the ResourceConfig. See also, if you require web.xml configuration.

That's about it. Below is a complete test using Jersey Test Framework. You can run it like any other JUnit test. The SessionFactory used is just a dummy class, not the actual Hibernate SessionFactory. It is only to keep the example as short as possible, but just replace it with your regular Hibernate initialization code.

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

Another thing you might also want to consider is the closing of the SessionFactorys. Though the Factory has a dispose() method, it is not reliably called by Jersey. You may want to look into an ApplicationEventListener. You can inject the SessionFactorys into it, and shut them down on the close event.

like image 136
Paul Samsotha Avatar answered Nov 15 '22 03:11

Paul Samsotha