SpringBoot Data JPA 集成多租户

news2025/2/4 12:02:40

背景:

​ iot-kit项目用的是SpringBoot JPA,不是Mybatis,项目中需要引入多租户。

文章中心思想: 通过Hibernate Filters 和AspectJ 切面编程,实现SpringBoot JPA多租户

什么是多租户

​ 多租户我理解就是一个网站允许你多个公司去登录,每个公司都有他们独立的数据,互相之间的数据能做到独立、隔离。比如像阿里云,华为云这些网站,肯定有很多公司把部署在云服务器上面,每个公司就是一个租户。

多租户的三种形式

一个租户一个数据库

SeparateDatabaseMultiTenancy

​ 每个租户都有他自己的独立数据库,这种模式中,租户的数据是能做到物理隔离的,隔离性和安全性最好。一个租户一个数据库,能确保租户之间的数据做到彻底隔离,但这种方式的代价是每个数据库都得重复定义,维护起来也很低效,比如说,你要加一个字段,每个数据库都得加一编,非常麻烦。

相同Schema不同数据库

SeparateSchemaMultiTenancy

​ 在共享数据库实例时,对每个租户使用单独的Schema。每个租户的数据通过数据库引擎提供的独立模式的语义进行逻辑隔离。这个模式由每个租户的单独数据库用户所拥有,数据库引擎的安全机制将进一步保证数据的隐私性和机密性(但是在这种情况下,数据库连接池不能被数据访问层重用)。

相同数据库,相同表,增加tenantId字段区分

SingleDatabaseMultiTenancy

这种模式,共享享通的数据,共享享通的表,通过字段tenantId区分不同租户,这中模式是绝大多数公司采用的。本文讲的也是这种模式。

​ 最后一种模式主要通过Hibernate 过滤器和切面技术实现。

在Hibernate 的 5.4.x之前的版本,虽然有MultiTenancyStrategy.DISCRIMINATOR这种策略,但是他们一直都没实现,参考下面的JIRA:https://hibernate.atlassian.net/browse/HHH-6054

image-20230716075448279

​ 也就是说Hibernate 5.4.x的版本 官方并没有提供多租户的具体实现。

​ 直到Hibernate 6出来了,官方才正式支持共享数据库共享实例。但是呢,这个Hibernate 6需要使用 Spring Boot 3,而这个 Spring Boot 3又需要把JDK升级到17,这样就没得玩了,因为目前大多数的公司,都是用jdk8 或者jdk11的。

https://spring.io/blog/2022/05/24/preparing-for-spring-boot-3-0

image-20230716080153612

所以为了不升级spring boot版本以及JDK版本,我们就用另外一种形式实现多租户:Hibernate 过滤器 和 切面,说了那么多废话,下面进入正文

进入正文

Jpa Entity Listener

​ Hibernate的Entity Listener 允许我们在JPA的生命周期(新增、删除、更新)中,添加一个监听器。通过这个监听器,我们就能够对实体类做一些额外的操作,例如增加属性tenantId。假设我们自定义的监听器是TenantAware,那所有实现这个接口的实体类都可以实现如下所示的功能: 在更新、删除、插入之前,给entity增加一个租户id的字段。

public interface TenantAware {

    void setTenantId(String tenantId);
    
}

public class TenantListener {

    @PreUpdate
    @PreRemove
    @PrePersist
    public void setTenant(TenantAware entity) {
        final String tenantId = TenantContext.getTenantId();
        entity.setTenantId(tenantId);
    }
}

​ 如果是超级管理员,也额外设置租户id吗?超级管理员肯定不用管啊,那我们在获取到租户id的时候,就判断一下,当前租户是不是超级管理员,如果是,那就直接忽略,不处理即可。

public class TenantListener {

    @PreUpdate
    @PreRemove
    @PrePersist
    public void setTenant(TenantAware entity) {
        final String tenantId = TenantContext.getTenantId();
        if(!"000000".equals(tenantId)){
                entity.setTenantId(tenantId);
        }
    }
}
Hibernate Filter

​ 上面的监听器,只能在监听新增、删除、更新这3个操作,对于查询,就无能为力了。好在有Hibernate Filter。

​ Hibernate Filter的过滤器有一个机制:只要实体类上有@Filter,那么这个实体的所有查询语句,都能被过滤器拦截,如下所示:

@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "string")})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")

