国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Spring Boot 構(gòu)建多租戶SaaS平臺(tái)核心技術(shù)指南

learning / 2843人閱讀

摘要:概述筆者從年開(kāi)始接觸,即多租戶或多承租軟件應(yīng)用平臺(tái)并一直從事相關(guān)領(lǐng)域的架構(gòu)設(shè)計(jì)及研發(fā)工作。今天要聊的是使用快速構(gòu)建獨(dú)立數(shù)據(jù)庫(kù)共享數(shù)據(jù)庫(kù)獨(dú)立的多租戶系統(tǒng)。

本次教程所涉及到的源碼已上傳至Github,如果你不需要繼續(xù)閱讀下面的內(nèi)容,你可以直接點(diǎn)擊此鏈接獲取源碼內(nèi)容。github.com/ramostear/u…

1. 概述

筆者從2014年開(kāi)始接觸SaaS(Software as a Service),即多租戶(或多承租)軟件應(yīng)用平臺(tái);并一直從事相關(guān)領(lǐng)域的架構(gòu)設(shè)計(jì)及研發(fā)工作。機(jī)緣巧合,在筆者本科畢業(yè)設(shè)計(jì)時(shí)完成了一個(gè)基于SaaS的高效財(cái)務(wù)管理平臺(tái)的課題研究,從中收獲頗多。最早接觸SaaS時(shí),國(guó)內(nèi)相關(guān)資源匱乏,唯一有的參照資料是《互聯(lián)網(wǎng)時(shí)代的軟件革命:SaaS架構(gòu)設(shè)計(jì)》(葉偉等著)一書。最后課題的實(shí)現(xiàn)是基于OSGI(Open Service Gateway Initiative)Java動(dòng)態(tài)模塊化系統(tǒng)規(guī)范來(lái)實(shí)現(xiàn)的。

時(shí)至今日,五年的時(shí)間過(guò)去了,軟件開(kāi)發(fā)的技術(shù)發(fā)生了巨大的改變,筆者所實(shí)現(xiàn)SaaS平臺(tái)的技術(shù)棧也更新了好幾波,真是印證了那就話:“山重水盡疑無(wú)路,柳暗花明又一村”。基于之前走過(guò)的許多彎路和踩過(guò)的坑,以及近段時(shí)間有許多網(wǎng)友問(wèn)我如何使用Spring Boot實(shí)現(xiàn)多租戶系統(tǒng),決定寫一篇文章聊一聊關(guān)于SaaS的硬核技術(shù)。

說(shuō)起SaaS,它只是一種軟件架構(gòu),并沒(méi)有多少神秘的東西,也不是什么很難的系統(tǒng),我個(gè)人的感覺(jué),SaaS平臺(tái)的難度在于商業(yè)上的運(yùn)營(yíng),而非技術(shù)上的實(shí)現(xiàn)。就技術(shù)上來(lái)說(shuō),SaaS是這樣一種架構(gòu)模式:它讓多個(gè)不同環(huán)境的用戶使用同一套應(yīng)用程序,且保證用戶之間的數(shù)據(jù)相互隔離。現(xiàn)在想想看,這也有點(diǎn)共享經(jīng)濟(jì)的味道在里面。

筆者在這里就不再深入聊SaaS軟件成熟度模型和數(shù)據(jù)隔離方案對(duì)比的事情了。今天要聊的是使用Spring Boot快速構(gòu)建獨(dú)立數(shù)據(jù)庫(kù)/共享數(shù)據(jù)庫(kù)獨(dú)立Schema的多租戶系統(tǒng)。我將提供一個(gè)SaaS系統(tǒng)最核心的技術(shù)實(shí)現(xiàn),而其他的部分有興趣的朋友可以在此基礎(chǔ)上自行擴(kuò)展。

2. 嘗試了解多租戶的應(yīng)用場(chǎng)景

