当前位置: 首页 > news >正文

【知识点】OkHttp 原理 8 连问

前言

OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客

但是OKHttp的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点.

本文从几个问题出发梳理OKHttp相关知识点,以便快速构建OKHttp知识体系,如果对你有用,欢迎点赞~

本文主要包括以下内容

  1. OKHttp请求的整体流程是怎样的?

  1. OKHttp分发器是怎样工作的?

  1. OKHttp拦截器是如何工作的?

  1. 应用拦截器和网络拦截器有什么区别?

  1. OKHttp如何复用TCP连接?

  1. OKHttp空闲连接如何清除?

  1. OKHttp有哪些优点?

  1. OKHttp框架中用到了哪些设计模式?

1. OKHttp请求整体流程介绍

首先来看一个最简单的Http请求是如何发送的。

val okHttpClient = OkHttpClient()val request: Request = Request.Builder().url("https://www.google.com/").build()okHttpClient.newCall(request).enqueue(object :Callback{overridefunonFailure(call: Call, e: IOException) {}overridefunonResponse(call: Call, response: Response) {}})
复制代码

这段代码看起来比较简单,OkHttp请求过程中最少只需要接触OkHttpClient、Request、Call、 Response,但是框架内部会进行大量的逻辑处理。

所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。

关于分发器与拦截器,我们在这里先简单介绍下,后续会有更加详细的讲解

  • 分发器:内部维护队列与线程池,完成请求调配;

  • 拦截器:五大默认拦截器完成整个请求过程。

整个网络请求过程大致如上所示

  1. 通过建造者模式构建OKHttpClient与 Request

  1. OKHttpClient通过newCall发起一个新的请求

  1. 通过分发器维护请求队列与线程池,完成请求调配

  1. 通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作

  1. 得到网络请求结果

2. OKHttp分发器是怎样工作的?

分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表,等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求

而这里同步请求各异步请求又略有不同

同步请求

synchronizedvoidexecuted(RealCall call) {runningSyncCalls.add(call);
}
复制代码

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可

异步请求

synchronizedvoidenqueue(AsyncCall call) {//请求数最大不超过64,同一Host请求不能超过5个if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) 	  {runningAsyncCalls.add(call);executorService().execute(call);} else {readyAsyncCalls.add(call);}
}
复制代码

当正在执行的任务未超过最大限制64,同时同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

每个任务完成后,都会调用分发器的finished方法,这里面会取出等待队列中的任务继续执行

3. OKHttp拦截器是怎样工作的?

经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了

# RealCalloverridefunexecute(): Response {try {client.dispatcher.executed(this)return getResponseWithInterceptorChain()} finally {client.dispatcher.finished(this)}}
复制代码

我们再来看下RealCall的execute方法,可以看出,最后返回了getResponseWithInterceptorChain,责任链的构建与处理其实就是在这个方法里面

internalfungetResponseWithInterceptorChain(): Response {// Build a full stack of interceptors.val interceptors = mutableListOf<Interceptor>()interceptors += client.interceptorsinterceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptorif (!forWebSocket) {interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0)val response = chain.proceed(originalRequest)}
复制代码

如上所示,构建了一个OkHttp拦截器的责任链

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

如上所示责任链添加的顺序及作用如下表所示:

拦截器

作用

应用拦截器

拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。

RetryAndFollowUpInterceptor

处理错误重试和重定向

BridgeInterceptor

应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。

CacheInterceptor

缓存拦截器,如果命中缓存则不会发起网络请求。

ConnectInterceptor

连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。

networkInterceptors(网络拦截器)

用户自定义拦截器,通常用于监控网络层的数据传输。

CallServerInterceptor

请求拦截器,在前置准备工作完成后,真正发起了网络请求。

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptor的intercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回,如下图所示:

4. 应用拦截器和网络拦截器有什么区别?

从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别

  1. 首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。

  1. 其次,除了CallServerInterceptor之外,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。

  1. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

5. OKHttp如何复用TCP连接?

ConnectInterceptor的主要工作就是负责建立TCP连接,建立TCP连接需要经历三次握手四次挥手等操作,如果每个HTTP请求都要新建一个TCP消耗资源比较多

而Http1.1已经支持keep-alive,即多个Http请求复用一个TCP连接,OKHttp也做了相应的优化,下面我们来看下OKHttp是怎么复用TCP连接的

ConnectInterceptor中查找连接的代码会最终会调用到ExchangeFinder.findConnection方法,具体如下:

# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled)throws IOException {synchronized (connectionPool) {// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)releasedConnection = transmitter.connection;result = transmitter.connection;if (result == null) {// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {result = transmitter.connection;}}}synchronized (connectionPool) {if (newRouteSelection) {//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)routes = routeSelection.getAll();if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {foundPooledConnection = true;result = transmitter.connection;}}// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,connectionRetryEnabled, call, eventListener);synchronized (connectionPool) {// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {// 如果获取到,就关闭我们创建里的连接,返回获取的连接result = transmitter.connection;} else {//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池connectionPool.put(result);}}return result;
}
复制代码

