Spring AMQP源碼解析

目錄

channel和connection的區別

自動裝配RabbitAutoConfiguration

消息發送流程

獲取connection對象

獲取channel對象

AMQConnection讀取frame幀并回調publishconfirm和publishreturn

MainLoop線程監聽

執行回調


channel和connection的區別

Spring AMQP?是 Spring 框架對 AMQP(高級消息隊列協議)的支持,提供了一個高級抽象層,使得在 Spring 項目中使用消息隊列變得更加方便。

在源碼中會出現Channel和Connecttion的概念,我先來解釋一下

TCP連接:TCP連接是傳輸層面上的連接,通常是通過IP地址和端口號建立的,RabbitMQ使用TCP協議進行網絡通信,所有的消息傳遞都是在TCP連接上進行的。

RabbitMQ的連接:RabbitMQ的連接是指通過TCP建立的連接,通常是指Connection對,RabbitMQ在一個TCP連接上可以創建多個邏輯連接(即Channel)。

? ? ? ? RabbitMQ的設計理念是盡量減少TCP連接的數量,推薦使用一個TCP連接來承載多個Channel,這種設計可以減少網絡開銷,提高性能,同時也簡化了連接管理。

自動裝配RabbitAutoConfiguration

@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
@EnableConfigurationProperties({RabbitProperties.class})
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean({RabbitOperations.class})
public RabbitTemplate rabbitTemplate(RabbitProperties properties, ObjectProvider<MessageConverter> messageConverter, ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers, ConnectionFactory connectionFactory) {PropertyMapper map = PropertyMapper.get();RabbitTemplate template = new RabbitTemplate(connectionFactory);messageConverter.ifUnique(template::setMessageConverter);template.setMandatory(this.determineMandatoryFlag(properties));RabbitProperties.Template templateProperties = properties.getTemplate();if (templateProperties.getRetry().isEnabled()) {template.setRetryTemplate((new RetryTemplateFactory((List)retryTemplateCustomizers.orderedStream().collect(Collectors.toList()))).createRetryTemplate(templateProperties.getRetry(), Target.SENDER));}templateProperties.getClass();map.from(templateProperties::getReceiveTimeout).whenNonNull().as(Duration::toMillis).to(template::setReceiveTimeout);templateProperties.getClass();map.from(templateProperties::getReplyTimeout).whenNonNull().as(Duration::toMillis).to(template::setReplyTimeout);templateProperties.getClass();map.from(templateProperties::getExchange).to(template::setExchange);templateProperties.getClass();map.from(templateProperties::getRoutingKey).to(template::setRoutingKey);templateProperties.getClass();map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue);return template;
}@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {PropertyMapper map = PropertyMapper.get();CachingConnectionFactory factory = new CachingConnectionFactory((com.rabbitmq.client.ConnectionFactory)this.getRabbitConnectionFactoryBean(properties).getObject());properties.getClass();map.from(properties::determineAddresses).to(factory::setAddresses);properties.getClass();map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);properties.getClass();map.from(properties::getPublisherConfirmType).whenNonNull().to(factory::setPublisherConfirmType);RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();channel.getClass();map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);channel.getClass();map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis).to(factory::setChannelCheckoutTimeout);RabbitProperties.Cache.Connection connection = properties.getCache().getConnection();connection.getClass();map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);connection.getClass();map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);connectionNameStrategy.getClass();map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);return factory;
}}

@ConfigurationProperties(prefix = "spring.rabbitmq"
)
public class RabbitProperties {private String host = "localhost";private int port = 5672;private String username = "guest";private String password = "guest";private final Ssl ssl = new Ssl();private String virtualHost;private String addresses;@DurationUnit(ChronoUnit.SECONDS)private Duration requestedHeartbeat;private boolean publisherReturns;private CachingConnectionFactory.ConfirmType publisherConfirmType;private Duration connectionTimeout;private final Cache cache = new Cache();private final Listener listener = new Listener();private final Template template = new Template();private List<Address> parsedAddresses;}

這里就是spring自動裝配的流程,其完整流程就是SpringBoot啟動->@SpringBootApplication->@EnableAutoConfiguration->AutoConfigurationImportSelector掃描META-INF/spring.factories->加載RabbitAutoConfiguration->創建RabbitMQ相關Bean

