1. 概述
笔者从2014年开始接触SaaS(Software as a Service),即多租户(或多承租)软件应用平台;并一直从事相关领域的架构设计及研发工作。机缘巧合,在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究,从中收获颇多。最早接触SaaS时,国内相关资源匮乏,唯一有的参照资料是《互联网时代的软件革命:SaaS架构设计》(叶伟等著)一书。最后课题的实现是基于OSGI(Open Service Gateway Initiative)Java动态模块化系统规范来实现的。
时至今日,五年的时间过去了,软件开发的技术发生了巨大的改变,笔者所实现SaaS平台的技术栈也更新了好几波,真是印证了那就话:“山重水尽疑无路,柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑,以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统,决定写一篇文章聊一聊关于SaaS的硬核技术。
说起SaaS,它只是一种软件架构,并没有多少神秘的东西,也不是什么很难的系统,我个人的感觉,SaaS平台的难度在于商业上的运营,而非技术上的实现。就技术上来说,SaaS是这样一种架构模式:它让多个不同环境的用户使用同一套应用程序,且保证用户之间的数据相互隔离。现在想想看,这也有点共享经济的味道在里面。
笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现,而其他的部分有兴趣的朋友可以在此基础上自行扩展。
2. 尝试了解多租户的应用场景
假设我们需要开发一个应用程序,并且希望将同一个应用程序销售给N家客户使用。在常规情况下,我们需要为此创建N个Web服务器(Tomcat),N个数据库(DB),并为N个客户部署相同的应用程序N次。现在,如果我们的应用程序进行了升级或者做了其他任何的改动,那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来,如果业务开始增长,客户由原来的N个变成了现在的N+M个,我们将面临N个应用程序和M个应用程序版本维护,设备维护以及成本控制的问题。运维几乎要哭死在机房了...
为了解决上述的问题,我们可以开发多租户应用程序,我们可以根据当前用户是谁,从而选择对应的数据库。例如,当请求来自A公司的用户时,应用程序就连接A公司的数据库,当请求来自B公司的用户时,自动将数据库切换到B公司数据库,以此类推。从理论上将没有什么问题,但我们如果考虑将现有的应用程序改造成SaaS模式,我们将遇到第一个问题:如果识别请求来自哪一个租户?如何自动切换数据源?
3. 维护、识别和路由租户数据源
我们可以提供一个独立的库来存放租户信息,如数据库名称、链接地址、用户名、密码等,这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决,下面列举几个常用的方式:
-  1.可以通过域名的方式来识别租户:我们可以为每一个租户提供一个唯一的二级域名,通过二级域名就可以达到识别租户的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我们识别租户的关键信息。 
-  2.可以将租户信息作为请求参数传递给服务端,为服务端识别租户提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。 
-  3.可以在请求头(Header)中设置租户信息,例如JWT等技术,服务端通过解析Header中相关参数以获得租户信息。 
-  4.在用户成功登录系统后,将租户信息保存在Session中,在需要的时候从Session取出租户信息。 
解决了上述问题后,我们再来看看如何获取客户端传入的租户信息,以及在我们的业务代码中如何使用租户信息(最关键的是DataSources的问题)。
我们都知道,在启动Spring Boot应用程序之前,就需要为其提供有关数据源的配置信息(有使用到数据库的情况下),按照一开始的需求,有N个客户需要使用我们的应用程序,我们就需要提前配置好N个数据源(多数据源),如果N<50,我认为我还能忍受,如果更多,这样显然是无法接受的。为了解决这一问题,我们需要借助Hibernate 5提供的动态数据源特性,让我们的应用程序具备动态配置客户端数据源的能力。简单来说,当用户请求系统资源时,我们将用户提供的租户信息(tenantId)存放在ThreadLoacal中,紧接着获取TheadLocal中的租户信息,并根据此信息查询单独的租户库,获取当前租户的数据配置信息,然后借助Hibernate动态配置数据源的能力,为当前请求设置数据源,最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息(租户数据库配置库),其余的数据源动态查询配置。接下来,我们将快速的演示这一功能。
4. 项目构建
我们将使用Spring Boot 2.1.5版本来实现这一演示项目,首先你需要在Maven配置文件中加入如下的一些配置:
  
   - 
    
     
    
    
     
      <dependencies>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-starter
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-devtools
      </artifactId>
     
    
- 
    
     
    
    
     
                  
      <scope>runtime
      </scope>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.projectlombok
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>lombok
      </artifactId>
     
    
- 
    
     
    
    
     
                  
      <optional>true
      </optional>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-starter-test
      </artifactId>
     
    
- 
    
     
    
    
     
                  
      <scope>test
      </scope>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-starter-data-jpa
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-starter-web
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-configuration-processor
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>mysql
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>mysql-connector-java
      </artifactId>
     
    
- 
    
     
    
    
     
                  
      <version>5.1.47
      </version>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.springframework.boot
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>spring-boot-starter-freemarker
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
              
      <dependency>
     
    
