SpringBoot实战:SpringBoot多租户配置与实现


一、介绍

在现代软件开发领域,多租户架构已成为一种广受欢迎的设计范式。这种架构巧妙地使多个独立客户(或称租户)能够共享同一应用程序的底层实例,同时严格保障每个租户数据的隔离性与安全性,有效促进了资源的高效利用。本文将深入剖析如何在Spring Boot框架中配置并实现多租户支持,通过详尽的Java代码示例,帮助读者深刻理解多租户架构的精髓与实施细节,从而能够在实际项目中灵活应用这一强大技术。

什么是多租户架构?
多租户架构,作为一种先进的软件架构模式,其核心在于允许单个应用程序实例同时为多个租户(或客户)提供服务。在这种架构下,每个租户的数据和配置均保持高度的隔离性,以确保不同租户之间的数据安全性与隐私保护。多租户架构的实现策略多样,主要包括以下三种方式:
  1. 共享数据库,独立数据表:此模式下,所有租户共同使用一个数据库实例,但每个租户都拥有自己独立的数据表,以此实现数据的逻辑隔离。
  2. 共享数据库,共享数据表:在这种实现中,所有租户不仅共享同一个数据库实例,还共享相同的数据表。为了区分不同租户的数据,通常会在数据表中引入租户标识字段,以此实现数据的物理共存、逻辑隔离。
  3. 独立数据库:这是最为严格的数据隔离方式,每个租户都拥有自己独立的数据库实例。这种方式虽然能够最大限度地保证数据的安全性和隔离性,但也可能带来较高的资源消耗和管理成本。

二、SpringBoot 配置多租户

实现步骤:

  1. 设置基本项目结构。

  2. 配置数据源和Hibernate拦截器。

  3. 创建租户解析器。

  4. 配置实体和存储库。

  5. 测试多租户的实现。

基本项目结构:

在项目的pom.xml文件中添加相关依赖

<dependencies>
    <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>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
配置数据源和Hibernate拦截器
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

创建控制类,用来实现添加用户上下文信息和拦截数据库作用:

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
 
import java.io.Serializable;
 
@Component
public class TenantInterceptor extends EmptyInterceptor {
 
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        // 在保存实体时添加租户标识
        for (int i = 0; i < propertyNames.length; i++) {
            if ("tenantId".equals(propertyNames[i])) {
                state[i] = TenantContext.getCurrentTenant();
                return true;
            }
        }
        return false;
    }
 
    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        // 在更新实体时添加租户标识
        for (int i = 0; i < propertyNames.length; i++) {
            if ("tenantId".equals(propertyNames[i])) {
                currentState[i] = TenantContext.getCurrentTenant();
                return true;
            }
        }
        return false;
    }
}

配置 TenantContext 上下文,保存当前租户唯一标识:

public class TenantContext {
    private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
 
    public static void setCurrentTenant(String tenantId) {
        currentTenant.set(tenantId);
    }
 
    public static String getCurrentTenant() {
        return currentTenant.get();
    }
 
    public static void clear() {
        currentTenant.remove();
    }
}

创建解析器,从中提取当前租户唯一标识:

租户解析器,作为一个关键组件,其职责在于从传入的请求中提取出租户的唯一标识,并将该标识设置到TenantContext中,以便应用程序的后续部分能够基于这一租户标识进行数据的隔离访问。为了实现租户标识的自动提取与设置,我们可以巧妙地利用Spring框架提供的过滤器(Filter)机制。通过定义一个自定义的过滤器,我们可以在请求到达控制器之前,就先行完成租户标识的解析与上下文设置工作。

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
 
public class TenantFilter implements Filter {
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
{
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String tenantId = httpRequest.getHeader("X-TenantID");
        if (tenantId != null) {
            TenantContext.setCurrentTenant(tenantId);
        }
 
        try {
            chain.doFilter(request, response);
        } finally {
            TenantContext.clear();
        }
    }
 
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
 
    @Override
    public void destroy() {
    }
}

注册过滤器

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class TenantConfiguration {
 
    @Bean
    public FilterRegistrationBean<TenantFilter> tenantFilter() {
        FilterRegistrationBean<TenantFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TenantFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

创建实体类并配置存储库

//实体类import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Product {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String tenantId;
    private String name;
    private Double price;
 
    // Getter和Setter方法
}



//存储库
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
import java.util.List;
 
@RestController
@RequestMapping("/products")
public class ProductController {
 
    @Autowired
    private ProductRepository productRepository;
 
    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productRepository.save(product);
    }
 
    @GetMapping
    public List<Product> getProducts() {
        String tenantId = TenantContext.getCurrentTenant();
        return productRepository.findByTenantId(tenantId);
    }
}

接下来创建一个controller 来测试多租户功能:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
import java.util.List;
 
@RestController
@RequestMapping("/products")
public class ProductController {
 
    @Autowired
    private ProductRepository productRepository;
 
    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productRepository.save(product);
    }
 
    @GetMapping
    public List<Product> getProducts() {
        String tenantId = TenantContext.getCurrentTenant();
        return productRepository.findByTenantId(tenantId);
    }
}


# 创建产品,租户ID为tenant1
curl -H "X-TenantID: tenant1" -X POST -d '{"name": "Product A", "price": 10.99}' -H "Content-Type: application/json" http://localhost:8080/products
 
# 创建产品,租户ID为tenant2
curl -H "X-TenantID: tenant2" -X POST -d '{"name": "Product B", "price": 15.99}' -H "Content-Type: application/json" http://localhost:8080/products
 
# 获取产品,租户ID为tenant1
curl -H "X-TenantID: tenant1" http://localhost:8080/products
 
# 获取产品,租户ID为tenant2
curl -H "X-TenantID: tenant2" http://localhost:8080/products


原文始发于微信公众号(Java技术前沿):SpringBoot实战:SpringBoot多租户配置与实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/299742.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!