假設(shè)我們需要開(kāi)發(fā)一個(gè)應(yīng)用程序,并且希望將同一個(gè)應(yīng)用程序銷售給N家客戶使用。在常規(guī)情況下,我們需要為此創(chuàng)建N個(gè)Web服務(wù)器(Tomcat),N個(gè)數(shù)據(jù)庫(kù)(DB),并為N個(gè)客戶部署相同的應(yīng)用程序N次?,F(xiàn)在,如果我們的應(yīng)用程序進(jìn)行了升級(jí)或者做了其他任何的改動(dòng),那么我們就需要更新N個(gè)應(yīng)用程序同時(shí)還需要維護(hù)N臺(tái)服務(wù)器。接下來(lái),如果業(yè)務(wù)開(kāi)始增長(zhǎng),客戶由原來(lái)的N個(gè)變成了現(xiàn)在的N+M個(gè),我們將面臨N個(gè)應(yīng)用程序和M個(gè)應(yīng)用程序版本維護(hù),設(shè)備維護(hù)以及成本控制的問(wèn)題。運(yùn)維幾乎要哭死在機(jī)房了...

為了解決上述的問(wèn)題,我們可以開(kāi)發(fā)多租戶應(yīng)用程序,我們可以根據(jù)當(dāng)前用戶是誰(shuí),從而選擇對(duì)應(yīng)的數(shù)據(jù)庫(kù)。例如,當(dāng)請(qǐng)求來(lái)自A公司的用戶時(shí),應(yīng)用程序就連接A公司的數(shù)據(jù)庫(kù),當(dāng)請(qǐng)求來(lái)自B公司的用戶時(shí),自動(dòng)將數(shù)據(jù)庫(kù)切換到B公司數(shù)據(jù)庫(kù),以此類推。從理論上將沒(méi)有什么問(wèn)題,但我們?nèi)绻紤]將現(xiàn)有的應(yīng)用程序改造成SaaS模式,我們將遇到第一個(gè)問(wèn)題:如果識(shí)別請(qǐng)求來(lái)自哪一個(gè)租戶?如何自動(dòng)切換數(shù)據(jù)源?

3. 維護(hù)、識(shí)別和路由租戶數(shù)據(jù)源

我們可以提供一個(gè)獨(dú)立的庫(kù)來(lái)存放租戶信息,如數(shù)據(jù)庫(kù)名稱、鏈接地址、用戶名、密碼等,這可以統(tǒng)一的解決租戶信息維護(hù)的問(wèn)題。租戶的識(shí)別和路由有很多種方法可以解決,下面列舉幾個(gè)常用的方式:

1.可以通過(guò)域名的方式來(lái)識(shí)別租戶:我們可以為每一個(gè)租戶提供一個(gè)唯一的二級(jí)域名,通過(guò)二級(jí)域名就可以達(dá)到識(shí)別租戶的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我們識(shí)別租戶的關(guān)鍵信息。

2.可以將租戶信息作為請(qǐng)求參數(shù)傳遞給服務(wù)端,為服務(wù)端識(shí)別租戶提供支持,如saas.example.com");

3.可以在請(qǐng)求頭(Header)中設(shè)置租戶信息,例如JWT等技術(shù),服務(wù)端通過(guò)解析Header中相關(guān)參數(shù)以獲得租戶信息。

4.在用戶成功登錄系統(tǒng)后,將租戶信息保存在Session中,在需要的時(shí)候從Session取出租戶信息。

解決了上述問(wèn)題后,我們?cè)賮?lái)看看如何獲取客戶端傳入的租戶信息,以及在我們的業(yè)務(wù)代碼中如何使用租戶信息(最關(guān)鍵的是DataSources的問(wèn)題)。