- 
    
     
    
    
     
                  
      <groupId>org.apache.commons
      </groupId>
     
    
- 
    
     
    
    
     
                  
      <artifactId>commons-lang3
      </artifactId>
     
    
- 
    
     
    
    
     
              
      </dependency>
     
    
- 
    
     
    
    
     
          
      </dependencies>
     
    
 然后提供一个可用的配置文件,并加入如下的内容:
  
   - 
    
     
    
    
     
      spring:
     
    
- 
    
     
    
    
     
        freemarker:
     
    
- 
    
     
    
    
     
          cache: false
     
    
- 
    
     
    
    
     
          template-loader-path:
     
    
- 
    
     
    
    
     
          - classpath:/templates/
     
    
- 
    
     
    
    
     
          prefix:
     
    
- 
    
     
    
    
     
          suffix: .html
     
    
- 
    
     
    
    
     
        resources:
     
    
- 
    
     
    
    
     
          static-locations:
     
    
- 
    
     
    
    
     
          - classpath:/static/
     
    
- 
    
     
    
    
     
        devtools:
     
    
- 
    
     
    
    
     
          restart:
     
    
- 
    
     
    
    
     
            enabled: true
     
    
- 
    
     
    
    
     
        jpa:
     
    
- 
    
     
    
    
     
          database: mysql
     
    
- 
    
     
    
    
     
          show-sql: true
     
    
- 
    
     
    
    
     
          generate-ddl: false
     
    
- 
    
     
    
    
     
          hibernate:
     
    
- 
    
     
    
    
     
            ddl-auto: 
      none
     
    
- 
    
     
    
    
     
      una:
     
    
- 
    
     
    
    
     
        
      master:
     
    
- 
    
     
    
    
     
          
      datasource:
     
    
- 
    
     
    
    
     
            
      url: 
      jdbc:
      mysql:
      //localhost:3306/master_tenant?useSSL=false
     
    
- 
    
     
    
    
     
            
      username: root
     
    
- 
    
     
    
    
     
            
      password: root
     
    
- 
    
     
    
    
     
            
      driverClassName: com.mysql.jdbc.Driver
     
    
- 
    
     
    
    
     
            
      maxPoolSize: 
      10
     
    
- 
    
     
    
    
     
            
      idleTimeout: 
      300000
     
    
- 
    
     
    
    
     
            
      minIdle: 
      10
     
    
- 
    
     
    
    
     
            
      poolName: master-database-connection-pool
     
    
- 
    
     
    
    
     
      logging:
     
    
- 
    
     
    
    
     
        
      level:
     
    
- 
    
     
    
    
     
          
      root: warn
     
    
- 
    
     
    
    
     
          
      org:
     
    
- 
    
     
    
    
     
            
      springframework:
     
    
- 
    
     
    
    
     
              
      web: debug
     
    
- 
    
     
    
    
     
            
      hibernate: debug
     
    
 由于采用Freemarker作为视图渲染引擎,所以需要提供Freemarker的相关技术una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息,你可以理解为主库。
接下来,我们需要关闭Spring Boot自动配置数据源的功能,在项目主类上添加如下的设置:
  
   - 
    
     
    
    
     
      @SpringBootApplication(exclude = {
      DataSourceAutoConfiguration.
      class})
     
    
- 
    
     
    
    
     
      public 
      class 
      UnaSaasApplication {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      public 
      static 
      void 
      main(
      String[] args) {
     
    
- 
    
     
    
    
     
              
      SpringApplication.
      run(
      UnaSaasApplication.
      class, args);
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      }
     
    
最后,让我们看看整个项目的结构:

5. 实现租户数据源查询模块
我们将定义一个实体类存放租户数据源信息,它包含了租户名,数据库连接地址,用户名和密码等信息,其代码如下:
  
   - 
    
     
    
    
     
      @Data
     
    
- 
    
     
    
    
     
      @Entity
     
    
- 
    
     
    
    
     
      @Table(name = 
      "MASTER_TENANT")
     
    
- 
    
     
    
    
     
      @NoArgsConstructor
     
    
- 
    
     
    
    
     
      @AllArgsConstructor
     
    
- 
    
     
    
    
     
      @Builder
     
    
- 
    
     
    
    
     
      public class MasterTenant implements Serializable{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Id
     
    
- 
    
     
    
    
     
          
      @Column(name=
      "ID")
     
    
- 
    
     
    
    
     
          private String id;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = 
      "TENANT")
     
    
- 
    
     
    
    
     
          
      @NotEmpty(message = 
      "Tenant identifier must be provided")
     
    
- 
    
     
    
    
     
          private String tenant;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = 
      "URL")
     
    
- 
    
     
    
    
     
          
      @Size(max = 
      256)
     
    
- 
    
     
    
    
     
          
      @NotEmpty(message = 
      "Tenant jdbc url must be provided")
     
    
- 
    
     
    
    
     
          private String url;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = 
      "USERNAME")
     
    
