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,即可实现对数据库中的表,进行租户过滤

属性名类型默认值描述
TenantLineHandlerTenantLineHandler租户处理器( TenantId 行级 )

实现租户过滤

  1. 这里我们通过匿名内部类实现TenantLineHandler。(也可以新建立一个类实现)
  2. 向MybatisPlusInterceptor添加我们所实现的匿名内部类
  3. 注入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查询结果日志:

image-20231124182325830

insert插入日志:

在这里插入图片描述

update修改日志:

在这里插入图片描述

delete删除日志:

在这里插入图片描述

三. 深入使用

3.1 前言

Saas多租户使用时,其实面临着许多技术问题,我拿几个公司所遇见的经典问题进行举例

  1. 如何传递tenat_id
  2. 有一些配置表,永远不需要租户过滤
  3. 在一个请求里,不需要租户过滤
  4. 在一个请求里,部分数据db需要租户过滤,部分数据db不需要租户过滤
  5. 微服务feign传递(由于我的代码项目是单体架构,我只简单赘述如何实现)

3.2 租户主体设值,取值

一个租户的设值,一般分为两种:

  1. 一个请求里,附带租户值,一般放在请求头(header)
  2. 自定义设值引用

设值后,需要找个地方存储,这里引用alibaba的TransmittableThreadLocal进行值的存储,也方便后续取值,所以这里新建立一个TenantContextHolder类

TENAN_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方法进行一些逻辑处理就可以

实现步骤

  1. 在application.yml进行表的配置
  2. 读取配置,并在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是需要进行租户过滤的,那这时加配置文件是行不通的

⭐️解决

  1. TenantContextHolder里定义一个过滤租户的threadlocal
  2. 在DeepTenantConfig的ignoreTables()方法新增逻辑判断

TENANT_SKIP:进行租户是否过滤,也就是对db是否进行租户过滤的判断(where tenant_id = ?)

