Spring boot core technology guide for building multi tenant SaaS platform
Program description DD 2021-06-25 02:37:48

1. summary

The author from the 2014 Contact began in SaaS(Software as a Service), I.e. multi tenant ( Or multi tenancy ) Software application platform ; And has been engaged in architecture design and R & D work in related fields . A coincidence , In my undergraduate graduation project, I completed a project based on SaaS Research on the efficient financial management platform , There's a lot to gain from it . First contact SaaS when , Lack of relevant domestic resources , The only reference available is 《 The software revolution in the Internet era :SaaS Architecture design 》( Ye Wei is waiting for ) A Book . The final project is based on OSGI(Open Service Gateway Initiative)Java Dynamic modular system specification to achieve .

today , Five years have passed , Great changes have taken place in the technology of software development , What I have achieved SaaS The technology stack of the platform has also been updated for several waves , That's true. Then :“ There is no way for mountains, heavy waters , Another village ”. Based on many detours and depressions , And recently many netizens have asked me how to use Spring Boot Implement multi tenant system , Decided to write an article about SaaS Hard core technology .

Speaking of SaaS, It's just a software architecture , There is not much mystery , It's not a very difficult system , My personal feeling ,SaaS The difficulty of the platform lies in the business operation , It's not a technical implementation . Technically ,SaaS It's such an architectural pattern : It allows users of multiple environments to use the same set of applications , And ensure that the data between users is isolated from each other . Now think about , It's also a bit of a sharing economy .

I will not talk about it in depth here SaaS Software maturity model and data isolation scheme contrast things . Today I want to talk about using Spring Boot Build independent databases quickly / The shared database is independent Schema The multi tenant system . I will provide a SaaS The core technology of the system , Other interested friends can expand on this basis .

2. Try to understand the multi tenant application scenario

Suppose we need to develop an application , And want to sell the same application to N Used by customers . Under normal circumstances , We need to create N individual Web The server (Tomcat),N A database (DB), And for N Customers deploy the same application N Time . Now? , If we upgrade our application or make any other changes , Then we need to update N Applications also need to be maintained N Servers . Next , If the business starts to grow , The customer is from the original N One becomes the present N+M individual , We will face N Applications and M Application version maintenance , Equipment maintenance and cost control . Operation and maintenance almost cry to death in the computer room …

In order to solve the above problems , We can develop multi tenant applications , We can according to the current user who , Then choose the corresponding database . for example , When the request comes from A The company's users , The application connects A Company's database , When the request comes from B The company's users , Automatically switch the database to B Company database , And so on . In theory, there will be no problem , But if we think about transforming an existing application into SaaS Pattern , We're going to have the first problem : If you identify which tenant the request comes from ? How to switch data sources automatically ?

3. maintain 、 Identify and route tenant data sources

We can provide a separate library to store tenant information , Such as database name 、 Link address 、 user name 、 Password etc. , This can solve the problem of tenant information maintenance . There are many ways to identify and route tenants , Here are some common ways :

  • 1. Tenants can be identified by domain names : We can provide a unique secondary domain name for each tenant , The ability to identify tenants can be achieved through the secondary domain name , Such as tenantone.example.com,tenant.example.com;tenantone and tenant It's the key information that we identify tenants .
  • 2. Tenant information can be passed to the server as request parameters , Provide support for identifying tenants on the server side , Such as saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2. The parameters are tenantId Is the key information that the application identifies the tenant .
  • 3. Can be in the request header (Header) Set tenant information in , for example JWT Technology , The server uses parsing Header To get tenant information .
  • 4. After the user successfully logs in to the system , Store tenant information in Session in , From when needed Session Take out tenant information .

After solving the above problems , Let's take a look at how to get the tenant information passed in by the client , And how to use tenant information in our business code ( The key is DataSources The problem of ).

We all know , Start up Spring Boot Before the application , You need to provide configuration information about the data source ( In case of using the database ), According to the requirements at the beginning , Yes N Customers need to use our application , We need to configure it in advance N Data sources ( Multiple data sources ), If N<50, I think I can stand , If more , This is obviously unacceptable . To solve this problem , We need the help of Hibernate 5 Dynamic data source features provided , Let our applications have the ability to dynamically configure client data sources . Simply speaking , When a user requests system resources , We will provide the tenant information provided by users (tenantId) Store in ThreadLoacal in , And then get TheadLocal Tenant information in , And according to this information to query a separate tenant Library , Get the data configuration information of the current tenant , And then with the help of Hibernate The ability to dynamically configure data sources , Set the data source for the current request , Last user's request . In this way, we only need to maintain a data source configuration information in the application ( Tenant database configuration library ), The rest of the data source dynamic query configuration . Next , We will quickly demonstrate this function .

4. The project build

We will use Spring Boot 2.1.5 Version to implement this demo project , First you need to be in Maven Add the following configurations to the configuration file :

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

Then provide an available configuration file , And add the following :

