前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用Spring Boot,JPA,Hibernate和Postgres的多租户应用程序

使用Spring Boot,JPA,Hibernate和Postgres的多租户应用程序

作者头像
Java架构师历程
发布2018-09-26 15:15:54
7.6K1
发布2018-09-26 15:15:54
举报
文章被收录于专栏:Java架构师历程Java架构师历程
1.使用SPRING BOOT,JPA,HIBERNATE和POSTGRES的多租户应用程序

多租户是一种方法,应用程序实例由不同的客户使用,从而降低软件开发和部署成本,与单一租户解决方案相比,在这种解决方案中,需要触及多个部分以提供新客户端或更新现有租户。

实施这种架构有多种众所周知的策略,从高度孤立(如单租户)到共享的一切。

在这篇文章中,我将回顾使用Spring Boot,JPA,Hibernate和Postgres来检查多个数据库和一个API服务的多租户解决方案。

2.需求
  • Java 8或Java 7.对于Java 7,内部的java.version属性pom.xml需要相应更新。
  • Maven 3.3.x
  • 熟悉Spring框架。
  • Postgres服务器或Docker主机。
3.设置POSTGRES DVD租用数据库

asimio / db_dvdrental 集成测试中使用Spring Boot,Postgres和Docker创建的Docker映像将用于启动两个容器,每个容器映射到不同的Docker主机端口:

代码语言:javascript
复制
docker run -d -p 5432:5432 -e DB_NAME=db_dvdrental -e DB_USER=user_dvdrental -e DB_PASSWD=changeit asimio/db_dvdrental:latest
83c9ac6f53b4995cb38796b70593585fbab8cc7ad15bcc580d28f773d9621055
docker run -d -p 5532:5432 -e DB_NAME=db_dvdrental -e DB_USER=user_dvdrental -e DB_PASSWD=changeit asimio/db_dvdrental:latest
004bf55f9576361bb3a674e31bcb4d6f20ca7c875fe91e146289ec8aaf7abe27

另一种方法是在同一台服务器上创建数据库,但在保持相同模式的同时对其进行不同的命名。

4.区分租户

现在数据库设置可以区分他们更新数据库中的一行,5532因此可以根据租户信息清楚地使用哪一个数据库:

代码语言:javascript
复制
psql -h 172.16.69.133 -p 5532 -U user_dvdrental -d db_dvdrental
psql (9.4.4, server 9.5.3)
WARNING: psql major version 9.4, server major version 9.5.
         Some psql features might not work.
Type "help" for help.

db_dvdrental=> select * from Actor where actor_id = 1;
 actor_id | first_name | last_name |      last_update
----------+------------+-----------+------------------------
        1 | Penelope   | Guiness   | 2013-05-26 14:47:57.62
(1 row)

db_dvdrental=> update actor set first_name = 'Orlando', last_name = 'Otero' where actor_id = 1;
UPDATE 1

db_dvdrental=> \q
5.创建弹簧引导程序
代码语言:javascript
复制
curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.3.RELEASE -d dependencies=actuator,web,data-jpa -d language=java -d type=maven-project -d baseDir=springboot-hibernate-multitenancy -d groupId=com.mushsoft.demo.api -d artifactId=springboot-hibernate-multitenancy -d version=0-SNAPSHOT | tar -xzvf -

这个命令将在一个文件夹中创建一个Maven项目,该文件夹springboot-hibernate-multitenancy中随附的源代码中使用的大多数依赖项都被命名。

或者,也可以使用Spring Initializr工具生成,然后选择Actuator,Web和JPA依赖项,如下所示:

6. JPA实体

使用Spring Boot,Postgres和Docker集成测试中也介绍了从数据库模式生成JPA实体,因此我只需将com.mushsoft.dvdrental.model它的Bitbucket随附的源代码src/main/java文件复制到文件夹即可。

7.配置持久层

由于演示应用程序将支持多租户,因此需要手动配置持久层,与所有Spring应用程序类似。它将由定义和配置组成:

  • Hibernate,JPA和数据源属性。
  • 数据源bean。
  • 实体管理器工厂bean。
  • 事务管理器bean。
  • Spring Data JPA和事务支持(通过@Transactional注释)配置。

为了实现这一点,我们首先从Spring Boot应用程序入口点开始排除一些Spring Boot AutoConfiguration行为,这意味着应用程序需要显式配置数据源,Hibernate和JPA相关的bean:

Application.java

代码语言:javascript
复制
package com.mushsoft.demo.main;
...
@SpringBootApplication(
  exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class },
  scanBasePackages = { "com.mushsoft.demo.config", "com.mushsoft.demo.rest" }
)
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

com.mushsoft.demo.config并且com.mushsoft.demo.rest包将被扫描以查找@Component衍生的注释类。

