作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Andre Hildinger
Verified Expert in Engineering
17 Years of Experience

andr是一位多才多艺的开发人员,拥有10多年的行业经验. 他精通Java、Java EE、JavaScript等.

Share

当我们谈论云应用程序时,每个客户端都有自己独立的数据, 我们需要考虑如何存储和操作这些数据. 即使有所有伟大的NoSQL解决方案, 有时我们仍然需要使用老式的关系数据库. 首先想到的分离数据的解决方案是在每个表中添加标识符, 所以可以单独处理. 这是可行的,但是如果客户端请求他们的数据库呢? 要检索隐藏在其他记录中的所有记录将是非常麻烦的.

带有Hibernate的多租户Java EE应用程序

Java中的多租户使用Hibernate比以往任何时候都更容易.

不久前,Hibernate团队提出了一个解决这个问题的方案. 它们提供了一些扩展点,使人们能够控制从何处检索数据. 该解决方案可以选择通过标识符列控制数据, 多个数据库, 还有多种模式. 本文将介绍多模式解决方案.

所以,让我们开始工作吧!

Getting Started

如果你是一个更 有经验的Java开发人员 并且知道如何配置一切, 或者如果您已经有了自己的Java EE项目, 你可以跳过这一节.

首先,我们必须创建一个新的Java项目. 我正在使用Eclipse和Gradle, 但是您可以使用自己喜欢的IDE和构建工具, 比如IntelliJ和Maven.

如果你想使用和我一样的工具,你可以按照下面的步骤来创建你的项目:

  • Install Gradle plugin on Eclipse
  • Click on File -> New -> Other…
  • 找到Gradle (STS)并单击Next
  • 通知一个名称,并为示例项目选择Java快速入门
  • Click Finish

Great! 这应该是初始文件结构:

javaee-mt
|- src/main/java
| - src / main /资源
|- src/test/java
| - src /测试/资源
| JRE系统库
Gradle Dependencies
|- build
|- src
|- build.gradle

您可以删除源文件夹中的所有文件,因为它们只是示例文件.

要运行项目, I use Wildfly, 我将展示如何配置它(你也可以在这里使用你最喜欢的工具):

  • 下载Wildfly: http://wildfly./downloads/(我使用版本10)
  • Unzip the file
  • Install the JBoss Tools插件 on Eclipse
  • On the Servers tab, right-click any blank area and choose New -> Server
  • Choose Wildfly 10.x (9.如果10不可用,x也可以工作,这取决于您的Eclipse版本)
  • 单击Next,选择Create New Runtime(下一页),然后再次单击Next
  • 选择解压Wildfly的文件夹作为主目录
  • Click Finish

现在,让我们配置Wildfly来了解数据库:

  • 进入Wildfly文件夹中的bin文件夹
  • Execute add-user.bat or add-user.sh(取决于您的操作系统)
  • 按照以下步骤将用户创建为Manager
  • 在Eclipse中,再次转到Servers选项卡,右键单击您创建的服务器并选择Start
  • 在浏览器上访问http://localhost:9990,这是管理界面
  • 输入您刚刚创建的用户的凭据
  • 部署数据库的驱动程序jar:
    1. 转到Deployment选项卡并单击Add
    2. 单击Next,选择驱动程序jar文件
    3. 单击Next并完成
  • 进入Configuration选项卡
  • Choose Subsystems -> Datasources -> Non-XA
  • 单击Add,选择数据库并单击Next
  • 为数据源指定一个名称,然后单击Next
  • 选择Detect Driver选项卡并选择您刚刚部署的驱动程序
  • 输入数据库信息并单击Next
  • 如果要确保前一步的信息正确,请单击“测试连接”
  • Click Finish
  • 返回到Eclipse并停止正在运行的服务器
  • 右键单击它,选择添加和删除
  • 将您的项目添加到右侧
  • Click Finish

好了,我们将Eclipse和Wildfly配置在一起!

这是项目外部所需的所有配置. 让我们转到项目配置.

引导项目

现在我们已经配置了Eclipse和Wildfly,并创建了我们的项目, 我们需要配置我们的项目.

我们要做的第一件事是编辑build.gradle. 它应该是这样的:

应用插件:'java'
应用插件:'war'
应用插件:'eclipse'
应用插件:'eclipse-wtp'

sourccompatibility = '1.8'
compileJava.options.encoding = 'UTF-8'

compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'


repositories {
    jcenter()
}

eclipse {
    wtp {
    }
}

dependencies {
    providedCompile”组织.hibernate: hibernate-entitymanager: 5.0.7.Final'
    providedCompile”组织.jboss.resteasy: resteasy-jaxrs: 3.0.14.Final'
    providedCompile javax: javaee-api: 7.0'
}

依赖项都声明为" providedCompile ", 因为这个命令不会在最终的war文件中添加依赖项. Wildfly已经有了这些依赖项,否则它会与应用程序的依赖项产生冲突.

At this point, 您可以右键单击您的项目, select Gradle (STS) -> Refresh All to import the dependencies we just declared.

是时候创建和配置“持久性”了.xml”文件,该文件包含Hibernate需要的信息:

  • 在src/main/resource source文件夹中,创建一个名为META-INF的文件夹
  • 在这个文件夹中,创建一个名为persistence的文件.xml

该文件的内容必须类似于以下内容, 更改jta-data-source以匹配您在Wildfly中创建的数据源 package com.toptal.andrehil.mt.hibernate 到您将在下一节中创建的包(除非您选择相同的包名):



    
        java:/JavaEEMTDS
        
            
            
            
        
    

Hibernate类

添加到持久性的配置.xml指向两个自定义类MultiTenantProvider和SchemaResolver. 第一个类负责提供配置了正确模式的连接. 第二个类负责解析要使用的模式的名称.