我們都知道,在啟動(dòng)Spring Boot應(yīng)用程序之前,就需要為其提供有關(guān)數(shù)據(jù)源的配置信息(有使用到數(shù)據(jù)庫(kù)的情況下),按照一開(kāi)始的需求,有N個(gè)客戶需要使用我們的應(yīng)用程序,我們就需要提前配置好N個(gè)數(shù)據(jù)源(多數(shù)據(jù)源),如果N<50,我認(rèn)為我還能忍受,如果更多,這樣顯然是無(wú)法接受的。為了解決這一問(wèn)題,我們需要借助Hibernate 5提供的動(dòng)態(tài)數(shù)據(jù)源特性,讓我們的應(yīng)用程序具備動(dòng)態(tài)配置客戶端數(shù)據(jù)源的能力。簡(jiǎn)單來(lái)說(shuō),當(dāng)用戶請(qǐng)求系統(tǒng)資源時(shí),我們將用戶提供的租戶信息(tenantId)存放在ThreadLoacal中,緊接著獲取TheadLocal中的租戶信息,并根據(jù)此信息查詢多帶帶的租戶庫(kù),獲取當(dāng)前租戶的數(shù)據(jù)配置信息,然后借助Hibernate動(dòng)態(tài)配置數(shù)據(jù)源的能力,為當(dāng)前請(qǐng)求設(shè)置數(shù)據(jù)源,最后之前用戶的請(qǐng)求。這樣我們就只需要在應(yīng)用程序中維護(hù)一份數(shù)據(jù)源配置信息(租戶數(shù)據(jù)庫(kù)配置庫(kù)),其余的數(shù)據(jù)源動(dòng)態(tài)查詢配置。接下來(lái),我們將快速的演示這一功能。

4. 項(xiàng)目構(gòu)建

我們將使用Spring Boot 2.1.5版本來(lái)實(shí)現(xiàn)這一演示項(xiàng)目,首先你需要在Maven配置文件中加入如下的一些配置:

<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starterartifactId>
		dependency>

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-devtoolsartifactId>
			<scope>runtimescope>
		dependency>
		<dependency>
			<groupId>org.projectlombokgroupId>
			<artifactId>lombokartifactId>
			<optional>trueoptional>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-data-jpaartifactId>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-configuration-processorartifactId>
		dependency>
		<dependency>
			<groupId>mysqlgroupId>
			<artifactId>mysql-connector-javaartifactId>
			<version>5.1.47version>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-freemarkerartifactId>
		dependency>
		<dependency>
			<groupId>org.apache.commonsgroupId>
			<artifactId>commons-lang3artifactId>
		dependency>
	dependencies>

然后提供一個(gè)可用的配置文件,并加入如下的內(nèi)容:

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");
      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的相關(guān)技術(shù)

una:master:datasource配置項(xiàng)就是上面說(shuō)的統(tǒng)一存放租戶信息的數(shù)據(jù)源配置信息,你可以理解為主庫(kù)。

接下來(lái),我們需要關(guān)閉Spring Boot自動(dòng)配置數(shù)據(jù)源的功能,在項(xiàng)目主類上添加如下的設(shè)置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class UnaSaasApplication {

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

}

最后,讓我們看看整個(gè)項(xiàng)目的結(jié)構(gòu):

5. 實(shí)現(xiàn)租戶數(shù)據(jù)源查詢模塊

我們將定義一個(gè)實(shí)體類存放租戶數(shù)據(jù)源信息,它包含了租戶名,數(shù)據(jù)庫(kù)連接地址,用戶名和密碼等信息,其代碼如下:

@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接口,快速實(shí)現(xiàn)對(duì)數(shù)據(jù)源的CURD操作,同時(shí)提供了一個(gè)通過(guò)租戶名查找租戶數(shù)據(jù)源的接口,其代碼如下:

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);
}

業(yè)務(wù)層提供通過(guò)租戶名獲取租戶數(shù)據(jù)源信息的服務(wù)(其余的服務(wù)各位可自行添加):

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);
}

最后,我們需要關(guān)注的重點(diǎn)是配置主數(shù)據(jù)源(Spring Boot需要為其提供一個(gè)默認(rèn)的數(shù)據(jù)源)。在配置之前,我們需要獲取配置項(xiàng),可以通過(guò)@ConfigurationProperties("una.master.datasource")獲取配置文件中的相關(guān)配置信息:

@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();
    }
}

接下來(lái)是配置自定義的數(shù)據(jù)源,其源碼如下:

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;
    }
}

在改配置類中,我們主要提供包掃描路徑,實(shí)體管理工程,事務(wù)管理器和數(shù)據(jù)源配置參數(shù)的配置。

6. 實(shí)現(xiàn)租戶業(yè)務(wù)模塊

