深入浅出:在 Spring Boot 中构建实时应用 - 全面掌握 WebSocket

前言:为什么需要 WebSocket?

在传统的 Web 应用中,通信模式主要是 HTTP 请求-响应。客户端(通常是浏览器)发起一个请求,服务器处理后返回一个响应,然后连接关闭。这种模式对于获取网页内容、提交表单等操作非常有效。

然而,随着 Web 应用的复杂化,我们越来越多地需要实时、双向、持续的通信能力。例如:

  • 在线聊天室: 用户发送消息,所有在线用户能立即看到。
  • 实时通知: 新邮件、好友请求、系统告警需要即时推送给用户。
  • 股票行情/数据仪表盘: 价格、状态需要秒级甚至毫秒级更新。
  • 在线游戏: 玩家状态、游戏事件需要实时同步。
  • 协作编辑: 多人同时编辑文档,彼此的修改需要实时可见。

如果使用传统的 HTTP 轮询(Polling)或长轮询(Long Polling)来实现这些功能,会带来巨大的服务器压力、延迟高、效率低下。WebSocket 协议的出现,正是为了解决这些问题。

WebSocket 是什么?

WebSocket 是一种在单个 TCP 连接上进行全双工(full-duplex)通信的协议。它允许服务器主动向客户端推送数据,而无需客户端先发起请求。一旦建立连接,客户端和服务器就可以像打电话一样,随时向对方发送消息,实现真正的实时双向通信。

Spring Boot 如何简化 WebSocket 开发?

Spring Boot 提供了强大的 spring-boot-starter-websocket 模块,它基于 Spring Framework 的 WebSocket 支持,极大地简化了在 Spring 应用中集成 WebSocket 的过程。它不仅支持原始的 WebSocket API,还集成了 STOMP(Simple Text Oriented Messaging Protocol)协议,使得消息的发布/订阅、点对点通信、用户特定消息等复杂场景变得异常简单。


