Mybatis-Plus 租户使用
Mybatis-Plus 租户使用
文章目录
- Mybatis-Plus 租户使用
- 一. 前言
- 1.1 租户存在的意义
- 1.2 租户框架
- 二. Mybatis-plus 租户
- 2.1 租户处理器
- 2.2 前置准备
- 1. 依赖
- 2. 表及数据准备
- 3. 代码生成器
- 2.3 使用
- 三. 深入使用
- 3.1 前言
- 3.2 租户主体设值,取值
- 3.3 部分表全量db操作
- 3.4 全量db操作
- 3.5 自由控制全量/租户对db操作
- 3.6 feign传递租户
- 四. 源码地址
一. 前言
1.1 租户存在的意义
多租户数据隔离与管理是在一个共享的软件应用程序中托管多个租户(客户)的数据,并确保每个租户的数据完全隔离、安全和可管理。需要支持相同数据(记录)可由具有不同容量的多个租户。
1.2 租户框架
mybatis-plus租户插件使用
由于业务数据,都需要通过对数据库的crud,而此时mybatis-plus作为orm框架,提供了租户功能,我们可以合理利用拓展,应用于自己的业务范畴中
二. Mybatis-plus 租户
2.1 租户处理器
我们通过实现TenantLineHandler,即可实现对数据库中的表,进行租户过滤
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| TenantLineHandler | TenantLineHandler | 租户处理器( TenantId 行级 ) |
实现租户过滤
- 这里我们通过匿名内部类实现TenantLineHandler。(也可以新建立一个类实现)
- 向MybatisPlusInterceptor添加我们所实现的匿名内部类
- 注入bean
ps:配置完后,就可实现对表进行租户筛选,读者们可自行测试
由于后面我对该代码进行了加工,该代码会出现一些自定义逻辑在里面,但原理是不变的
/*** @author zhanghp* @since 2023/11/20 14:29*/
@Configuration
public class TenantConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {// 默认租户为1return new LongValue(1);}@Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件@Overridepublic boolean ignoreTable(String tableName) {return false;}}));return interceptor;}}
2.2 前置准备
1. 依赖
各依赖版本继承至父依赖,可自行查看
<dependencies><!-- 启动 --><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><optional>true</optional></dependency><!-- aop --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><scope>provided</scope></dependency><!-- test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><!-- mysql jdbc --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- 代码生成器 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId></dependency><!-- 生成器模版引擎 --><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId></dependency><!-- alibaba thread local --><dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- hutool all --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency></dependencies>
2. 表及数据准备
完整配置:application.yml
这里我沿用了Spring自带的属性配置spring.sql.init,初始化数据库的schema,data。
建表sql | 数据
当启动项目后,自动进行建表,初始化表及相应的数据
spring.io官网对配置文件的概述:可自行搜索
spring.sql.init解锁更多使用方式
- 指定数据库配置
- 指定出初始化的建表sql,及对应的数据
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: zhp.1221url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCsql:init:# 建表sqlschema-locations: classpath:db/schema.sql# 数据data-locations: classpath:db/data.sql# 生成模式mode: always
3. 代码生成器
调用代码生成Generate.java,自动生成表实体类的相关类
这里就不进行展示了,自动代码生成若有不懂的地方可看Mybatis plus 自动生成代码与自定义模板
2.3 使用
🌳MpTest.java:测试对db的增删改查
/*** @author zhanghp* @since 2023/11/20 14:21*/
public class MpTest extends AppTest {@Resourceprivate DemoMapper demoMapper;@Testpublic void select() {demoMapper.selectList(null);}@Testpublic void insert() {demoMapper.insert(Demo.builder().id(100).age(18).name("草莓熊").build());}@Testpublic void update() {demoMapper.updateById(Demo.builder().id(100).age(20).name("皮卡").build());}@Testpublic void delete() {demoMapper.deleteById(Demo.builder().id(100).build());}}
select查询结果日志:

insert插入日志:

update修改日志:

delete删除日志:

三. 深入使用
3.1 前言
Saas多租户使用时,其实面临着许多技术问题,我拿几个公司所遇见的经典问题进行举例
- 如何传递tenat_id
- 有一些配置表,永远不需要租户过滤
- 在一个请求里,不需要租户过滤
- 在一个请求里,部分数据db需要租户过滤,部分数据db不需要租户过滤
- 微服务feign传递(由于我的代码项目是单体架构,我只简单赘述如何实现)
- …
3.2 租户主体设值,取值
一个租户的设值,一般分为两种:
- 一个请求里,附带租户值,一般放在请求头(header)
- 自定义设值引用
设值后,需要找个地方存储,这里引用alibaba的TransmittableThreadLocal进行值的存储,也方便后续取值,所以这里新建立一个TenantContextHolder类
TENAN_ID:进行值的存储与读取
TENANT_SKIP:进行租户是否过滤,也就是对db是否进行租户过滤的判断(where tenant_id = ?),下一节会讲到
@UtilityClass:对所有方法加static,对类加final,不允许new
/*** @author zhanghp* @since 2023/11/20 20:28*/
@Data
@UtilityClass
public class TenantContextHolder {/*** tenant_id*/private final ThreadLocal<Long> TENAT_ID = new TransmittableThreadLocal<>();/*** 获取租户* @return 租户id*/public Long getTenantId() {return TENAT_ID.get();}/*** 设置租户* @param tenantId 租户*/public void setTenantId(Long tenantId) {TENAT_ID.set(tenantId);}/*** 清空租户信息*/public void clear() {TENAT_ID.remove();}}
当有了这个类后,那我们需要重新实现TenantLineHandler,由于之前已经定义了一个TenantConfig的bean来对租户处理器并注入到容器中,若我们再次定义一个会导致bean冗余,让spring找不到使用哪个,进而导致启动报错。
那么问题来了,有没有一种方式我既要能控制新定义的TenangLineHandler存在,老的也兼容呢?
这时我们使用一下@ConditionalOnMissingBean使用在TenantConfig上,和@Order使用在DeepTenantConfig即可,读者可自行浏览
DeepTenantConfig
@ConditionalOnProperty(prefix = “custom.tenant”, value = “deep-config”, havingValue = “true”):
代表我是否要使用这个bean注入,为什么要有这个注解来控制bean注入呢?是因为要兼容上一章的租户使用TenantConfig,选择注入哪个,若没有该注解,则都会默认注入DeepTenantConfig的bean,控制该bean的注入在application.yml里的custom.tenant.deep-config
/*** @author zhanghp* @since 2023/11/24 17:51*/
@Configuration
public class DeepTenantConfig {@Bean@Order(Integer.MIN_VALUE)@ConditionalOnProperty(prefix = "custom.tenant", value = "deep-config", havingValue = "true")public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {// 获取租户Long tenantId = TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId == null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件@Overridepublic boolean ignoreTable(String tableName) {// 租户为空,则对该表不进行租户的操作Long tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return Boolean.TRUE;}return false;}}));return interceptor;}}
这些都处理好后,我们在配置中把custom.tenant.deep-config设置为true,这样读取的是我们上面定义的bean
我们在创建一个请求处理器类,进行租户的处理
TenantInterceptor
当一次请求进来,会进行租户值当处理,并存储在刚才我们所建立的TenantContextHolder类里,进行租户值处理
/*** @author zhanghp* @date 2023-11-25 13:48*/
public class TenantInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (StrUtil.isNotBlank("tenant-id")) {TenantContextHolder.setTenantId(Convert.toLong(tenantId));}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {TenantContextHolder.clear();}
}
将拦截器添加到拦截器注册类里
/*** @author zhanghp* @date 2023-11-25 16:53*/
@Configuration
public class CustomInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(getRequestContextInterceptor());}@Beanpublic TenantInterceptor getRequestContextInterceptor() {return new TenantInterceptor();}
}
现在我们创建一个controller
/*** <p>* mybatis - demo表 前端控制器* </p>** @author zhp* @since 2023-11-20*/
@RestController
@RequestMapping("/zhanghp/demo")
public class DemoController {@Resourceprivate DemoService demoService;@GetMapping("/get")public void getall(){List<Demo> all = demoService.getAll();if (IterUtil.isEmpty(all)) {return;}all.forEach(System.out::println);}
}
现在我们打开apifox/postman,进行一次请求操作,看是否会生效
- http://localhost:8888/zhanghp/demo/get
- 请求头添加tenant_id的值
发送请求