注意:排除前面提到的自动配置行为也可以使用application.yml中的以下设置来完成:

代码语言:javascript
复制
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
      - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
7.1 HIBERNATE,JPA和数据库属性

application.yml

代码语言:javascript
复制
...
spring:
  jpa:
    database: POSTGRESQL
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    generate-ddl: false
    hibernate:
      ddl-auto: none
...
multitenancy:
  dvdrental:
    dataSources:
      -
        tenantId: tenant_1
        url: jdbc:postgresql://172.16.69.133:5432/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
      -
        tenantId: tenant_2
        url: jdbc:postgresql://172.16.69.133:5532/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
...

简单的JPA,Hibernate和数据源配置属性。没有DDL将产生或执行,因为数据库架构已经到位。该数据源的前缀为multitenancy.dvdrental读入的Java类的属性感谢YAML支持加入到春天,但更多关于这个未来。

MultiTenantJpaConfiguration.java

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 @Configuration
 4 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 5 @ImportResource(locations = { "classpath:applicationContent.xml" })
 6 @EnableTransactionManagement
 7 public class MultiTenantJpaConfiguration {
 8 
 9   @Autowired
10   private JpaProperties jpaProperties;
11 
12   @Autowired
13   private MultiTenantDvdRentalProperties multiTenantDvdRentalProperties;
14 ...
15 }

这是所有与JPA相关的bean都被实例化的Java类。@Configuration指定这个类将提供定义Bean的@Bean注解方法,这些方法将由Spring容器管理。

另请注意,作为第4行中的@EnableConfigurationProperties注释的结果,JpaProperties和MultiTenantDvdRentalProperties实例是如何被注入的。

JpaProperties由设置弹簧引导,它将包括前缀配置属性spring.jpa所定义的前面

MultiTenantDvdRentalProperties是一个简单的Java类,如下所示,为此演示创建,并将包含前缀为的属性multitenancy.dvdrental,它基本上是租户信息和数据源数据,用于建立与数据库的连接。

MultiTenantDvdRentalProperties.java

代码语言:javascript
复制
package com.mushsoft.demo.config.dvdrental;
...
@Configuration
@ConfigurationProperties(prefix = "multitenancy.dvdrental")
public class MultiTenantDvdRentalProperties {

  private List<DataSourceProperties> dataSourcesProps;
  // Getters and Setters

  public static class DataSourceProperties extends org.springframework.boot.autoconfigure.jdbc.DataSourceProperties {

    private String tenantId;
    // Getters and Setters
  }
}
7.2数据库BEAN

MultiTenantJpaConfiguration.java 继续:

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 @Configuration
 4 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 5 @ImportResource(locations = { "classpath:applicationContent.xml" })
 6 @EnableTransactionManagement
 7 public class MultiTenantJpaConfiguration {
 8 ...
 9   @Bean(name = "dataSourcesDvdRental" )
10   public Map<String, DataSource> dataSourcesDvdRental() {
11     Map<String, DataSource> result = new HashMap<>();
12     for (DataSourceProperties dsProperties : this.multiTenantDvdRentalProperties.getDataSources()) {
13       DataSourceBuilder factory = DataSourceBuilder
14         .create()
15         .url(dsProperties.getUrl())
16         .username(dsProperties.getUsername())
17         .password(dsProperties.getPassword())
18         .driverClassName(dsProperties.getDriverClassName());
19       result.put(dsProperties.getTenantId(), factory.build());
20     }
21     return result;
22   }
23 ...
24 }

这是一个bean,它使用前面描述的MultiTenantDvdRentalProperties类的注入实例将每个租户id与其数据源进行映射。

7.3实体经理工厂BEAN

MultiTenantJpaConfiguration.java 继续:

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 @Configuration
 4 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 5 @ImportResource(locations = { "classpath:applicationContent.xml" })
 6 @EnableTransactionManagement
 7 public class MultiTenantJpaConfiguration {
 8 ...
 9   @Bean
10   public MultiTenantConnectionProvider multiTenantConnectionProvider() {
11     // Autowires dataSourcesDvdRental
12     return new DvdRentalDataSourceMultiTenantConnectionProviderImpl();
13   }
14 
15   @Bean
16   public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
17     return new TenantDvdRentalIdentifierResolverImpl();
18   }
19 
20   @Bean
21   public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
22     CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
23 
24     Map<String, Object> hibernateProps = new LinkedHashMap<>();
25     hibernateProps.putAll(this.jpaProperties.getProperties());
26     hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
27     hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
28     hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
29 
30     // No dataSource is set to resulting entityManagerFactoryBean
31     LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
32     result.setPackagesToScan(new String[] { Actor.class.getPackage().getName() });
33     result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
34     result.setJpaPropertyMap(hibernateProps);
35 
36     return result;
37   }
38 ...
39 }

