【stomp 实战】spring websocket 接收消息源码分析

news2025/1/10 23:41:49

后台消息的发送过程,我们通过spring websocket用户消息发送源码分析已经了解了。我们再来分析一下后端接收消息的过程。这个过程和后端发送消息过程有点类似。

前端发送消息

前端发送消息给服务端的示例如下:
发送给目的/app/echo一个消息。

//主动发送消息给服务器,对应的后端topic为/app/echo
function send() {
    var value = document.getElementById("content").value;
    var msg = {
        msgType: 1,
        content: value
    };
    stompClient.send("/app/echo", {}, JSON.stringify(msg));
    //stompClient.send("/app/echo2", {}, JSON.stringify(msg));
}

后端接收消息的配置

 @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /**
         * 这里表示前端往/app路径推送, 如果后端定义一个controller ->@MessageMapping("/echo"),
         * stompClient.send("/app/echo",{},...)
         * 这时,消息会被推送到注解对应的@MessageMapping("/echo")方法上
         */
        registry.setApplicationDestinationPrefixes("/app");
    }

后端配置/app前缀。
这个前缀和哪里结合起来用呢,来看下面的代码

@Slf4j
@Controller
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class StompController {
    private final SimpMessageSendingOperations msgOperations;
    private final SimpUserRegistry simpUserRegistry;

    /**
     * 回音消息,将用户发来的消息内容加上 Echo 前缀后推送回客户端
     */
    @MessageMapping("/echo")
    public void echo(Principal principal, Msg msg) {
        String username = principal.getName();
        msg.setContent("Echo: " + msg.getContent());
        msgOperations.convertAndSendToUser(username, "/topic/answer", msg);
        int userCount = simpUserRegistry.getUserCount();
        int sessionCount = simpUserRegistry.getUser(username).getSessions().size();
        log.info("当前本系统总在线人数: {}, 当前用户: {}, 该用户的客户端连接数: {}", userCount, username, sessionCount);
    }
}

实际上就和这个echo方法结合一起用的。 @MessageMapping(“/echo”)中的/echo和前缀结合一起,就是/app/echo。
因此,这个echo方法,就是接收前端发送消息的方法入口。

源码分析

消息处理器的注册

在 spring websocket源码分析之握手请求的处理这一节中,在完成websocket握手请求后,我们看到了如下的代码。

	public void onOpen(final javax.websocket.Session session, EndpointConfig config) {
		this.wsSession.initializeNativeSession(session);

		// The following inner classes need to remain since lambdas would not retain their
		// declared generic types (which need to be seen by the underlying WebSocket engine)

		if (this.handler.supportsPartialMessages()) {
			session.addMessageHandler(new MessageHandler.Partial<String>() {
				@Override
				public void onMessage(String message, boolean isLast) {
					handleTextMessage(session, message, isLast);
				}
			});
			session.addMessageHandler(new MessageHandler.Partial<ByteBuffer>() {
				@Override
				public void onMessage(ByteBuffer message, boolean isLast) {
					handleBinaryMessage(session, message, isLast);
				}
			});
		}
		else {
			session.addMessageHandler(new MessageHandler.Whole<String>() {
				@Override
				public void onMessage(String message) {
					handleTextMessage(session, message, true);
				}
			});
			session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
				@Override
				public void onMessage(ByteBuffer message) {
					handleBinaryMessage(session, message, true);
				}
			});
		}

		session.addMessageHandler(new MessageHandler.Whole<javax.websocket.PongMessage>() {
			@Override
			public void onMessage(javax.websocket.PongMessage message) {
				handlePongMessage(session, message.getApplicationData());
			}
		});

		try {
			this.handler.afterConnectionEstablished(this.wsSession);
		}
		catch (Exception ex) {
			ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
		}
	}

代码总结:

  • 这里入参传了一个javax.websocket.Session。这个可以理解为当前Websocket连接。
  • 原来这个Session可以给自己添加messageHandler,那当有消息来的时候,就会经过这些handler来进行处理。
  • 那这个hander就是处理业务消息的重点了
    看一下这个hander是怎么处理消息的
private void handleTextMessage(javax.websocket.Session session, String payload, boolean isLast) {
		TextMessage textMessage = new TextMessage(payload, isLast);
		try {
			this.handler.handleMessage(this.wsSession, textMessage);
		}
		catch (Exception ex) {
			ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);
		}
	}