上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接

  1. 首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)

  1. 若没有 已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address一致——host、port、代理等一致,且匹配的连接可以接受新的请求。

  1. 若从连接池没有获取到,则传入routes再次尝试获取,这主要是针对Http2.0的一个操作,Http2.0可以复用square.com与square.ca的连接

  1. 若第二次也没有获取到,就创建RealConnection实例,进行TCP + TLS握手,与服务端建立连接。

  1. 此时为了确保Http2.0连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。

  1. 第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。

以上就是连接拦截器尝试复用连接的操作,流程图如下:

6. OKHttp空闲连接如何清除?

上面说到我们会建立一个TCP连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp是如何做到的呢?

  # RealConnectionPoolprivateval cleanupQueue: TaskQueue = taskRunner.newQueue()privateval cleanupTask = object : Task("$okHttpName ConnectionPool") {overridefunrunOnce(): Long = cleanup(System.nanoTime())}long cleanup(long now) {int inUseConnectionCount = 0;//正在使用的连接数int idleConnectionCount = 0;//空闲连接数RealConnection longestIdleConnection = null;//空闲时间最长的连接long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间//遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();//若连接正在使用,continue,正在使用连接数+1if (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}//空闲连接数+1idleConnectionCount++;// 赋值最长的空闲时间和对应连接long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {connections.remove(longestIdleConnection);} elseif (idleConnectionCount > 0) {// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理return keepAliveDurationNs - longestIdleDurationNs;} elseif (inUseConnectionCount > 0) {//连接没有空闲的,就5分钟后再尝试清理.return keepAliveDurationNs;} else {// 没有连接,不清理cleanupRunning = false;return -1;}}//关闭移除的连接closeQuietly(longestIdleConnection.socket());//关闭移除后 立刻 进行下一次的 尝试清理return0;}
复制代码

思路还是很清晰的:

  1. 在将连接加入连接池时就会启动定时任务

  1. 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。

  1. 没有空闲连接就等5分钟后再尝试清理。

  1. 没有连接不清理。

流程如下图所示:

7. OKHttp有哪些优点?

  1. 使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  1. 扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求

  1. 功能强大,支持Spdy、Http1.X、Http2、以及WebSocket等多种协议

  1. 通过连接池复用底层TCP(Socket),减少请求延时

  1. 无缝的支持GZIP减少数据流量

  1. 支持数据缓存,减少重复的网络请求

  1. 支持请求失败自动重试主机的其他ip,自动重定向

8. OKHttp框架中用到了哪些设计模式?

  1. 构建者模式:OkHttpClient与Request的构建都用到了构建者模式

  1. 外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  1. 责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置

  1. 享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池

总结

本文主要梳理了OKHttp原理相关知识点,并回答了以下问题:

  1. OKHttp请求的整体流程是怎样的?

  1. OKHttp分发器是怎样工作的?

  1. OKHttp拦截器是如何工作的?

  1. 应用拦截器和网络拦截器有什么区别?

  1. OKHttp如何复用TCP连接?

  1. OKHttp空闲连接如何清除?

  1. OKHttp有哪些优点?

  1. OKHttp框架中用到了哪些设计模式?

相关文章:

