Spring Boot 構建多租戶SaaS平臺核心技術指南

本次教程所涉及到的源碼已上傳至Github,若是你不須要繼續閱讀下面的內容,你能夠直接點擊此連接獲取源碼內容。github.com/ramostear/u…html

1. 概述

筆者從2014年開始接觸SaaS(Software as a Service),即多租戶(或多承租)軟件應用平臺;並一直從事相關領域的架構設計及研發工做。機緣巧合,在筆者本科畢業設計時完成了一個基於SaaS的高效財務管理平臺的課題研究,從中收穫頗多。最先接觸SaaS時,國內相關資源匱乏,惟一有的參照資料是《互聯網時代的軟件革命:SaaS架構設計》(葉偉等著)一書。最後課題的實現是基於OSGI(Open Service Gateway Initiative)Java動態模塊化系統規範來實現的。java

時至今日,五年的時間過去了,軟件開發的技術發生了巨大的改變,筆者所實現SaaS平臺的技術棧也更新了好幾波,真是印證了那就話:「山重水盡疑無路,柳暗花明又一村」。基於以前走過的許多彎路和踩過的坑,以及近段時間有許多網友問我如何使用Spring Boot實現多租戶系統,決定寫一篇文章聊一聊關於SaaS的硬核技術。mysql

提及SaaS,它只是一種軟件架構,並無多少神祕的東西,也不是什麼很難的系統,我我的的感受,SaaS平臺的難度在於商業上的運營,而非技術上的實現。就技術上來講,SaaS是這樣一種架構模式:它讓多個不一樣環境的用戶使用同一套應用程序,且保證用戶之間的數據相互隔離。如今想一想看,這也有點共享經濟的味道在裏面。git

筆者在這裏就再也不深刻聊SaaS軟件成熟度模型和數據隔離方案對比的事情了。今天要聊的是使用Spring Boot快速構建獨立數據庫/共享數據庫獨立Schema的多租戶系統。我將提供一個SaaS系統最核心的技術實現,而其餘的部分有興趣的朋友能夠在此基礎上自行擴展。github

2. 嘗試瞭解多租戶的應用場景

假設咱們須要開發一個應用程序,而且但願將同一個應用程序銷售給N家客戶使用。在常規狀況下,咱們須要爲此建立N個Web服務器(Tomcat),N個數據庫(DB),併爲N個客戶部署相同的應用程序N次。如今,若是咱們的應用程序進行了升級或者作了其餘任何的改動,那麼咱們就須要更新N個應用程序同時還須要維護N臺服務器。接下來,若是業務開始增加,客戶由原來的N個變成了如今的N+M個,咱們將面臨N個應用程序和M個應用程序版本維護,設備維護以及成本控制的問題。運維幾乎要哭死在機房了...web

爲了解決上述的問題,咱們能夠開發多租戶應用程序,咱們能夠根據當前用戶是誰,從而選擇對應的數據庫。例如,當請求來自A公司的用戶時,應用程序就鏈接A公司的數據庫,當請求來自B公司的用戶時,自動將數據庫切換到B公司數據庫,以此類推。從理論上將沒有什麼問題,但咱們若是考慮將現有的應用程序改形成SaaS模式,咱們將遇到第一個問題:若是識別請求來自哪個租戶?如何自動切換數據源?算法

3. 維護、識別和路由租戶數據源

咱們能夠提供一個獨立的庫來存放租戶信息,如數據庫名稱、連接地址、用戶名、密碼等,這能夠統一的解決租戶信息維護的問題。租戶的識別和路由有不少種方法能夠解決,下面列舉幾個經常使用的方式:spring

  • 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的問題)。sql

咱們都知道,在啓動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;

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:22 * @modify by : * @since: */
@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;

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:26 * @modify by : * @since: */

public interface MasterTenantService {
    /** * Using custom tenant name query * @param tenant tenant name * @return masterTenant */
    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;

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:31 * @modify by : * @since: */
@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()}
        );

        //Setting a name for the persistence unit as Spring sets it as 'default' if not defined.
        lb.setPersistenceUnitName("master-database-persistence-unit");

        //Setting Hibernate as the JPA provider.
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        lb.setJpaVendorAdapter(vendorAdapter);

        //Setting the hibernate properties
        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. 配置攔截器

咱們須要提供一個租戶信息的攔截器,用以獲取租戶標識符,其源代碼和配置攔截器的源代碼以下:

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-23:17 * @modify by : * @since: */
@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;

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-22:38 * @modify by : * @since: */
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    /** * 默認的租戶ID */
    private static final String DEFAULT_TENANT = "tenant_1";

    /** * 解析當前租戶的ID * @return */
    @Override
    public String resolveCurrentTenantIdentifier() {
        //經過租戶上下文獲取租戶ID,此ID是用戶登陸時在header中進行設置的
        String tenant = TenantContextHolder.getTenant();
        //若是上下文中沒有找到該租戶ID,則使用默認的租戶ID,或者直接報異常信息
        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的基本特性是否具有。登陸控制器代碼以下:

/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/27 0027-0:18 * @modify by : * @since: */
@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應用平臺的最核心技術手段,並非一個完整可用的項目代碼,如用戶的認證、受權等並未出如今本文中。額外的業務模塊感興趣的朋友能夠在此設計基礎上自行擴展,如對其中的代碼有任何的疑問,歡迎你們在下方給我留言。

原文:www.ramostear.com/articles/sp…

做者:譚朝紅

相關文章
相關標籤/搜索