为了让entityManagerFactory bean可以感知多租户,它的配置属性需要包含多租户策略,多租户连接提供程序和租户标识符解析器实现,这些都是在26到28行以及JPA中配置的在application.yml中定义并在这里解释的属性。

至于多租户策略,Hibernate支持:

战略

实施细节

数据库

每个租户都有一个数据库。

SCHEMA

每个租户的架构。

DISCRIMINATOR

用于指定不同租户的一个或多个表列。在Hibernate 5中添加

需求不是将数据源设置为entityManagerFactory bean,因为它将从下面详细介绍的MultiTenantConnectionProvider和CurrentTenantIdentifierResolver实现中检索。

DvdRentalDataSourceMultiTenantConnectionProviderImpl.java

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 public class DvdRentalDataSourceMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
 4 ...
 5   @Autowired
 6   private Map<String, DataSource> dataSourcesDvdRental;
 7 
 8   @Override
 9   protected DataSource selectAnyDataSource() {
10     return this.dataSourcesDvdRental.values().iterator().next();
11   }
12 
13   @Override
14   protected DataSource selectDataSource(String tenantIdentifier) {
15     return this.dataSourcesDvdRental.get(tenantIdentifier);
16   }
17 ...
18 }

此MultiTenantConnectionProvider实现使用此处讨论的数据源Map来从租户标识符中查找预期的数据源,该标识符是从CurrentTenantIdentifierResolver实现中接下来查看的。

TenantDvdRentalIdentifierResolverImpl.java

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 public class TenantDvdRentalIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
 4 
 5   private static String DEFAULT_TENANT_ID = "tenant_1";
 6 
 7   @Override
 8   public String resolveCurrentTenantIdentifier() {
 9     String currentTenantId = DvdRentalTenantContext.getTenantId();
10     return (currentTenantId != null) ? currentTenantId : DEFAULT_TENANT_ID;
11   }
12 ...
13 }

用于此演示的CurrentTenantIdentifierResolver实现是一种简单的将租户选择委托给DvdRentalTenantContext静态方法的方法,该方法使用ThreadLocal引用来存储和检索租户数据。

这种方法的一个优点是,不需要使用请求URL或HTTP Header来解析租户标识符,而是可以在不需要启动servlet容器的情况下测试Repository层。

DvdRentalTenantContext.java

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 public class DvdRentalTenantContext {
 4 
 5   private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
 6 
 7   public static void setTenantId(String tenantId) {
 8     CONTEXT.set(tenantId);
 9   }
10 
11   public static String getTenantId() {
12     return CONTEXT.get();
13   }
14 
15   public static void clear() {
16     CONTEXT.remove();
17   }
18 }
7.4交易经理BEAN

MultiTenantJpaConfiguration.java 继续:

代码语言:javascript
复制
 1 package com.mushsoft.demo.config.dvdrental;
 2 ...
 3 @Configuration
 4 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 5 @ImportResource(locations = { "classpath:applicationContent.xml" })
 6 @EnableTransactionManagement
 7 public class MultiTenantJpaConfiguration {
 8 ...
 9   @Bean
10   public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
11     return entityManagerFactoryBean.getObject();
12   }
13 
14   @Bean
15   public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
16     SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
17     HibernateTransactionManager result = new HibernateTransactionManager();
18     result.setAutodetectDataSource(false);
19     result.setSessionFactory(sessionFactory);
20     return result;
21   }
22 ...
23 }

再次,这是我一直在审查所有与JPA相关的bean实例化的Java类。这里需要注意的重要的事情是,txManager bean需要解包EntityManagerFactory实现,在这种情况下,Hibernate的SessionFactory将AutodetectDataSource属性设置为false,这是多租户使用本文讨论的方法的要求。

7.5配置弹簧数据JPA和注释驱动的事务

applicationContent.xml

代码语言:javascript
复制
...
<jpa:repositories base-package="com.mushsoft.dvdrental.dao" transaction-manager-ref="txManager" />
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
...

通过在MultiTenantJpaConfiguration类中找到的@ImportResource注释导入,package包含Spring JPA Data实例化Repository(或Dao)bean的接口。com.mushsoft.dvdrental.dao

代码语言:javascript
复制
package com.mushsoft.dvdrental.dao;
...
public interface ActorDao extends JpaRepository<Actor, Integer> {
}

tx:注解驱动允许使用@Transactional注释的类方法的执行被包装在数据库事务中,而无需手动处理连接或事务。

8.休息层

REST层将实现一个Demo REST资源来演示本文描述的多租户方法。它将由REST资源,Spring拦截器组成,用于选择和设置租户标识符以及将拦截器与REST资源相关联的配置。