spring:
freemarker:
cache: false
template-loader-path:
- classpath:/templates/
prefix:
suffix: .html
resources:
static-locations:
- classpath:/static/
devtools:
restart:
enabled: true
jpa:
database: mysql
show-sql: true
generate-ddl: false
hibernate:
ddl-auto: none
una:
master:
datasource:
url: jdbc:mysql://localhost:3306/master_tenant?useSSL=false
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
maxPoolSize: 10
idleTimeout: 300000
minIdle: 10
poolName: master-database-connection-pool
logging:
level:
root: warn
org:
springframework:
web: debug
hibernate: debug

As a result of Freemarker As a view rendering engine , So we need to provide Freemarker Related technology

una:master:datasource Configuration item is the data source configuration information that stores tenant information in a unified way , You can understand the main library .

Next , We need to close Spring Boot Automatic configuration of data source function , Add the following settings to the project main class :

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

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

}

Last , Let's look at the structure of the whole project : img

5. Implement tenant data source query module

We will define an entity class to store tenant data source information , It contains the tenant name , Database connection address , User name and password etc , The code is as follows :

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

We will inherit the persistence layer JpaRepository Interface , Fast implementation of CURD operation , At the same time, it provides an interface to find tenant data source through tenant name , The code is as follows :

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

The business layer provides services to obtain tenant data source information through tenant name ( Other services can be added by you ):

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

Last , We need to focus on configuring the primary data source (Spring Boot You need to provide a default data source for it ). Before configuration , We need to get configuration items , Can pass @ConfigurationProperties(“una.master.datasource”) Get the configuration information in the configuration file :

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

Next, configure a custom data source , The source code is as follows :

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

In the configuration class , We mainly provide package scanning path , Physical management engineering , Configuration of transaction manager and data source configuration parameters .

6. Implement tenant business module

In this section , We only provide a user login scenario to demonstrate the tenant business module SaaS The function of . In fact, the body layer 、 The business layer and persistence layer are rooted in the common Spring Boot Web There is no difference in the project , You can't even feel it as a SaaS The code for the application .

First , Create a user entity User, The source code is as follows :

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

The business layer provides a service to retrieve user information based on user name , It will call the method of persistence layer to retrieve the user table of tenant according to user name , If you find a user record that meets the criteria , Then return the user information , If not found , Then return to null; The source code of persistence layer and business layer are as follows :

@Repository
public interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{

User findByUsername(String username);
}
@Service("userService")
public class UserServiceImpl implements UserService{

@Autowired
private UserRepository userRepository;

private static TwitterIdentifier identifier = new TwitterIdentifier();



@Override
public void save(User user) {
user.setId(identifier.generalIdentifier());
user.setTenant(TenantContextHolder.getTenant());
userRepository.save(user);
}

@Override
public User findById(String userId) {
Optional<User> optional = userRepository.findById(userId);
if(optional.isPresent()){
return optional.get();
}else{
return null;
}
}

@Override
public User findByUsername(String username) {
System.out.println(TenantContextHolder.getTenant());
return userRepository.findByUsername(username);
}

ad locum , We used Twitter Snow algorithm to achieve a ID generator .

7. Configure interceptors

We need to provide an interceptor for tenant information , To get the tenant identifier , Its source code and configuration interceptor's source code are as follows :

/**
* @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 Is the login path of the system , We need to exclude it from interceptors , Otherwise we will never be able to log in

8. Maintain tenant identity information

ad locum , We use ThreadLocal To store tenant identification information , Provide data support for dynamic setting of data source , This class provides setting tenant identity 、 There are three static methods to get tenant identity and clear tenant identity . The source code is as follows :

public class TenantContextHolder {

private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

public static void setTenant(String tenant){
CONTEXT.set(tenant);
}

public static String getTenant(){
return CONTEXT.get();
}

public static void clear(){
CONTEXT.remove();
}
}

This class is the key to realize dynamic data source settings

9. Dynamic data source switching

To achieve dynamic data source switching , We need to do this with two classes ,CurrentTenantIdentifierResolver and AbstractDataSourceBasedMultiTenantConnectionProviderImpl. We can see from their names that , One is responsible for resolving tenant identity , One is responsible for providing tenant data source information corresponding to tenant identity . First , We need to achieve CurrentTenantIdentifierResolver Interface resolveCurrentTenantIdentifier() and validateExistingCurrentSessions() Method , Complete the tenant identity resolution function . The source code of the implementation class is as follows :

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 {

/**
* Default tenant ID
*/
private static final String DEFAULT_TENANT = "tenant_1";

/**
* Analyze the current tenant's ID
* @return
*/
@Override
public String resolveCurrentTenantIdentifier() {
// Get tenants through tenant context ID, this ID It's when the user logs in header Set in
String tenant = TenantContextHolder.getTenant();
// If the tenant is not found in the context ID, Then use the default tenant ID, Or report abnormal information directly
return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;
}

@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}