- 
    
     
    
    
     
          
      @Size(min = 
      4,max = 
      30,message = 
      "db username length must between 4 and 30")
     
    
- 
    
     
    
    
     
          
      @NotEmpty(message = 
      "Tenant db username must be provided")
     
    
- 
    
     
    
    
     
          private String username;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = 
      "PASSWORD")
     
    
- 
    
     
    
    
     
          
      @Size(min = 
      4,max = 
      30)
     
    
- 
    
     
    
    
     
          
      @NotEmpty(message = 
      "Tenant db password must be provided")
     
    
- 
    
     
    
    
     
          private String password;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Version
     
    
- 
    
     
    
    
     
          private int version = 
      0;
     
    
- 
    
     
    
    
     
      }
     
    
 持久层我们将继承JpaRepository接口,快速实现对数据源的CURD操作,同时提供了一个通过租户名查找租户数据源的接口,其代码如下:
  
   - 
    
     
    
    
     
      package com.ramostear.una.saas.master.repository;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      import com.ramostear.una.saas.master.model.MasterTenant;
     
    
- 
    
     
    
    
     
      import org.springframework.
      data.jpa.repository.JpaRepository;
     
    
- 
    
     
    
    
     
      import org.springframework.
      data.jpa.repository.Query;
     
    
- 
    
     
    
    
     
      import org.springframework.
      data.repository.query.Param;
     
    
- 
    
     
    
    
     
      import org.springframework.stereotype.Repository;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      @Repository
     
    
- 
    
     
    
    
     
      public 
      interface 
      MasterTenantRepository 
      extends 
      JpaRepository<
      MasterTenant,String>{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Query("select p from MasterTenant p where p.tenant = :tenant")
     
    
- 
    
     
    
    
     
          MasterTenant findByTenant(
      @Param("tenant") String tenant);
     
    
- 
    
     
    
    
     
      }
     
    
 业务层提供通过租户名获取租户数据源信息的服务(其余的服务各位可自行添加):
  
   - 
    
     
    
    
     
      package com.ramostear.una.saas.master.service;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      import com.ramostear.una.saas.master.model.MasterTenant;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      public 
      interface 
      MasterTenantService {
     
    
- 
    
     
    
    
     
          
     
    
- 
    
     
    
    
     
          MasterTenant findByTenant(String tenant);
     
    
- 
    
     
    
    
     
      }
     
    
最后,我们需要关注的重点是配置主数据源(Spring Boot需要为其提供一个默认的数据源)。在配置之前,我们需要获取配置项,可以通过@ConfigurationProperties("una.master.datasource")获取配置文件中的相关配置信息:
  
   - 
    
     
    
    
     
      @Getter
     
    
- 
    
     
    
    
     
      @Setter
     
    
- 
    
     
    
    
     
      @Configuration
     
    
- 
    
     
    
    
     
      @ConfigurationProperties(
      "una.master.datasource")
     
    
- 
    
     
    
    
     
      public 
      class 
      MasterDatabaseProperties {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      String url;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      String password;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      String username;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      String driverClassName;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private long connectionTimeout;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private int maxPoolSize;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private long idleTimeout;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private int minIdle;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      String poolName;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      String 
      toString(
      ){
     
    
- 
    
     
    
    
     
              
      StringBuilder builder = 
      new 
      StringBuilder();
     
    
- 
    
     
    
    
     
              builder.
      append(
      "MasterDatabaseProperties [ url=")
     
    
- 
    
     
    
    
     
                      .
      append(url)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", username=")
     
    
- 
    
     
    
    
     
                      .
      append(username)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", password=")
     
    
- 
    
     
    
    
     
                      .
      append(password)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", driverClassName=")
     
    
- 
    
     
    
    
     
                      .
      append(driverClassName)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", connectionTimeout=")
     
    
- 
    
     
    
    
     
                      .
      append(connectionTimeout)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", maxPoolSize=")
     
    
- 
    
     
    
    
     
                      .
      append(maxPoolSize)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", idleTimeout=")
     
    
- 
    
     
    
    
     
                      .
      append(idleTimeout)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", minIdle=")
     
    
- 
    
     
    
    
     
                      .
      append(minIdle)
     
    
- 
    
     
    
    
     
                      .
      append(
      ", poolName=")
     
    
- 
    
     
    
    
     
                      .
      append(poolName)
     
    
- 
    
     
    
    
     
                      .
      append(
      "]");
     
    
- 
    
     
    
    
     
              
      return builder.
      toString();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 接下来是配置自定义的数据源,其源码如下:
  
   - 
    
     
    
    
     
      package com.ramostear.una.saas.master.config;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;
     
    
- 
    
     
    
    
     
      import com.ramostear.una.saas.master.model.MasterTenant;
     
    
- 
    
     
    
    
     
      import com.ramostear.una.saas.master.repository.MasterTenantRepository;
     
    
- 
    
     
    
    
     
      import com.zaxxer.hikari.HikariDataSource;
     
    