【知识点】OkHttp 原理 8 连问

前言OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便&#xff0c;扩展性强&#xff0c;功能强大&#xff0c;OKHttp源码与原理也是面试中的常客但是OKHttp的源码内容比较多&#xff0c;想要学习它的源码往往千头万绪&#xff0c;一时抓不住重点.本文从几个问题…...

【python】深入了解Selenium-PageObject

1、PageObject 定义 Page Object(简称PO)模式&#xff0c;是Selenium实战中最为流行&#xff0c;并且是自动化测试中最为熟悉和推崇的一种设计模式。在设计自动化测试时&#xff0c;把页面元素和元素的操作方法按照页面抽象出来&#xff0c;分离成一定的对象&#xff0c;然后再…...

PAT——7-4 简易测谎 (20 分)

测谎通常使用一套准备好的问题提问被测试者&#xff0c;通过分析被测试者的反应得到结果。比较高级的测谎技术会使用测谎仪&#xff0c;监视被测试者的生理活动状况。我们这里的简易测谎则是通过对问题答案的特征分析来做出判断。 首先我们要求被测试者做完 N 道单选题&#x…...

【力扣】 面试题 05.02.二进制数转字符串(超过c++100%)

二进制数转字符串。给定一个介于0和1之间的实数&#xff08;如0.72&#xff09;&#xff0c;类型为double&#xff0c;打印它的二进制表达式。如果该数字无法精确地用32位以内的二进制表示&#xff0c;则打印“ERROR”。示例1:输入&#xff1a;0.625输出&#xff1a;"0.10…...

软件质量保证与测试 课堂笔记

...

Costco好市多验厂百问百答

【Costco好市多验厂百问百答】美国仓储式超市Costco&#xff0c;中文好市多&#xff0c;近几年发展势头迅猛&#xff0c;大有赶超传统商超巨头沃尔玛之势。之前有出口企业反馈&#xff0c;Costco采购不仅量大&#xff0c;而且价格好&#xff0c;所以Costco成为国内出口企业纷纷…...

Nginx 通过 header 中的标识进行分发

Nginx可以根据请求头中自定义的标识将请求分发到不同的服务器。具体来说&#xff0c;可以使用map指令将请求头中的自定义标识映射为不同的后端服务器地址&#xff0c;然后使用proxy_pass指令将请求转发到对应的后端服务器。 以下是一个示例配置文件&#xff1a; http {map $h…...

如何实现《电子签名法》要求的可靠电子签名?

电子文档的电子签名怎么弄&#xff1f;我们在工作中经常需要在一些Word、pdf等电子版文件中插入签名&#xff0c;而很多人可能不知道&#xff0c;电子签名怎么弄&#xff1f;怎么做电子签名才有效&#xff1f;电子印章或签名图片属于电子签名吗&#xff1f;当工作或商务交易中&…...

工程项目管理软件有哪些?这六款很好用!

工程项目管理软件哪个好用&#xff1f;这六款很不错&#xff01; 在现代社会中&#xff0c;软件已经成为了企业信息化、项目管理等方面必不可少的工具。尤其是对于工程项目管理而言&#xff0c;借助软件进行协同、计划、控制等方面的工作&#xff0c;已经成为了必要的手段。但…...

多看看spdk代码学习

多看看spdk代码学习还是干货直接上代码简易讲解详细讲解一下这份代码还是干货直接上代码 #include <stdlib.h> #include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <signal.h> #include <stdbo…...

宾语从句it做形式主语的句子

It代替从句作形式主语的常见句型 一、it 代替连词 that 引导的从句作形式主语。 1、it be 过去分词 that 从句: It’s said that Tom has come back from abroad . It was reported that dozens of children died in the accident . 可用于该句型的过去分词还有&#xf…...

【C#基础】C# 文件与IO

序号系列文章9【C# 基础】C# 异常处理操作10【C#基础】C# 正则表达式11【C#基础】C# 预处理器指令文章目录前言1&#xff0c;文件和IO的概念2&#xff0c;文本文件操作2.1 File 类2.2 FileInfo 类2.3 FileStream 类2.4 StreamReader 类2.5 StreamWriter 类FileStream 和 Stream…...