第一部分:准备工作
  1. 创建 Spring Boot 项目

    使用 Spring Initializr (https://start.spring.io/) 创建一个新的项目。确保添加以下依赖:

    • Spring Web (spring-boot-starter-web)
    • Spring WebSocket (spring-boot-starter-websocket)
    • (可选) Thymeleaf (spring-boot-starter-thymeleaf) - 用于创建简单的 HTML 前端页面进行演示。
    • (可选) Lombok - 简化 Java 代码(如 @Data, @AllArgsConstructor)。

    pom.xml (Maven) 相关依赖示例:

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- 可选:用于模板渲染 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- 可选:简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
  2. 项目结构

    一个典型的结构可能如下:

    复制代码
    src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/websocketdemo/
    │   │       ├── WebSocketConfig.java
    │   │       ├── WebSocketController.java
    │   │       ├── model/
    │   │       │   └── Message.java
    │   │       └── WebSocketDemoApplication.java
    │   └── resources/
    │       ├── static/
    │       │   └── js/
    │       │       └── app.js
    │       └── templates/
    │           └── index.html
    └── test/
        └── ...

第二部分:配置 WebSocket (WebSocketConfig)

这是启用和配置 WebSocket 功能的核心步骤。我们需要创建一个配置类。

java 复制代码
package com.example.websocketdemo;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * WebSocket 配置类
 * @EnableWebSocketMessageBroker 注解启用 STOMP 消息代理功能。
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 配置消息代理(Message Broker)
     * 消息代理负责处理来自客户端的消息,并将消息广播给订阅了特定目的地的客户端。
     *
     * @param config MessageBrokerRegistry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 1. 启用一个简单的内存消息代理,用于处理以 "/topic" 或 "/queue" 开头的消息。
        //    - "/topic" 通常用于**发布/订阅**模式(一对多广播)。
        //    - "/queue" 通常用于**点对点**模式(一对一,但多个订阅者时会负载均衡)。
        config.enableSimpleBroker("/topic", "/queue");

        // 2. 定义应用目的地前缀。
        //    所有以 "/app" 开头的 STOMP 消息都会被路由到带有 @MessageMapping 注解的控制器方法中。
        //    例如:客户端发送到 "/app/hello" 的消息会被 @MessageMapping("/hello") 的方法处理。
        config.setApplicationDestinationPrefixes("/app");

        // (可选) 设置用户目的地前缀 (用于用户特定消息)
        // config.setUserDestinationPrefix("/user");
    }

    /**
     * 注册 STOMP 协议的 WebSocket 端点。
     * 客户端通过这些端点与服务器建立 WebSocket 连接。
     *
     * @param registry StompEndpointRegistry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 1. 注册一个名为 "/ws" 的端点。
        //    客户端将连接到 "ws://<server>:<port>/ws" (HTTP) 或 "wss://<server>:<port>/ws" (HTTPS)。
        registry.addEndpoint("/ws")

                // 2. 启用 SockJS 作为后备机制。
                //    SockJS 是一个 JavaScript 库,它在浏览器不支持原生 WebSocket 时,
                //    会尝试使用其他技术(如轮询)来模拟 WebSocket 行为,提高兼容性。
                //    客户端连接时,如果使用 SockJS,URL 会是 "/ws/sockjs/info" 等。
                .withSockJS();

        // (可选) 可以注册多个端点
        // registry.addEndpoint("/another-endpoint").withSockJS();
    }
}

关键点解析:

  • @EnableWebSocketMessageBroker: 这个注解是开启 Spring WebSocket 支持的关键,它启用了 STOMP 消息代理。
  • configureMessageBroker:
    • enableSimpleBroker(...): 启用一个简单的内存消息代理。对于生产环境,你可能需要集成更强大的消息代理,如 RabbitMQRedis (通过 @EnableStompBrokerRelay 配置),以实现集群部署和消息持久化。
    • setApplicationDestinationPrefixes(...): 定义了应用处理消息的前缀。/app 是约定俗成的前缀。
  • registerStompEndpoints:
    • addEndpoint("/ws"): 定义了 WebSocket 连接的实际路径。
    • .withSockJS(): 强烈建议启用,以确保在老旧浏览器或网络环境下的兼容性。

第三部分:定义消息模型 (Message.java)

创建一个简单的 POJO 类来表示我们要发送和接收的消息。

java 复制代码
package com.example.websocketdemo.model;

import lombok.Data;
import lombok.AllArgsConstructor;

/**
 * 消息实体类
 */
@Data
@AllArgsConstructor
public class Message {
    private String content; // 消息内容
    private String sender;  // 发送者
    private long timestamp; // 时间戳

    // 无参构造函数(JSON 反序列化需要)
    public Message() {}

    // (可选) 可以添加更多字段,如消息类型、接收者等
}

第四部分:创建 WebSocket 控制器 (WebSocketController.java)

这个控制器负责处理来自客户端的消息(通过 @MessageMapping)以及向客户端发送消息(通过 SimpMessagingTemplate)。

java 复制代码
package com.example.websocketdemo;

import com.example.websocketdemo.model.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;

import java.time.Instant;

/**
 * WebSocket 消息处理控制器
 */
@Controller // 使用 @Controller 而不是 @RestController,因为通常不直接返回 HTTP 响应
public class WebSocketController {

    // SimpMessagingTemplate 用于从服务器端任意位置向客户端发送消息
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 处理客户端发送到 "/app/hello" 的消息。
     * 此方法将处理消息,并将处理后的结果广播给所有订阅了 "/topic/greetings" 的客户端。
     *
     * @param message 客户端发送的原始消息 (Message 对象)
     * @return 处理后的消息 (Message 对象) - 这个返回值会被 @SendTo 指定的目的地接收
     * @throws Exception
     */
    @MessageMapping("/hello") // 监听目的地 "/app/hello"
    @SendTo("/topic/greetings") // 将方法返回值发送到 "/topic/greetings"
    public Message greeting(@Payload Message message) throws Exception {
        // 模拟一些处理延迟
        Thread.sleep(1000);

        // 返回一个处理后的消息,包含原内容、发送者和当前时间戳
        return new Message(
                "Hello, " + HtmlUtils.htmlEscape(message.getSender()) + "!",
                "Server",
                Instant.now().toEpochMilli()
        );
    }