下面是这两个类的实现:

公共类MultiTenantProvider实现MultiTenantConnectionProvider, ServiceRegistryAwareService {

    private static final serialVersionUID = 1L;
    private DataSource;

    @Override
    公共布尔支持侵略性释放(){
        return false;
    }
    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        try {
            final Context init = new InitialContext();
            dataSource = (dataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name
        } catch (final NamingException e) {
            抛出新的RuntimeException(e);
        }
    }
    @SuppressWarnings(“rawtypes”)
    @Override
    公共布尔isUnwrappableAs(类clazz) {
        return false;
    }
    @Override
    public  T unwrap(Class clazz) {
        return null;
    }
    @Override
    getAnyConnection()抛出SQLException {
        最终连接连接=数据源.getConnection();
        返回连接;
    }
    @Override
    getConnection(String tenantIdentifier)抛出SQLException {
        连接= getAnyConnection();
        try {
            connection.createStatement ().execute("SET SCHEMA '" + tenantIdentifier + "'");
        } catch (final SQLException e) {
            抛出新的HibernateException("试图修改schema [" + tenantIdentifier + "]时出错",e);
        }
        返回连接;
    }
    @Override
    公共void releaseAnyConnection(连接连接)抛出SQLException {
        try {
            connection.createStatement ().execute("SET SCHEMA 'public'");
        } catch (final SQLException e) {
            抛出新的HibernateException("试图更改schema [public]时出错",e);
        }
        connection.close();
    }
    @Override
    公共void releaseConnection(字符串tenantIdentifier,连接)抛出SQLException {
        releaseAnyConnection(连接);
    }
}

上述语句中使用的语法适用于PostgreSQL和其他一些数据库, 如果数据库有不同的语法来更改当前模式,则必须更改此设置.

公共类SchemaResolver实现CurrentTenantIdentifierResolver {

    private String tenantIdentifier = "public";

    @Override
    resolveCurrentTenantIdentifier() {
        返回tenantIdentifier;
    }
    @Override
    公共布尔validateexistingcurrentssessions () {
        return false;
    }
    公共无效setTenantIdentifier(字符串tenantIdentifier) {
        this.tenantIdentifier = tenantIdentifier;
    }
}

此时,已经可以测试应用程序了. For now, 我们的解析器直接指向一个硬编码的公共模式, 但它已经被调用了. 为此,请停止正在运行的服务器,然后重新启动它. 您可以尝试在调试模式下运行它,并在上述类的任何点设置断点,以检查它是否正在工作.

解析器的实际使用

那么,解析器如何包含模式的正确名称呢?

实现这一点的一种方法是在所有请求的头中保留一个标识符,然后创建一个过滤器来注入模式的名称.

让我们实现一个过滤器类来举例说明这种用法. 解析器可以通过Hibernate的SessionFactory访问, 因此,我们将利用它来获取它并注入正确的模式名称.

@Provider
AuthRequestFilter实现ContainerRequestFilter

    @PersistenceUnit(unitName = "pu")
    实体管理工厂;

    @Override
    public void filter(ContainerRequestContext)抛出IOException {
        最终SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory ();
        SchemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver ();

        用户名= containerRequestContext.getHeaderString(“用户名”);
        schemaResolver.setTenantIdentifier(用户名);
    }
}

Now, 当任何类获得EntityManager来访问数据库时, 它已经配置了正确的模式.

为了简单起见, 这里展示的实现是直接从头中的字符串获取标识符, 但是使用身份验证令牌并将标识符存储在令牌中是一个好主意. 如果你有兴趣了解更多关于这个主题, 我建议看看JSON Web令牌(JWT). JWT是一个用于令牌操作的漂亮而简单的库.

如何使用这一切

一切都配置好了, 在与之交互的实体和/或类中不需要做任何其他事情 EntityManager. 从EntityManager运行的任何内容都将被定向到由创建的过滤器解析的模式.

Now, 您所需要做的就是在客户端拦截请求,并在要发送到服务器端的报头中注入标识符/令牌.

在实际应用程序中,您将拥有更好的身份验证方法. 然而,多租户的一般概念将保持不变.

本文末尾的链接指向用于编写本文的项目. 它使用Flyway创建2个模式,并包含一个名为Car的实体类和一个名为rest的服务类 CarService 可以用来测试这个项目. 您可以遵循以下所有步骤, 而不是创建你自己的项目, 你可以克隆它,然后用这个. Then, 当运行时,你可以使用一个简单的HTTP客户端(如Chrome邮差扩展),并使一个GET请求http://localhost:8080/javaee-mt/rest/cars与头键:值:

  • username:joe; or
  • username:fred.

By doing this, 请求将返回不同的值, 哪些在不同的模式中, 一只叫乔,另一只叫弗雷德。.

Final Words

这并不是Java世界中创建多租户应用程序的唯一解决方案, 但这是实现这一目标的简单方法.

需要记住的一点是,在使用多租户配置时,Hibernate不会生成DDL. 我的建议是看看Flyway或liquubase, 哪些库可以很好地控制数据库的创建. 这是一件很好的事情,即使您不打算使用多租户, 因为Hibernate团队建议不要在生产中使用他们的自动数据库生成.

用于创建本文和环境配置的源代码可在 github.com/andrehil/JavaEEMT

聘请Toptal这方面的专家.
Hire Now
Andre Hildinger

Andre Hildinger

Verified Expert in Engineering
17 Years of Experience

森林公园,伊利诺伊州,美国

2016年2月29日成为会员

About the author

andr是一位多才多艺的开发人员,拥有10多年的行业经验. 他精通Java、Java EE、JavaScript等.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal开发者

Join the Toptal® community.