结果日志打印

3.3 部分表全量db操作
⭐️场景:
其实在业务中,有一些配置表,字典表其实是不需要租户的,就比如城市配置表,城市就这些,加租户也是没有任何意义的
⭐️解决:
实现其实也是很简单,只需要在DeepTenantConfig里的的ignoreTables方法进行一些逻辑处理就可以
实现步骤:
- 在application.yml进行表的配置
- 读取配置,并在DeepTenantConfig添加自己的逻辑
这里由于读取了配置,我喜欢把自定义的配置定义一个类,通过该类对配置读取,读者可自行查看,这里就不赘述了
配置:
custom:tenant:# 是否注入deepTenantConfig的bean开关deep-config: true# 租户字段名称tenant-id: tenant_id# 忽略租户的表ignore-tables:- demo01- demo02
逻辑新增
/*** @author zhanghp* @since 2023/11/24 17:51*/
@Configuration
public class DeepTenantConfig {@Bean@Order(Integer.MIN_VALUE)@ConditionalOnProperty(prefix = "custom.tenant", value = "deep-config", havingValue = "true")public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {// 获取租户Long tenantId = TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId == null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}@Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件@Overridepublic boolean ignoreTable(String tableName) {Long tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return Boolean.TRUE;}// 忽略表对配置为空,则所有表都进行租户操作if (IterUtil.isEmpty(properties.getIgnoreTables())) {return false;}// 指定的表永远不进行租户操作if (properties.getIgnoreTables().contains(tableName)) {return true;}return false;}}));return interceptor;}
}
读者自行练习就可,就不再演示
3.4 全量db操作
⭐️场景:
在一个请求里,这个请求涉及到的表是不需要租户过滤,那么读者就会想到,那在上一节的配置文件加啊。
这里的一个请求不需要对A表进行过滤,但另一个请求对A是需要进行租户过滤的,那这时加配置文件是行不通的
⭐️解决:
- TenantContextHolder里定义一个过滤租户的threadlocal
- 在DeepTenantConfig的ignoreTables()方法新增逻辑判断
/*** @author zhanghp* @since 2023/11/20 20:28*/
@Data
@UtilityClass
public class TenantContextHolder {/*** tenant_id*/private final ThreadLocal<Long> TENAT_ID = new TransmittableThreadLocal<>();/*** 租户过滤标识*/private final ThreadLocal<Boolean> TENANT_SKIP = new TransmittableThreadLocal<>();/*** 获取租户* @return 租户id*/public Long getTenantId() {return TENAT_ID.get();}/*** 设置租户* @param tenantId 租户*/public void setTenantId(Long tenantId) {TENAT_ID.set(tenantId);}/*** 设置是否过滤的标识*/public void setTenantSkip() {TENANT_SKIP.set(Boolean.TRUE);}/*** 获取是否跳过租户过滤的标识* @return true-过滤 false-不过滤*/public Boolean getTenantSkip() {return TENANT_SKIP.get() != null && TENANT_SKIP.get();}/*** 清空租户信息*/public void clear() {TENAT_ID.remove();clearSkip();}/*** 清空租户过滤标识*/public void clearSkip(){TENANT_SKIP.remove();}}
DeepTenantConfig
/*** @author zhanghp* @since 2023/11/24 17:51*/
@Configuration
public class DeepTenantConfig {@Bean@Order(Integer.MIN_VALUE)@ConditionalOnProperty(prefix = "custom.tenant", value = "deep-config", havingValue = "true")public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {@Overridepublic Expression getTenantId() {// 获取租户Long tenantId = TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId == null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}@Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件@Overridepublic boolean ignoreTable(String tableName) {// 1.是否跳过租户对该表对操作if (TenantContextHolder.getTenantSkip()) {return Boolean.TRUE;}// 2.租户为空,则对该表不进行租户的操作Long tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return Boolean.TRUE;}// 3.忽略表对配置为空,则所有表都进行租户操作if (IterUtil.isEmpty(properties.getIgnoreTables())) {return false;}// 4.指定的表永远不进行租户操作if (properties.getIgnoreTables().contains(tableName)) {return true;}return false;}}));return interceptor;}}
在controller层新加一个TenantContextHolder.setTenantSkip()即可
@GetMapping("/get2")public void getall2(){TenantContextHolder.setTenantSkip();List<Demo> all = demoService.list();if (IterUtil.isEmpty(all)) {return;}all.forEach(System.out::println);}
读者自行练习即可
3.5 自由控制全量/租户对db操作
⭐️场景:
在一个请求里,进行了对A表和B表操作,但是A表是不需要租户,而B表又是需要租户
⭐️解决:
- 对A表的操作提取为一个a方法,对B的操作提取出一个b方法
- 利用上节的TENANT_SKIP让a方法跳过租户
- 当a方法执行后,利用切面进行TENANT_SKIP的清空
- 这时b方法就会带着租户对B表进行处理
这里我写了一个注解@TenanClear
作用域:方法,类
作用:执行当前方法里的所有db操作,都不带租户,通过属性globalFlag进行控制后续方法是否添加租户过滤
ps:是=实现原理在 TenantClearAspect
/*** @author zhanghp* @since 2023/11/21 20:41*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantClear {/*** <p>执行该方法后,是否全局过滤租户标志</p>* <ul>* <li>true:执行完该线程之前,后续对db操作不添加租户操作</li>* <li>false:执行完当前方法后,后续方法的db操作添加租户操作</li>* </ul>** @return 默认后续不添加租户操作*/boolean globalFlag() default true;
}
该注解的作用:当添加该注解后当前方法进行的所有db操作都不带租户处理,后续方法执行是否带租户,通过属性globalFlag决定
创建a方法,并创建b方法
在a方法添加@TenantClear(globalFlag = false),然后在controller层先调用a,在调用b
输出结果,a不带租户,b带租户
/*** <p>* mybatis - demo表 服务实现类* </p>** @author zhp* @since 2023-11-20*/
@Service
public class DemoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements DemoService {@Overridepublic void b() {super.list();}@Override@TenantClear(globalFlag = false)public void a(){super.list();}}
在controller层调用
@GetMapping("/tenant-clear")public void tenantClear(){Console.log("执行a方法");demoService.a();Console.log("执行b方法");demoService.b();}
结果打印

3.6 feign传递租户
由于本项目是单体项目,不涉及微服务,在这里只是简单概述
实现步骤:
- 实现feign自带的原生扩展RequestInterceptor类
- 重写apply方法,处理tenant-id的传递
public class CustomerFeignInterceptor implements RequestInterceptor {public CustomerFeignInterceptor() {}public void apply(RequestTemplate requestTemplate) {if (TenantContextHolder.getTenantId() == null) {log.debug("租户ID为空,feign不传递");} else {requestTemplate.header("tenant-id", new String[]{TenantContextHolder.getTenantId().toString()});}}
}
四. 源码地址
⭐️gitee:https://gitee.com/zhp1221/java/tree/master/lab_02_mybatis_plus/tenant
相关文章:
Mybatis-Plus 租户使用
Mybatis-Plus 租户使用 文章目录 Mybatis-Plus 租户使用一. 前言1.1 租户存在的意义1.2 租户框架 二. Mybatis-plus 租户2.1 租户处理器2.2 前置准备1. 依赖2. 表及数据准备3. 代码生成器 2.3 使用 三. 深入使用3.1 前言3.2 租户主体设值,取值3.3 部分表全量db操作3…...
vue el-table (固定列+滚动列)【横向滚动条】确定滚动条是在列头还是列尾
效果图: 代码实现: html: <script src"//unpkg.com/vue2/dist/vue.js"></script> <script src"//unpkg.com/element-ui2.15.14/lib/index.js"></script> <div id"app" style&quo…...
⑦【Redis GEO 】Redis常用数据类型:GEO [使用手册]
个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客 学习社区:进去逛一逛~ Redis GEO ⑦Redis GEO 基本操作命令1.geoadd …...
LeetCode 2304. 网格中的最小路径代价:DP
【LetMeFly】2304.网格中的最小路径代价:DP 力扣题目链接:https://leetcode.cn/problems/minimum-path-cost-in-a-grid/ 给你一个下标从 0 开始的整数矩阵 grid ,矩阵大小为 m x n ,由从 0 到 m * n - 1 的不同整数组成。你可以…...
c 实用化的文本终端实时显示摄像头视频
因为采用yuv格式,帧率都很低。图像会拖影。把图像尺寸尽量缩小,能大大改善。现在最麻烦的是图像上有黑色的闪影,不知是为啥?如是framebuffer引起的就无解了。终于找到问题了,是在显示前加了一条用黑色清屏造成的&#…...
CSS中常用的伪类选择器
一 、伪类(不存在的类,特殊的类) -伪类用来描述一个元素的特殊状态 比如:第一个元素,被点击的元素,鼠标移入的元素 -特点:一般请情况下,使用:开头 1、 :first-child …...
【python学习】中级篇-数据库操作:SQLite
SQLite是一个轻量级的数据库引擎,它可以嵌入到各种应用程序中。以下是SQLite的基本用法: 创建数据库文件 import sqlite3# 连接到一个不存在的数据库文件,如果文件不存在,将会自动创建一个新的数据库文件 conn sqlite3.connect…...
汇编-PROTO声明过程
64位汇编 64 模式中,PROTO 伪指令指定程序的外部过程,示例如下: ExitProcess PROTO ;指定外部过程,不需要参数.code main PROCmov ebx, 0FFFFFFFFh mov ecx,0 ;结束程序call ExitProcess ;调用外部过程main ENDP END 32位…...
MYSQL事务操作
...
自动化测试——自动卸载软件
📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…...
Linux - 系统调用(syscall)
说明 基于riscv64 soc linux_5.10.4平台,通过新增一个系统调用深入了解下系统调用实现原理。 简介 Linux 软件运行环境分为用户空间和内核空间,默认情况下,用户进程无法访问内核,既不能访问内核所在的内存空间,也不…...
c语言-冒泡排序
冒泡排序原理: 冒泡排序是一种简单直观的排序算法,它重复地遍历待排序的元素序列,比较相邻的两个元素,如果它们的顺序不符合要求(例如升序要求前面的元素小于后面的元素),则交换它们的位置。遍历…...
Mysql面经
Select语句的执行顺序 1、from 子句组装来自不同数据源的数据; 2、where 子句基于指定的条件对记录行进行筛选; 3、group by 子句将数据划分为多个分组; 4、使用聚集函数进行计算;AVG() SUM() MAX() MIN() COUNT() 5、使用 havin…...
1panel可视化Docker面板安装与使用
官网地址1Panel - 现代化、开源的 Linux 服务器运维管理面板 文章目录 目录 文章目录 前言 一、环境准备 二、使用步骤 1.安装命令 2.一些命令 3.使用 总结 前言 一、环境准备 虚拟机centos 已经安装好docker和 Docker Compose 或者都没安装 1panel会帮你自动安装 二、使用…...
es6中的import导入模块 和 export导出模块
es6中的import导入模块 和 export导出模块 一、定义二、使用1.默认导出导入2..命名导出导入3.命名导出(Named Export)与默认导出(Default Export)结合使用 三、总结 一、定义 功能:用于导入和导出模块的内容。 静态加载…...
WordPress插件开发教程手册 — 钩子(Hooks)
钩子是用一段代码添加/修改另外一段代码的方式,是 WordPress插件和主题与 WordPress 内核交互的基础,钩子在 WordPress 内核中也被广泛使用。WordPress 中有两种钩子,Action 和 Filter。使用钩子时,我们需要先编写一个自定义函数作…...
Python开发运维:Celery连接Redis
目录 一、理论 1.Celery 二、实验 1.Windows11安装Redis 2.Python3.8环境中配置Celery 3.celery的多目录结构异步执行 4.celery简单结构下的定时任务 三、问题 1.Celery命令报错 2.执行Celery命令报错 3.Win11启动Celery报ValueErro错误 4.Pycharm 无法 import 同目…...
JSP:JDBC
JDBC(Java Data Base Connectivity的缩写)是Java程序操作数据库的API,也是Java程序与数据库相交互的一门技术。 JDBC是Java操作数据库的规范,由一组用Java语言编写的类和接口组成,它对数据库的操作提供基本方法&#…...
能否在一台电脑上安全地登录多个Facebook账号?
Facebook是一个流量大、用户多的平台,许多人可能需要在一台设备上管理多个Facebook账号,无论是出于个人或职业需求,都能带来极大地便利。然而,保持每个账号的安全性和隐私性却是一个挑战。本文将介绍如何在一台电脑上安全地登录多…...
Banana Pi [BPi-R3-Mini] 回顾和主线 ImmortalWrt 固件支持
BananaPi BPi-R3 Mini 采用 MediaTek 830(4 个 A53,最高 2.0 GHz),具有 2 个 2.5 GbE、AX4200 2.4G/5G 无线和 USB 2.0 端口。它还具有两个 M.2 连接器,可用于 NVMe SSD 和 5G 模块(板上包含 Nano SIM 插槽…...
React 无状态组件的纯粹性:探讨引用透明性对 React 渲染性能与可预测性的核心贡献
各位同学好!欢迎来到今天的讲座。我是你们的老朋友,一个在 React 的世界里摸爬滚打多年,头发比发际线跑得还快的资深编程专家。今天我们要聊一个听起来非常“学术”,但实际上决定了你应用性能上限和代码可维护性下限的核心概念——…...
3种创新方法让Windows电脑直接安装安卓APK文件
3种创新方法让Windows电脑直接安装安卓APK文件 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为Windows系统无法直接运行安卓应用而烦恼吗?APK Instal…...
从云端到终端:深度解析语音唤醒KWS技术的演进与落地
1. 语音唤醒技术的前世今生 第一次在智能音箱上喊出"小爱同学"时,我盯着那个突然亮起的环形灯发呆——这玩意儿怎么知道我在叫它?后来才知道,这就是典型的KWS(Keyword Spotting)技术在发挥作用。简单来说&am…...
如何通过DXVK让Linux游戏性能提升40%:从Direct3D到Vulkan的完整迁移指南
如何通过DXVK让Linux游戏性能提升40%:从Direct3D到Vulkan的完整迁移指南 【免费下载链接】dxvk Vulkan-based implementation of D3D8, 9, 10 and 11 for Linux / Wine 项目地址: https://gitcode.com/gh_mirrors/dx/dxvk 还在为Linux上运行Windows游戏时的卡…...
通过GitLab API动态触发特定Job并传递参数
在持续集成和持续交付(CI/CD)流程中,灵活地触发特定Job并传递参数是一个常见需求,尤其是在需要根据不同的环境或参数来调整执行逻辑的时候。本文将探讨如何通过GitLab的API调用来实现这一目标。 背景介绍 假设我们有一个项目myproject,其中有一个.gitlab-ci.yml文件定义…...
【JVM深度解析】第24篇:JVM内存模型(JMM)核心原理
摘要 JMM(Java Memory Model,Java 内存模型)是 Java 并发编程的基础,它定义了线程之间共享变量的可见性、有序性问题,以及如何通过 Happens-Before 规则和内存屏障来解决这些问题。理解 JMM,你才能真正明白…...
WebSocket长连接优化:宠友IM源码中的心跳与断线重连机制
IM系统上线之后,最容易被忽略的一类问题不是发送失败,而是“看起来在线,实际上已经断了”。这种情况用户感知很直接:消息发不出去、收不到、需要反复重启应用。 宠友信息在「宠友IM」源码里,对WebSocket连接这一层做了…...
逆向网易云音乐加密接口:从搜索到播放的完整爬虫实践
1. 逆向网易云音乐加密接口的核心思路 第一次接触网易云音乐接口逆向时,我也被它复杂的加密机制搞得一头雾水。但经过多次实践后发现,整个过程其实可以拆解为两个关键环节:搜索关键词加密和歌曲ID加密。这两个环节就像接力赛的两棒࿰…...
Sketch 终极指南:Android 上最强大的图片加载库完全解析
Sketch 终极指南:Android 上最强大的图片加载库完全解析 【免费下载链接】sketch Sketch is an image loading library designed for Compose Multiplatform and Android View. It is powerful and rich in functions. In addition to basic functions, it also sup…...
滚动轴承动力学模型及程序分享
滚动轴承动力学模型附上程序和网上的paper 程序百分百为博主自研并且花费了较大精力,故可以保质保量,可以对照程序和文章学习建模,以便考虑新的因素,故对轴承动力学小白十分友好 后支持程序 刚性保持架模型:综合考虑滚…...
