MybatisPlus 多租户架构(Multi-tenancy)实现详解

“MybatisPlus 多租户架构(Multi-tenancy)实现详解”旨在为需要在一个应用中支持多个租户的开发人员提供一种解决方案。在这个架构中,多个租户可以共享相同的代码库和实例,并在逻辑上隔离数据。

实现多租户架构需要考虑以下三个方面:

  1. 租户隔离

使用 Mybatis-Plus 提供的 SqlParserInterceptor 对 SQL 进行拦截,通过替换表名、增加 WHERE 条件等方式实现租户隔离。具体实现代码如下:

public class MybatisPlusConfig {

    @Bean
    public SqlParserInterceptor sqlParserInterceptor() {
        return new SqlParserInterceptor() {
            @Override
            public void prepare(Invocation invocation) {
                if (TenantContextHolder.getTenantId() != null) {
                    MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());
                    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
                    BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
                    String sql = boundSql.getSql();
                    sql = sql.replaceAll(mappedStatement.getId() + ".", mappedStatement.getId() + "_" + TenantContextHolder.getTenantId() + ".");
                    sql = "select t.* from (" + sql + ") t where t.tenant_id = " + TenantContextHolder.getTenantId();
                    metaObject.setValue("delegate.boundSql.sql", sql);
                }
            }
        };
    }

}
  1. 租户切换

使用 ThreadLocal 实现当前租户ID在同一线程中的共享,在实际访问数据库时增加租户ID参数。具体实现代码如下:

public class TenantContextHolder {

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

    public static void setTenantId(String tenantId) {
        TENANT_ID.set(tenantId);
    }

    public static String getTenantId() {
        return TENANT_ID.get();
    }

    public static void clearTenantId() {
        TENANT_ID.remove();
    }

}
  1. 租户配置

为每一个租户配置独立的数据源,使用 Mybatis-Plus 提供的 DynamicDataSource 实现动态数据源切换。具体实现代码如下:

public class DataSourceConfig {

    private final static Map<String, DruidDataSource> DATA_SOURCE_MAP = new ConcurrentHashMap<>();

    private final static String DEFAULT_TENANT_ID = "1";

    @Bean("tenantDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource tenantDataSource() {
        return new DruidDataSource();
    }

    @Bean("dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier("tenantDataSource") DataSource tenantDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DEFAULT_TENANT_ID, tenantDataSource);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(tenantDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        DynamicDataSourceContextHolder.addDataSourceKeys(DEFAULT_TENANT_ID);
        DATA_SOURCE_MAP.put(DEFAULT_TENANT_ID, (DruidDataSource) tenantDataSource);
        return dynamicDataSource;
    }

    private static DruidDataSource createDataSource(String driverClassName, String url, String username, String password) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    public static void addDataSource(String tenantId, String driverClassName, String url, String username, String password) {
        DruidDataSource dataSource = createDataSource(driverClassName, url, username, password);
        DATA_SOURCE_MAP.put(tenantId, dataSource);
        DynamicDataSourceContextHolder.addDataSourceKeys(tenantId);
    }

    public static void removeDataSource(String tenantId) {
        DATA_SOURCE_MAP.remove(tenantId);
        DynamicDataSourceContextHolder.removeDataSourceKeys(tenantId);
    }

    public static void switchDataSource(String tenantId) throws SQLException {
        if (!DATA_SOURCE_MAP.containsKey(tenantId)) {
            throw new RuntimeException("tenantId not exists: " + tenantId);
        }
        DynamicDataSource dynamicDataSource = (DynamicDataSource) ApplicationContextHolder.getBean("dynamicDataSource");
        dynamicDataSource.setTargetDataSource(DATA_SOURCE_MAP.get(tenantId));
        TenantContextHolder.setTenantId(tenantId);
    }

}

示例一:基于 Header 实现租户切换

在这个示例中,我们将在 HTTP Header 中增加 Tenant ID,通过定义 Filter 将该 Header 设置为当前租户ID。具体实现代码如下:

public class HeaderTenantFilter implements Filter {

    private final static String TENANT_HEADER = "X-TENANT-ID";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String tenantId = httpServletRequest.getHeader(TENANT_HEADER);
        TenantContextHolder.setTenantId(tenantId != null ? tenantId : DataSourceConfig.DEFAULT_TENANT_ID);
        chain.doFilter(request, response);
        TenantContextHolder.clearTenantId();
    }

}

示例二:基于 sub-domain 实现租户切换

在这个示例中,我们将为每个租户分配独立的子域名,并通过定义 Servlet 拦截器解析当前访问的子域名并设置为当前租户ID。具体实现代码如下:

public class SubdomainTenantInterceptor extends HandlerInterceptorAdapter {