- 
    
     
    
    
     
      import lombok.extern.slf4j.Slf4j;
     
    
- 
    
     
    
    
     
      import org.hibernate.cfg.Environment;
     
    
- 
    
     
    
    
     
      import org.springframework.beans.factory.
      annotation.Autowired;
     
    
- 
    
     
    
    
     
      import org.springframework.beans.factory.
      annotation.Qualifier;
     
    
- 
    
     
    
    
     
      import org.springframework.context.
      annotation.Bean;
     
    
- 
    
     
    
    
     
      import org.springframework.context.
      annotation.Configuration;
     
    
- 
    
     
    
    
     
      import org.springframework.context.
      annotation.Primary;
     
    
- 
    
     
    
    
     
      import org.springframework.dao.
      annotation.PersistenceExceptionTranslationPostProcessor;
     
    
- 
    
     
    
    
     
      import org.springframework.
      data.jpa.repository.config.EnableJpaRepositories;
     
    
- 
    
     
    
    
     
      import org.springframework.orm.jpa.JpaTransactionManager;
     
    
- 
    
     
    
    
     
      import org.springframework.orm.jpa.JpaVendorAdapter;
     
    
- 
    
     
    
    
     
      import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
     
    
- 
    
     
    
    
     
      import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
     
    
- 
    
     
    
    
     
      import org.springframework.transaction.
      annotation.EnableTransactionManagement;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      import javax.persistence.EntityManagerFactory;
     
    
- 
    
     
    
    
     
      import javax.sql.DataSource;
     
    
- 
    
     
    
    
     
      import java.util.Properties;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      @Configuration
     
    
- 
    
     
    
    
     
      @EnableTransactionManagement
     
    
- 
    
     
    
    
     
      @EnableJpaRepositories(basePackages = {"com.ramostear.una.saas.master.model","com.ramostear.una.saas.master.repository"},
     
    
- 
    
     
    
    
     
                             entityManagerFactoryRef = "masterEntityManagerFactory",
     
    
- 
    
     
    
    
     
                             transactionManagerRef = "masterTransactionManager")
     
    
- 
    
     
    
    
     
      @Slf4j
     
    
- 
    
     
    
    
     
      public 
      class 
      MasterDatabaseConfig {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Autowired
     
    
- 
    
     
    
    
     
          
      private MasterDatabaseProperties masterDatabaseProperties;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = "masterDatasource")
     
    
- 
    
     
    
    
     
          
      public DataSource masterDatasource(){
     
    
- 
    
     
    
    
     
              log.info(
      "Setting up masterDatasource with :{}",masterDatabaseProperties.toString());
     
    
- 
    
     
    
    
     
              HikariDataSource datasource = new HikariDataSource();
     
    
- 
    
     
    
    
     
              datasource.setUsername(masterDatabaseProperties.getUsername());
     
    
- 
    
     
    
    
     
              datasource.setPassword(masterDatabaseProperties.getPassword());
     
    
- 
    
     
    
    
     
              datasource.setJdbcUrl(masterDatabaseProperties.getUrl());
     
    
- 
    
     
    
    
     
              datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());
     
    
- 
    
     
    
    
     
              datasource.setPoolName(masterDatabaseProperties.getPoolName());
     
    
- 
    
     
    
    
     
              datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());
     
    
- 
    
     
    
    
     
              datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());
     
    
- 
    
     
    
    
     
              datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());
     
    
- 
    
     
    
    
     
              datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());
     
    
- 
    
     
    
    
     
              log.info(
      "Setup of masterDatasource successfully.");
     
    
- 
    
     
    
    
     
              
      return datasource;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Primary
     
    
- 
    
     
    
    
     
          
      @Bean(name = "masterEntityManagerFactory")
     
    
- 
    
     
    
    
     
          
      public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){
     
    
- 
    
     
    
    
     
              LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();
     
    
- 
    
     
    
    
     
              lb.setDataSource(masterDatasource());
     
    
- 
    
     
    
    
     
              lb.setPackagesToScan(
     
    
- 
    
     
    
    
     
                 new String[]{MasterTenant.
      class.getPackage().getName(), MasterTenantRepository.
      class.getPackage().getName()}
     
    
- 
    
     
    
    
     
              );
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
              
     
    
- 
    
     
    
    
     
              lb.setPersistenceUnitName(
      "master-database-persistence-unit");
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
              
     
    
- 
    
     
    
    
     
              JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
     
    
- 
    
     
    
    
     
              lb.setJpaVendorAdapter(vendorAdapter);
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
              
     
    
- 
    
     
    
    
     
              lb.setJpaProperties(hibernateProperties());
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
              log.info(
      "Setup of masterEntityManagerFactory successfully.");
     
    
- 
    
     
    
    
     
              
      return lb;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = "masterTransactionManager")
     
    