在此小節(jié)中,租戶業(yè)務(wù)模塊我們僅提供一個(gè)用戶登錄的場(chǎng)景來(lái)演示SaaS的功能。其實(shí)體層、業(yè)務(wù)層和持久化層根普通的Spring Boot Web項(xiàng)目沒(méi)有什么區(qū)別,你甚至感覺(jué)不到它是一個(gè)SaaS應(yīng)用程序的代碼。

首先,創(chuàng)建一個(gè)用戶實(shí)體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;
}

業(yè)務(wù)層提供了一個(gè)根據(jù)用戶名檢索用戶信息的服務(wù),它將調(diào)用持久層的方法根據(jù)用戶名對(duì)租戶的用戶表進(jìn)行檢索,如果找到滿足條件的用戶記錄,則返回用戶信息,如果沒(méi)有找到,則返回null;持久層和業(yè)務(wù)層的源碼分別如下:

@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 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的雪花算法來(lái)實(shí)現(xiàn)了一個(gè)ID生成器。

7. 配置攔截器

我們需要提供一個(gè)租戶信息的攔截器,用以獲取租戶標(biāo)識(shí)符,其源代碼和配置攔截器的源代碼如下:

/**
 * @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是系統(tǒng)的登錄路徑,我們需要將其排除在攔截器攔截的范圍之外,否則我們永遠(yuǎn)無(wú)法進(jìn)行登錄

8. 維護(hù)租戶標(biāo)識(shí)信息

在這里,我們使用ThreadLocal來(lái)存放租戶標(biāo)識(shí)信息,為動(dòng)態(tài)設(shè)置數(shù)據(jù)源提供數(shù)據(jù)支持,該類提供了設(shè)置租戶標(biāo)識(shí)、獲取租戶標(biāo)識(shí)以及清除租戶標(biāo)識(shí)三個(gè)靜態(tài)方法。其源碼如下:

public class TenantContextHolder {

    private static final ThreadLocal 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();
    }
}

此類時(shí)實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源設(shè)置的關(guān)鍵

9. 動(dòng)態(tài)數(shù)據(jù)源切換

要實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換,我們需要借助兩個(gè)類來(lái)完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。從它們的命名上就可以看出,一個(gè)負(fù)責(zé)解析租戶標(biāo)識(shí),一個(gè)負(fù)責(zé)提供租戶標(biāo)識(shí)對(duì)應(yīng)的租戶數(shù)據(jù)源信息。

首先,我們需要實(shí)現(xiàn)CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租戶標(biāo)識(shí)的解析功能。實(shí)現(xiàn)類的源碼如下:

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 {

    /**
     * 默認(rèn)的租戶ID
     */
    private static final String DEFAULT_TENANT = "tenant_1";

    /**
     * 解析當(dāng)前租戶的ID
     * @return
     */
    @Override
    public String resolveCurrentTenantIdentifier() {
        //通過(guò)租戶上下文獲取租戶ID,此ID是用戶登錄時(shí)在header中進(jìn)行設(shè)置的
        String tenant = TenantContextHolder.getTenant();
        //如果上下文中沒(méi)有找到該租戶ID,則使用默認(rèn)的租戶ID,或者直接報(bào)異常信息
        return StringUtils.isNotBlank(tenant)");@Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

此類的邏輯非常簡(jiǎn)單,就是從ThreadLocal中獲取當(dāng)前設(shè)置的租戶標(biāo)識(shí)符

有了租戶標(biāo)識(shí)符解析類之后,我們需要擴(kuò)展租戶數(shù)據(jù)源提供類,實(shí)現(xiàn)從數(shù)據(jù)庫(kù)動(dòng)態(tài)查詢租戶數(shù)據(jù)源信息,其源碼如下:

@Slf4j
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{

    private static final long serialVersionUID = -7522287771874314380L;

    @Autowired
    private MasterTenantRepository masterTenantRepository;

    private Map dataSources = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        if(dataSources.isEmpty()){
            List 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 tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.get(tenant);
    }
}

在該類中,通過(guò)查詢租戶數(shù)據(jù)源庫(kù),動(dòng)態(tài)獲得租戶數(shù)據(jù)源信息,為租戶業(yè)務(wù)模塊的數(shù)據(jù)源配置提供數(shù)據(jù)數(shù)據(jù)支持。

最后,我們還需要提供租戶業(yè)務(wù)模塊數(shù)據(jù)源配置,這是整個(gè)項(xiàng)目核心的地方,其代碼如下:

@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 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;
    }
}

