Springboot JPA 集成多租户

news2024/11/27 16:42:44

背景:

​ iot-kit项目用的是jpa,不是mybatis,项目中需要引入多租户

参考文章:

【讲解多租户的实现与原理】 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

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

什么是多租户

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

多租户的三种形式

一个租户一个数据库

SeparateDatabaseMultiTenancy

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

相同Schema不同数据库

SeparateSchemaMultiTenancy

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

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

SingleDatabaseMultiTenancy

这种模式,也是绝大多数公司采用的。

上面说了多租户的3中模式,本文采用最后一种模式。这种模式主要通过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

Entity Listener 允许我们在对一个实体进行新增、删除、更新的时候,做监听动作。当监听到对数据库的一条记录进行新增、删除、更新的时候,我们就可以做一些额外的操作了,例如增加一个字段值,tenantId。

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

@PreUpdate、@PreRemove、@PrePersist 在实体更新、删除、插入的时候,触发这个监听器,然后给实体类设置租户ID。

然后再定义一个抽象实体类,并继承这个接口,如下所示:

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

}

最后,我们把所有的Java 实体类,都继承这个抽象类:AbstractBaseEntity,如下所示:

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

到目前为止,可以对新增、删除、更新的操作进行拦截,然后设置对应的tenantId了。但是,查询的时候,仍然没发做到拦截。因此,不得不使用到AOP。

这个AOP跟我们平时用的不太一样,必须要在启动类的类路径下定义一个aop.xml文件,如下:

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

image-20230716081748567

定义了aop.xml,还需要再代码中增加一个切面,代码如下:

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

}

然后再pom.xml中添加aop依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

除此之外,在启动类还有一个特别的地方,那就是使用注解:@EnableLoadTimeWeaving

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

最后,我们需要再增加一个JVM参数(pom的编译插件)

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

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

image-20230716082442095

如果是打包成jar,那么运行时,需要增加参数:java -javaagent:spring-instrument.jar -jar app.jar

代码仓库地址:

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

image-20230716083712891

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

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

相关文章

pytest实现用例间参数传递的方式

pytest实现用例间参数传递的方式 一、通过conftest创建全局变量二、使用tmpdir_factory方法 我们在做接口自动化测试的时候&#xff0c;会经常遇到这种场景&#xff1a;接口A的返回结果中的某个字段&#xff0c;是接口B的某个字段的入参。如果是使用postman&#xff0c;那我们可…

前端精度丢失处理

前端操作数据时&#xff0c;如果数据超出一定范围会出现精度丢失的问题&#xff0c;这是因为&#xff0c;在传输过程中&#xff0c;数据类型被转换成Number&#xff0c;Number的精度范围在2^53之间&#xff0c;即 -9007199254740991 ~ 9007199254740991&#xff0c;超出范围就会…

Unity游戏源码分享-迷你高尔夫球游戏MiniGolfConstructionKitv1.1

Unity游戏源码分享-迷你高尔夫球游戏MiniGolfConstructionKitv1.1 有多个游戏关卡 工程地址&#xff1a;https://download.csdn.net/download/Highning0007/88052881

Unity游戏源码分享-射酒瓶游戏Demo

Unity游戏源码分享-射酒瓶游戏Demo 工程地址&#xff1a;https://download.csdn.net/download/Highning0007/88052883

财务报表制作:哪些软件值得推荐?

当今&#xff0c;企业需要准确、及时地制作各种会计报表&#xff0c;以便管理者更好地掌握财务状况。然而&#xff0c;使用传统的纸质方式进行制表常常会出现复杂、繁琐等问题&#xff0c;降低制表效率。因此&#xff0c;使用会 计软件成为了制表的首选。 传统制作财务报表的方…

前端理解的HTTP缓存(缓存的过程/策略/控制机制/作用和应用)

目录 一、HTTP缓存有什么作用&#xff1f; 二、 浏览器的缓存策略有哪些&#xff1f; 1、强缓存&#xff08;Expires、Cache-control&#xff09; 2、协商缓存&#xff08;Last-Modified、ETag&#xff09; 3、缓存过程是什么&#xff1f; 三、浏览器缓存控制机制有哪些&…

八股文(消息队列)

文章目录 1. RabbitMQ特点2. 如何保证消息的可靠性3. RabbitMQ消息的顺序性4. 实现RabbitMQ的高可用性5. 如何解决消息队列的延时以及过期失效问题&#xff1f;6. RabbitMQ死信队列7. RabbitMQ延迟队列8.RabbitMQ的工作模式9. RabbitMQ消息如何传输10. 核心概念10.1 生产者和消…

MATLAB 常用函数小结

文章目录 【 生成 】随机数零或一其他 【 提取 】【 统计 】【 算法 】【 文件 】 【 生成 】 随机数 rand() rand(a,b) 生成区间 (0,1) 内均匀分布的随机数字组成的 axb 列矩阵。 均值0.5&#xff0c;方差1/120.08333。k*rand(a,b) 生成区间 (0,k) 内服从均匀分布的随机数字组…

