聊聊Java的SPI机制
个人自建博客地址
什么是SPI呢?
SPI全称Service Provider Interface,翻译过来就是服务提供者接口。调用方提供接口声明,服务提供方对接口进行实现,提供服务的一种机制,服务提供方往往是第三方或者是外部扩展。
下面是一段java.util.ServiceLoader
类的注释:
A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application’s class path or by some other platform-specific means.
翻译:
服务提供者是对某一服务的具体实现。在服务提供者中的类一般会实现服务所定义的接口,并继承服务本身定义的类。服务提供者可以通过扩展的方式安装到Java平台的实现中,也就是说,将jar文件放置到标准的扩展目录之一。此外,服务提供者还可以通过将其添加到应用程序的类路径,或者使用其他与平台相关的手段来使其可用。
可以这样理解,Java提供了一种机制可以帮我们务发现加载某个接口的实现类,实现类不在本模块中,实现类可以由第三方提供,可以是依赖的jar或是其他扩展方式。
SPI的好处是什么?
SPI机制使用了接口,自然有接口的特点,面相接口编程,提供制定标准,实现由是实现者提供。
- **解耦和可扩展性:**SPI将接口与实现分离,我们就可以在不修改接口的情况下,轻松替换实现和新增新的实现,这也有利于模块化开发的扩展。
- **标准化:**SPI提供了一种标准化的方式来定义和实现服务,这样不同的开发者可以遵循相同的规则来提供和消费服务,减少了集成时的混乱和错误。
SPI原理
用一个示例画一个SPI原理图如下:
ServiceInterface
是一个定义了服务方法的接口。ServiceProviderA
和ServiceProviderB
是实现了ServiceInterface
的具体服务提供者。ServiceLoader
负责加载服务并调用。
Java SPI ServiceLoader工作流程
- 首先在服务调用者中有一个功能接口
A
- 第三方服务提供者作为插件模块要实现这个功能,首先有一个实现类
com.test.AImpl
实现这个接口,然后在自己的模块里的META-INF/services/
目录下创建com.test.A文件,
,这里文件名是A接口的全限定名,文件内容就是com.test.AImpl
,也就是实现类的全限定名。
- 第三方服务提供者作为插件模块要实现这个功能,首先有一个实现类
- 服务调用者使用ServiceLoader 创建加载器,根据接口精确遍历
META-INF/services/
目录对对应接口的实现类进行反射并实例化,这样我们就可以获得的根据A
接口的不同实现了。
接下来上示例代码
定义服务接口:
package com.example.service;/*** 定义演出接口*/
public interface Perform {void show();
}
负责表演歌曲的服务提供者:
package com.example.serviceprovider1;public class Singer implements com.example.service.Perform {public void show() {System.out.println("表演歌曲节目");}
}
服务提供者所在jar中:
文件名:META-INF/services/com.test.A
内容:com.example.serviceprovider1.Singer
负责表演舞蹈的服务提供者:
package com.example.serviceprovider;import com.example.service.Perform;/*** 舞者提供才艺目*/
public class Dancer implements Perform {public void show() {System.out.println("表演跳舞节目");}}
服务提供者所在jar中:
文件名:META-INF/services/com.test.A
内容:com.example.serviceprovider.Dancer
调用者:
package com.example.serviceuser;import com.example.service.Perform;import java.util.Iterator;
import java.util.ServiceLoader;public class MainTestSpi {public static void main(String[] args) {ServiceLoader<Perform> serviceLoader = ServiceLoader.load(Perform.class);Iterator<Perform> iterator = serviceLoader.iterator();while (iterator.hasNext()) {Perform perform = iterator.next();perform.show();}}
}
调用结果:
表演跳舞节目
表演歌曲节目
不同框架的SPI思想实现之JDBC
我们先说JDBC中的SPI机制实现
- JDK中
java.sql.Driver
接口定义了定义了驱动与数据库交互的标准方法。 - 不同的数据库厂商提供具体的驱动实现类,例如MySQL驱动实现了
Driver
接口的connect()
方法,用于建立数据库连接。 - 每个驱动JAR包的
META-INF/services
目录下需创建一个以接口全限定名(如java.sql.Driver
)命名的文件,文件内容为实现类的全限定名(如com.mysql.cj.jdbc.Driver
)。 - ServiceLoader通过此文件发现并加载驱动。
这是Mysql JDBC通过SPI机制注册驱动的核心代码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//static {try {java.sql.DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}/*** Construct a new driver and register it with DriverManager* * @throws SQLException* if a database error occurs.*/public Driver() throws SQLException {// Required for Class.forName().newInstance()}
}
当SerciceLoader的Iterator
调用next()
方法时,就会触发java.sql.DriverManager.registerDriver(new Driver());
对数据库厂商的驱动进行注册。
加载和注册驱动的过程如下:
Spring Boot 自动装配也体现了SPI思想
自动装配是 SPI 的“升级版”
- 隐式接口:用注解和文件约定替代显式接口,降低侵入性。
- 动态加载:通过条件注解实现按需装配,而非一次性加载所有实现类。
- 开箱即用:通过 Starter 依赖传递,开发者只需关注业务逻辑,无需手动配置。
自动装配流程
-
Spring Boot 通过
SpringFactoriesLoader
扫描所有依赖中的以下文件:-
旧方式:
META-INF/spring.factories
(键为EnableAutoConfiguration
)。 -
新方式(Spring Boot 2.7+):
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
。
-
# AutoConfiguration.imports
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
...
-
过滤和排序配置类
-
去重与过滤:排除重复的配置类,并根据
@AutoConfigureOrder
、@Order
注解排序。 -
排除不需要的配置:通过
spring.autoconfigure.exclude
配置或@EnableAutoConfiguration(exclude=...)
排除特定配置类。
-
-
条件化评估(Conditional Evaluation)
Spring Boot 通过 @Conditional
系列注解 动态决定是否启用某个配置类或 Bean。常见的条件注解包括:
注解 | 作用 |
---|---|
@ConditionalOnClass | 类路径中存在指定类时生效。 |
@ConditionalOnMissingBean | 容器中不存在指定 Bean 时生效。 |
@ConditionalOnProperty | 配置文件中存在指定属性且匹配值时生效。 |
@ConditionalOnWebApplication | 应用是 Web 应用时生效。 |
示例:
java
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // 存在 DataSource 类时生效
public class DataSourceAutoConfiguration {@Bean@ConditionalOnMissingBean // 容器中没有 DataSource Bean 时生效public DataSource dataSource() {return new HikariDataSource();}
}
- 加载并注册 Bean
通过条件评估的配置类中的 @Bean
方法会被执行,生成的 Bean 实例注册到 Spring 容器中。
-
自动装配的优先级
-
用户自定义 Bean 优先:如果用户手动定义了某个 Bean(如
@Bean
方法),自动配置的 Bean 会被跳过(由@ConditionalOnMissingBean
控制)。 -
配置类加载顺序:通过
@AutoConfigureOrder
或@Order
控制配置类的执行顺序(值越小优先级越高)。
-
自动装配的触发时机
自动装配在 Spring 容器的 refresh()
阶段完成,具体步骤如下:
- 准备环境(Environment):加载配置文件(如
application.properties
)。 - 创建
BeanFactory
:初始化 Spring 容器的 Bean 工厂。 - 执行
BeanFactoryPostProcessor
:处理 Bean 工厂的后期处理(如解析@Configuration
类)。 - 加载自动配置类:通过
AutoConfigurationImportSelector
选择并加载符合条件的配置类。 - 注册 Bean 定义:将自动配置类中的 Bean 定义注册到容器。
- 实例化单例 Bean:完成所有 Bean 的初始化。
因此,Spring Boot 自动装配虽然没有传统意义上的接口,但通过标准化约定和条件化注解,更灵活地实现了 SPI 的核心思想:解耦服务提供者与消费者,实现模块化扩展。
Java 自身的SPI通过ServiceLoader
实现,使用起来简单,但是没有条件过滤,不便于按需加载。Spring Boot的自动装配,Dubbo的扫描扩展等其他框架都根据自身需求实现了更好用的SPI服务加载流程。
SPI思想的应用场景
SPI(服务提供者接口)主要用于解耦接口与实现,支持模块化,插件化扩展。典型场景包括:
-
框架扩展:如JDBC驱动加载(Java SPI)、Dubbo的协议扩展(自适应SPI)。
-
插件系统:日志组件(Log4j2的
@Plugin
)。 -
配置自动化:Spring Boot Starter通过
AutoConfiguration.imports
实现“开箱即用”。 -
服务治理:微服务中动态加载服务发现(如Spring Cloud)、配置中心扩展。
-
跨平台适配:如SLF4J绑定不同日志实现,屏蔽底层差异。
SPI通过约定发现+动态加载,提升系统灵活性和可维护性。
相关文章:
聊聊Java的SPI机制
个人自建博客地址 什么是SPI呢? SPI全称Service Provider Interface,翻译过来就是服务提供者接口。调用方提供接口声明,服务提供方对接口进行实现,提供服务的一种机制,服务提供方往往是第三方或者是外部扩展。 下面…...
【 实战案例篇三】【某金融信息系统项目管理案例分析】
大家好,今天咱们来聊聊金融行业的信息系统项目管理。这个话题听起来可能有点专业,但别担心,我会尽量用大白话给大家讲清楚。金融行业的信息系统项目管理,说白了就是如何高效地管理那些复杂的IT项目,确保它们按时、按预算、按质量完成。咱们今天不仅会聊到一些理论,还会通…...
go并发编程
https://www.bilibili.com/video/BV16A4y1f7EX sync包工具 Mutex:同一时间只有一个goroutine持有锁,其它申请该锁的goroutine会被阻塞。RWMutex:允许多个协程同时读取共享数据,但写入时需要独占锁。WaitGroup:等待一组…...
使用Python开发以太坊智能合约:轻松入门与深度探索
使用Python开发以太坊智能合约:轻松入门与深度探索 随着区块链技术的快速发展,以太坊作为最为成熟和广泛使用的智能合约平台,成为了开发去中心化应用(DApp)的核心工具。智能合约不仅是区块链技术的基础,更…...
Python从0到100(八十九):Resnet、LSTM、Shufflenet、CNN四种网络分析及对比
前言: 零基础学Python:Python从0到100最新最全教程。 想做这件事情很久了,这次我更新了自己所写过的所有博客,汇集成了Python从0到100,共一百节课,帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…...
密码学(哈希函数)
4.1 Hash函数与数据完整性 数据完整性: 检测传输消息(加密或未加密)的修改。 密码学Hash函数: 构建某些数据的简短“指纹”;如果数据被篡改,则该指纹(以高概率)不再有效。Hash函数…...
设计模式Python版 备忘录模式
文章目录 前言一、备忘录模式二、备忘录模式示例1三、备忘录模式示例2 前言 GOF设计模式分三大类: 创建型模式:关注对象的创建过程,包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式:…...
CES Asia 2025聚焦量子计算,多领域进展引关注
作为亚洲地区极具影响力的科技盛会,CES Asia 2025第七届亚洲消费电子技术贸易展(赛逸展)将在首都北京举办。本届展会以“创新、智能、互联”为主题,将全方位展示全球消费科技领域的最新成果与发展趋势。其中,量子计算作…...
MySQL索引深度剖析:从数据结构到实际应用
引言 在数据库系统中,索引是提高查询效率的关键技术之一。MySQL作为最流行的关系型数据库之一,其索引机制尤为重要。本文将剖析MySQL索引的数据结构、分类、创建方式以及实际应用场景,帮助读者更好地理解和应用索引技术。 主体部分 1. MyS…...
【deepseek】本地部署+RAG知识库挂载+对话测试
文章目录 前言一、Deepseek模型下载(以7B为例)二、RAG本地知识库挂载三、创建本地对话脚本四、结果展示 前言 本文主要涵盖Deepseek在ubuntu系统中的部署全流程,包括模型的下载、系统部署、本地文档向量化、向量列表存储、RAG知识库挂载、对话测试等内容 一、Deeps…...
Vue.js 组件开发全面详解及应用案例
Vue.js 的组件化开发是其核心特性之一,使得代码复用、维护和扩展变得更加容易。以下是关于 Vue.js 组件开发的全面解析,并附带一个实际应用案例。 一、组件基础概念 1. 什么是组件? 组件是 Vue 应用的基本构建块,封装了 HTML、C…...
java面试场景问题
还在补充,这几天工作忙,闲了会把答案附上去,也欢迎各位大佬评论区讨论 1.不用分布式锁如何防重复提交 方法 1:基于唯一请求 ID(幂等 Token) 思路:前端生成 一个唯一的 requestId(…...
MySQL数据库基本概念
目录 什么是数据库 从软件角度出发 从网络角度出发 MySQL数据库的client端和sever端进程 mysql的client端进程连接sever端进程 mysql配置文件 MySql存储引擎 MySQL的sql语句的分类 数据库 库的操作 创建数据库 不同校验规则对查询的数据的影响 不区分大小写 区…...
【wiki知识库】07.用户管理后端SpringBoot部分
目录 一、今日目标 二、??SpringBoot部分类的添加 2.1 使用逆向工程新增User模块 2.2 UserQueryParam添加 2.3 UserSaveParam添加 2.4 UserResetPasswordParam添加 2.5 UserQueryVo添加 2.6 SnowFlake工具类 三、??后端新增接口? 3.1 /user/list接口添加 3.2 /…...
千峰React:案例二
完成对html文档还有css的引入,引入一下数据: import { func } from prop-types import ./购物车样式.css import axios from axios import { useImmer } from use-immer import { useEffect } from reactfunction Item() {return (<li classNameacti…...
Junit框架缺点
JUnit 是 Java 生态中最流行的单元测试框架,广泛应用于单元测试和集成测试中。尽管它功能强大且易于使用,但也存在一些缺陷和局限性。以下是 JUnit 的主要缺点: 1. 功能相对固定 问题:JUnit 的核心功能相对固定,缺乏灵…...
计算机毕业设计SpringBoot+Vue.js公司日常考勤系统(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
Python线程池知多少
目录 目标 Python版本 官方文档 概述 线程池 实战 创建线程池的基本语法 批量提交任务 生产者&消费者模型 目标 掌握线程池的基本概念和使用方法。 Python版本 Python 3.9.18 官方文档 concurrent.futures — Launching parallel taskshttps://docs.python.org/3…...
MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 6
从6到12章将会是重中之重,请一定好好看 第06章_索引的数据结构 1.为什么使用索引 索引是存储引擎用于快速找到数据记录的一种数据结构,就好比一本教课书的目录部分,通过目录中找到对应文章的页码,便可快速定位到需要的文章。MySQL中也是一…...
C++动态与静态转换区别详解
文章目录 前言一、 类型检查的时机二、安全性三、适用场景四、代码示例对比总结 前言 在 C 中,dynamic_cast 和 static_cast 是两种不同的类型转换操作符,主要区别体现在类型检查的时机、安全性和适用场景上。以下是它们的核心区别: 一、 类…...
面向AI 的前端发展及初识大模型
AI带来的开发范式迁移 随着AI的涌现,对前端的发展也有着非常大的影响,总结过去前端的发展路径,目前应该属于又一次的大规模的开发范式迁移阶段。上一个阶段是从jquery到React/Vue/Angular迁移(jquery之前的就不讨论了)…...
Java入门的基础学习
Java的基础语法知识 一 初始Java二 Java数据类型和变量1.字面常量2.数据类型基本数据类型引用数据类型 3.变量整型变量浮点型变量字符型变量布尔型变量 4.类型转化和提升类型转化类型提升 三 运算符1.算数运算符2.关系操作符3.逻辑运算符:&&,||&…...
万字详解 MySQL MGR 高可用集群搭建
文章目录 1、MGR 前置介绍 1.1、什么是 MGR1.2、MGR 优点1.3、MGR 缺点1.4、MGR 适用场景 2、MySQL MGR 搭建流程 2.1、环境准备2.2、搭建流程 2.2.1、配置系统环境2.2.2、安装 MySQL2.2.3、配置启动 MySQL2.2.4、修改密码、设置主从同步2.2.5、安装 MGR 插件 3、MySQL MGR 故…...
脚本无法获取响应主体(原因:CORS Missing Allow Credentials)
背景: 前端的端口号8080,后端8000。需在前端向后端传一个参数,让后端访问数据库去检测此参数是否出现过。涉及跨域请求,一直有这个bug是404文件找不到。 在修改过程当中不小心删除了一段代码,出现了这个bug࿰…...
GD32F450 使用
GB32F450使用 1. 相关知识2. 烧写程序3. SPI3.1 spi基础3.2 spi代码 4. 串口4.1 串口引脚4.2 串口通信代码 问题记录1. 修改晶振频率 注意:GD32F450 总共有三种封装形式,本文所述的相关代码和知识,均为 GD32F450IX 系列。 1. 相关知识 参数配…...
神经网络代码入门解析
神经网络代码入门解析 import torch import matplotlib.pyplot as pltimport randomdef create_data(w, b, data_num): # 数据生成x torch.normal(0, 1, (data_num, len(w)))y torch.matmul(x, w) b # 矩阵相乘再加bnoise torch.normal(0, 0.01, y.shape) # 为y添加噪声…...
Android 数据库查询对比(APN案例)
功能背景 APN 数据通常存储在数据库中,由TelephonyProvider提供。当用户进入APN设置界面时,Activity会启动,AOSP源码通过ContentResolver查询APN数据。关键分析点在于这个查询操作是否在主线程执行,因为主线程上的耗时操作会导致…...
神卓 S500 异地组网设备实现监控视频异地组网的详细步骤
一、设备与环境准备 硬件清单 主设备:神卓 S500 异地组网路由器 1子设备:神卓 S500 或兼容设备 N(需通过官网认证)监控设备:支持 RTSP/ONVIF 协议的 NVR、摄像头网络要求:各网点需稳定联网(推荐…...
golang安装(1.23.6)
1.切换到安装目录 cd /usr/local 2.下载安装包 wget https://go.dev/dl/go1.23.6.linux-amd64.tar.gz 3.解压安装包 sudo tar -C /usr/local -xzf go1.23.6.linux-amd64.tar.gz 4.配置环境变量 vi /etc/profile export PATH$…...
leetcode35.搜索插入位置
题目: 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出…...
LeetCode第57题_插入区间
LeetCode 第57题:插入区间 题目描述 给你一个 无重叠的 ,按照区间起始端点排序的区间列表。在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。 难度 中…...
人工智能之数学基础:线性代数中矩阵的运算
本文重点 矩阵的运算在解决线性方程组、描述线性变换等方面发挥着至关重要的作用。通过对矩阵进行各种运算,可以简化问题、揭示问题的本质特征。在实际应用中,我们可以利用矩阵运算来处理图像变换、数据分析、电路网络等问题。深入理解和掌握矩阵的运算,对于学习线性代数以…...
SQL Server 创建用户并授权
创建用户前需要有一个数据库,创建数据库命令如下: CREATE DATABASE [数据库名称]; CREATE DATABASE database1;一、创建登录用户 方式1:SQL命令 命令格式:CREATE LOGIN [用户名] WITH PASSWORD ‘密码’; 例如,创…...
MySQL双主搭建-5.7.35
文章目录 上传并安装MySQL 5.7.35双主复制的配置实例一:172.25.0.19:实例二:172.25.0.20: 配置复制用户在实例 1 (172.25.0.19)上执行:在实例 2 (172.25.0.20)上执行&…...
RNN实现精神分裂症患者诊断(pytorch)
RNN理论知识 RNN(Recurrent Neural Network,循环神经网络) 是一种 专门用于处理序列数据(如时间序列、文本、语音、视频等)的神经网络。与普通的前馈神经网络(如 MLP、CNN)不同,RNN…...
Python中字符串的常用操作
一、r原样输出 在 Python 中,字符串前加 r(即 r"string" 或 rstring)表示创建一个原始字符串(raw string)。下面详细介绍原始字符串的特点、使用场景及与普通字符串的对比。 特点 忽略转义字符࿱…...
uniapp 本地数据库多端适配实例(根据运行环境自动选择适配器)
项目有个需求,需要生成app和小程序,app支持离线数据库,如果当前没有网络提醒用户开启离线模式,所以就随便搞了下,具体的思路就是: 一个接口和多个实现类(类似后端的模板设计模式)&am…...
Spring Cloud Gateway 整合Spring Security
做了一个Spring Cloud项目,网关采用 Spring Cloud Gateway,想要用 Spring Security 进行权限校验,由于 Spring Cloud Gateway 采用 webflux ,所以平时用的 mvc 配置是无效的,本文实现了 webflu 下的登陆校验。 1. Sec…...
【异地访问本地DeepSeek】Flask+内网穿透,轻松实现本地DeepSeek的远程访问
写在前面:本博客仅作记录学习之用,部分图片来自网络,如需引用请注明出处,同时如有侵犯您的权益,请联系删除! 文章目录 前言依赖Flask构建本地网页访问LM Studio 开启网址访问DeepSeek 调用模板Flask 访问本…...
Windows对比MacOS
Windows对比MacOS 文章目录 Windows对比MacOS1-环境变量1-Windows添加环境变量示例步骤 1:打开环境变量设置窗口步骤 2:添加系统环境变量 2-Mac 系统添加环境变量示例步骤 1:打开终端步骤 2:编辑环境变量配置文件步骤 3࿱…...
React实现无缝滚动轮播图
实现效果: 由于是演示代码,我是直接写在了App.tsx里面在 文件位置如下: App.tsx代码如下: import { useState, useEffect, useCallback, useRef } from "react"; import { ImageContainer } from "./view/ImageC…...
Ubuntu20.04确认cuda和cudnn已经安装成功
当我们通过官网安装cuda和cudnn时,终端执行完命令后我们仍不能确定是否已经安装成功。接下来教大家用几句命令测试。 cuda 检测版本号 nvcc -V如果输出如下,则安装成功。 可以看到版本号是11.2 cudnn检测版本号 有两种命令:如果你的cudn…...
sqlilab 46 关(布尔、时间盲注)
sqlilabs 46关(布尔、时间盲注) 46关有变化了,需要我们输入sort,那我们就从sort1开始 递增测试: 发现测试到sort4就出现报错: 我们查看源码: 从图中可看出:用户输入的sort值被用于查…...
AI时代保护自己的隐私
人工智能最重要的就是数据,让我们面对现实,大多数人都不知道他们每天要向人工智能提供多少数据。你输入的每条聊天记录,你发出的每条语音命令,人工智能生成的每张图片、电子邮件和文本。我建设了一个网站(haptool.com),…...
模型优化之强化学习(RL)与监督微调(SFT)的区别和联系
强化学习(RL)与监督微调(SFT)是机器学习中两种重要的模型优化方法,它们在目标、数据依赖、应用场景及实现方式上既有联系又有区别。 想了解有关deepseek本地训练的内容可以看我的文章: 本地基于GGUF部署的…...
Buildroot 添加自定义模块-内置文件到文件系统
目录 概述实现步骤1. 创建包目录和文件结构2. 配置 Config.in3. 定义 cp_bin_files.mk4. 添加源文件install.shmy.conf 5. 配置与编译 概述 Buildroot 是一个高度可定制和模块化的嵌入式 Linux 构建系统,适用于从简单到复杂的各种嵌入式项目. buildroot的源码中bui…...
蓝牙接近开关模块感应开锁手机靠近解锁支持HID低功耗
ANS-BT101M是安朔科技推出的蓝牙接近开关模块,低功耗ble5.1,采用UART通信接口,实现手机自动无感连接,无需APP,人靠近车门自动开锁,支持苹果、安卓、鸿蒙系统,也可以通过手机手动开锁或上锁&…...
计算机毕业设计SpringBoot+Vue.js基于工程教育认证的计算机课程管理平台(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
企业知识库搭建:14款开源与免费系统选择
本文介绍了以下14 款知识库管理系统:1.Worktile;2.PingCode;3.石墨文档; 4. 语雀; 5. 有道云笔记; 6. Bitrix24; 7. Logseq等。 在如今的数字化时代,企业和团队面临着越来越多的信息…...
蓝桥杯(握手问题)
小蓝组织了一场算法交流会议,总共有 50 人参加了本次会议。在会议上,大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手 (且仅有一次)。 但有 7个人,这 7 人彼此之间没有进行握手 (但这 7 人与除这 7 人以外…...