    private final static String HTTP_SCHEMA = "http";
    private final static String HTTPS_SCHEMA = "https";
    private final static String DEFAULT_DOMAIN = "example.com";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String domain = (request.getServerName()).toLowerCase();
        String schema = request.isSecure() ? HTTPS_SCHEMA : HTTP_SCHEMA;
        String tenantId = getTenantIdBySubdomain(domain);
        String redirectUrl = getRedirectUrl(schema, domain);
        if (StringUtils.isBlank(tenantId)) {
            response.sendRedirect(redirectUrl);
            return false;
        } else {
            TenantContextHolder.setTenantId(tenantId);
            return true;
        }
    }

    private String getTenantIdBySubdomain(String domain) {
        String tenantId = null;
        if (StringUtils.isNotBlank(domain)) {
            String[] subdomains = domain.split("\\.");
            if (subdomains.length > 0) {
                tenantId = subdomains[0];
            }
        }
        return tenantId;
    }

    private String getRedirectUrl(String schema, String domain) {
        return schema + "://" + DOMAIN + "/no-tenant";
    }

}

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:MybatisPlus 多租户架构(Multi-tenancy)实现详解 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • 使用java8的方法引用替换硬编码的示例代码

    当编写Java代码时,我们经常会使用硬编码方式来实现一些操作。而Java8引入的方法引用却可以使我们的代码更加简洁而且易于维护。下面是使用Java8的方法引用替换硬编码代码的完整攻略: 1. 什么是方法引用 方法引用是一种可以用来简化Lambda表达式的写法,可以用过已有的方法来引用类的实例或类静态方法。可以将方法引用看成是Lambda表达式的精简写法。 2…

    Java 2023年5月19日
    00
  • Java中JDBC的使用教程详解

    Java中JDBC的使用教程详解 JDBC(Java Database Connectivity)是Java语言操作数据库的标准规范。本文将详细讲解Java中JDBC的使用教程,包括开发环境搭建、JDBC连接MySQL数据库、CRUD操作、事务管理等内容。 开发环境搭建 在使用JDBC之前,需要安装Java开发环境和MySQL数据库,并将MySQL JDBC…

    Java 2023年5月19日
    00
  • 深度分析java dump文件

    以下是“深度分析java dump文件”的完整攻略: 什么是Java Dump文件 Java Dump文件是在Java应用程序运行时出现异常或死锁等问题时自动或手动导出的一种快照文件。它记录了Java虚拟机(JVM)在某个时间点上的内存状态,可以用于问题排查和调试。 如何生成Java Dump文件 可以通过以下两种方式生成Java Dump文件: JCons…

    Java 2023年5月20日
    00
  • Java由浅入深全面讲解方法的使用

    Java由浅入深全面讲解方法的使用 什么是方法? 方法是一组可以被重复使用的代码块。它可以接受参数并返回结果。在Java中,方法是类的基本组成部分,通过方法可以完成对类的成员变量进行操作,并实现不同功能的代码块重用。 如何定义方法? 在Java中,方法由方法名和一对括号()组成,括号中可以定义传递给方法的参数列表。方法的代码块用{}包围。定义方法的基本语法如…

    Java 2023年5月26日
    00
  • springboot+dynamicDataSource动态添加切换数据源方式

    使用 Spring Boot,可以动态添加切换数据源,需要用到Spring JDBC模块中的 AbstractRoutingDataSource 类和 DynamicDataSourceHolder 维护一个存储当前使用的数据源 key 的 ThreadLocal 对象。步骤如下: 导入依赖 首先,在 pom.xml 中导入 Spring Boot 和 Sp…

    Java 2023年5月20日
    00
  • 详解SpringMVC拦截器(资源和权限管理)

    以下是关于“详解SpringMVC拦截器(资源和权限管理)”的完整攻略,其中包含两个示例。 详解SpringMVC拦截器(资源和权限管理) Spring MVC是一个基于Java的Web框架,它可以帮助我们快速开发Web应用程序。拦截器是Spring MVC的一个重要组件,它可以帮助我们实现资源和权限管理。本文将介绍如何使用SpringMVC拦截器实现资源和…

    Java 2023年5月17日
    00
  • Spring MVC的国际化实现代码

    Spring MVC的国际化实现代码攻略 在Spring MVC中,我们可以使用国际化来实现多语言支持。本文将详细讲解Spring MVC的国际化实现代码,包括如何配置国际化资源文件、如何使用MessageSource对象获取国际化信息等。 配置国际化资源文件 在Spring MVC中,我们可以使用.properties文件来存储国际化信息。下面是一个示例代…

    Java 2023年5月18日
    00
  • java8中Stream的使用以及分割list案例

    Java 8中添加了Stream API,提供了一种新的操作集合和数组的方式,它使得我们可以更加便捷地进行集合和数组的处理操作,同时也可以编写更为可读性高和简洁的代码。以下是Java 8中Stream的使用以及分割List的攻略。 Stream的使用 基本概念 Stream是Java 8中提供的一种数据流的方式,它是一种高效、强大和易用的API。它通过函数式…

    Java 2023年5月26日
    00
合作推广
合作推广
分享本页
返回顶部