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 SessionFactory
s.
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?
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 Factory
s, 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.
Create a Factory
for the English SessionFactory
public class EnglishSessionFactoryFactory implements Factory<SessionFactory> {
@Override
public SessionFactory provide() {
...
}
@Override
public void dispose(SessionFactory t) {}
}
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 SessionFactory
s will be binded in singleton scope and by name.
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 SessionFactory
s 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;
}
}
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;
}
}
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
SessionFactory
s are bound by name. Which is used for @Named
injection, as you can see in step 3.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.
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 SessionFactory
s. 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 SessionFactory
s into it, and shut them down on the close event.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With