RabbitProperties類加了@ConfigurationProperties會去讀取配置文件中的參數,否則就提供類屬性里面的默認配置,RabbitAutoConfiguration用@Bean注解通過方法將RabbitTemplate,CachingConnectionFactory都會注冊成bean,在方法里面會注入RabbitProperties的bean給他們設置參數。

消息發送流程

protected void sendToRabbit(Channel channel, String exchange, String routingKey, boolean mandatory, Message message) throws IOException {AMQP.BasicProperties convertedMessageProperties = this.messagePropertiesConverter.fromMessageProperties(message.getMessageProperties(), this.encoding);channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody());
}
public class Message implements Serializable {private static final long serialVersionUID = -7177590352110605597L;private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();private static final Set<String> whiteListPatterns = new LinkedHashSet(Arrays.asList("java.util.*", "java.lang.*"));private static String bodyEncoding;private final MessageProperties messageProperties;private final byte[] body;public Message(byte[] body, MessageProperties messageProperties) {this.body = body;this.messageProperties = messageProperties;}}

在通過RabbitTemplate的convertAndSend()方法發送消息的時候,先判斷是否定義了RetryTemplate,在RetryTemplate對象里面會定義重試的次數,間隔,RetryTemplate對象是否為null來判斷是否要重試,如果開啟了重試就調調用RetryTemplate的doexecute方法,并傳入RecoveryCallback,用于處理所有重試失敗后的邏輯,如果沒有開啟重試直接調用doExecute進行處理。

doExecute會通過CachingConnectionFactory來獲取channel,最后通過dosend()方法發送消息。dosend方法先進行setupConfirm()方法再調用sendToRabbit發消息,RabbitTemplate內部會將消息包裝成Message對象,通過Channel.basicPublish()方法發送消息,Message內部有一個byte字節數組封裝要發送的消息,還有MessageProperties封裝一些屬性,像消息id,做一個防止重復消費消息

獲取connection對象

public final Connection createConnection() throws AmqpException {if (this.stopped) {throw new AmqpApplicationContextClosedException("The ApplicationContext is closed and the ConnectionFactory can no longer create connections.");} else {synchronized(this.connectionMonitor) {if (this.cacheMode == CachingConnectionFactory.CacheMode.CHANNEL) {if (this.connection.target == null) {this.connection.target = super.createBareConnection();if (!this.checkoutPermits.containsKey(this.connection)) {this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize));}this.connection.closeNotified.set(false);this.getConnectionListener().onCreate(this.connection);}return this.connection;} else {return this.cacheMode == CachingConnectionFactory.CacheMode.CONNECTION ? this.connectionFromCache() : null;}}}
}

CachingConnectionFactory的createConnection方法創建連接,CachingConnectionFactory內部定義了一個Object對象作為鎖connectionMonitor,在獲取連接的時候會進行一個上鎖的操作,判斷采用的策略

獲取channel對象

