日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

Spring Boot 构建多租户 SaaS 平台核心技术指南

發(fā)布時(shí)間:2025/3/21 javascript 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring Boot 构建多租户 SaaS 平台核心技术指南 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1. 概述

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

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

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

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

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

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

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

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

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

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

  • 可以將租戶信息作為請求參數(shù)傳遞給服務(wù)端,為服務(wù)端識別租戶提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的參數(shù)tenantId就是應(yīng)用程序識別租戶的關(guān)鍵信息。

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

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

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

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

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

我們將使用Spring Boot?2.1.5版本來實(shí)現(xiàn)這一演示項(xiàng)目,首先你需要在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>

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

spring:freemarker:cache:falsetemplate-loader-path:- classpath:/templates/prefix:suffix:.htmlresources:static-locations:- classpath:/static/devtools:restart:enabled:truejpa:database:mysqlshow-sql:truegenerate-ddl:falsehibernate:ddl-auto:none una:master:datasource:url:jdbc:mysql://localhost:3306/master_tenant?useSSL=falseusername:rootpassword:rootdriverClassName:com.mysql.jdbc.DrivermaxPoolSize:10idleTimeout:300000minIdle:10poolName:master-database-connection-pool logging:level:root:warnorg:springframework:web:debughibernate:debug

由于采用Freemarker作為視圖渲染引擎,所以需要提供Freemarker的相關(guān)技術(shù)

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

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

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) publicclass 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ù)庫連接地址,用戶名和密碼等信息,其代碼如下:

@Data @Entity @Table(name = "MASTER_TENANT") @NoArgsConstructor @AllArgsConstructor @Builder publicclass 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;@Versionprivateint version = 0; }

持久層我們將繼承JpaRepository接口,快速實(shí)現(xiàn)對數(shù)據(jù)源的CURD操作,同時(shí)提供了一個(gè)通過租戶名查找租戶數(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 publicinterface MasterTenantRepository extends JpaRepository<MasterTenant,String>{@Query("select p from MasterTenant p where p.tenant = :tenant")MasterTenant findByTenant(@Param("tenant") String tenant); }

業(yè)務(wù)層提供通過租戶名獲取租戶數(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:*/publicinterface 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),可以通過@ConfigurationProperties("una.master.datasource")獲取配置文件中的相關(guān)配置信息:

@Getter @Setter @Configuration @ConfigurationProperties("una.master.datasource") publicclass MasterDatabaseProperties {private String url;private String password;private String username;private String driverClassName;privatelong connectionTimeout;privateint maxPoolSize;privatelong idleTimeout;privateint minIdle;private String poolName;@Overridepublic 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();} }

接下來是配置自定義的數(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 publicclass MasterDatabaseConfig {@Autowiredprivate 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 propertieslb.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;}@Beanpublic PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){returnnew 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è)用戶登錄的場景來演示SaaS的功能。其實(shí)體層、業(yè)務(wù)層和持久化層根普通的Spring Boot?Web項(xiàng)目沒有什么區(qū)別,你甚至感覺不到它是一個(gè)SaaS應(yīng)用程序的代碼。

首先,創(chuàng)建一個(gè)用戶實(shí)體User,其源碼如下:

@Entity @Table(name = "USER") @Data @NoArgsConstructor @AllArgsConstructor @Builder publicclass User implements Serializable {privatestaticfinallong 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ù)用戶名對租戶的用戶表進(jìn)行檢索,如果找到滿足條件的用戶記錄,則返回用戶信息,如果沒有找到,則返回null;持久層和業(yè)務(wù)層的源碼分別如下:

@Repository publicinterface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{User findByUsername(String username); } @Service("userService") publicclass UserServiceImpl implements UserService{@Autowiredprivate UserRepository userRepository;privatestatic TwitterIdentifier identifier = new TwitterIdentifier();@Overridepublic void save(User user) {user.setId(identifier.generalIdentifier());user.setTenant(TenantContextHolder.getTenant());userRepository.save(user);}@Overridepublic User findById(String userId) {Optional<User> optional = userRepository.findById(userId);if(optional.isPresent()){return optional.get();}else{returnnull;}}@Overridepublic User findByUsername(String username) {System.out.println(TenantContextHolder.getTenant());return userRepository.findByUsername(username);}

在這里,我們采用了Twitter的雪花算法來實(shí)現(xiàn)了一個(gè)ID生成器。

7. 配置攔截器

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

/*** @author : Created by Tan Chaohong (alias:ramostear)* @create-time 2019/5/26 0026-23:17* @modify by :* @since:*/ @Slf4j publicclass TenantInterceptor implements HandlerInterceptor{@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String tenant = request.getParameter("tenant");if(StringUtils.isBlank(tenant)){response.sendRedirect("/login.html");returnfalse;}else{TenantContextHolder.setTenant(tenant);returntrue;}} } @Configuration publicclass InterceptorConfig extends WebMvcConfigurationSupport {@Overrideprotected void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**").excludePathPatterns("/login.html");super.addInterceptors(registry);} }

/login.html是系統(tǒng)的登錄路徑,我們需要將其排除在攔截器攔截的范圍之外,否則我們永遠(yuǎn)無法進(jìn)行登錄

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

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

publicclass TenantContextHolder {privatestaticfinal 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();} }

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

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

要實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換,我們需要借助兩個(gè)類來完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。從它們的命名上就可以看出,一個(gè)負(fù)責(zé)解析租戶標(biāo)識,一個(gè)負(fù)責(zé)提供租戶標(biāo)識對應(yīng)的租戶數(shù)據(jù)源信息。首先,我們需要實(shí)現(xiàn)CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租戶標(biāo)識的解析功能。實(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:*/publicclass CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {/*** 默認(rèn)的租戶ID*/privatestaticfinal String DEFAULT_TENANT = "tenant_1";/*** 解析當(dāng)前租戶的ID* @return*/@Overridepublic String resolveCurrentTenantIdentifier() {//通過租戶上下文獲取租戶ID,此ID是用戶登錄時(shí)在header中進(jìn)行設(shè)置的String tenant = TenantContextHolder.getTenant();//如果上下文中沒有找到該租戶ID,則使用默認(rèn)的租戶ID,或者直接報(bào)異常信息return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;}@Overridepublic boolean validateExistingCurrentSessions() {returntrue;} }

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

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

@Slf4j @Configuration publicclass DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{privatestaticfinallong serialVersionUID = -7522287771874314380L;@Autowiredprivate MasterTenantRepository masterTenantRepository;private Map<String,DataSource> dataSources = new TreeMap<>();@Overrideprotected 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();} @Overrideprotected 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);} }

在該類中,通過查詢租戶數(shù)據(jù)源庫,動態(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") publicclass TenantDataSourceConfig {@Bean("jpaVendorAdapter")public JpaVendorAdapter jpaVendorAdapter(){returnnew 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(){returnnew DataSourceBasedMultiTenantConnectionProviderImpl();}@Bean(name = "currentTenantIdentifierResolver")public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){returnnew 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;} }

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

10. 應(yīng)用測試

最后,我們通過一個(gè)簡單的登錄案例來測試本次課程中的SaaS應(yīng)用程序,為此,需要提供一個(gè)Controller用于處理用戶登錄邏輯。在本案例中,沒有嚴(yán)格的對用戶密碼進(jìn)行加密,而是使用明文進(jìn)行比對,也沒有提供任何的權(quán)限認(rèn)證框架,知識單純的驗(yàn)證SaaS的基本特性是否具備。登錄控制器代碼如下:

/*** @author : Created by Tan Chaohong (alias:ramostear)* @create-time 2019/5/27 0027-0:18* @modify by :* @since:*/ @Controller publicclass LoginController {@Autowiredprivate 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";}} }

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

在登錄窗口中輸入對應(yīng)的租戶名,用戶名和密碼,測試是否能夠正常到達(dá)主頁。可以多增加幾個(gè)租戶和用戶,測試用戶是否正常切換到對應(yīng)的租戶下。

總結(jié)

以上是生活随笔為你收集整理的Spring Boot 构建多租户 SaaS 平台核心技术指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。