- 
    
     
    
    
     
          
      public JpaTransactionManager masterTransactionManager(
      @Qualifier("masterEntityManagerFactory")EntityManagerFactory emf){
     
    
- 
    
     
    
    
     
              JpaTransactionManager transactionManager = new JpaTransactionManager();
     
    
- 
    
     
    
    
     
              transactionManager.setEntityManagerFactory(emf);
     
    
- 
    
     
    
    
     
              log.info(
      "Setup of masterTransactionManager successfully.");
     
    
- 
    
     
    
    
     
              
      return transactionManager;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean
     
    
- 
    
     
    
    
     
          
      public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){
     
    
- 
    
     
    
    
     
              
      return new PersistenceExceptionTranslationPostProcessor();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private Properties hibernateProperties(){
     
    
- 
    
     
    
    
     
              Properties properties = new Properties();
     
    
- 
    
     
    
    
     
              properties.put(Environment.DIALECT,
      "org.hibernate.dialect.MySQL5Dialect");
     
    
- 
    
     
    
    
     
              properties.put(Environment.SHOW_SQL,
      true);
     
    
- 
    
     
    
    
     
              properties.put(Environment.FORMAT_SQL,
      true);
     
    
- 
    
     
    
    
     
              properties.put(Environment.HBM2DDL_AUTO,
      "update");
     
    
- 
    
     
    
    
     
              
      return properties;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 在改配置类中,我们主要提供包扫描路径,实体管理工程,事务管理器和数据源配置参数的配置。
6. 实现租户业务模块
在此小节中,租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别,你甚至感觉不到它是一个SaaS应用程序的代码。
首先,创建一个用户实体User,其源码如下:
  
   - 
    
     
    
    
     
      @Entity
     
    
- 
    
     
    
    
     
      @Table(name = "USER")
     
    
- 
    
     
    
    
     
      @Data
     
    
- 
    
     
    
    
     
      @NoArgsConstructor
     
    
- 
    
     
    
    
     
      @AllArgsConstructor
     
    
- 
    
     
    
    
     
      @Builder
     
    
- 
    
     
    
    
     
      public 
      class 
      User 
      implements 
      Serializable {
     
    
- 
    
     
    
    
     
          
      private static 
      final long serialVersionUID = -
      156890917814957041L;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Id
     
    
- 
    
     
    
    
     
          
      @Column(name = "ID")
     
    
- 
    
     
    
    
     
          
      private String id;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = "USERNAME")
     
    
- 
    
     
    
    
     
          
      private String username;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = "PASSWORD")
     
    
- 
    
     
    
    
     
          
      @Size(min = 6,max = 22,message = "User password must be provided and length between 6 and 22.")
     
    
- 
    
     
    
    
     
          
      private String password;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Column(name = "TENANT")
     
    
- 
    
     
    
    
     
          
      private String tenant;
     
    
- 
    
     
    
    
     
      }
     
    
 业务层提供了一个根据用户名检索用户信息的服务,它将调用持久层的方法根据用户名对租户的用户表进行检索,如果找到满足条件的用户记录,则返回用户信息,如果没有找到,则返回null;持久层和业务层的源码分别如下:
  
   - 
    
     
    
    
     
      @Repository
     
    
- 
    
     
    
    
     
      public 
      interface 
      UserRepository 
      extends 
      JpaRepository<
      User,
      String>,
      JpaSpecificationExecutor<
      User>{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      User 
      findByUsername(
      String username);
     
    
- 
    
     
    
    
     
      }
     
    
- 
    
     
    
    
     
      @Service(
      "userService")
     
    
