摘要:概述筆者從年開(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)。
1. 概述本次教程所涉及到的源碼已上傳至Github,如果你不需要繼續(xù)閱讀下面的內(nèi)容,你可以直接點(diǎn)擊此鏈接獲取源碼內(nèi)容。github.com/ramostear/u…
筆者從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);
}
7. 配置攔截器在這里,我們采用了Twitter的雪花算法來(lái)實(shí)現(xiàn)了一個(gè)ID生成器。
我們需要提供一個(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);
}
}
8. 維護(hù)租戶標(biāo)識(shí)信息/login.html是系統(tǒng)的登錄路徑,我們需要將其排除在攔截器攔截的范圍之外,否則我們永遠(yuǎn)無(wú)法進(jìn)行登錄
在這里,我們使用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();
}
}
9. 動(dòng)態(tài)數(shù)據(jù)源切換此類時(shí)實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源設(shè)置的關(guān)鍵
要實(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;
}
}
10. 應(yīng)用測(cè)試在改配置文件中,大部分內(nèi)容與主數(shù)據(jù)源的配置相同,唯一的區(qū)別是租戶標(biāo)識(shí)解析器與租戶數(shù)據(jù)源補(bǔ)給源的設(shè)置,它將告訴Hibernate在執(zhí)行數(shù)據(jù)庫(kù)操作命令前,應(yīng)該設(shè)置什么樣的數(shù)據(jù)庫(kù)連接信息,以及用戶名和密碼等信息。
最后,我們通過(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
摘要:而且,用友云配置中心以服務(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ō)就是...
摘要:神策數(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)...
摘要:今天逛了逛,順手精選出了一下近幾個(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,順...
摘要:一體化所能產(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...
摘要:混合云所提供的靈活性和控制能力是它在可預(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ì)算模型的原...
閱讀 2320·2021-09-29 09:42
閱讀 556·2021-09-06 15:02
閱讀 2595·2021-09-02 15:40
閱讀 2111·2019-08-30 14:23
閱讀 1860·2019-08-30 13:48
閱讀 1289·2019-08-26 12:01
閱讀 957·2019-08-26 11:53
閱讀 2141·2019-08-23 18:31