这个handler,对应的实现是:SockJsWebSocketHandler
进入handleMessage看一下处理逻辑,原来是将消息分为三类

  • 文本消息
  • 二进制消息
  • 心跳消息
    这三种消息,分别进行处理
@Override
	public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
		if (message instanceof TextMessage) {
			handleTextMessage(session, (TextMessage) message);
		}
		else if (message instanceof BinaryMessage) {
			handleBinaryMessage(session, (BinaryMessage) message);
		}
		else if (message instanceof PongMessage) {
			handlePongMessage(session, (PongMessage) message);
		}
		else {
			throw new IllegalStateException("Unexpected WebSocket message type: " + message);
		}
	}

我们一般处理的是文本消息

	@Override
	public void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {
		this.sockJsSession.handleMessage(message, wsSession);
	}

又交给sockJsSession来处理消息。
再看下WebSocketServerSockJsSession的handlerMessage方法。
往下,找到了delegateMessages。

//WebSocketServerSockJsSession
	public void handleMessage(TextMessage message, WebSocketSession wsSession) throws Exception {
		String payload = message.getPayload();
		if (!StringUtils.hasLength(payload)) {
			return;
		}
		String[] messages;
		try {
			messages = getSockJsServiceConfig().getMessageCodec().decode(payload);
		}
		catch (Exception ex) {
			logger.error("Broken data received. Terminating WebSocket connection abruptly", ex);
			tryCloseWithSockJsTransportError(ex, CloseStatus.BAD_DATA);
			return;
		}
		if (messages != null) {
			delegateMessages(messages);
		}
	}

public void delegateMessages(String... messages) throws SockJsMessageDeliveryException {
		for (int i = 0; i < messages.length; i++) {
			try {
				if (isClosed()) {
					logUndeliveredMessages(i, messages);
					return;
				}
				this.handler.handleMessage(this, new TextMessage(messages[i]));
			}
			catch (Exception ex) {
				if (isClosed()) {
					if (logger.isTraceEnabled()) {
						logger.trace("Failed to handle message '" + messages[i] + "'", ex);
					}
					logUndeliveredMessages(i, messages);
					return;
				}
				throw new SockJsMessageDeliveryException(this.id, getUndelivered(messages, i), ex);
			}
		}
	}

可以看到delegateMessages实际上是把消息一条条处理。交给了handler来处理。
这里的hander是什么?SubProtocolWebSocketHandler。

//SubProtocolWebSocketHandler
	@Override
	public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
		WebSocketSessionHolder holder = this.sessions.get(session.getId());
		if (holder != null) {
			session = holder.getSession();
		}
		SubProtocolHandler protocolHandler = findProtocolHandler(session);
		protocolHandler.handleMessageFromClient(session, message, this.clientInboundChannel);
		if (holder != null) {
			holder.setHasHandledMessages();
		}
		checkSessions();
	}

这里就是通过session取出子协议处理器,这里实际上就一个实现,是StompSubProtocolHandler。