在改配置文件中,大部分內(nèi)容與主數(shù)據(jù)源的配置相同,唯一的區(qū)別是租戶標(biāo)識(shí)解析器與租戶數(shù)據(jù)源補(bǔ)給源的設(shè)置,它將告訴Hibernate在執(zhí)行數(shù)據(jù)庫(kù)操作命令前,應(yīng)該設(shè)置什么樣的數(shù)據(jù)庫(kù)連接信息,以及用戶名和密碼等信息。

10. 應(yīng)用測(cè)試

最后,我們通過(guò)一個(gè)簡(jiǎn)單的登錄案例來(lái)測(cè)試本次課程中的SaaS應(yīng)用程序,為此,需要提供一個(gè)Controller用于處理用戶登錄邏輯。在本案例中,沒(méi)有嚴(yán)格的對(duì)用戶密碼進(jìn)行加密,而是使用明文進(jìn)行比對(duì),也沒(méi)有提供任何的權(quán)限認(rèn)證框架,知識(shí)單純的驗(yàn)證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";
        }
    }
}

在啟動(dòng)項(xiàng)目之前,我們需要為主數(shù)據(jù)源創(chuàng)建對(duì)應(yīng)的數(shù)據(jù)庫(kù)和數(shù)據(jù)表,用于存放租戶數(shù)據(jù)源信息,同時(shí)還需要提供一個(gè)租戶業(yè)務(wù)模塊數(shù)據(jù)庫(kù)和數(shù)據(jù)表,用來(lái)存放租戶業(yè)務(wù)數(shù)據(jù)。一切準(zhǔn)備就緒后,啟動(dòng)項(xiàng)目,在瀏覽器中輸入:http://localhost:8080/login.html

在登錄窗口中輸入對(duì)應(yīng)的租戶名,用戶名和密碼,測(cè)試是否能夠正常到達(dá)主頁(yè)??梢远嘣黾訋讉€(gè)租戶和用戶,測(cè)試用戶是否正常切換到對(duì)應(yīng)的租戶下。

總結(jié)

在這里,我分享了使用Spring Boot+JPA快速實(shí)現(xiàn)多租戶應(yīng)用程序的方法,此方法只涉及了實(shí)現(xiàn)SaaS應(yīng)用平臺(tái)的最核心技術(shù)手段,并不是一個(gè)完整可用的項(xiàng)目代碼,如用戶的認(rèn)證、授權(quán)等并未出現(xiàn)在本文中。額外的業(yè)務(wù)模塊感興趣的朋友可以在此設(shè)計(jì)基礎(chǔ)上自行擴(kuò)展,如對(duì)其中的代碼有任何的疑問(wèn),歡迎大家在下方給我留言。

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

作者:譚朝紅

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/7865.html