private Channel getChannel(ChannelCachingConnectionProxy connection, boolean transactional) {Semaphore permits = null;if (this.channelCheckoutTimeout > 0L) {permits = this.obtainPermits(connection);}LinkedList<ChannelProxy> channelList = this.determineChannelList(connection, transactional);ChannelProxy channel = null;if (connection.isOpen()) {channel = this.findOpenChannel(channelList, channel);if (channel != null && this.logger.isTraceEnabled()) {this.logger.trace("Found cached Rabbit Channel: " + channel.toString());}}if (channel == null) {try {channel = this.getCachedChannelProxy(connection, channelList, transactional);} catch (RuntimeException var7) {if (permits != null) {permits.release();if (this.logger.isDebugEnabled()) {this.logger.debug("Could not get channel; released permit for " + connection + ", remaining:" + permits.availablePermits());}}throw var7;}}return channel;
}
private ChannelProxy findOpenChannel(LinkedList<ChannelProxy> channelList, ChannelProxy channelArg) {ChannelProxy channel = channelArg;synchronized(channelList) {while(!channelList.isEmpty()) {channel = (ChannelProxy)channelList.removeFirst();if (this.logger.isTraceEnabled()) {this.logger.trace(channel + " retrieved from cache");}if (channel.isOpen()) {break;}this.cleanUpClosedChannel(channel);channel = null;}return channel;}
}
private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection, LinkedList<ChannelProxy> channelList, boolean transactional) {Channel targetChannel = this.createBareChannel(connection, transactional);if (this.logger.isDebugEnabled()) {this.logger.debug("Creating cached Rabbit Channel from " + targetChannel);}this.getChannelListener().onCreate(targetChannel, transactional);Class[] interfaces;if (!CachingConnectionFactory.ConfirmType.CORRELATED.equals(this.confirmType) && !this.publisherReturns) {interfaces = new Class[]{ChannelProxy.class};} else {interfaces = new Class[]{ChannelProxy.class, PublisherCallbackChannel.class};}return (ChannelProxy)Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(), interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channelList, transactional));
}
private Channel createBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) {if (this.cacheMode == CachingConnectionFactory.CacheMode.CHANNEL) {// 檢查連接是否斷開if (!this.connection.isOpen()) {synchronized(this.connectionMonitor) {// 雙重檢查,確保在獲取鎖后連接仍然是關閉狀態if (!this.connection.isOpen()) {// 通知連接關閉事件this.connection.notifyCloseIfNecessary();}// 再次檢查并重建連接if (!this.connection.isOpen()) {// 清除舊連接this.connection.target = null;// 創建新連接this.createConnection();}}}
}return this.doCreateBareChannel(this.connection, transactional);} else if (this.cacheMode == CachingConnectionFactory.CacheMode.CONNECTION) {if (!connection.isOpen()) {synchronized(this.connectionMonitor) {((LinkedList)this.allocatedConnectionNonTransactionalChannels.get(connection)).clear();((LinkedList)this.allocatedConnectionTransactionalChannels.get(connection)).clear();connection.notifyCloseIfNecessary();this.refreshProxyConnection(connection);}}return this.doCreateBareChannel(connection, transactional);} else {return null;}
}
private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean transactional) {Channel channel = conn.createBareChannel(transactional);if (!CachingConnectionFactory.ConfirmType.NONE.equals(this.confirmType)) {try {((Channel)channel).confirmSelect();} catch (IOException var5) {this.logger.error("Could not configure the channel to receive publisher confirms", var5);}}if ((CachingConnectionFactory.ConfirmType.CORRELATED.equals(this.confirmType) || this.publisherReturns) && !(channel instanceof PublisherCallbackChannelImpl)) {channel = this.publisherChannelFactory.createChannel((Channel)channel, this.getChannelsExecutor());}if (channel != null) {((Channel)channel).addShutdownListener(this);}return (Channel)channel;
}
public class ChannelN extends AMQChannel implements Channel {private static final String UNSPECIFIED_OUT_OF_BAND = "";private static final Logger LOGGER = LoggerFactory.getLogger(ChannelN.class);private final Map<String, Consumer> _consumers;private final Collection<ReturnListener> returnListeners;private final Collection<ConfirmListener> confirmListeners;}

CachingConnectionFactory調用getChannel()獲取channel,會用一個map存儲Connection,和一個 Semaphore來保證每個Connection的channel數,會再獲取到存儲channel的LinkList,實現Channel的復用。

獲取LinkList中的連接會先上個鎖,防止并發下產生問題。

如果從LinkList中獲取的channel為null則通過getCachedChannelProxy()方法去獲取channel的一個代理對象

getCachedChannelProxy()方法先通過createBareChannel方法獲取一個targetchannel,會通過Jdk代理生成ChannelProxy對象,會判斷有沒有開啟retrun機制,開啟了代理實現相應的接口,CachingConnectionFactory內部定義了一個CachedChannelInvocationHandler

createBareChannel方法會先判斷有沒有創建connection對象,若是connection對象為null則采用了雙重檢驗的方法,判斷加鎖再判斷來防止多線程創建的問題,最后調用doCreateBareChannel方法創建channel,存在直接調用doCreateBareChannel。

doCreateBareChannel通過connection創建channel,再判斷是否開啟confirmType,publisherReturns,在channel接口的實現類會有returnListeners,confirmListeners存儲。

AMQConnection讀取frame幀并回調publishconfirm和publishreturn

MainLoop線程監聽