//StompSubProtocolHandler
	@Override
	public void handleMessageFromClient(WebSocketSession session,
			WebSocketMessage<?> webSocketMessage, MessageChannel outputChannel) {

		List<Message<byte[]>> messages;
		try {
			ByteBuffer byteBuffer;
			if (webSocketMessage instanceof TextMessage) {
				byteBuffer = ByteBuffer.wrap(((TextMessage) webSocketMessage).asBytes());
			}
			else if (webSocketMessage instanceof BinaryMessage) {
				byteBuffer = ((BinaryMessage) webSocketMessage).getPayload();
			}
			else {
				return;
			}

			BufferingStompDecoder decoder = this.decoders.get(session.getId());
			if (decoder == null) {
				if (!session.isOpen()) {
					logger.trace("Dropped inbound WebSocket message due to closed session");
					return;
				}
				throw new IllegalStateException("No decoder for session id '" + session.getId() + "'");
			}

			messages = decoder.decode(byteBuffer);
			if (messages.isEmpty()) {
				if (logger.isTraceEnabled()) {
					logger.trace("Incomplete STOMP frame content received in session " +
							session + ", bufferSize=" + decoder.getBufferSize() +
							", bufferSizeLimit=" + decoder.getBufferSizeLimit() + ".");
				}
				return;
			}
		}
		catch (Throwable ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to parse " + webSocketMessage +
						" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);
			}
			handleError(session, ex, null);
			return;
		}

		for (Message<byte[]> message : messages) {
			StompHeaderAccessor headerAccessor =
					MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
			Assert.state(headerAccessor != null, "No StompHeaderAccessor");

			StompCommand command = headerAccessor.getCommand();
			boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);

			boolean sent = false;
			try {

				headerAccessor.setSessionId(session.getId());
				headerAccessor.setSessionAttributes(session.getAttributes());
				headerAccessor.setUser(getUser(session));
				if (isConnect) {
					headerAccessor.setUserChangeCallback(user -> {
						if (user != null && user != session.getPrincipal()) {
							this.stompAuthentications.put(session.getId(), user);
						}
					});
				}
				headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
				if (!detectImmutableMessageInterceptor(outputChannel)) {
					headerAccessor.setImmutable();
				}

				if (logger.isTraceEnabled()) {
					logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
				}

				if (isConnect) {
					this.stats.incrementConnectCount();
				}
				else if (StompCommand.DISCONNECT.equals(command)) {
					this.stats.incrementDisconnectCount();
				}

				try {
					SimpAttributesContextHolder.setAttributesFromMessage(message);
					sent = outputChannel.send(message);

					if (sent) {
						if (this.eventPublisher != null) {
							Principal user = getUser(session);
							if (isConnect) {
								publishEvent(this.eventPublisher, new SessionConnectEvent(this, message, user));
							}
							else if (StompCommand.SUBSCRIBE.equals(command)) {
								publishEvent(this.eventPublisher, new SessionSubscribeEvent(this, message, user));
							}
							else if (StompCommand.UNSUBSCRIBE.equals(command)) {
								publishEvent(this.eventPublisher, new SessionUnsubscribeEvent(this, message, user));
							}
						}
					}
				}
				finally {
					SimpAttributesContextHolder.resetAttributes();
				}
			}
			catch (Throwable ex) {
				if (logger.isDebugEnabled()) {
					logger.debug("Failed to send message to MessageChannel in session " + session.getId(), ex);
				}
				else if (logger.isErrorEnabled()) {
					// Skip unsent CONNECT messages (likely auth issues)
					if (!isConnect || sent) {
						logger.error("Failed to send message to MessageChannel in session " + session.getId() +
								":" + ex.getMessage());
					}
				}
				handleError(session, ex, message);
			}
		}
	}

代码很长,总结一下:

  • 1、消息报文的编码处理,转换成Message对象
  • 2、StompHeaderAccessor的处理,包括设置user、session等
  • 3、调用outputChannel发送消息:outputChannel.send(message);
  • 4、如果发送消息成功,则发送相应的事件消息,有以下几类事件:SessionConnectEvent、SessionSubscribeEvent、SessionUnsubscribeEvent。

MessageChannel发送消息过程

outputChannel.send(message),发送消息,这个似乎似曾相识。在 【stomp 实战】spring websocket用户消息发送源码分析 这一节中,我们也看到过这个类。在服务端往客户端发送消息时,也有这个MessageChannel的出现。

//AbstractMessageChannel
	@Override
	public final boolean send(Message<?> message, long timeout) {
		Assert.notNull(message, "Message must not be null");
		Message<?> messageToUse = message;
		ChannelInterceptorChain chain = new ChannelInterceptorChain();
		boolean sent = false;
		try {
			messageToUse = chain.applyPreSend(messageToUse, this);
			if (messageToUse == null) {
				return false;
			}
			sent = sendInternal(messageToUse, timeout);
			chain.applyPostSend(messageToUse, this, sent);
			chain.triggerAfterSendCompletion(messageToUse, this, sent, null);
			return sent;
		}
		catch (Exception ex) {
			chain.triggerAfterSendCompletion(messageToUse, this, sent, ex);
			if (ex instanceof MessagingException) {
				throw (MessagingException) ex;
			}
			throw new MessageDeliveryException(messageToUse,"Failed to send message to " + this, ex);
		}
		catch (Throwable err) {
			MessageDeliveryException ex2 =
					new MessageDeliveryException(messageToUse, "Failed to send message to " + this, err);
			chain.triggerAfterSendCompletion(messageToUse, this, sent, ex2);
			throw ex2;
		}
	}
  • 构造了一个拦截链,在发送前,可以进行前置处理和后置处理。这个拦截链就是扩展的关键了。我们可以定义自己的拦截器,在发送消息前后进行拦截处理。这里spring给我们的扩展点。
  • 通过sendInternal将消息发送出去
    然后我们Debug看看这个sendInternal
    在这里插入图片描述
    看到有三个MessageHandler
  • WebSocketAnnotationMethodMessageHandler
  • SimpleBrokerMessageHandler
  • UserDestinationMessageHandler
    这里依次会调用这三个handler来发送消息。一般情况下,只会有一个handler来处理
    我们示例中发送的消息destination是/app/echo,对应着一个方法。 这里当然是WebSocketAnnotationMethodMessageHandler来处理了。
    这里封装成一个Task,执行其run方法。在executor不为空的时候,是异步发送的。