/*** @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表又是需要租户

⭐️解决

  1. 对A表的操作提取为一个a方法,对B的操作提取出一个b方法
  2. 利用上节的TENANT_SKIP让a方法跳过租户
  3. 当a方法执行后,利用切面进行TENANT_SKIP的清空
  4. 这时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传递租户

由于本项目是单体项目,不涉及微服务,在这里只是简单概述

实现步骤:

  1. 实现feign自带的原生扩展RequestInterceptor类
  2. 重写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

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

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

相关文章

【古诗生成AI实战】之三——任务加载器与预处理器

本章内容属于数据处理阶段&#xff0c;将分别介绍任务加载器task和预处理器processor。 [1] 数据集 在深入探讨数据处理的具体步骤之前&#xff0c;让我们先了解一下我们将要使用的数据集的形式。 本项目采用的是七绝数据集&#xff0c;总计83072条古诗&#xff0c;其形式如下&…

大语言模型损失函数详解

我们可以把语言模型分为两类&#xff1a; 自动回归式语言模型&#xff1a;自动回归式语言模型在本质上是单向的&#xff0c;也就是说&#xff0c;它只沿着一个方向阅读句子。正向&#xff08;从左到右&#xff09;预测&#xff1b;反向&#xff08;从右到左&#xff09;预测。…

linux复习笔记04(小滴课堂)

软件安装rpm方式介绍&#xff1a; 先去挂载光盘&#xff1a; 要确保这是已连接状态。 我们查看到已经挂载成功了。 进到这个目录下。 我们可以看到这有很多rpm软件包。 man rpm: 可以看到很多参数&#xff0c;但是我们不需要全部掌握。 举例&#xff1a; 这就是告诉我们需要安…

2017年五一杯数学建模C题宜居城市问题值解题全过程文档及程序

2017年五一杯数学建模 C题 宜居城市问题 原题再现 城市宜居性是当前城市科学研究领域的热点议题之一&#xff0c;也是政府和城市居民密切关注的焦点。建设宜居城市已成为现阶段我国城市发展的重要目标,对提升城市居民生活质量、完善城市功能和提高城市运行效率具有重要意义。…

深信服超融合一体机提示:内存ECC

PS&#xff1a;此事件分享主要来源于季度巡检时发现的超融合一体机红灯闪烁异常&#xff0c;接入IPMI端口查看日志发现持续提示内存ECC&#xff1b; 因为是只有3.05这一天发现了有这个告警的提示&#xff0c;所以当时清除了日志以后重启了BMC服务就解决了&#xff1b;但是如果清…

【虚拟机】在VM中安装 CentOS 7

1.2.创建虚拟机 Centos7是比较常用的一个Linux发行版本&#xff0c;在国内的使用比例还是比较高的。 大家首先要下载一个Centos7的iso文件&#xff0c;我在资料中给大家准备了一个mini的版本&#xff0c;体积不到1G&#xff0c;推荐大家使用&#xff1a; 我们在VMware《主页》…

ctfshow刷题web入门--1--ljcsd

文章目录 ctf.show。信息搜集web1web2web3web4web5web6web7web8web9web10web11web12web13web14web15web16web17web18web19web20。爆破。知识1.1 播种随机数生成器-mt_srand。参考web21--重点web22--做不出来web23web24web25web26web27web28。。。命令执行。知识1 绕过正则表达式…

Windows安装Python环境(V3.6)

文章目录 一&#xff1a;进入网址&#xff1a;https://www.python.org/downloads/ 二&#xff1a;执行安装包 默认C盘&#xff0c;选择自定义安装目录 记得勾选add python path 下面文件夹最好不要有 . 等特殊符号 可以创建 python36 如果安装失败Option可以选默认的&#x…

PCIE链路训练-状态机描述4

Recovery Recovery.RcvrLock &#xff08;1&#xff09;如果link是在8.0GT/s或以上的速率工作&#xff0c;那么rx只会认为当前lane获得Block alignment之后收到的TS0&#xff0c;TS1&#xff0c;TS2是有效的。如果进入当前状态是从L1或recovery.speed或L0s&#xff0c;获取Blo…

【第三节:微信小程序 3、app.js配置】微信小程序入门,以思维导图的方式展开3

目录 提供了2个函数&#xff1a; app.js配置 【第三节&#xff1a;微信小程序 3、app.js配置】微信小程序入门&#xff0c;以思维导图的方式展开3 提供了2个函数&#xff1a; app() getApp() --------------------------- app.js配置 App() 功能 Ap…

基于springboot实现高校食堂移动预约点餐系统【项目源码】计算机毕业设计

基于springboot实现高校食堂移动预约点餐系统演示 Java语言简介 Java是由SUN公司推出&#xff0c;该公司于2010年被oracle公司收购。Java本是印度尼西亚的一个叫做爪洼岛的英文名称&#xff0c;也因此得来java是一杯正冒着热气咖啡的标识。Java语言在移动互联网的大背景下具备…

如何减少40%的Docker构建时间

随着Docker的普及&#xff0c;许多公司的产品会将组件构建为Docker镜像。但随着时间的推移&#xff0c;一些镜像变得越来越大&#xff0c;对应的CI构建也变得越来越慢。 如果能在喝完一杯咖啡的时间&#xff08;不超过5分钟&#xff09;内完成构建&#xff0c;将是一个理想状态…

Proteus仿真--基于字符液晶显示的频率计

本文介绍基于数码管的频率计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 本设计中80C51单片机作为主控&#xff0c;用字符液晶作为显示模块&#xff0c;按下按键K1后可进行频率测量并显示 仿真运行视频 Proteus仿真--基于字符液晶显示的频率计 附完…

设计模式—接口隔离原则(ISP)

1.背景 2002 年罗伯特C.马丁给“接口隔离原则”的定义是&#xff1a;客户端不应该被迫依赖于它不使用的方法&#xff08;Clients should not be forced to depend on methods they do not use&#xff09;。该原则还有另外一个定义&#xff1a;一个类对另一个类的依赖应该建立…

sql语句在字段中使用select

有两个表如下&#xff1b;产品表&#xff0c;产品评论表&#xff1b; 查询全部产品信息和每种产品的评论数量&#xff1b; 这也是子查询的一种&#xff1b; select * from product1; select * from comment; SELECT product1.*,(select count(id) from comment where product1…

PCIE链路训练-状态机描述3

Configuration.Idle 1.当使用8b/10b编码时&#xff0c;non-flit模式下&#xff0c;在所用配置的lane上发送s Idle data Symbols&#xff0c;在flit mode下发送IDLE flit。 2.linkup 0 link两端的component均支持64.0GT/s的速率&#xff0c;根据进入此状态之前发送的8个TS2或…

【Linux】进程间通信

进程间通信 1. 进程间通信介绍1.1 进程间通信目的1.2 进程间通信发展1.3 进程间通信分类1.4 进程间通信的本质理解 2. 管道3. 匿名管道3.1 pipe()函数3.2 站在文件描述符角度-深度理解管道3.3 站在内核角度-管道本质3.4 匿名管道使用步骤3.4 管道读写规则3.5 管道的读与写的五种…

一文带你了解机器翻译的前世今生

引言 我们都知道谷歌翻译&#xff0c;这个网站可以像变魔术一样在100 种不同的人类语言之间进行翻译。它甚至可以在我们的手机和智能手表上使用&#xff1a; 谷歌翻译背后的技术被称为机器翻译。它的出现改变了世界交流方式。 事实证明&#xff0c;在过去几年中&#xff0c;深…

springboot核心原理之@SpringbootApplication

1.SpringbootApplication Configuration标志的类 在spring ioc启动的时候就会加载创建这个类对象 EnableAutoConfiguration 中有两个注解 &#xff08;1&#xff09;AutoConfigurationPackage 扫描主程序包(主程序main所在包及其子包) 可以看到这个类 &#xff1a; static c…

Java多线程并发中部分不并发的问题

写Java实验发现个有意思的问题 三个线程&#xff0c;一个线程打印字符a&#xff0c;一个线程打印字符b&#xff0c;另一个线程打印数字&#xff0c;多次运行结果都是先打印混合输出的ab&#xff0c;完了再打印数字 有图有真相&#xff0c;我运行了10次 完整的代码是这个 clas…