public class AMQConnection {private final MainLoop mainLoop;private volatile ChannelManager _channelManager;// 連接啟動時會啟動MainLoop線程public void startMainLoop() {MainLoop loop = new MainLoop();String name = "AMQP Connection " + this.getHostAddress() + ":" + this.getPort();this.mainLoopThread = Environment.newThread(this.threadFactory, loop, name);this.mainLoopThread.start();
}
public Channel createChannel(int channelNumber) throws IOException {this.ensureIsOpen();ChannelManager cm = this._channelManager;if (cm == null) {return null;} else {Channel channel = cm.createChannel(this, channelNumber);this.metricsCollector.newChannel(channel);return channel;}
}// MainLoop是一個獨立線程,負責從socket讀取數據// 從socket讀取AMQP幀private class MainLoop implements Runnable {private MainLoop() {}public void run() {boolean shouldDoFinalShutdown = true;try {while(AMQConnection.this._running) {Frame frame = AMQConnection.this._frameHandler.readFrame();AMQConnection.this.readFrame(frame);}} catch (Throwable var6) {if (var6 instanceof InterruptedException) {shouldDoFinalShutdown = false;} else {AMQConnection.this.handleFailure(var6);}} finally {if (shouldDoFinalShutdown) {AMQConnection.this.doFinalShutdown();}}}
}
private void readFrame(Frame frame) throws IOException {if (frame != null) {this._missedHeartbeats = 0;if (frame.type != 8) {if (frame.channel == 0) {this._channel0.handleFrame(frame);} else if (this.isOpen()) {ChannelManager cm = this._channelManager;if (cm != null) {ChannelN channel;try {channel = cm.getChannel(frame.channel);} catch (UnknownChannelException var5) {LOGGER.info("Received a frame on an unknown channel, ignoring it");return;}channel.handleFrame(frame);}}}} else {this.handleSocketTimeout();}}
}

public class ChannelManager {
private final Map<Integer, ChannelN> _channelMap;private ChannelN addNewChannel(AMQConnection connection, int channelNumber) {if (this._channelMap.containsKey(channelNumber)) {throw new IllegalStateException("We have attempted to create a channel with a number that is already in use. This should never happen. Please report this as a bug.");} else {ChannelN ch = this.instantiateChannel(connection, channelNumber, this.workService);this._channelMap.put(ch.getChannelNumber(), ch);return ch;}
}
}

AMQConnection是一些對frame幀,連接啟動時會啟動MainLoop線程,MainLoop是一個獨立線程,負責從socket讀取數據, 從socket讀取AMQP幀,根據channel號找到對應的channel并處理幀,根據channel號找到對應的channel并處理幀,在AMQConnection 有個ChannelManager類型的對象,依靠他來管理創建的channel,保存在一個map里面,key為序列號,value是channel。

執行回調

public class PublisherCallbackChannelImpl implements PublisherCallbackChannel, ConfirmListener, ReturnListener, ShutdownListener {private static final MessagePropertiesConverter CONVERTER = new DefaultMessagePropertiesConverter();private static final long RETURN_CALLBACK_TIMEOUT = 60L;private final Log logger = LogFactory.getLog(this.getClass());private final Channel delegate;private final ConcurrentMap<String, PublisherCallbackChannel.Listener> listeners = new ConcurrentHashMap();private final Map<PublisherCallbackChannel.Listener, SortedMap<Long, PendingConfirm>> pendingConfirms = new ConcurrentHashMap();private final Map<String, PendingConfirm> pendingReturns = new ConcurrentHashMap();private final SortedMap<Long, PublisherCallbackChannel.Listener> listenerForSeq = new ConcurrentSkipListMap();}
public void handleAck(long seq, boolean multiple) {if (this.logger.isDebugEnabled()) {this.logger.debug(this.toString() + " PC:Ack:" + seq + ":" + multiple);}this.processAck(seq, true, multiple, true);
}public void handleNack(long seq, boolean multiple) {if (this.logger.isDebugEnabled()) {this.logger.debug(this.toString() + " PC:Nack:" + seq + ":" + multiple);}this.processAck(seq, false, multiple, true);
}
public synchronized void addPendingConfirm(PublisherCallbackChannel.Listener listener, long seq, PendingConfirm pendingConfirm) {SortedMap<Long, PendingConfirm> pendingConfirmsForListener = (SortedMap)this.pendingConfirms.get(listener);Assert.notNull(pendingConfirmsForListener, "Listener not registered: " + listener + " " + this.pendingConfirms.keySet());pendingConfirmsForListener.put(seq, pendingConfirm);this.listenerForSeq.put(seq, listener);if (pendingConfirm.getCorrelationData() != null) {String returnCorrelation = pendingConfirm.getCorrelationData().getId();if (StringUtils.hasText(returnCorrelation)) {this.pendingReturns.put(returnCorrelation, pendingConfirm);}}}

