javascript
Spring Boot 构建多租户 SaaS 平台核心技术指南
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. 維護、識別和路由租戶數據源
我們可以提供一個獨立的庫來存放租戶信息,如數據庫名稱、鏈接地址、用戶名、密碼等,這可以統一的解決租戶信息維護的問題。租戶的識別和路由有很多種方法可以解決,下面列舉幾個常用的方式:
-
可以通過域名的方式來識別租戶:我們可以為每一個租戶提供一個唯一的二級域名,通過二級域名就可以達到識別租戶的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我們識別租戶的關鍵信息。
-
可以將租戶信息作為請求參數傳遞給服務端,為服務端識別租戶提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的參數tenantId就是應用程序識別租戶的關鍵信息。
-
可以在請求頭(Header)中設置租戶信息,例如JWT等技術,服務端通過解析Header中相關參數以獲得租戶信息。
-
在用戶成功登錄系統后,將租戶信息保存在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: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的相關技術
una:master:datasource配置項就是上面說的統一存放租戶信息的數據源配置信息,你可以理解為主庫。
接下來,我們需要關閉Spring Boot自動配置數據源的功能,在項目主類上添加如下的設置:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) publicclass UnaSaasApplication {public static void main(String[] args) {SpringApplication.run(UnaSaasApplication.class, args);}}最后,讓我們看看整個項目的結構:
5. 實現租戶數據源查詢模塊
我們將定義一個實體類存放租戶數據源信息,它包含了租戶名,數據庫連接地址,用戶名和密碼等信息,其代碼如下:
@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接口,快速實現對數據源的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 publicinterface 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:*/publicinterface 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") 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();} }接下來是配置自定義的數據源,其源碼如下:
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;} }在改配置類中,我們主要提供包掃描路徑,實體管理工程,事務管理器和數據源配置參數的配置。
6. 實現租戶業務模塊
在此小節中,租戶業務模塊我們僅提供一個用戶登錄的場景來演示SaaS的功能。其實體層、業務層和持久化層根普通的Spring Boot?Web項目沒有什么區別,你甚至感覺不到它是一個SaaS應用程序的代碼。
首先,創建一個用戶實體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; }業務層提供了一個根據用戶名檢索用戶信息的服務,它將調用持久層的方法根據用戶名對租戶的用戶表進行檢索,如果找到滿足條件的用戶記錄,則返回用戶信息,如果沒有找到,則返回null;持久層和業務層的源碼分別如下:
@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的雪花算法來實現了一個ID生成器。
7. 配置攔截器
我們需要提供一個租戶信息的攔截器,用以獲取租戶標識符,其源代碼和配置攔截器的源代碼如下:
/*** @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是系統的登錄路徑,我們需要將其排除在攔截器攔截的范圍之外,否則我們永遠無法進行登錄
8. 維護租戶標識信息
在這里,我們使用ThreadLocal來存放租戶標識信息,為動態設置數據源提供數據支持,該類提供了設置租戶標識、獲取租戶標識以及清除租戶標識三個靜態方法。其源碼如下:
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();} }此類時實現動態數據源設置的關鍵
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:*/publicclass CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {/*** 默認的租戶ID*/privatestaticfinal String DEFAULT_TENANT = "tenant_1";/*** 解析當前租戶的ID* @return*/@Overridepublic String resolveCurrentTenantIdentifier() {//通過租戶上下文獲取租戶ID,此ID是用戶登錄時在header中進行設置的String tenant = TenantContextHolder.getTenant();//如果上下文中沒有找到該租戶ID,則使用默認的租戶ID,或者直接報異常信息return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;}@Overridepublic boolean validateExistingCurrentSessions() {returntrue;} }此類的邏輯非常簡單,就是從ThreadLocal中獲取當前設置的租戶標識符
有了租戶標識符解析類之后,我們需要擴展租戶數據源提供類,實現從數據庫動態查詢租戶數據源信息,其源碼如下:
@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);} }在該類中,通過查詢租戶數據源庫,動態獲得租戶數據源信息,為租戶業務模塊的數據源配置提供數據數據支持。
最后,我們還需要提供租戶業務模塊數據源配置,這是整個項目核心的地方,其代碼如下:
@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;} }在改配置文件中,大部分內容與主數據源的配置相同,唯一的區別是租戶標識解析器與租戶數據源補給源的設置,它將告訴Hibernate在執行數據庫操作命令前,應該設置什么樣的數據庫連接信息,以及用戶名和密碼等信息。
10. 應用測試
最后,我們通過一個簡單的登錄案例來測試本次課程中的SaaS應用程序,為此,需要提供一個Controller用于處理用戶登錄邏輯。在本案例中,沒有嚴格的對用戶密碼進行加密,而是使用明文進行比對,也沒有提供任何的權限認證框架,知識單純的驗證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";}} }在啟動項目之前,我們需要為主數據源創建對應的數據庫和數據表,用于存放租戶數據源信息,同時還需要提供一個租戶業務模塊數據庫和數據表,用來存放租戶業務數據。一切準備就緒后,啟動項目,在瀏覽器中輸入:http://localhost:8080/login.html
在登錄窗口中輸入對應的租戶名,用戶名和密碼,測試是否能夠正常到達主頁。可以多增加幾個租戶和用戶,測試用戶是否正常切換到對應的租戶下。
總結
以上是生活随笔為你收集整理的Spring Boot 构建多租户 SaaS 平台核心技术指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 性能提升约 7 倍!Apache Fli
- 下一篇: 我觉得有不少人被Spring带着跑偏了!