华为OD机试真题 Java 实现【分割数组的最大差值】【2023 B卷 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#…

layui实现选项卡(万字博客!!!)

今日金句 努力不一定会被看到&#xff0c;但成功会 文章目录 前言一、什么是Tab选项卡二、Tab分类2.1 默认风格2.2 动态Tab2.3 Hash Tab2.4 简洁Tab2.5 卡片Tab2.6 响应式Tab2.7 带删除的Tab2.8 Js代码 三、实例3.1 引入html代码3.2 编写Js代码3.3 优化1&#xff1a;对应名称、…

javascript编写奇迹mu原版(含服务端)(7) 地图界面

其实主要的功能还是聊天&#xff0c;打怪我打算弄成自动的。可以传送到地图怪密集的地方打怪&#xff0c;然后获得经验和道具&#xff0c;也可发广告收消息。地图上那些是石头等不可移动的区域。怪和人都还没标上去&#xff0c;暂时想的是在地图上标一个红色的点。

人工智能自然语言处理:N-gram和TF-IDF模型详解

人工智能自然语言处理&#xff1a;N-gram和TF-IDF模型详解 1.N-gram 模型 N-Gram 是一种基于统计语言模型的算法。它的基本思想是将文本里面的内容按照字节进行大小为 N 的滑动窗口操作&#xff0c;形成了长度是 N 的字节片段序列。 每一个字节片段称为 gram&#xff0c;对所…

第一阶段-第九章 Python的异常、模块与包

目录 一、了解异常  1.学习目标  2.什么是异常  3.bug单词的诞生  4.本节的演示  5.本小节的总结 二、异常的捕获方法  1.学习目标  2.为什么要捕获异常  3.如何进行异常的捕获&#xff08;异常为常规的、指定的、多个时&#xff0c;捕获所有异常、异常else、f…

跟我一起从零开始学python(八)全栈开发

前言 回顾之前讲了python语法编程 &#xff0c;必修入门基础和网络编程&#xff0c;多线程/多进程/协程等方面的内容&#xff0c;后续讲到了数据库编程篇MySQL&#xff0c;Redis&#xff0c;MongoDB篇&#xff0c;和机器学习前面没看的也不用往前翻&#xff0c;系列文已经整理…

【ArcGIS Pro微课1000例】0028:绘制酒店分布热力图(POI数据)

本文讲解在ArcGIS Pro中文版中,基于长沙市酒店宾馆分布矢量点数据(POI数据)绘制酒店分布热力图。 文章目录 一、加载酒店分布数据二、绘制热度图参考阅读: 【GeoDa实用技巧100例】004:绘制长沙市宾馆热度图 【ArcGIS微课1000例】0070:制作宾馆酒店分布热度热力图 一、加载…

【java爬虫】将优惠券数据存入数据库排序查询

本文是在之前两篇文章的基础上进行写作的 (1条消息) 【java爬虫】使用selenium爬取优惠券_haohulala的博客-CSDN博客 (1条消息) 【java爬虫】使用selenium获取某宝联盟淘口令_haohulala的博客-CSDN博客 前两篇文章介绍了如何获取优惠券的基础信息&#xff0c;本文将获取到的…

mybatis-plus中的逻辑删除

官网&#xff1a;逻辑删除 | MyBatis-Plus 1.数据库字段 得有一个字段用来表示是否被删除。 记得加上注解TableLogic 也可以加上值&#xff0c;表示被删除具体得值&#xff0c;和没有被删除具体的值。 TableLogic(value "1",delval "0") 源码&#…

go语言中的string类型简介

在 Go 中&#xff0c;String 是一种不可变的类型&#xff0c;不能被修改。 在 Go 语言中&#xff0c;字符串由 Unicode 字符组成&#xff0c;每个字符都可以用一个或多个字节来表示。我们使用双引号或反引号来定义字符串&#xff0c;使用反引号定义的字符串不会对其内容进行任何…

FPGA Verilog移位寄存器应用:边沿检测、信号同步、毛刺滤波

文章目录 1. 端口定义2. 边沿检测3. 信号同步4. 信号滤波5. 源码6. 总结 输入信号的边沿检测、打拍同步、毛刺滤波处理&#xff0c;是FPGA开发的基础知识&#xff0c;本文介绍基于移位寄存器的方式&#xff0c;实现以上全部功能&#xff1a;上升沿、下降沿、双边沿检测、输入信…

21.基于注解的自动装配

基于注解的自动装配 通过Autowired注解即可完成自动装配 Autowired注解标识的位置 成员变量上&#xff1a;直接标记Autowired注解即可完成自动装配&#xff0c;不需要提供setXxx()方法成员变量set方法上&#xff1a;直接标记Autowired注解即可完成自动装配成员变量赋值的有参…