    /**
     * 处理客户端发送到 "/app/chat" 的消息。
     * 这个方法展示了如何使用 SimpMessagingTemplate 进行更灵活的消息发送。
     * 它不会返回值给 @SendTo,而是直接使用 messagingTemplate 发送消息。
     *
     * @param message 客户端发送的聊天消息
     */
    @MessageMapping("/chat")
    public void handleChatMessage(@Payload Message message) {
        // 可以在这里进行消息验证、存储到数据库等操作
        // ...

        // 使用 SimpMessagingTemplate 将消息广播给所有订阅了 "/topic/chat" 的客户端
        messagingTemplate.convertAndSend("/topic/chat", message);

        // (示例) 向特定用户发送消息 (需要配置用户目的地前缀)
        // messagingTemplate.convertAndSendToUser("username", "/queue/private", privateMessage);
    }

    /**
     * (可选) 示例:从服务器内部其他地方(如定时任务、服务)触发消息发送
     */
    // @Scheduled(fixedRate = 5000)
    // public void sendServerTime() {
    //     Message timeMessage = new Message("Server Time: " + Instant.now(), "System", Instant.now().toEpochMilli());
    //     messagingTemplate.convertAndSend("/topic/greetings", timeMessage);
    // }
}

关键点解析:

  • @Controller: 标记为控制器。
  • @MessageMapping("/hello"): 将方法映射到 STOMP 消息的目的地 /app/hello。客户端发送到 /app/hello 的消息会触发此方法。
  • @Payload: 明确指定参数是从消息体(Payload)中提取并反序列化为 Message 对象的。
  • @SendTo("/topic/greetings"): 指定该方法的返回值应该发送到 /topic/greetings 这个目的地。所有订阅了此目的地的客户端都会收到此消息。
  • SimpMessagingTemplate: 这是一个强大的工具,允许你在代码的任何地方(而不仅限于 @MessageMapping 方法)发送消息。convertAndSend(destination, payload) 方法会将 payload 对象序列化(通常是 JSON)并发送到指定的 destinationconvertAndSendToUser(user, destination, payload) 用于向特定用户发送消息(需要配置用户目的地前缀和用户识别机制)。

第五部分:创建前端页面 (index.html)

使用 Thymeleaf 创建一个简单的 HTML 页面来测试我们的 WebSocket 功能。

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Spring Boot WebSocket Demo</title>
    <!-- 引入 SockJS 客户端库 (如果配置了 withSockJS) -->
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <!-- 引入 STOMP 客户端库 -->
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@6.1.0/bundles/stomp.umd.min.js"></script>
    <!-- (可选) Bootstrap 用于美化 -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div class="container mt-5">
      <h1>WebSocket Chat & Greeting Demo</h1>

      <div class="row">
        <div class="col-md-6">
          <h3>Send Greeting</h3>
          <form id="greetingForm">
            <div class="mb-3">
              <label for="greetingSender" class="form-label">Your Name:</label>
              <input
                type="text"
                class="form-control"
                id="greetingSender"
                placeholder="Enter your name"
                required
              />
            </div>
            <button type="submit" class="btn btn-primary">Send Greeting</button>
          </form>
        </div>

        <div class="col-md-6">
          <h3>Chat Room</h3>
          <form id="chatForm">
            <div class="mb-3">
              <label for="chatSender" class="form-label">Nickname:</label>
              <input
                type="text"
                class="form-control"
                id="chatSender"
                placeholder="Enter nickname"
                required
              />
            </div>
            <div class="mb-3">
              <label for="chatMessage" class="form-label">Message:</label>
              <textarea
                class="form-control"
                id="chatMessage"
                rows="3"
                placeholder="Type your message..."
                required
              ></textarea>
            </div>
            <button type="submit" class="btn btn-success">Send Message</button>
          </form>
        </div>
      </div>

      <div class="row mt-4">
        <div class="col-md-6">
          <h3>Greetings Received</h3>
          <ul id="greetingList" class="list-group"></ul>
        </div>
        <div class="col-md-6">
          <h3>Chat Messages</h3>
          <ul id="chatList" class="list-group"></ul>
        </div>
      </div>
    </div>

    <!-- 引入自定义 JavaScript -->
    <script th:src="@{/js/app.js}"></script>
  </body>