进入SendTask,看一下run方法

//
public void run() {
	Message<?> message = this.inputMessage;
	try {
		message = applyBeforeHandle(message);
		if (message == null) {
			return;
		}
		this.messageHandler.handleMessage(message);
		triggerAfterMessageHandled(message, null);
	}
	catch (Exception ex) {
		triggerAfterMessageHandled(message, ex);
		if (ex instanceof MessagingException) {
			throw (MessagingException) ex;
		}
		String description = "Failed to handle " + message + " to " + this + " in " + this.messageHandler;
		throw new MessageDeliveryException(message, description, ex);
	}
	catch (Throwable err) {
		String description = "Failed to handle " + message + " to " + this + " in " + this.messageHandler;
		MessageDeliveryException ex2 = new MessageDeliveryException(message, description, err);
		triggerAfterMessageHandled(message, ex2);
		throw ex2;
	}
}

这里的关键点是:this.messageHandler.handleMessage(message);
/app/echo会进入AbstractMethodMessageHandler

// AbstractMethodMessageHandler
	@Override
	public void handleMessage(Message<?> message) throws MessagingException {
		String destination = getDestination(message);
		if (destination == null) {
			return;
		}
		String lookupDestination = getLookupDestination(destination);
		if (lookupDestination == null) {
			return;
		}

		MessageHeaderAccessor headerAccessor = MessageHeaderAccessor.getMutableAccessor(message);
		headerAccessor.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, lookupDestination);
		headerAccessor.setLeaveMutable(true);
		message = MessageBuilder.createMessage(message.getPayload(), headerAccessor.getMessageHeaders());

		if (logger.isDebugEnabled()) {
			logger.debug("Searching methods to handle " +
					headerAccessor.getShortLogMessage(message.getPayload()) +
					", lookupDestination='" + lookupDestination + "'");
		}

		handleMessageInternal(message, lookupDestination);
		headerAccessor.setImmutable();
	}

	protected void handleMessageInternal(Message<?> message, String lookupDestination) {
		List<Match> matches = new ArrayList<>();

		List<T> mappingsByUrl = this.destinationLookup.get(lookupDestination);
		if (mappingsByUrl != null) {
			addMatchesToCollection(mappingsByUrl, message, matches);
		}
		if (matches.isEmpty()) {
			// No direct hits, go through all mappings
			Set<T> allMappings = this.handlerMethods.keySet();
			addMatchesToCollection(allMappings, message, matches);
		}
		if (matches.isEmpty()) {
			handleNoMatch(this.handlerMethods.keySet(), lookupDestination, message);
			return;
		}

		Comparator<Match> comparator = new MatchComparator(getMappingComparator(message));
		matches.sort(comparator);
		if (logger.isTraceEnabled()) {
			logger.trace("Found " + matches.size() + " handler methods: " + matches);
		}

		Match bestMatch = matches.get(0);
		if (matches.size() > 1) {
			Match secondBestMatch = matches.get(1);
			if (comparator.compare(bestMatch, secondBestMatch) == 0) {
				Method m1 = bestMatch.handlerMethod.getMethod();
				Method m2 = secondBestMatch.handlerMethod.getMethod();
				throw new IllegalStateException("Ambiguous handler methods mapped for destination '" +
						lookupDestination + "': {" + m1 + ", " + m2 + "}");
			}
		}

		handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message);
	}
  • handleMessage 主要做一些消息的处理
  • handleMessageInternal就是关键点了。
    • 根据destination找到mappings,即我们注解中配置的url
    • 正常情况下,会找到一个匹配的url,这个url会对应一个method,调用下面的方法执行后续逻辑。handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message)