​ 有了上面的监听器(Hibernate Filter)和过滤器(Jpa Entity Listener),我们就能把两者整合到一起,然后就可以对实体的增删改查进行拦截操作了。下面具体说说怎么整合。

@MappedSuperclass
@Getter
@Setter
@NoArgsConstructor
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "string")})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
@EntityListeners(TenantListener.class)
public abstract class AbstractBaseEntity implements TenantAware, Serializable {
    private static final long serialVersionUID = 1L;

    @Size(max = 30)
    @Column(name = "tenant_id")
    private String tenantId;

    public AbstractBaseEntity(String tenantId) {
        this.tenantId = tenantId;
    }

}

为了实现多租户功能,所有的租户都必须继承这抽象类:AbstractBaseEntity,如下所示:

@Entity
public class Product extends AbstractBaseEntity {
...
}

理论上,事情到这里就结束了,但是…

​ 但是,一个定义在实体类上的@Filter注解,是不会自动生效的。当一个查询请求发送过来的时候,就会打开一个Hibernate的Session会话,这个Filter过滤器需要配置才能应用到这个session上。因为这个session是在运行时期动态创建的(一般是一个事务创建一次session),所以我们不能这样把这个Filter应用到Hibernate的会话上。

​ 这时候,切面编程Aspect派上用场。

​ 通过AspectJ 来定义execution points(执行点),就能在这些执行点上进行拦截操作,从而达到注入额外逻辑的目的。这,正是我们在那个Hibernate Filter那里需要用!!

​ 通过AspectJ,我们可以在每一个Hibernate Session创建的时候进行拦截操作,然后把Filter过滤器应用到每一个session会话上。但是我们并不能用Spring内置的轻量级切面进行拦截,因为Spring只能管理它自己容器内的bean。我们这里的Hibernate Session对象,是不属于Spring bean的,自然不能像我们平时那样使用切面技术。

​ 为了能够在运行期(runtime)随心所欲的拦截非SPring 容器下的对象(当前这里指Hibernate Session对象),AspectJ需要把定义好的切面织入到类中,织入的方法有两种:编译期(compile-time)、运行期(load-time)。编译器和运行期的说明,参考底部的名词解释。因为加载期的侵入性比较小,所以我们选择在加载期织入。

​ 配置AspectJ 在运行期织入,只需要把下面的配置文件,放到类路径下即可:

image-20230716081748567

<aspectj>
    <weaver options="-Xreweavable -verbose -showWeaveInfo">
        <include within="se.callista.blog.service.multi_tenancy.aspect.TenantFilterAspect"/>
        <include within="org.hibernate.internal.SessionFactoryImpl.SessionBuilderImpl"/>
    </weaver>

    <aspects>
        <aspect name="se.callista.blog.service.multi_tenancy.aspect.TenantFilterAspect"/>
    </aspects>
</aspectj>

参数说明:

​ 1.1 <weaver> 元素:指定了 AspectJ 织入器的配置选项

-Xreweavable 选项用于支持增量编译和重新织入,允许在运行时修改切面并动态重新织入到目标类中。

-verbose 选项用于在织入过程中输出详细信息,可以帮助调试和观察织入的效果。

-showWeaveInfo 选项用于在织入过程中显示织入的详细信息,包括被织入的类和方法等。

<include> 元素:用于指定需要织入的目标类或切点

​ 在这个示例中,指定了 se.callista.blog.service.multi_tenancy.aspect.TenantFilterAspectorg.hibernate.internal.SessionFactoryImpl.SessionBuilderImpl 这两个类。

se.callista.blog.service.multi_tenancy.aspect.TenantFilterAspect 是自定义的切面类,其中定义了租户过滤器的逻辑。

org.hibernate.internal.SessionFactoryImpl.SessionBuilderImpl 是 Hibernate 框架的类,通过指定该类,可以将切面织入到 Hibernate SessionBuilderImpl 类中。

​ 1.2 <aspects> 元素:用于指定要加载的 AspectJ 切面。在这个示例中,指定了 se.callista.blog.service.multi_tenancy.aspect.TenantFilterAspect 这个切面。

​ 上面的配置文件,定义了一个切面:TenantFilterAspect,以及把这个切面织入的类:SessionBuilderImpl。此外,还需要在代码中,定义以下切面:

@Aspect
public class TenantFilterAspect {
    @Pointcut("execution (* org.hibernate.internal.SessionFactoryImpl.SessionBuilderImpl.openSession(..))")
    public void openSession() {
    }

    @AfterReturning(pointcut = "openSession()", returning = "session")
    public void afterOpenSession(Object session) {
        if (session != null && Session.class.isInstance(session)) {
            final String tenantId = TenantContext.getTenantId();
            if (tenantId != null) {
                org.hibernate.Filter filter = ((Session) session).enableFilter("tenantFilter");
                filter.setParameter("tenantId", tenantId);
            }
        }
    }
}

​ 当一个Hibernate 会话打开的时候,切入点(@Pointcut注解)可以在查询会话打开之前,执行一些额外的逻辑,我们这里是把Hibernate Filter注入进去。

为了让AspectJ在运行时(Load-time weaving(LTW))动态织入,我们需要在启动类中使用注解:@EnableLoadTimeWeaving,以及继承SpringBootServletInitializer ,如下所示:

@SpringBootApplication
@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
public class MultiTenantServiceApplication extends SpringBootServletInitializer {
...
}

​ 最后,我们需要把spring 的instrumentation agent 和AspectJ的aspectjweaver agent作为参数,传入到jvm参数中,如下所示:

java -javaagent:spring-instrument.jar -javaagent:aspectjweaver.jar -jar app.jar

​ 同时spring-boot-maven-plugin插件增加配置及参数

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <agents>
                        <agent>${project.build.directory}/spring-instrument-${spring-framework.version}.jar</agent>
                        <agent>${project.build.directory}/aspectjweaver-${aspectj.version}.jar</agent>
                    </agents>
                </configuration>
            </plugin>

参数说明:

  ${project.build.directory}/spring-instrument-${spring-framework.version}.jar

上面代码是一个Maven构建项目中的路径表达式,用于指定Spring Instrumentation Agent JAR文件的位置。

${project.build.directory} 表示Maven构建时生成的目标目录,通常是target目录。

${spring-framework.version} 是一个变量,表示Spring Framework的版本号。

因此,${project.build.directory}/spring-instrument-${spring-framework.version}.jar 表示在目标目录下,根据Spring Framework的版本号拼接出的Spring Instrumentation Agent JAR文件的路径。这个路径用于在构建过程中引用Spring Instrumentation Agent JAR文件,并将其用作Java代理加载到JVM中。

​ 如果是在idea中启动,需要像下面一样增加JVM参数:-javaagent:D:\code\blog-multitenancy\multi-tenant-service\target\spring-instrument-5.3.18.jar 这个spring-instrument-xxxx.jar包编译后,在target目录下面可以找到,如果找不到,就去maven仓库,路径:

image-20230716082442095

记住,

如果是部署到服务器上,打包成jar,那么运行时,需要增加参数:java -javaagent:spring-instrument.jar -jar app.jar。

名词解释说明:

​ Load-time weaving(LTW)是一种在运行时动态织入代码的技术,用于增强或修改应用程序的行为。它是一种AOP(面向切面编程)的实现方式之一。

在 Java 中,通常情况下,代码的编织发生在编译时或类加载时。而 Load-time weaving 允许在应用程序运行时动态地织入代码,即在类加载时进行字节码修改,实现对目标类的增强。

Load-time weaving 的实现依赖于 JVM 提供的 Instrumentation API。通过使用 Instrumentation API,可以在类加载时修改字节码,使得切面逻辑能够被织入目标类的方法中。这种方式相比于静态编织(AspectJ 编译器)或运行时编织(Spring AOP)更加灵活,因为可以在运行时动态决定何时、何地、以及如何织入代码。

Load-time weaving 的主要优点是可以在不修改源代码的情况下增加、修改或删除目标类的行为。它适用于那些无法通过静态编织或运行时编织实现的场景,例如对第三方库进行增强、在容器环境中动态织入切面等。

在 Spring 框架中,Load-time weaving 可以通过 Spring 的 Instrumentation agent 和相关配置来实现。Spring 的 Instrumentation agent 是一个 Java 代理,用于加载并启动一个字节码增强器,从而实现在类加载时进行字节码修改和切面织入的功能。

使用 Load-time weaving 可以在运行时动态织入切面逻辑,实现对目标类的增强,从而实现例如事务管理、日志记录、性能监控等功能。

参考文章:

【讲解多租户的实现与原理】 https://www.bilibili.com/video/BV1F84y1T7yf/?share_source=copy_web&vd_source=981718c4abc87423399e43793a5d3763

https://callistaenterprise.se/blogg/teknik/2020/10/17/multi-tenancy-with-spring-boot-part5/

https://www.baeldung.com/jpa-entity-lifecycle-events

https://nullbeans.com/how-to-use-prepersist-and-preupdate-in-jpa-hibernate/

https://stackoverflow.com/questions/14379365/jpa-entity-with-abstract-class-inheritance

https://blog.csdn.net/JavaSupeMan/article/details/125179429

https://plus-doc.dromara.org/#/ruoyi-vue-plus/quickstart/init

代码仓库地址:

https://gitee.com/gxnualbert/multi-tenant/commits/master

image-20230716083712891

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/761891.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【EXCEL】通过url获取网页表格数据

目录 0.环境 1.背景 2.具体操作 0.环境 windows excel2021 1.背景 之前我用python的flask框架的爬虫爬取过豆瓣网的电影信息&#xff0c;没想到excel可以直接通过url去获取网页表格内的信息&#xff0c;比如下图这是电影信息界面 即将上映电影 (douban.com) 通过excel操作&…

Cache——让CPU更快地执行你的代码

概要 Cache对性能的影响 首先我们要知道&#xff0c;CPU访问内存时&#xff0c;不是直接去访问内存的&#xff0c;而是先访问缓存&#xff08;cache&#xff09;。 当缓存中已经有了我们要的数据时&#xff0c;CPU就会直接从缓存中读数据&#xff0c;而不是从内存中读。 CPU…

Python基础编程案例之编写交互式博客系统

文章目录 1、博客系统的需求描述2、面向用户层面各功能的设计思路与代码编写2.1.定义文章库2.2.文章的发布2.3.删除文章2.4.修改文章的标题以及内容2.5.在评论区添加评论2.6.删除文章中的某条评论2.7.阅读文章2.8.对文章进行点赞2.9.对文章进行收藏2.10.对文章进行打赏2.11.查询…

WorkPlus AI助理:结合ChatGPT对话能力与企业数据,助力企业级AI构建!

WorkPlus AI助理是基于GPT和私有数据构建智能知识库和个性化AI&#xff0c;能够帮助企业生成博客、白皮书、社交媒体帖子、新闻稿等等&#xff0c;这些内容可以用于推广产品、服务&#xff0c;增强品牌形象和知名度。此外&#xff0c;利用WorkPlus AI助理还可以生成电子邮件、利…

基于linux串口实现语音刷抖音

目录 1.开发逻辑图及模块 2.编程实现语音和开发板通信 3.手机接入Linux热拔插相关,打开手机开发者模式允许USB调试 4.用shell指令来操作手机屏幕&#xff0c;模拟手动滑屏幕 5.最终主程序代码 1.开发逻辑图及模块 逻辑图&#xff1a; 模块 &#xff08;1&#xff09;语音…

读kafka生产端源码,窥kafka设计之道(上)

1. kafka 高吞吐之道-------异步提交批量发送 简约的发送接口----后面隐藏着并不简单的设计 kafka发送消息的接口非常简约&#xff0c;在简约的表面上&#xff0c;其背后却并不简单。先看下发送接口 kafkaProducer.send(new ProducerRecord(topic,msg), new Callback() {Ove…

8、链路层以太网协议,ARP协议32

网络层IP协议描述了通信中的起点到终点&#xff0c;但是数据不是飞过去的&#xff0c;是经过了大量的中间节点转发完成的。 一、以太网协议 1、MAC地址 物理硬件地址&#xff0c;是每一块网卡在出厂时设定的地址&#xff0c;固定且不可修改&#xff08;早期&#xff0c;现在可…

当DevOps遇到AI,黑马迎来3.0时代丨IDCF

随着GhatGPT的爆火&#xff0c;人工智能和研发效能&#xff0c;无疑成为了2023的两个最重要的关键词。大规模语言模型LLM和相关应用的快速发展正在对研发团队的工作方式产生深远影响&#xff0c;这几乎象征着新的生产力革命的到来。 那么&#xff0c;作为一名工程师&#xff0…

Chat GPT是什么,初学者怎么使用Chat GPT,需要注意些什么