</html>

第六部分:编写前端 JavaScript (app.js)

这是前端与 WebSocket 交互的核心逻辑。

javascript 复制代码
// 定义全局变量
let stompClient = null
let connected = false

// 页面加载完成后执行
document.addEventListener("DOMContentLoaded", function () {
  connect()
})

// 连接到 WebSocket 服务器
function connect() {
  // 1. 创建 SockJS 实例,连接到后端配置的端点 "/ws"
  //    如果后端没有配置 withSockJS,则使用 new WebSocket("ws://localhost:8080/ws");
  const socket = new SockJS("/ws") // 注意:路径是相对于当前页面的,这里假设在根路径

  // 2. 使用 SockJS 实例创建 STOMP 客户端
  stompClient = Stomp.over(socket)

  // 3. 连接到 STOMP 代理
  stompClient.connect(
    {},
    function (frame) {
      console.log("Connected: " + frame)
      connected = true
      // 更新 UI 状态 (可选)
      // document.getElementById('connectionStatus').innerHTML = 'Connected';

      // 4. 订阅目的地 "/topic/greetings"
      //    当服务器向 "/topic/greetings" 发送消息时,onGreetingReceived 函数会被调用
      stompClient.subscribe("/topic/greetings", onGreetingReceived)

      // 5. 订阅目的地 "/topic/chat"
      stompClient.subscribe("/topic/chat", onChatMessageReceived)
    },
    function (error) {
      console.error("Connection error: " + error)
      connected = false
      // 重连逻辑 (可选)
      // setTimeout(function() { connect(); }, 5000);
    }
  )
}

// 处理从 "/topic/greetings" 接收到的消息
function onGreetingReceived(payload) {
  const message = JSON.parse(payload.body)
  const greetingList = document.getElementById("greetingList")
  const item = document.createElement("li")
  item.textContent = `[${new Date(message.timestamp).toLocaleTimeString()}] ${
    message.sender
  }: ${message.content}`
  item.className = "list-group-item list-group-item-info"
  greetingList.appendChild(item)
  // 自动滚动到底部
  greetingList.scrollTop = greetingList.scrollHeight
}

// 处理从 "/topic/chat" 接收到的消息
function onChatMessageReceived(payload) {
  const message = JSON.parse(payload.body)
  const chatList = document.getElementById("chatList")
  const item = document.createElement("li")
  item.textContent = `[${new Date(message.timestamp).toLocaleTimeString()}] ${
    message.sender
  }: ${message.content}`
  item.className = "list-group-item"
  chatList.appendChild(item)
  chatList.scrollTop = chatList.scrollHeight
}

// 处理 "Send Greeting" 表单提交
document
  .getElementById("greetingForm")
  .addEventListener("submit", function (event) {
    event.preventDefault() // 阻止表单默认提交行为
    const senderInput = document.getElementById("greetingSender")
    const sender = senderInput.value.trim()
    if (sender && connected) {
      // 发送消息到目的地 "/app/hello"
      // 消息体是一个 JSON 字符串
      stompClient.send(
        "/app/hello",
        {},
        JSON.stringify({ sender: sender, content: "Greeting Request" })
      )
      senderInput.value = "" // 清空输入框
    } else if (!connected) {
      alert("WebSocket not connected!")
    }
  })

