authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
安德烈·希尔丁格的头像

Andre Hildinger

André is a versatile and talented developer with 10+ years of industry experience. 他精通Java、Java EE、JavaScript等.

工作经验

17

Share

When we talk about cloud applications where each client has their own separate data, 我们需要考虑如何存储和操作这些数据. 即使有所有伟大的NoSQL解决方案, 有时我们仍然需要使用老式的关系数据库. The first solution that might come to mind to separate data is to add an identifier in every table, 所以可以单独处理. 这是可行的,但是如果客户端请求他们的数据库呢? It would be very cumbersome to retrieve all those records hidden among the others.

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

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

不久前,Hibernate团队提出了一个解决这个问题的方案. They provide some extension points that enable one to control from where data should be retrieved. This solution has the option to control the data via an identifier column, 多个数据库, 还有多种模式. 本文将介绍多模式解决方案.

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

Getting Started

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

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

If you want to use the same tools as me, you can follow these steps to create your project:

  • 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

You can delete all files that come inside the source folders, as they are just sample files.

要运行项目, I use Wildfly, and I will show how to configure it (again you can use your favorite tool here):

  • 下载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
  • 选择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
  • In Eclipse, go to the Servers tab again, right-click on the server you created and select Start
  • On your browser, access http://localhost:9990, which is the Management Interface
  • 输入您刚刚创建的用户的凭据
  • 部署数据库的驱动程序jar:
    1. 转到Deployment选项卡并单击Add
    2. 单击Next,选择驱动程序jar文件
    3. 单击Next并完成
  • 进入Configuration选项卡
  • Choose Subsystems -> Datasources -> Non-XA
  • 单击Add,选择数据库并单击Next
  • 为数据源指定一个名称,然后单击Next
  • 选择Detect Driver选项卡并选择您刚刚部署的驱动程序
  • 输入数据库信息并单击Next
  • Click Test Connection if you want to make sure the information of the prior step is correct
  • 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 already has these dependencies, and it would cause conflicts with the app’s ones otherwise.

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

该文件的内容必须类似于以下内容, changing jta-data-source to match the datasource you created in Wildfly and the package com.toptal.andrehil.mt.hibernate to the one you are going to create in the next section (unless you choose the same package name):



    
        java:/JavaEEMTDS
        
            
            
            
        
    

Hibernate类

添加到持久性的配置.xml指向两个自定义类MultiTenantProvider和SchemaResolver. The first class is responsible for providing connections configured with the right schema. The second class is responsible for resolving the name of the schema to be used.

下面是这两个类的实现:

public class MultiTenantProvider implements 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
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        连接= getAnyConnection();
        try {
            connection.createStatement ().execute("SET SCHEMA '" + tenantIdentifier + "'");
        } catch (final SQLException e) {
            throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e);
        }
        返回连接;
    }
    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        try {
            connection.createStatement ().execute("SET SCHEMA 'public'");
        } catch (final SQLException e) {
            抛出新的HibernateException("试图更改schema [public]时出错",e);
        }
        connection.close();
    }
    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        releaseAnyConnection(连接);
    }
}

The syntax being used in the statements above work with PostgreSQL and some other databases, this must be changed in case your database has a different syntax to change the current schema.

公共类SchemaResolver实现CurrentTenantIdentifierResolver {

    private String tenantIdentifier = "public";

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

此时,已经可以测试应用程序了. For now, 我们的解析器直接指向一个硬编码的公共模式, 但它已经被调用了. 为此,请停止正在运行的服务器,然后重新启动它. You can try to run it in debug mode and place breakpoint at any point of the classes above to check if it is working.

解析器的实际使用

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

One way to achieve this is to keep an identifier in the header of all requests and then create a filter to inject the name of the schema.

让我们实现一个过滤器类来举例说明这种用法. 解析器可以通过Hibernate的SessionFactory访问, so we will take advantage of that to get it and inject the right schema name.

@Provider
AuthRequestFilter实现ContainerRequestFilter

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

    @Override
    public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory ();
        SchemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver ();

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

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

为了简单起见, the implementation shown here is getting the identifier directly from a string in the header, but it is a good idea to use an authentication token and store the identifier in the token. 如果你有兴趣了解更多关于这个主题, 我建议看看JSON Web令牌(JWT). JWT是一个用于令牌操作的漂亮而简单的库.

如何使用这一切

一切都配置好了, there is nothing else needed to do in your entities and/or classes that interact with EntityManager. Anything you run from an EntityManager will be directed to the schema resolved by the created filter.

Now, all you need to do is to intercept requests on the client side and inject the identifier/token in the header to be sent to the server side.

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

The link at the end of the article points to the project used to write this article. It uses Flyway to create 2 schemas and contains an entity class called Car and a rest service class called CarService 可以用来测试这个项目. 您可以遵循以下所有步骤, 而不是创建你自己的项目, 你可以克隆它,然后用这个. Then, when running you can use a simple HTTP client (like Postman extension for Chrome) and make a GET request to http://localhost:8080/javaee-mt/rest/cars with the headers key:value:

  • username:joe; or
  • username:fred.

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

Final Words

This is not the only solution to create multitenancy applications in the Java world, 但这是实现这一目标的简单方法.

One thing to keep in mind is that Hibernate doesn’t generate DDL when using multitenancy configuration. 我的建议是看看Flyway或liquubase, 哪些库可以很好地控制数据库的创建. 这是一件很好的事情,即使您不打算使用多租户, as the Hibernate team advises to not use their auto database generation in production.

The source code used to create this article and environment configuration can be found at github.com/andrehil/JavaEEMT

聘请Toptal这方面的专家.
Hire Now
安德烈·希尔丁格的头像
Andre Hildinger

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

Member since 2016年2月29日

About the author

André is a versatile and talented developer with 10+ years of industry experience. 他精通Java、Java EE、JavaScript等.

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

工作经验

17

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

订阅意味着同意我们的 privacy policy

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

订阅意味着同意我们的 privacy policy

Toptal开发者

Join the Toptal® community.