基于带有 Jersey 的服务器主机的 Hibernate 持久性上下文

Hibernate persistence context based on server host with Jersey

我有一个用 Java 编写的 运行 Jersey 应用程序,使用 Hibernate 作为 JPA 实现并使用 Guice 将所有服务绑定在一起。

我的用例是让一个应用程序实例服务于多个本地化,在不同的主机下可用。简单的例子是 application.comapplication.fr 上的英文版和法文版。根据触发的主机,我需要切换应用程序以使用不同的数据库。

目前,我只有一个单例 SessionFactory 配置,所有数据访问对象都使用它,只提供对一个数据库的访问。

我正在尝试想出最简单的方法来将有关国家/地区上下文的信息从资源(我可以从请求上下文中获取它)一路传递到 DAO,这需要 select 多个 SessionFactory 之一。

我可以在每个服务方法中传递一个参数,但这看起来很乏味。我想过使用一个注册表,它有一个由 Jersey 过滤器设置的当前国家/地区参数的 ThreadLocal 实例,但是线程局部变量会在使用执行器等时中断

有什么优雅的方法可以做到这一点吗?

我不是 Guice 用户,所以这个答案使用 Jersey 的 DI 框架,HK2。在基本配置层面,HK2 与 Guice 配置没有太大区别。例如,对于 Guice,您有 AbstractModule,而 HK2 有 AbstractBinder。对于这两个组件,您将使用类似的 bind(..).to(..).in(Scope) 语法。一个区别是 Guice 是 bind(Contract).to(Impl),而 HK2 是 bind(Impl).to(Contract).

HK2 也有 Factorys,允许更复杂地创建可注入对象。对于您的工厂,您将使用语法 bindFactory(YourFactory.class).to(YourContract.class).

话虽这么说,您可以使用类似以下的内容来实现您的用例。

  1. 为英语创建一个FactorySessionFactory

    public class EnglishSessionFactoryFactory implements Factory<SessionFactory> {
        @Override
        public SessionFactory provide() {
           ...
        }
        @Override
        public void dispose(SessionFactory t) {}
    }
    
  2. 为法语创建 Factory SessionFactory

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

    注意以上两个 SessionFactory 将在单例范围内按名称绑定。

  3. 创建另一个 Factory 将在请求范围内,这将使用请求上下文信息。该工厂将通过名称(使用名称绑定)注入上述两个 SessionFactory,并根据任何请求上下文信息,return 适当的 SessionFactory。下面的例子简单地使用了一个查询参数

    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. 然后你可以将 SessionFactory(我们会给它一个不同的名字)注入你的 dao。

    public class IDaoImpl implements IDao {
    
        private final SessionFactory sessionFactory;
    
        @Inject
        public IDaoImpl(@Named("SessionFactory") SessionFactory sessionFactory) {
            this.sessionFactory = sessionFactory;
        }
    }
    
  5. 要将所有内容绑定在一起,您将使用类似于以下实现的 AbstractBinder

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

    下面是活页夹的一些注意事项

    • 两种不同的语言特定 SessionFactory 由名称绑定。用于 @Named 注入,如您在步骤 3 中所见。
    • 做出决定的请求范围工厂也被赋予了名称。
    • 你会注意到 proxy(true).proxyForSameScope(false)。这是必需的,因为我们假设 IDao 将是一个单例,并且由于 "chosen" SessionFactory 我们在请求范围内,我们无法注入实际的 SessionFactory,因为它会随着请求的不同而变化,所以我们需要注入一个代理。如果 IDao 是请求范围的,而不是单例,那么我们可以省略这两行。将 dao 请求限定范围可能会更好,但我只是想展示它应该如何作为一个单例来完成。

      另请参阅 Injecting Request Scoped Objects into Singleton Scoped Object with HK2 and Jersey,了解有关此主题的更多检查。

  6. 然后你只需要在 Jersey 注册 AbstractBinder。为此,您可以只使用 ResourceConfigregister(...) 方法。 See also,如果需要web.xml配置。

就是这样。下面是使用 Jersey Test Framework 的完整测试。您可以 运行 它像任何其他 JUnit 测试一样。使用的 SessionFactory 只是一个虚拟 class,而不是实际的 Hibernate SessionFactory。这只是为了让示例尽可能简短,但只需将其替换为常规的 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 
 * 
 * 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));
    }
}

您可能还需要考虑的另一件事是 SessionFactory 的关闭。虽然 Factory 有一个 dispose() 方法,但它不能被 Jersey 可靠地调用。您可能想查看 ApplicationEventListener。您可以将 SessionFactory 注入其中,并在关闭事件时将其关闭。