// 处理 "Send Message" 表单提交
document
  .getElementById("chatForm")
  .addEventListener("submit", function (event) {
    event.preventDefault()
    const senderInput = document.getElementById("chatSender")
    const messageInput = document.getElementById("chatMessage")
    const sender = senderInput.value.trim()
    const content = messageInput.value.trim()
    if (sender && content && connected) {
      // 发送消息到目的地 "/app/chat"
      const chatMessage = {
        sender: sender,
        content: content,
        timestamp: new Date().getTime(), // 客户端时间戳,服务器会用自己的
      }
      stompClient.send("/app/chat", {}, JSON.stringify(chatMessage))
      // 清空输入框
      messageInput.value = ""
      // (可选) 立即将消息显示在本地聊天列表(回显),服务器广播后会再次收到
      // onChatMessageReceived({body: JSON.stringify(chatMessage)});
    } else if (!connected) {
      alert("WebSocket not connected!")
    }
  })

// (可选) 断开连接函数
function disconnect() {
  if (stompClient) {
    stompClient.disconnect()
    connected = false
    console.log("Disconnected")
    // 更新 UI 状态
    // document.getElementById('connectionStatus').innerHTML = 'Disconnected';
  }
}

// 页面卸载时断开连接
window.addEventListener("beforeunload", function () {
  disconnect()
})

关键点解析:

  • SockJS('/ws'): 创建 SockJS 连接,路径必须与后端 WebSocketConfigaddEndpoint("/ws") 一致。
  • Stomp.over(socket): 使用 SockJS 连接创建 STOMP 客户端。
  • stompClient.connect(headers, connectCallback, errorCallback): 连接到 STOMP 代理。headers 通常为空对象 {}
  • stompClient.subscribe(destination, callback): 订阅一个目的地。callback 函数接收一个 payload 参数,其 body 属性是服务器发送的原始消息字符串(通常是 JSON)。
  • stompClient.send(destination, headers, body): 向指定目的地发送消息。body 是消息内容(字符串)。
  • JSON.parse(payload.body): 将接收到的 JSON 字符串解析成 JavaScript 对象。
  • JSON.stringify(object): 将 JavaScript 对象序列化成 JSON 字符串发送。

第七部分:运行与测试
  1. 启动应用: 运行 WebSocketDemoApplication.javamain 方法。
  2. 访问页面: 打开浏览器,访问 http://localhost:8080 (或你配置的端口和路径)。
  3. 观察控制台: 打开浏览器的开发者工具(F12),查看 Network 和 Console 标签页。你应该能看到 SockJS 或 WebSocket 连接建立成功 (CONNECTED 帧)。
  4. 测试功能:
    • 在 "Send Greeting" 区域输入名字并点击 "Send Greeting"。稍等 1 秒,你会在 "Greetings Received" 列表中看到服务器返回的 "Hello, [你的名字]!" 消息。
    • 在 "Chat Room" 区域输入昵称和消息,点击 "Send Message"。消息会立即出现在 "Chat Messages" 列表中(因为服务器广播回所有客户端,包括发送者)。
    • 打开多个浏览器标签页或窗口访问同一个页面。在一个窗口发送消息,其他所有窗口都会实时收到更新!这就是 WebSocket 的魔力。