This kind of logic is very simple , It's from ThreadLocal Gets the tenant identifier of the current setting

With the tenant identifier resolution class , We need to extend the tenant data source provider class , Realize dynamic query of tenant data source information from database , The source code is as follows :

@Slf4j
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{

private static final long serialVersionUID = -7522287771874314380L;
@Autowired
private MasterTenantRepository masterTenantRepository;

private Map<String,DataSource> dataSources = new TreeMap<>();

@Override
protected DataSource selectAnyDataSource() {
if(dataSources.isEmpty()){
List<MasterTenant> tenants = masterTenantRepository.findAll();
tenants.forEach(masterTenant->{
dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));
});
}
return dataSources.values().iterator().next();
}
@Override
protected DataSource selectDataSource(String tenant) {
if(!dataSources.containsKey(tenant)){
List<MasterTenant> tenants = masterTenantRepository.findAll();
tenants.forEach(masterTenant->{
dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));
});
}
return dataSources.get(tenant);
}
}

In this class , By querying the tenant data source database , Get tenant data source information dynamically , Provide data support for data source configuration of tenant business module .

Last , We also need to provide tenant business module data source configuration , This is the core of the whole project , The code is as follows :

@Slf4j
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {
"com.ramostear.una.saas.tenant.model",
"com.ramostear.una.saas.tenant.repository"
})
@EnableJpaRepositories(basePackages = {
"com.ramostear.una.saas.tenant.repository",
"com.ramostear.una.saas.tenant.service"
},entityManagerFactoryRef = "tenantEntityManagerFactory"
,transactionManagerRef = "tenantTransactionManager")
public class TenantDataSourceConfig {

@Bean("jpaVendorAdapter")
public JpaVendorAdapter jpaVendorAdapter(){
return new HibernateJpaVendorAdapter();
}
@Bean(name = "tenantTransactionManager")
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}

@Bean(name = "datasourceBasedMultiTenantConnectionProvider")
@ConditionalOnBean(name = "masterEntityManagerFactory")
public MultiTenantConnectionProvider multiTenantConnectionProvider(){
return new DataSourceBasedMultiTenantConnectionProviderImpl();
}
@Bean(name = "currentTenantIdentifierResolver")
public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
return new CurrentTenantIdentifierResolverImpl();
}

@Bean(name = "tenantEntityManagerFactory")
@ConditionalOnBean(name = "datasourceBasedMultiTenantConnectionProvider")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider,
@Qualifier("currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver
){
LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();
localBean.setPackagesToScan(
new String[]{
User.class.getPackage().getName(),
UserRepository.class.getPackage().getName(),
UserService.class.getPackage().getName()

}
);
localBean.setJpaVendorAdapter(jpaVendorAdapter());
localBean.setPersistenceUnitName("tenant-database-persistence-unit");
Map<String,Object> properties = new HashMap<>();
properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);
properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);
properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
properties.put(Environment.SHOW_SQL,true);
properties.put(Environment.FORMAT_SQL,true);
properties.put(Environment.HBM2DDL_AUTO,"update");
localBean.setJpaPropertyMap(properties);
return localBean;
}
}

In the change configuration file , Most of the content is the same as the configuration of the main data source , The only difference is the settings of the tenant identity resolver and the tenant data source supply source , It will tell Hibernate Before executing the database operation command , What kind of database connection information should be set , As well as user name and password information .

10. Application testing

Last , We use a simple login case to test the SaaS Applications , So , Need to provide a Controller For handling user login logic . In this case , There is no strict encryption of user passwords , It's using plaintext for comparison , There is no authority authentication framework , The simple verification of knowledge SaaS Are the basic characteristics of . The login controller code is as follows :

/**
* @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";
}
}
}

Before starting the project , We need to create the corresponding database and data table for the main data source , Used to store tenant data source information , At the same time, we need to provide a tenant business module database and data table , Used to store tenant business data . When everything is ready , Start project , Enter... In the browser :http://localhost:8080/login.html

img

Enter the corresponding tenant name in the login window , User name and password , Test whether the home page can be reached normally . Can add a few more tenants and users , Test whether the user can switch to the corresponding tenant normally .

summary

ad locum , I shared the use of Spring Boot+JPA Fast implementation of multi tenant applications , This method only involves the implementation SaaS The core technology of application platform , It's not a complete usable project code , Such as user authentication 、 Authorization and so on do not appear in this article . Additional business module interested friends can expand on this design , If you have any questions about the code , Welcome to leave a message below .

The source code involved in this tutorial has been uploaded to Github, If you don't need to read the following , You can directly click this link to get the source content .https://github.com/ramostear/una-saas-toturial

The author of this article : Fox under the tree ,
Link to the original text :https://my.oschina.net/ramostear/blog/3136000
The copyright belongs to the author , Please indicate the author of the reprint 、 original text 、 Translators and other source information
Please bring the original link to reprint ,thank
Similar articles