PublisherCallbackChannelImpl是channel的實現類,若是傳遞了correlationData會轉化成PendingConfirm放到map里面,Listener是key,PendingConfirm是value的key。當broker確認消息后,會觸發channel的confirm監聽器,找到對應的CorrelationData,執行回調。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/79761.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/79761.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/79761.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux系統安裝PaddleDetection

一、安裝cuda 1. 查看設備 先輸入nvidia-smi&#xff0c;查看設備支持的最大cuda版本&#xff0c;選擇官網中支持的cuda版本 https://www.paddlepaddle.org.cn/install/quick?docurl/documentation/docs/zh/install/conda/linux-conda.html 2. 下載CUDA并安裝 使用快捷鍵…

Linux系統中的時間同步服務

1.時間同步&#xff1a;多主機協作工作&#xff0c;時間應該保持一致&#xff0c;如加密協議、日志、集群等&#xff0c;利用NTP&#xff08;Network Time Protocol&#xff09;協議使得各個主機時間達到同步。 ntp:將系統時鐘和世界協調時UTC同步&#xff0c;精度在局域網內可…

【Linux筆記】系統的延遲任務、定時任務極其相關命令(at、crontab極其黑白名單等)

一、延時任務 1、概念 延時任務&#xff08;Delayed Jobs&#xff09;通常指在指定時間或特定條件滿足后執行的任務。常見的實現方式包括 at 和 batch 命令&#xff0c;以及結合 cron 的調度功能。 2、命令 延時任務的命令最常用的是at命令&#xff0c;第二大節會詳細介紹。…

軟考 系統架構設計師系列知識點 —— 黑盒測試與白盒測試(1)

本文內容參考&#xff1a; 黑盒測試和白盒測試詳解-CSDN博客 軟件測試中的各種覆蓋&#xff08;Coverage&#xff09;詳解-CSDN博客 特此致謝&#xff01; 零、概述 黑盒測試又名為功能測試&#xff0c;主要目的是發現軟件設計的需求或者是軟件設計規格說明書中的錯誤缺陷。…

yolov11 epoch100輪 訓練筆記5 kaggle comet

Football Players Detection using YOLOV11 | Kaggle !pip install comet_ml import comet_mlcomet_ml.login(project_name"c") Comet - Build Better Models Faster yolov11訓練 100輪一眨眼訓練完了 然而comet接不到yolo的sdk 優秀 訓練17輪map就0.99了 v5訓練100…

Ubuntu K8S(1.28.2) 節點/etc/kubernetes/manifests 不存在

Ubuntu K8S(1.28.2) 節點/etc/kubernetes/manifests 不存在 在查看日志&#xff08;journalctl -xefu kubelet&#xff09;時發現各節點/etc/kubernetes/manifests 不存在&#xff0c;但主節點沒有異常 21080 file.go:104] "Unable to read config path" err"…

neo4j基礎操作:命令行增刪改查

目錄 一&#xff0c;Neo4j的增 1.1.新增節點 1.2.新增關系 1.2.1創建節點時&#xff0c;創建關系 1.2.2在已有的節點上&#xff0c;創建關系 二&#xff0c;Neo4j的刪除 2.1刪除節點 2.1.1無關系的節點刪除 2.1.2 有關系的節點刪除 三&#xff0c;節點修改 3.1 給節點…

rollout 是什么:機器學習(強化學習)領域

rollout 是什么:機器學習(強化學習)領域 指從特定初始狀態開始,按照某個策略或模型進行一系列動作和狀態轉移,直到達到終止狀態或預定時間步數 。比如: 迷宮任務:強化學習代理在迷宮中,從起始點出發,按某策略(如隨機選方向走)進行移動,直到找到出口或達到最大移動…

stm32之TIM定時中斷詳解