第八部分:高级主题与最佳实践
  1. 用户认证与授权 (Security):

    • 通常需要将 WebSocket 连接与用户的登录会话关联。可以在 WebSocketConfigregisterStompEndpoints 中添加拦截器,或者在 HttpSessionHandshakeInterceptor 中将用户信息存入 WebSocketSession 的属性。
    • 使用 Spring Security 保护 /ws 端点,确保只有认证用户才能连接。
    • @MessageMapping 方法上使用 @PreAuthorize 进行细粒度权限控制。
    • 使用 messagingTemplate.convertAndSendToUser(username, destination, payload) 向特定用户发送私有消息。需要配置 setUserDestinationPrefix("/user")
  2. 消息代理 (Message Broker):

    • Simple Broker: 适用于单机部署的简单应用。在集群环境下,不同实例间的客户端无法互相通信。

    • STOMP Broker Relay (推荐生产环境): 配置 Spring Boot 应用连接到外部的、功能更强大的 STOMP 消息代理(如 RabbitMQ, ActiveMQ, Redis)。

      java 复制代码
      // 在 WebSocketConfig 中
      @Override
      public void configureMessageBroker(MessageBrokerRegistry config) {
          // 配置应用目的地前缀
          config.setApplicationDestinationPrefixes("/app");
          // 配置用户目的地前缀
          config.setUserDestinationPrefix("/user");
      
          // 启用 STOMP 代理中继,连接到外部的 Broker
          config.enableStompBrokerRelay("/topic", "/queue")
                .setRelayHost("localhost") // 外部 Broker 的主机
                .setRelayPort(61613)      // STOMP 端口 (RabbitMQ 默认 61613)
                .setClientLogin("guest")  // Broker 用户名
                .setClientPasscode("guest"); // Broker 密码
      }
      • 优势: 支持集群、消息持久化、更复杂的消息路由、高可用性。
  3. 异常处理:

    • 可以使用 @ControllerAdvice@MessageExceptionHandler 注解来处理 @MessageMapping 方法中抛出的异常,并向客户端发送错误消息。
    • 处理连接断开 (WebSocketSession 关闭) 的逻辑。
  4. 性能与监控:

    • 监控连接数、消息吞吐量。
    • 考虑消息大小和频率,避免网络拥塞。
    • 对于高并发场景,优化线程池配置。
  5. 前端库选择:

    • @stomp/stompjs 是目前最流行和维护良好的 STOMP 客户端库。
    • sockjs-client 是 SockJS 的官方库。

第九部分:总结

通过本文的详细步骤,我们成功地在 Spring Boot 应用中集成并实现了 WebSocket 功能。我们学习了:

  • 核心概念: WebSocket 协议、STOMP、消息代理、发布/订阅模式。
  • 配置: 使用 @EnableWebSocketMessageBrokerWebSocketMessageBrokerConfigurer 进行配置。
  • 后端开发: 使用 @MessageMapping, @SendTo, SimpMessagingTemplate 处理消息和发送消息。
  • 前端开发: 使用 sockjs-client@stomp/stompjs 库建立连接、订阅、发送消息。
  • 高级主题: 安全、消息代理、异常处理。

Spring Boot 的 WebSocket 支持使得构建实时 Web 应用变得相对简单和高效。掌握这些知识,你就可以为你的应用添加强大的实时交互能力了。

下一步:

  • 尝试集成 Spring Security 进行用户认证。
  • 将简单消息代理替换为 RabbitMQ 或 Redis。
  • 实现更复杂的聊天功能,如群组、在线状态、消息历史记录。
  • 探索 WebSocket 在游戏、协作工具等领域的应用。

参考资料:

希望这篇详尽的指南能帮助你顺利在 Spring Boot 项目中应用 WebSocket!

相关推荐
小码哥_常28 分钟前
别再被误导!try...catch性能大揭秘
后端
苍何3 小时前
30分钟用 Agent 搓出一家跨境网店,疯了
后端
ssshooter3 小时前
Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑
前端·后端·ios
追逐时光者3 小时前
一个基于 .NET Core + Vue3 构建的开源全栈平台 Admin 系统
后端·.net
程序员飞哥3 小时前
90后大龄程序员失业4个月终于上岸了
后端·面试·程序员
彭于晏Yan5 小时前
Redisson分布式锁
spring boot·redis·分布式
GetcharZp5 小时前
Git 命令行太痛苦?这款 75k Star 的神级工具,让你告别“合并冲突”恐惧症!
后端
Victor3566 小时前
MongoDB(69)如何进行增量备份?
后端
Victor3566 小时前
MongoDB(70)如何使用副本集进行备份?
后端
千寻girling6 小时前
面试官 : “ 说一下 Python 中的常用的 字符串和数组 的 方法有哪些 ? ”
人工智能·后端·python