protected void handleMatch(T mapping, HandlerMethod handlerMethod, String lookupDestination, Message<?> message) {
		if (logger.isDebugEnabled()) {
			logger.debug("Invoking " + handlerMethod.getShortLogMessage());
		}
		handlerMethod = handlerMethod.createWithResolvedBean();
		InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
		if (this.handlerMethodLogger != null) {
			invocable.setLogger(this.handlerMethodLogger);
		}
		invocable.setMessageMethodArgumentResolvers(this.argumentResolvers);
		try {
			Object returnValue = invocable.invoke(message);
			MethodParameter returnType = handlerMethod.getReturnType();
			if (void.class == returnType.getParameterType()) {
				return;
			}
			if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) {
				ListenableFuture<?> future = this.returnValueHandlers.toListenableFuture(returnValue, returnType);
				if (future != null) {
					future.addCallback(new ReturnValueListenableFutureCallback(invocable, message));
				}
			}
			else {
				this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
			}
		}
		catch (Exception ex) {
			processHandlerMethodException(handlerMethod, ex, message);
		}
		catch (Throwable ex) {
			Exception handlingException =
					new MessageHandlingException(message, "Unexpected handler method invocation error", ex);
			processHandlerMethodException(handlerMethod, handlingException, message);
		}
	}

这里最重要的就是 invocable.invoke(message);。即调用反射来执行目标方法。这里代码之所以比较复杂,是处理入参和返回值。这里不是我们研究的重点。就不再分析了。

整个流程总结如下
在这里插入图片描述

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

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

相关文章

线程安全的概念及原因