DemoResource.java

代码语言:javascript
复制
package com.mushsoft.demo.rest;
...
@RestController
@RequestMapping(value = "/demo")
@Transactional
public class DemoResource {

  @Autowired
  private ActorDao actorDao;

  @RequestMapping(method = RequestMethod.GET)
  public String getDemo() {
    Actor actor = this.actorDao.getOne(1);
    return String.format("[actor: %s %s], [DemoResource instance: %s], [ActorDao instance: %s]", actor.getFirstName(),
      actor.getLastName(), this, this.actorDao);
  }
...
}

为了保持这篇文章和示例代码的简单性,我决定将Repository依赖项注入到REST相关类中,在一个更严重或复杂的应用程序中,我会建议实现一个Service类,其中将使用一个或多个Dao依赖关系以及对象映射器/转换器,以防止模型泄漏到资源层。

DvdRentalMultiTenantInterceptor.java

代码语言:javascript
复制
package com.mushsoft.demo.rest;
...
public class DvdRentalMultiTenantInterceptor extends HandlerInterceptorAdapter {

  private static final String TENANT_HEADER_NAME = "X-TENANT-ID";

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String tenantId = request.getHeader(TENANT_HEADER_NAME);
    DvdRentalTenantContext.setTenantId(tenantId);
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    DvdRentalTenantContext.clear();
  }
...
}

这个Spring拦截器将使用包含在DvdRentalTenantContext中的基于ThreadLocal的实现来设置通过HTTP Header传递的租户信息。另一个选择是在URL中传递租户标识符或通过BEARER标记。尽管这篇文章使用了拦截器,但servlet过滤器可能已经被实现并被配置

WebMvcConfiguration.java

代码语言:javascript
复制
package com.mushsoft.demo.rest;
...
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new DvdRentalMultiTenantInterceptor());
  }
...
}

此配置由Spring Boot自动完成,但需要明确配置为将DvdRentalMultiTenantInterceptor拦截器与REST请求关联。

9.运行演示服务
代码语言:javascript
复制
cd <path to service>/springboot-hibernate-multitenancy/
mvn spring-boot:run

向DemoResource类中/demo实现的资源发送请求,在头中传递租户信息:X-TENANT-ID

9.1租客1
代码语言:javascript
复制
curl -v -H "X-TENANT-ID: tenant_1" "http://localhost:8800/demo"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8800 (#0)
> GET /demo HTTP/1.1
> Host: localhost:8800
> User-Agent: curl/7.51.0
> Accept: */*
> X-TENANT-ID: tenant_1
>
< HTTP/1.1 200
< X-Application-Context: application:8800
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 193
< Date: Sat, 07 Jan 2017 04:43:47 GMT
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
[actor: Penelope Guiness], [DemoResource instance: com.mushsoft.demo.rest.DemoResource@6b2e9db2], [ActorDao instance: org.springframework.data.jpa.repository.support.SimpleJpaRepository@7e970e0c]
9.2租户2
代码语言:javascript
复制
curl -v -H "X-TENANT-ID: tenant_2" "http://localhost:8800/demo"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8800 (#0)
> GET /demo HTTP/1.1
> Host: localhost:8800
> User-Agent: curl/7.51.0
> Accept: */*
> X-TENANT-ID: tenant_2
>
< HTTP/1.1 200
< X-Application-Context: application:8800
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 190
< Date: Sat, 07 Jan 2017 04:39:18 GMT
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
[actor: Orlando Otero], [DemoResource instance: com.mushsoft.demo.rest.DemoResource@6b2e9db2], [ActorDao instance: org.springframework.data.jpa.repository.support.SimpleJpaRepository@7e970e0c]

请注意响应中的actor部分如何变化,X-TENANT-ID因为每个请求的标头中都会传递不同的承租人。另外值得一提的是,DemoResource和ActorDao实例的实例ID 相同,这意味着即使多租户已完成,它们仍然是使用正确数据源的单例实例。

10.参考文献
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年3月2日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.使用SPRING BOOT,JPA,HIBERNATE和POSTGRES的多租户应用程序
  • 2.需求
  • 3.设置POSTGRES DVD租用数据库
  • 4.区分租户
  • 5.创建弹簧引导程序
  • 6. JPA实体
  • 7.配置持久层
    • 7.1 HIBERNATE,JPA和数据库属性
      • 7.2数据库BEAN
        • 7.3实体经理工厂BEAN
          • 7.4交易经理BEAN
            • 7.5配置弹簧数据JPA和注释驱动的事务
            • 8.休息层
            • 9.运行演示服务
              • 9.1租客1
                • 9.2租户2
                • 10.参考文献
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档