相關(guān)文章

  • 用友云微服務(wù)架構(gòu)下配置文件管理利器:配置中心

    摘要:而且,用友云配置中心以服務(wù)的方式提供統(tǒng)一的管理界面,結(jié)合用友云的認(rèn)證中心可以提供可靠的安全保障。 微服務(wù)架構(gòu)是這幾年IT領(lǐng)域的一個(gè)高頻詞匯,越來(lái)越多的項(xiàng)目和應(yīng)用正在以微服務(wù)的思想進(jìn)行重構(gòu)。相比于單體應(yīng)用和SOA架構(gòu),微服務(wù)優(yōu)勢(shì)也逐漸凸顯,被廣大架構(gòu)師和技術(shù)人員引入和推崇。當(dāng)然,單體應(yīng)用、SOA、微服務(wù)等各有優(yōu)勢(shì)和不足。單體架構(gòu)在早期的企業(yè)內(nèi)部信息化或者搭建中小型項(xiàng)目時(shí)很常見(jiàn),簡(jiǎn)單說(shuō)就是...

    jayce 評(píng)論0 收藏0
  • 神策數(shù)據(jù)關(guān)海南:營(yíng)銷策略引擎解讀,以平臺(tái)構(gòu)建營(yíng)銷新生態(tài)

    摘要:神策數(shù)據(jù)關(guān)海南營(yíng)銷策略引擎解讀,以平臺(tái)化構(gòu)建營(yíng)銷新生態(tài)計(jì)算引擎圖片神策數(shù)據(jù)關(guān)海南營(yíng)銷策略引擎解讀,以平臺(tái)化構(gòu)建營(yíng)銷新生態(tài)微信在神策數(shù)據(jù)驅(qū)動(dòng)大會(huì)現(xiàn)場(chǎng),神策營(yíng)銷云架構(gòu)師關(guān)海南發(fā)表了題為營(yíng)銷策略引擎的技術(shù)演進(jìn)的演講。 ??在??神策 2021 數(shù)據(jù)驅(qū)動(dòng)大會(huì)??現(xiàn)場(chǎng),神策營(yíng)銷云架構(gòu)師關(guān)海南發(fā)表了題為《營(yíng)銷策略引擎(Express)...

    番茄西紅柿 評(píng)論0 收藏2637
  • 近幾個(gè)月Github上最熱門的Java項(xiàng)目一覽

    摘要:今天逛了逛,順手精選出了一下近幾個(gè)月以來(lái)上最熱門的個(gè)項(xiàng)目。相關(guān)閱讀正式開(kāi)源,幫助應(yīng)用快速容器化未來(lái)可能會(huì)上熱門的項(xiàng)目地址介紹哈哈,皮一下很開(kāi)心。這是我自己開(kāi)源的一份文檔,目前仍在完善中,歡迎各位英雄好漢一起完善。 showImg(https://segmentfault.com/img/remote/1460000015766827?w=391&h=220);今天逛了逛Github,順...

    cyqian 評(píng)論0 收藏0
  • 為什么說(shuō)一體化SaaS是未來(lái)的必然趨勢(shì)?

    摘要:一體化所能產(chǎn)生的管理智慧及企業(yè)整體效率的價(jià)值遠(yuǎn)遠(yuǎn)大于將其自動(dòng)化所消耗的時(shí)間及成本,將成為未來(lái)企業(yè)管理的發(fā)展趨勢(shì)。近幾年來(lái),移動(dòng)互聯(lián)、人工智能、大數(shù)據(jù)以及云計(jì)算等技術(shù)的發(fā)展,為企業(yè)級(jí)SaaS應(yīng)用提供了基礎(chǔ)條件,使得SaaS產(chǎn)品的成熟度不斷提高。各類產(chǎn)品在不斷突破界限,進(jìn)行跨界和融合,開(kāi)始滲透在整個(gè)企業(yè)中。在這樣的時(shí)代背景下,SaaS在企業(yè)應(yīng)用方面將會(huì)呈現(xiàn)怎樣的發(fā)展趨勢(shì)呢?根據(jù)國(guó)外權(quán)威機(jī)構(gòu) Bl...

    meteor199 評(píng)論0 收藏0
  • 轉(zhuǎn)向混合云,如何確保它的設(shè)計(jì)是安全的?

    摘要:混合云所提供的靈活性和控制能力是它在可預(yù)見(jiàn)的未來(lái)有望成為主流云計(jì)算模型的原因。一種新的計(jì)算方法遷移到云計(jì)算并不意味著完全放棄控制權(quán),但它確實(shí)需要接受一種基于身份數(shù)據(jù)和工作負(fù)載而不是底層平臺(tái)的新安全思維。許多組織對(duì)云計(jì)算都有非常好的初次體驗(yàn),因此它們很快就想轉(zhuǎn)移到一個(gè)混合云環(huán)境中,在私有云和公共云之間共享數(shù)據(jù)和工作負(fù)載。混合云所提供的靈活性和控制能力是它在可預(yù)見(jiàn)的未來(lái)有望成為主流云計(jì)算模型的原...

    svtter 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<