- 
    
     
    
    
     
      public 
      class 
      UserServiceImpl 
      implements 
      UserService{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Autowired
     
    
- 
    
     
    
    
     
          
      private 
      UserRepository userRepository;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      static 
      TwitterIdentifier identifier = 
      new 
      TwitterIdentifier();
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      void 
      save(
      User user) {
     
    
- 
    
     
    
    
     
              user.
      setId(identifier.
      generalIdentifier());
     
    
- 
    
     
    
    
     
              user.
      setTenant(
      TenantContextHolder.
      getTenant());
     
    
- 
    
     
    
    
     
              userRepository.
      save(user);
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      User 
      findById(
      String userId) {
     
    
- 
    
     
    
    
     
              
      Optional<
      User> optional = userRepository.
      findById(userId);
     
    
- 
    
     
    
    
     
              
      if(optional.
      isPresent()){
     
    
- 
    
     
    
    
     
                  
      return optional.
      get();
     
    
- 
    
     
    
    
     
              }
      else{
     
    
- 
    
     
    
    
     
                  
      return 
      null;
     
    
- 
    
     
    
    
     
              }
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      User 
      findByUsername(
      String username) {
     
    
- 
    
     
    
    
     
              
      System.
      out.
      println(
      TenantContextHolder.
      getTenant());
     
    
- 
    
     
    
    
     
              
      return userRepository.
      findByUsername(username);
     
    
- 
    
     
    
    
     
          }
     
    
 在这里,我们采用了Twitter的雪花算法来实现了一个ID生成器。
7. 配置拦截器
我们需要提供一个租户信息的拦截器,用以获取租户标识符,其源代码和配置拦截器的源代码如下:
  
   - 
    
     
    
    
     
      @Slf4j
     
    
- 
    
     
    
    
     
      public 
      class 
      TenantInterceptor 
      implements 
      HandlerInterceptor{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      boolean 
      preHandle
      (HttpServletRequest request, HttpServletResponse response, Object handler) 
      throws Exception {
     
    
- 
    
     
    
    
     
              
      String 
      tenant 
      = request.getParameter(
      "tenant");
     
    
- 
    
     
    
    
     
              
      if(StringUtils.isBlank(tenant)){
     
    
- 
    
     
    
    
     
                  response.sendRedirect(
      "/login.html");
     
    
- 
    
     
    
    
     
                  
      return 
      false;
     
    
- 
    
     
    
    
     
              }
      else{
     
    
- 
    
     
    
    
     
                  TenantContextHolder.setTenant(tenant);
     
    
- 
    
     
    
    
     
                  
      return 
      true;
     
    
- 
    
     
    
    
     
              }
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
- 
    
     
    
    
     
      @Configuration
     
    
- 
    
     
    
    
     
      public 
      class 
      InterceptorConfig 
      extends 
      WebMvcConfigurationSupport {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      protected 
      void 
      addInterceptors
      (InterceptorRegistry registry) {
     
    
- 
    
     
    
    
     
              registry.addInterceptor(
      new 
      TenantInterceptor()).addPathPatterns(
      "/**").excludePathPatterns(
      "/login.html");
     
    
- 
    
     
    
    
     
              
      super.addInterceptors(registry);
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 /login.html是系统的登录路径,我们需要将其排除在拦截器拦截的范围之外,否则我们永远无法进行登录
8. 维护租户标识信息
在这里,我们使用ThreadLocal来存放租户标识信息,为动态设置数据源提供数据支持,该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下:
  
   - 
    
     
    
    
     
      public 
      class 
      TenantContextHolder {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      static final 
      ThreadLocal<
      String> 
      CONTEXT = 
      new 
      ThreadLocal<>();
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      public 
      static 
      void 
      setTenant(
      String tenant){
     
    
- 
    
     
    
    
     
              
      CONTEXT.
      set(tenant);
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      public 
      static 
      String 
      getTenant(
      ){
     
    
- 
    
     
    
    
     
              
      return 
      CONTEXT.
      get();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      public 
      static 
      void 
      clear(
      ){
     
    
- 
    
     
    
    
     
              
      CONTEXT.
      remove();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 此类时实现动态数据源设置的关键
9. 动态数据源切换
要实现动态数据源切换,我们需要借助两个类来完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出,一个负责解析租户标识,一个负责提供租户标识对应的租户数据源信息。
首先,我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租户标识的解析功能。实现类的源码如下:
  
   - 
    
     
    
    
     
      package com.
      ramostear.
      una.
      saas.
      tenant.
      config;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      import com.
      ramostear.
      una.
      saas.
      context.
      TenantContextHolder;
     
    
- 
    
     
    
    
     
      import org.
      apache.
      commons.
      lang3.
      StringUtils;
     
    
- 
    
     
    
    
     
      import org.
      hibernate.
      context.
      spi.
      CurrentTenantIdentifierResolver;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
      public 
      class 
      CurrentTenantIdentifierResolverImpl 
      implements 
      CurrentTenantIdentifierResolver {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
     
    
- 
    
     
    
    
     
          
      private 
      static final 
      String 
      DEFAULT_TENANT = 
      "tenant_1";
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      String 
      resolveCurrentTenantIdentifier(
      ) {
     
    
- 
    
     
    
    
     
              
     
    
- 
    
     
    
    
     
              
      String tenant = 
      TenantContextHolder.
      getTenant();
     
    
- 
    
     
    
    
     
              
     
    
- 
    
     
    
    
     
              
      return 
      StringUtils.
      isNotBlank(tenant)?
      tenant:
      DEFAULT_TENANT;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      public 
      boolean 
      validateExistingCurrentSessions(
      ) {
     
    
- 
    
     
    
    
     
              
      return 
      true;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 此类的逻辑非常简单,就是从ThreadLocal中获取当前设置的租户标识符
有了租户标识符解析类之后,我们需要扩展租户数据源提供类,实现从数据库动态查询租户数据源信息,其源码如下:
  
   - 
    
     
    
    
     
      @Slf4j
     
    
- 
    
     
    
    
     
      @Configuration
     
    
- 
    
     
    
    
     
      public 
      class 
      DataSourceBasedMultiTenantConnectionProviderImpl 
      extends 
      AbstractDataSourceBasedMultiTenantConnectionProviderImpl{
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      static final long serialVersionUID = -7522287771874314380L;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Autowired
     
    
- 
    
     
    
    
     
          
      private 
      MasterTenantRepository masterTenantRepository;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      private 
      Map<
      String,
      DataSource> dataSources = 
      new 
      TreeMap<>();
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      protected 
      DataSource 
      selectAnyDataSource(
      ) {
     
    
- 
    
     
    
    
     
              
      if(dataSources.
      isEmpty()){
     
    
- 
    
     
    
    
     
                  
      List<
      MasterTenant> tenants = masterTenantRepository.
      findAll();
     
    
- 
    
     
    
    
     
                  tenants.
      forEach(masterTenant->{
     
    
- 
    
     
    
    
     
                      dataSources.
      put(masterTenant.
      getTenant(), 
      DataSourceUtils.
      wrapperDataSource(masterTenant));
     
    
- 
    
     
    
    
     
                  });
     
    
- 
    
     
    
    
     
              }
     
    
- 
    
     
    
    
     
              
      return dataSources.
      values().
      iterator().
      next();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Override
     
    
- 
    
     
    
    
     
          
      protected 
      DataSource 
      selectDataSource(
      String tenant) {
     
    
- 
    
     
    
    
     
              
      if(!dataSources.
      containsKey(tenant)){
     
    
- 
    
     
    
    
     
                  
      List<
      MasterTenant> tenants = masterTenantRepository.
      findAll();
     
    
- 
    
     
    
    
     
                  tenants.
      forEach(masterTenant->{
     
    
- 
    
     
    
    
     
                      dataSources.
      put(masterTenant.
      getTenant(),
      DataSourceUtils.
      wrapperDataSource(masterTenant));
     
    
- 
    
     
    
    
     
                  });
     
    
- 
    
     
    
    
     
              }
     
    
- 
    
     
    
    
     
              
      return dataSources.
      get(tenant);
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 在该类中,通过查询租户数据源库,动态获得租户数据源信息,为租户业务模块的数据源配置提供数据数据支持。
最后,我们还需要提供租户业务模块数据源配置,这是整个项目核心的地方,其代码如下:
  
   - 
    
     
    
    
     
      @Slf4j
     
    
- 
    
     
    
    
     
      @Configuration
     
    
- 
    
     
    
    
     
      @EnableTransactionManagement
     
    
- 
    
     
    
    
     
      @ComponentScan(basePackages = {
     
    
- 
    
     
    
    
     
              
      "com.ramostear.una.saas.tenant.model",
     
    
- 
    
     
    
    
     
              
      "com.ramostear.una.saas.tenant.repository"
     
    
- 
    
     
    
    
     
      })
     
    
- 
    
     
    
    
     
      @EnableJpaRepositories(basePackages = {
     
    
- 
    
     
    
    
     
              
      "com.ramostear.una.saas.tenant.repository",
     
    
- 
    
     
    
    
     
              
      "com.ramostear.una.saas.tenant.service"
     
    
- 
    
     
    
    
     
      },entityManagerFactoryRef = 
      "tenantEntityManagerFactory"
     
    
- 
    
     
    
    
     
      ,transactionManagerRef = 
      "tenantTransactionManager")
     
    
- 
    
     
    
    
     
      public 
      class 
      TenantDataSourceConfig {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(
      "jpaVendorAdapter")
     
    
- 
    
     
    
    
     
          
      public 
      JpaVendorAdapter 
      jpaVendorAdapter(
      ){
     
    
- 
    
     
    
    
     
              
      return 
      new 
      HibernateJpaVendorAdapter();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = 
      "tenantTransactionManager")
     
    
- 
    
     
    
    
     
          
      public 
      JpaTransactionManager 
      transactionManager(
      EntityManagerFactory entityManagerFactory){
     
    
- 
    
     
    
    
     
              
      JpaTransactionManager transactionManager = 
      new 
      JpaTransactionManager();
     
    
- 
    
     
    
    
     
              transactionManager.
      setEntityManagerFactory(entityManagerFactory);
     
    
- 
    
     
    
    
     
              
      return transactionManager;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = 
      "datasourceBasedMultiTenantConnectionProvider")
     
    
- 
    
     
    
    
     
          
      @ConditionalOnBean(name = 
      "masterEntityManagerFactory")
     
    
- 
    
     
    
    
     
          
      public 
      MultiTenantConnectionProvider 
      multiTenantConnectionProvider(
      ){
     
    
- 
    
     
    
    
     
              
      return 
      new 
      DataSourceBasedMultiTenantConnectionProviderImpl();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = 
      "currentTenantIdentifierResolver")
     
    
- 
    
     
    
    
     
          
      public 
      CurrentTenantIdentifierResolver 
      currentTenantIdentifierResolver(
      ){
     
    
- 
    
     
    
    
     
              
      return 
      new 
      CurrentTenantIdentifierResolverImpl();
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Bean(name = 
      "tenantEntityManagerFactory")
     
    
- 
    
     
    
    
     
          
      @ConditionalOnBean(name = 
      "datasourceBasedMultiTenantConnectionProvider")
     
    
- 
    
     
    
    
     
          
      public 
      LocalContainerEntityManagerFactoryBean 
      entityManagerFactory(
      
     
    
- 
    
     
    
    
     
                  
      @Qualifier(
      "datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider,
     
    
- 
    
     
    
    
     
                  
      @Qualifier(
      "currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver
     
    
- 
    
     
    
    
     
          ){
     
    
- 
    
     
    
    
     
              
      LocalContainerEntityManagerFactoryBean localBean = 
      new 
      LocalContainerEntityManagerFactoryBean();
     
    
- 
    
     
    
    
     
              localBean.
      setPackagesToScan(
     
    
- 
    
     
    
    
     
                      
      new 
      String[]{
     
    
- 
    
     
    
    
     
                              
      User.
      class.
      getPackage().
      getName(),
     
    
- 
    
     
    
    
     
                              
      UserRepository.
      class.
      getPackage().
      getName(),
     
    
- 
    
     
    
    
     
                              
      UserService.
      class.
      getPackage().
      getName()
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
                      }
     
    
- 
    
     
    
    
     
              );
     
    
- 
    
     
    
    
     
              localBean.
      setJpaVendorAdapter(
      jpaVendorAdapter());
     
    
- 
    
     
    
    
     
              localBean.
      setPersistenceUnitName(
      "tenant-database-persistence-unit");
     
    
- 
    
     
    
    
     
              
      Map<
      String,
      Object> properties = 
      new 
      HashMap<>();
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      MULTI_TENANT, 
      MultiTenancyStrategy.
      SCHEMA);
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      DIALECT,
      "org.hibernate.dialect.MySQL5Dialect");
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      SHOW_SQL,
      true);
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      FORMAT_SQL,
      true);
     
    
- 
    
     
    
    
     
              properties.
      put(
      Environment.
      HBM2DDL_AUTO,
      "update");
     
    
- 
    
     
    
    
     
              localBean.
      setJpaPropertyMap(properties);
     
    
- 
    
     
    
    
     
              
      return localBean;
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 在改配置文件中,大部分内容与主数据源的配置相同,唯一的区别是租户标识解析器与租户数据源补给源的设置,它将告诉Hibernate在执行数据库操作命令前,应该设置什么样的数据库连接信息,以及用户名和密码等信息。
10. 应用测试
最后,我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序,为此,需要提供一个Controller用于处理用户登录逻辑。在本案例中,没有严格的对用户密码进行加密,而是使用明文进行比对,也没有提供任何的权限认证框架,知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下:
  
   - 
    
     
    
    
     
      @Controller
     
    
- 
    
     
    
    
     
      public 
      class 
      LoginController {
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @Autowired
     
    
- 
    
     
    
    
     
          
      private 
      UserService userService;
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @GetMapping(
      "/login.html")
     
    
- 
    
     
    
    
     
          
      public 
      String 
      login(
      ){
     
    
- 
    
     
    
    
     
              
      return 
      "/login";
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
      
     
    
- 
    
     
    
    
     
          
      @PostMapping(
      "/login")
     
    
- 
    
     
    
    
     
          
      public 
      String 
      login(
      @RequestParam(name = "username") String username, @RequestParam(name = "password")String password, ModelMap model){
     
    
- 
    
     
    
    
     
              
      System.
      out.
      println(
      "tenant:"+
      TenantContextHolder.
      getTenant());
     
    
- 
    
     
    
    
     
              
      User user = userService.
      findByUsername(username);
     
    
- 
    
     
    
    
     
              
      if(user != 
      null){
     
    
- 
    
     
    
    
     
                  
      if(user.
      getPassword().
      equals(password)){
     
    
- 
    
     
    
    
     
                      model.
      put(
      "user",user);
     
    
- 
    
     
    
    
     
                      
      return 
      "/index";
     
    
- 
    
     
    
    
     
                  }
      else{
     
    
- 
    
     
    
    
     
                      
      return 
      "/login";
     
    
- 
    
     
    
    
     
                  }
     
    
- 
    
     
    
    
     
              }
      else{
     
    
- 
    
     
    
    
     
                  
      return 
      "/login";
     
    
- 
    
     
    
    
     
              }
     
    
- 
    
     
    
    
     
          }
     
    
- 
    
     
    
    
     
      }
     
    
 在启动项目之前,我们需要为主数据源创建对应的数据库和数据表,用于存放租户数据源信息,同时还需要提供一个租户业务模块数据库和数据表,用来存放租户业务数据。一切准备就绪后,启动项目,在浏览器中输入:http://localhost:8080/login.html

在登录窗口中输入对应的租户名,用户名和密码,测试是否能够正常到达主页。可以多增加几个租户和用户,测试用户是否正常切换到对应的租户下。
总结
在这里,我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法,此方法只涉及了实现SaaS应用平台的最核心技术手段,并不是一个完整可用的项目代码,如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展.
转载:https://blog.csdn.net/z_ssyy/article/details/128484045
 
					