目录 Chat GPT是什么 初学者怎么使用Chat GPT 使用Chat GPT需要注意什么 一些简单的prompt示例 Chat GPT是什么 Chat GPT是由OpenAI开发的一种大型语言模型&#xff0c;它基于GPT&#xff08;Generative Pre-trained Transformer&#xff09;架构。GPT是一种基于深度学习的…

【Matlab】智能优化算法_遗传算法GA

【Matlab】智能优化算法_遗传算法GA 1.背景介绍2.数学模型3.文件结构4.详细代码及注释4.1 crossover.m4.2 elitism.m4.3 GeneticAlgorithm.m4.4 initialization.m4.5 Main.m4.6 mutation.m4.7 selection.m4.8 Sphere.m 5.运行结果6.参考文献 1.背景介绍 遗传算法&#xff08;Ge…

(学习笔记)TCP 为什么是三次握手?不是两次、四次?

常规回答&#xff1a;“因为三次握手才能保证双方具有接收和发送的能力” 原因一&#xff1a;避免历史连接 三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 假设&#xff1a;客户端先发送了SYN(seq90)报文&#xff0c;然后客户端宕机了&#xff0c;而且这个SYN报…

一种电动汽车智能充电及收费云平台管理方案

摘要&#xff1a;对于电动汽车来说&#xff0c;主要是借助电力作为能源&#xff0c;有着多方面的优点。但是也存在着一定的问题&#xff0c;尤其在续航能力上相对较差。因此&#xff0c;在实际工作中要正确利用现代科学技术&#xff0c;让电动汽车实现智能充电。在研究中所涉及…

JavaScript中的let、const和var

在 JavaScript 中&#xff0c;let、const 和 var 是用于声明变量的关键字&#xff0c;在使用时有以下区别&#xff1a; 作用域&#xff1a;let 和 const 声明的变量具有块级作用域&#xff0c;只能在声明它的块中访问。而 var 声明的变量则是函数作用域或全局作用域&#xff0…

MS31001低压 5V DC 电机驱动

MS31001 是一款低压 5V 直流电机驱动芯片&#xff0c;为摄像机、 消费类产品、玩具和其他低压或者电池供电的运动控制类应用 提供了集成的电机驱动解决方案。 MS31001 能提供高达 0.8A 的输出电流。可以工作在 2.0~5.5V 的电源电压上。 MS31001 具有 PWM &#xff08…

NSSCTF随机一题

[GXYCTF 2019]Ping Ping Ping 应该是命令注入的题目&#xff0c;直接先ping一下本地&#xff0c;发现url栏有ip传参变化 接着就是利用命令注入符&#xff0c;尝试注入 它好像真的在ping&#xff0c;执行得特别慢&#xff0c;利用ls&#xff0c;查询到了flag文件 发现空格过…

LeetCode·每日一题·415. 字符串相加·模拟

作者&#xff1a;小迅 链接&#xff1a;https://leetcode.cn/problems/add-strings/solutions/2347085/mo-ni-zhu-shi-chao-ji-xiang-xi-by-xun-ge-fges/ 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商…

自适应巡航控制系统研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 据统计, 我国交通事故造成的伤亡人数每年超过10万人, 其中驾驶员人为原因 (疲劳、酒驾、误操作等) 所致事故逐渐升高.汽车交通…

Python多线程同步编程Event使用方法

一、Event简介 Event是Python多线程同步编程中的一种同步原语&#xff0c;它可以帮助我们协调多个线程的操作&#xff0c;以达到线程间传递信号、同步操作等目的。 Event主要有两种状态&#xff1a;已设置和未设置。当Event被设置时&#xff0c;所有等待该Event的线程都会被唤…

数据结构-Java逆天操作

本文章会对Java线性表的相关知识进行讲解&#xff0c;也会以Java代码示例来进行解释 这里写目录标题 本文章会对Java线性表的相关知识进行讲解&#xff0c;也会以Java代码示例来进行解释对线性表的讲解分析定义可以表示为线性表可以用多种方式来表示和实现&#xff0c;常见的实…

数据结构与算法——什么是单链表,链式存储结构详解

前面详细地介绍了顺序表&#xff0c;本节给大家介绍另外一种线性存储结构——链表。 链表&#xff0c;别名链式存储结构或单链表&#xff0c;用于存储逻辑关系为 "一对一" 的数据。与顺序表不同&#xff0c;链表不限制数据的物理存储状态&#xff0c;换句话说&#…