目錄 1.引入1.1 簡介1.2 類型1.2.1 基本定時器1.2.2 通用定時器1. 觸發控制單元 (Trigger Control Unit)2. 輸入捕獲單元 (Input Capture Unit)3. 輸出比較單元 (Output Compare Unit)4. CNT 計數器5. 自動重裝載寄存器 (ARR)6. 預分頻器 (PSC)7. 中斷與 DMA 事件8. 剎車功能 (…

centos8源碼安裝openssl

前言&#xff1a; 在使用python3.11部署運行FastAPI時&#xff0c;由于其uvicorn需要使用openssl模塊&#xff0c;導致沒有安裝openssl的服務器項目運行不起來. 【第一步】 我的網盤下載openssl-1.1.1n.tar.gz 提取碼: vay9 【第二步】 上傳到服務器解壓 tar -zxvf opens…

vue3 動態修改系統title

vue3 動態修改系統title 修改前 修改后 1、封裝 useTitle 工具函數 創建組合式 API&#xff0c;通過 watchEffect 監聽標題變化&#xff1a; // composables/useTitle.js import { ref, watchEffect } from vue;export function useTitle(initialTitle) {const title r…

比較兩種判斷相同二叉樹的方法:遞歸與遍歷序列對比

在二叉樹操作中&#xff0c;判斷兩棵樹是否相同是一個常見的問題。本文將對比兩種不同的解決方案&#xff1a;遞歸法和遍歷序列對比法&#xff0c;分析它們的優缺點&#xff0c;并探討為何遞歸法是更優的選擇。 問題描述 給定兩棵二叉樹的根節點 p 和 q&#xff0c;判斷它們是…

從0開始學習大模型--Day01--大模型是什么

初識大模型 在平時遇到問題時&#xff0c;我們總是習慣性地去運用各種搜索引擎如百度、知乎、CSDN等平臺去搜索答案&#xff0c;但由于搜索到的內容質量參差不齊&#xff0c;檢索到的內容只是單純地根據關鍵字給出內容&#xff0c;往往看了幾個網頁都找不到答案&#xff1b;而…

【AI大模型】SpringBoot整合Spring AI 核心組件使用詳解

目錄 一、前言 二、Spring AI介紹 2.1 Spring AI介紹 2.2 Spring AI主要特點 2.3 Spring AI核心組件 2.4 Spring AI應用場景 2.5 Spring AI優勢 2.5.1 與 Spring 生態無縫集成 2.5.2 模塊化設計 2.5.3 簡化 AI 集成 2.5.4 支持云原生和分布式計算 2.5.5 安全性保障…

洛谷 P9007 [入門賽 #9] 最澄澈的空與海 (Hard Version)

這道題可不入門。 [Problem Discription] \color{blue}{\texttt{[Problem Discription]}} [Problem Discription] 給定 n n n&#xff0c;求有多少組 ( x , y , z ) (x,y,z) (x,y,z) 滿足&#xff1a; x ? y z n ! x-\dfrac{y}{z}n! x?zy?n! x ? y z n ! n \dfrac{x-y…

PostgreSQL 的 pg_stat_file 函數

PostgreSQL 的 pg_stat_file 函數 pg_stat_file 是 PostgreSQL 提供的一個系統管理函數&#xff0c;用于獲取文件系統上文件的元數據信息。這個函數對于數據庫管理員進行文件級別的監控和診斷非常有用。 一 函數基本語法 pg_stat_file(filename text [, missing_ok boolean …

關于麒麟服務器實現docker-compose服務開機自啟

我本地服務器環境是麒麟V10版本&#xff1a; 首先確定docker-compose服務絕對路徑命令&#xff1a; which docker-compose我這里輸出是&#xff1a;/usr/bin/docker-compose 編輯服務文件&#xff1a; sudo vim /etc/systemd/system/docker-compose-webup.service[Unit] Desc…

基于 jQuery 實現復選框全選與選中項查詢功能

在 Web 開發中&#xff0c;復選框是常見的交互元素&#xff0c;尤其是在涉及批量操作、數據篩選等場景時&#xff0c;全選功能和選中項查詢功能顯得尤為重要。本文將介紹如何使用 HTML、CSS 和 jQuery 實現一個具備全選、反選以及選中項查詢功能的復選框組&#xff0c;幫助開發…

AfuseKt2.4.2 | 支持阿里云盤、Alist等平臺視頻播放,具備自動海報墻刮削功能的強大播放器

AfuseKt是一款功能強大的安卓端在線視頻播放器&#xff0c;支持播放阿里云盤、Alist、WebDAV等平臺的視頻內容。它具備自動海報墻刮削功能&#xff0c;能自動生成影片信息和海報墻&#xff0c;提供良好的視覺體驗。此外&#xff0c;它還支持倍速播放、字幕、音軌切換等多種實用…

Netlink在SONiC中的應用

Netlink在SONiC中的應用 Netlink介紹 Netlink 是 Linux 內核態程序與用戶空間程序之間進行通信的機制之一&#xff0c;原本是用于傳遞網絡協議棧中的各種控制消息。它采用和套接字&#xff08;socket&#xff09;編程接口相同的形式&#xff0c;常用于配置內核網絡子系統&…