1.观察线程不安全 public class ThreadDemo {static class Counter {public int count 0;void increase() {count;}}public static void main(String[] args) throws InterruptedException {final Counter counter new Counter();Thread t1 new Thread(() -> {for (int …

ES:聚合查询语法

基础查询结构&#xff1a; GET http://ip:prot/textbook/_search { "query" : { ...query子句... }, "aggs" : { "agg_name":{ "agg_type": { "agg_arg": agg_arg_value } } }, "sort" : { ..sor…

Cesium--加载天地图

背景&#xff1a;vue-admin-temlate cesium 天地图 天地图地址&#xff1a;国家地理信息公共服务平台 天地图 步骤一&#xff1a;申请成为天地图开发者&#xff0c;创建应用 1,天地图使用方法&#xff08;点击开发资源即可看到此页面&#xff09; 2,点击控制台-登录账号 …

13:HAL---SPI

目录 一:SPL通信 1:简历 2:硬件电路 3:移动数据图 4:SPI时序基本单元 A : 开/ 终条件 B:SPI时序基本单元 A:模式0 B:模式1 C:模式2 D:模式3 C:SPl时序 A:发送指令 B: 指定地址写 C:指定地址读 5&#xff1a;NSS(CS) 6&#xff1a;时钟 二: W25Q64 1:简历 2…

Star-CCM+通过将所有部件创建一个区域的方式分配至区域后子区域的分离,子区域材料属性的赋值,以及物理连续体的创建方法介绍

前言 上次介绍了将零部件分配至区域的方法与各个方法之间的区别&#xff0c;本文将继续上次的讲解&#xff0c;将其中的“将所有部件分配至一个区域”的应用进行补充。 如下图所示&#xff0c;按照将所有部件创建一个区域的方式分配至区域后&#xff0c;在区域下就会有一个区域…

springboot+vue实现登录注册,短信注册以及微信扫描登录

说明&#xff1a;微信扫描登录需要微信注册--要钱&#xff0c;感谢尚硅谷提供的免费接口&#xff1b;短信注册需要阿里云的注册很麻烦并且短信费&#xff0c;没有接口&#xff0c;所以不打算实现&#xff0c;不过能做出效果。 目录 一、建立数据库 二、后端idea实现接口 1.…

全球首发:抗量子、以太坊兼容测试网正式上线

量子计算机将有能力破解目前互联网上使用的主要加密算法&#xff0c;影响的领域包括银行应用程序、电子邮件服务和社交媒体平台。 2023年5月7日&#xff0c;QANplatform推出了全球首个兼容以太坊的抗量子区块链测试网&#xff0c;此举将使开发者能够使用任何编程语言来编写智能…

thinkphp6使用layui分页组件做分页效果

博主用的是layui2.9.8的版本&#xff0c;但这个版本的分页组件是动态效果的&#xff0c;但我需要的是静态分页&#xff0c;所以我自己封装了一个生成layui的分页代码生成代码。代码如下&#xff1a; 1、先创建文件&#xff0c;路径是extent/layui/LayuiPage.php&#xff0c;加…

Java实战:验证改进的哥德巴赫猜想

改进的哥德巴赫猜想&#xff08;Improved Goldbach’s Conjecture&#xff09;声称每个大于5的奇数都可以表示为三个素数之和。这个猜想是对原始哥德巴赫猜想的扩展&#xff0c;针对奇数的情况。原始哥德巴赫猜想是指每个大于2的偶数都可以表示为两个素数之和。尽管改进的哥德巴…

ROS 2边学边练(45)-- 构建一个能动的机器人模型

前言 在上篇中我们搭建了一个机器人模型(其由各个关节&#xff08;joint&#xff09;和连杆&#xff08;link&#xff09;组成)&#xff0c;此篇我们会通过设置关节类型来实现机器人的活动。 在ROS中&#xff0c;关节一般有无限旋转&#xff08;continuous&#xff09;,有限旋转…

el-dialog设置el-head固定

0 效果 1 代码 ::v-deep .adTextDetailDialogClass .el-dialog__body{max-height: calc(100vh - 150px);overflow: auto;border-top:1px solid #dfdfdf;border-bottom:1px solid #dfdfdf; } ::v-deep .adTextDetailDialogClass .el-dialog{position: fixed;height:fit-content;…

15-LINUX--线程的创建与同步

一.线程 1.线程的概念 线程是进程内部的一条执行序列或执行路径&#xff0c;一个进程可以包含多条线程。 2.线程的三种实现方式 ◼ 内核级线程&#xff1a;由内核创建&#xff0c;创建开销大&#xff0c;内核能感知到线程的存在 ◼ 用户级线程&#xff1a;线程的创建有用户空…

springboot 引入第三方bean

如何进行第三方bean的定义 参数进行自动装配

数据库中索引的底层原理和SQL优化

文章目录 关于索引B 树的特点MySQL 为什么使用 B 树&#xff1f; 索引分类聚簇索引 和 非聚簇索引覆盖索引索引的最左匹配原则索引与NULL索引的代价大表结构修改 SQL优化EXPLAIN命令选择索引列其它细节 关于索引 索引是一种用来加快查找效率的数据结构&#xff0c;可以简单粗暴…

探索黏土特效?推荐这三款软件!

在数字化时代&#xff0c;我们拥有无数的工具来释放我们的创造力和想象力。其中&#xff0c;黏土特效软件就是一种能够将你的照片或图像转化为可爱、生动的黏土动画的工具。这些软件以其独特的视觉效果和易于使用的特性&#xff0c;吸引了大量的用户。下面&#xff0c;我们将为…

gorm-sharding分表插件升级版

代码地址&#xff1a; GitHub - 137/gorm-sharding: Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充&#xff0c;带来的额外开销极小。对开发者友好、透明&#xff0c;使用上与普通 SQL、Gorm 查询无差别.解决了原生s…

FreeRTOS学习 -- 任务相关API函数

一、任务创建和删除API函数 FreeRTOS 最基本的功能就是任务管理&#xff0c;而任务管理最基本的操作就是创建和删除任务。 FreeRTOS的任务创建和删除API函数如下&#xff1a; 1、函数 xTaskCreate() 此函数用来创建一个任务&#xff0c;任务需要 RAM 来保存于任务有关的状…

nginx的应用部署nginx

这里写目录标题 nginxnginx的优点什么是集群常见的集群什么是正向代理、反向代理、透明代理常见的代理技术正向代理反向代理透明代理 nginx部署 nginx nginx&#xff08;发音同enginex&#xff09;是一款轻量级的Web服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&…

每日一题 第九十七期 洛谷 [NOIP2000 提高组] 方格取数

[NOIP2000 提高组] 方格取数 题目背景 NOIP 2000 提高组 T4 题目描述 设有 N N N \times N NN 的方格图 ( N ≤ 9 ) (N \le 9) (N≤9)&#xff0c;我们将其中的某些方格中填入正整数&#xff0c;而其他的方格中则放入数字 0 0 0。如下图所示&#xff08;见样例&#xf…

同步电机原理解析

同步电机 同步带年纪&#xff0c;顾名思义无论负载如何&#xff0c;都能以恒定的速度运转&#xff0c;它以高效率著称 这种恒速特性是通过恒定磁场和旋转磁场的相互作用实现的&#xff0c;与其他电机一样&#xff0c;同步电机由定子和转子组成&#xff0c;定子铁芯由硅片层叠而…