死锁相关介绍【内含哲学家就餐问题】

死锁 死锁是这样一种情形&#xff1a;多个线程同时被阻塞&#xff0c;它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞&#xff0c;因此程序不可能正常终止。 场景1&#xff1a;一个线程&#xff0c;一把锁 一个线程&#xff0c;一把锁&#xff0c;线程…...

Java的Groovy执行器内存泄露(MetaSpace)问题分析与解决办法

环境与背景 在java程序中通过GroovyScriptEvaluator执行器创建脚本Script对象调用Groovy脚本语言来完成某些功能, ,会通过AppClassLoader或者GroovyClassLoader去生产一个随机的名称的Groovy的Script类对象,导致元数据,产生的class类会被AppClassLoader或者GroovyClassLoader内…...

【linux】进程信号——信号的产生

进程信号一、信号概念1.1 信号理解二、产生信号2.1 通过键盘产生信号2.2 捕捉信号自定义signal2.3 系统调用接口产生信号2.3.1 向任意进程发送任意信号kill2.3.2 给自己发送任意信号raise2.3.3 给自己发送指定信号abort2.3.4 理解2.4 硬件异常产生信号2.4.1 除0异常2.4.2 野指针…...

部署OpenStack

部署 1. 环境配置 配置主机名 使用CRT软件连接controller节点和compute节点&#xff0c;用户名默认为root&#xff0c;密码默认为000000。连接上之后&#xff0c;使用linux命令修改节点主机名。 [rootcontroller ~]# hostnamectl set-hostname controller [rootcontroller …...

Java 运算符与类型转化

Java 运算符与类型转化 1 算术运算符 Java中的算术运算符主要有&#xff08;加&#xff09;、-&#xff08;减&#xff09;、*&#xff08;乘&#xff09;、/&#xff08;除&#xff09;、%&#xff08;求余&#xff09;&#xff0c;它们都是二元运算符。 2 自增和自减运算…...

《C++ Primer Plus》第18章:探讨 C++ 新标准(2)

移动语义和右值引用 现在介绍本书前面未讨论的主题。C11 支持移动语义&#xff0c;这就提出了一些问题&#xff1a;为何需要移动语义&#xff1f;什么是移动语义&#xff1f;C11 如何支持它&#xff1f;下面首先讨论第一个问题。 为何需要移动语义 先来看 C11 之前的复制过程…...

QML定时器

QML使用Timer使用定时器 Timer 计时器可用于触发操作一次&#xff0c;或以给定的间隔重复触发。 常用属性&#xff1a; interval 设置触发器之间的间隔&#xff08;以毫秒为单位&#xff09;。 默认间隔为 1000 毫秒。 repeat 设置重复&#xff0c;为真&#xff0c;则以指定的…...

第三章 opengl之纹理

OpenGL纹理纹理环绕方式纹理过滤多级渐远纹理加载和创建纹理stb_image.h生成纹理纹理的应用纹理单元纹理 用stb_image.h库&#xff0c;原先用SOIL库也可以实现。 可以为每个顶点添加颜色来增加图形的细节。但是想得到一个真实的图形&#xff0c;需要足够多的顶点&#xff0c;…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现

目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密

在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

MySQL账号权限管理指南:安全创建账户与精细授权技巧

在MySQL数据库管理中&#xff0c;合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号&#xff1f; 最小权限原则&#xf…...

Java + Spring Boot + Mybatis 实现批量插入

在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法&#xff1a;使用 MyBatis 的 <foreach> 标签和批处理模式&#xff08;ExecutorType.BATCH&#xff09;。 方法一&#xff1a;使用 XML 的 <foreach> 标签&#xff…...

Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换

目录 关键点 技术实现1 技术实现2 摘要&#xff1a; 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式&#xff08;自动驾驶、人工驾驶、远程驾驶、主动安全&#xff09;&#xff0c;并通过实时消息推送更新车…...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧

上周三&#xff0c;HubSpot宣布已构建与ChatGPT的深度集成&#xff0c;这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋&#xff0c;但同时也存在一些关于数据安全的担忧。 许多网络声音声称&#xff0c;这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...