【SpringBoot 3.x 第29节】还在用轮询实现即时通讯?WebSocket能否彻底颠覆你的推送方案?

🏆本文收录于《滚雪球学Spring Boot》,专门攻坚指数提升,2025 年国内最系统+最强(更新中)。
  
本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

🌟 前言:从轮询地狱到实时天堂的救赎之路

  各位亲爱的程序猿们!😭 还记得那些年我们为了实现一个"实时"聊天功能,疯狂使用Ajax轮询的黑暗日子吗?每隔几秒就向服务器发个请求问:"有新消息吗?有新消息吗?"服务器被问得不胜其烦,带宽被浪费得一塌糊涂,用户体验差得让人想砸手机!😤

  但是!WebSocket的出现就像是黑暗中的一束光,彻底改变了我们对实时通信的认知!🌟 今天,就让我这个在实时推送领域摸爬滚打了N年的老码农,带大家深入探索WebSocket的神奇世界!相信我,学会了WebSocket,你就掌握了现代Web应用的核心竞争力!💪

🎯 目录导航

  • 🤔 WebSocket到底解决了什么痛点?
  • ⚡ Spring Boot中的WebSocket实现深度剖析
  • 🛠️ STOMP协议:让WebSocket更加优雅
  • 💻 实战案例:从零搭建实时聊天系统
  • 🔥 集群部署:Redis+WebSocket的完美配合
  • 📊 监控与调试:让你的WebSocket应用稳如老狗
  • 💡 性能优化:千万级连接的终极秘籍
  • 🎊 总结:实时推送的未来趋势

🤔 WebSocket到底解决了什么痛点?

  说起WebSocket之前的时代,那真是一把辛酸泪啊!😢 想要实现实时功能,我们只能用各种奇葩的方案来"模拟"实时效果。

📡 传统方案的各种坑

1. 短轮询:服务器被烦死 🔄

// 😰 每秒发一次请求,服务器:我太难了!
setInterval(function() {
    $.get('/api/messages/new', function(data) {
        if (data.length > 0) {
            updateMessageList(data);
        }
    });
}, 1000); // 每秒钟问一次:有新消息吗?

2. 长轮询:连接超时头疼

// 服务器端要hold住连接,各种超时问题
@GetMapping("/messages/longpoll")
public DeferredResult<List<Message>> longPoll() {
    DeferredResult<List<Message>> result = new DeferredResult<>(30000L);
    // 30秒内如果没有新消息,就超时返回
    // 各种边界情况处理,头都大了!😵‍💫
    return result;
}

3. SSE:单向推送的局限 📤

// 只能服务器向客户端推送,客户端想发消息?另想办法!
@GetMapping(value = "/events", produces = "text/event-stream")
public SseEmitter streamEvents() {
    SseEmitter emitter = new SseEmitter();
    // 只能单向推送,想要双向通信?做梦!😅
    return emitter;
}

🚀 WebSocket的革命性变化

  WebSocket的出现彻底改变了游戏规则!它提供了真正的全双工通信,就像是在客户端和服务器之间建立了一条专属的"热线电话"!📞

WebSocket的核心优势:

  • 真正的双向通信 🔄 - 客户端和服务器都可以主动发消息
  • 低延迟 ⚡ - 没有HTTP请求头的冗余,数据传输更高效
  • 持久连接 🔗 - 一次握手,长期使用
  • 更少的服务器资源消耗 💚 - 不用频繁建立连接
// 🎉 WebSocket让实时通信变得如此简单!
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
    return chatMessage; // 就这么简单,消息瞬间推送给所有在线用户!
}

⚡ Spring Boot中的WebSocket实现深度剖析

  Spring Boot对WebSocket的支持那叫一个贴心!只需要几个注解,就能快速搭建一个WebSocket应用。但要想玩得溜,还得深入理解其中的奥秘!🔍

🎪 基础配置:让WebSocket跑起来

// 📝 WebSocket配置类 - 一切的开始
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new ChatWebSocketHandler(), "/websocket/chat")
                .setAllowedOrigins("*") // 跨域处理
                .withSockJS(); // SockJS支持,兜底方案
    }
}

// 🎭 自定义WebSocket处理器
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
    
    // 存储所有活跃连接 - 这就是我们的"用户在线列表"
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String userId = getUserId(session); // 从session中获取用户ID
        sessions.put(userId, session);
        log.info("用户{}连接成功,当前在线用户数:{}", userId, sessions.size()); // 😊 新用户上线
        
        // 向所有用户广播有新用户加入
        broadcastMessage(new ChatMessage("SYSTEM", userId + "加入了聊天室", MessageType.JOIN));
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String userId = getUserId(session);
        String content = message.getPayload();
        
        log.info("收到用户{}的消息:{}", userId, content);
        
        // 解析消息并广播
        ChatMessage chatMessage = parseMessage(content);
        chatMessage.setSender(userId);
        chatMessage.setTimestamp(System.currentTimeMillis());
        
        broadcastMessage(chatMessage); // 📢 消息瞬间传播给所有在线用户
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = getUserId(session);
        sessions.remove(userId);
        log.info("用户{}断开连接,当前在线用户数:{}", userId, sessions.size()); // 😢 用户离线
        
        // 广播用户离开消息
        broadcastMessage(new ChatMessage("SYSTEM", userId + "离开了聊天室", MessageType.LEAVE));
    }
    
    // 🎯 核心方法:消息广播
    private void broadcastMessage(ChatMessage message) {
        String messageJson = JsonUtils.toJson(message);
        
        sessions.values().parallelStream().forEach(session -> {
            try {
                if (session.isOpen()) {
                    session.sendMessage(new TextMessage(messageJson));
                }
            } catch (Exception e) {
                log.error("发送消息失败", e);
                // 连接异常,从列表中移除
                sessions.entrySet().removeIf(entry -> entry.getValue().equals(session));
            }
        });
        
        log.info("消息已广播给{}个在线用户", sessions.size());
    }
}

🎨 进阶功能:房间管理和私聊

  基础的群聊太简单了,让我们来点高级的!实现聊天室分组和私聊功能!🏠

// 🏰 聊天室管理器
@Service
public class ChatRoomManager {
    
    // 房间 -> 用户会话映射
    private final Map<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
    // 用户 -> 房间映射
    private final Map<String, String> userRooms = new ConcurrentHashMap<>();
    
    // 🚪 用户加入房间
    public void joinRoom(String userId, String roomId, WebSocketSession session) {
        // 先退出原房间
        leaveCurrentRoom(userId);
        
        roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session);
        userRooms.put(userId, roomId);
        
        log.info("用户{}加入房间{},房间内用户数:{}", userId, roomId, roomSessions.get(roomId).size());
        
        // 通知房间内其他用户
        broadcastToRoom(roomId, new ChatMessage("SYSTEM", 
            userId + "加入了房间", MessageType.JOIN), session);
    }
    
    // 📤 房间内消息广播
    public void broadcastToRoom(String roomId, ChatMessage message, WebSocketSession excludeSession) {
        Set<WebSocketSession> sessions = roomSessions.get(roomId);
        if (sessions == null || sessions.isEmpty()) return;
        
        String messageJson = JsonUtils.toJson(message);
        
        sessions.parallelStream()
            .filter(session -> !session.equals(excludeSession) && session.isOpen())
            .forEach(session -> {
                try {
                    session.sendMessage(new TextMessage(messageJson));
                } catch (Exception e) {
                    log.error("发送房间消息失败", e);
                    sessions.remove(session); // 清理无效连接
                }
            });
    }
    
    // 💌 私聊功能
    public void sendPrivateMessage(String fromUserId, String toUserId, String content) {
        WebSocketSession targetSession = findUserSession(toUserId);
        if (targetSession != null && targetSession.isOpen()) {
            try {
                ChatMessage privateMessage = new ChatMessage(fromUserId, content, MessageType.PRIVATE);
                targetSession.sendMessage(new TextMessage(JsonUtils.toJson(privateMessage)));
                log.info("私聊消息已发送:{} -> {}", fromUserId, toUserId);
            } catch (Exception e) {
                log.error("发送私聊消息失败", e);
            }
        } else {
            log.warn("目标用户{}不在线,私聊消息发送失败", toUserId);
        }
    }
}

🛠️ STOMP协议:让WebSocket更加优雅

  原生WebSocket虽然强大,但用起来还是有点底层。STOMP(Simple Text Oriented Messaging Protocol)就像是WebSocket的高级封装,让消息传递变得更加优雅!✨

🎪 STOMP配置与使用

// 🚀 STOMP配置 - 比原生WebSocket优雅多了
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue"); // 简单消息代理
        config.setApplicationDestinationPrefixes("/app"); // 应用目的地前缀
        config.setUserDestinationPrefix("/user"); // 用户专属前缀
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS(); // SockJS支持
    }
    
    // 🎯 自定义拦截器 - 身份验证和连接管理
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    // 连接时的身份验证
                    String token = accessor.getFirstNativeHeader("Authorization");
                    if (isValidToken(token)) {
                        String userId = extractUserId(token);
                        accessor.setUser(() -> userId); // 设置用户标识
                        log.info("用户{}通过WebSocket认证", userId); // ✅ 认证通过
                    } else {
                        log.warn("WebSocket认证失败"); // ❌ 认证失败
                        return null; // 阻止连接
                    }
                }
                
                return message;
            }
        });
    }
}

// 🎭 STOMP消息控制器
@Controller
public class WebSocketStompController {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @Autowired
    private ChatService chatService;
    
    // 📢 群聊消息处理
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage, 
                                  SimpMessageHeaderAccessor headerAccessor) {
        String userId = headerAccessor.getUser().getName();
        chatMessage.setSender(userId);
        chatMessage.setTimestamp(System.currentTimeMillis());
        
        // 保存消息到数据库
        chatService.saveMessage(chatMessage);
        
        log.info("群聊消息:{} -> {}", userId, chatMessage.getContent());
        return chatMessage; // 🚀 自动广播给所有订阅者
    }
    
    // 🏠 房间消息处理
    @MessageMapping("/chat.sendToRoom")
    public void sendToRoom(@Payload RoomMessage message, 
                          SimpMessageHeaderAccessor headerAccessor) {
        String userId = headerAccessor.getUser().getName();
        message.setSender(userId);
        
        // 发送到指定房间
        messagingTemplate.convertAndSend("/topic/room/" + message.getRoomId(), message);
        
        log.info("房间{}消息:{} -> {}", message.getRoomId(), userId, message.getContent());
    }
    
    // 💌 私聊消息处理
    @MessageMapping("/chat.sendPrivate")
    public void sendPrivateMessage(@Payload PrivateMessage message,
                                  SimpMessageHeaderAccessor headerAccessor) {
        String fromUserId = headerAccessor.getUser().getName();
        String toUserId = message.getToUserId();
        
        message.setFromUserId(fromUserId);
        message.setTimestamp(System.currentTimeMillis());
        
        // 发送给指定用户
        messagingTemplate.convertAndSendToUser(toUserId, "/queue/private", message);
        
        // 也给发送者一份(确认消息已发送)
        messagingTemplate.convertAndSendToUser(fromUserId, "/queue/private", message);
        
        log.info("私聊消息:{} -> {} : {}", fromUserId, toUserId, message.getContent());
    }
    
    // 📊 在线用户列表
    @MessageMapping("/chat.getOnlineUsers")
    @SendTo("/topic/onlineUsers")
    public List<OnlineUser> getOnlineUsers() {
        return chatService.getOnlineUsers(); // 返回当前在线用户列表
    }
}

🎨 前端JavaScript集成

// 🌐 前端WebSocket客户端实现
class ChatClient {
    constructor(userId, token) {
        this.userId = userId;
        this.token = token;
        this.stompClient = null;
        this.connected = false;
    }
    
    // 🔌 连接WebSocket
    connect() {
        const socket = new SockJS('/ws');
        this.stompClient = Stomp.over(socket);
        
        // 设置认证头
        const headers = {
            'Authorization': this.token
        };
        
        this.stompClient.connect(headers, 
            (frame) => {
                console.log('WebSocket连接成功: ' + frame);
                this.connected = true;
                this.onConnected(); // 🎉 连接成功回调
            },
            (error) => {
                console.error('WebSocket连接失败: ', error);
                this.onError(error); // 😰 连接失败处理
                
                // 5秒后重连
                setTimeout(() => this.connect(), 5000);
            }
        );
    }
    
    // ✅ 连接成功后的初始化
    onConnected() {
        // 订阅群聊消息
        this.stompClient.subscribe('/topic/public', (message) => {
            const chatMessage = JSON.parse(message.body);
            this.displayMessage(chatMessage); // 🎨 显示群聊消息
        });
        
        // 订阅私聊消息
        this.stompClient.subscribe('/user/queue/private', (message) => {
            const privateMessage = JSON.parse(message.body);
            this.displayPrivateMessage(privateMessage); // 💌 显示私聊消息
        });
        
        // 订阅在线用户列表更新
        this.stompClient.subscribe('/topic/onlineUsers', (message) => {
            const onlineUsers = JSON.parse(message.body);
            this.updateOnlineUsersList(onlineUsers); // 👥 更新在线用户
        });
        
        // 通知用户加入
        this.sendMessage({
            type: 'JOIN',
            content: this.userId + '加入了聊天室'
        });
    }
    
    // 📤 发送群聊消息
    sendMessage(message) {
        if (this.connected) {
            this.stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(message));
        } else {
            console.warn('WebSocket未连接,消息发送失败'); // ⚠️ 连接检查
        }
    }
    
    // 💌 发送私聊消息
    sendPrivateMessage(toUserId, content) {
        const privateMessage = {
            toUserId: toUserId,
            content: content,
            type: 'PRIVATE'
        };
        
        this.stompClient.send('/app/chat.sendPrivate', {}, JSON.stringify(privateMessage));
    }
    
    // 🔌 断开连接
    disconnect() {
        if (this.stompClient !== null) {
            this.stompClient.disconnect();
            this.connected = false;
            console.log('WebSocket已断开'); // 👋 再见
        }
    }
}

// 🎪 使用示例
const chatClient = new ChatClient('user123', 'jwt-token-here');
chatClient.connect();

// 发送消息
document.getElementById('sendBtn').addEventListener('click', function() {
    const messageContent = document.getElementById('messageInput').value;
    chatClient.sendMessage({
        content: messageContent,
        type: 'CHAT'
    });
});

💻 实战案例:从零搭建实时聊天系统

  光说不练假把式!让我们来搭建一个真正能用的实时聊天系统,包含用户认证、消息持久化、文件传输等实用功能!🛠️

🗄️ 数据库设计

-- 👤 用户表
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    nickname VARCHAR(100),
    avatar_url VARCHAR(255),
    status ENUM('ONLINE', 'OFFLINE', 'AWAY') DEFAULT 'OFFLINE',
    last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 🏠 聊天室表
CREATE TABLE chat_rooms (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    owner_id BIGINT,
    type ENUM('PUBLIC', 'PRIVATE') DEFAULT 'PUBLIC',
    max_members INT DEFAULT 100,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (owner_id) REFERENCES users(id)
);

-- 📝 消息表
CREATE TABLE messages (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_id BIGINT,
    sender_id BIGINT,
    receiver_id BIGINT, -- 私聊时使用
    content TEXT NOT NULL,
    message_type ENUM('TEXT', 'IMAGE', 'FILE', 'SYSTEM') DEFAULT 'TEXT',
    file_url VARCHAR(255), -- 文件消息的URL
    reply_to BIGINT, -- 回复消息ID
    is_deleted BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (room_id) REFERENCES chat_rooms(id),
    FOREIGN KEY (sender_id) REFERENCES users(id),
    FOREIGN KEY (receiver_id) REFERENCES users(id),
    FOREIGN KEY (reply_to) REFERENCES messages(id)
);

-- 👥 房间成员表
CREATE TABLE room_members (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_id BIGINT,
    user_id BIGINT,
    role ENUM('OWNER', 'ADMIN', 'MEMBER') DEFAULT 'MEMBER',
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (room_id) REFERENCES chat_rooms(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    UNIQUE KEY unique_room_user (room_id, user_id)
);

🎯 核心业务服务实现

// 💎 聊天服务核心实现
@Service
@Transactional
public class ChatService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private MessageRepository messageRepository;
    
    @Autowired
    private ChatRoomRepository chatRoomRepository;
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 💾 保存消息
    public Message saveMessage(ChatMessage chatMessage) {
        Message message = new Message();
        message.setContent(chatMessage.getContent());
        message.setMessageType(MessageType.valueOf(chatMessage.getType()));
        message.setSenderId(chatMessage.getSenderId());
        message.setRoomId(chatMessage.getRoomId());
        message.setCreatedAt(LocalDateTime.now());
        
        Message savedMessage = messageRepository.save(message);
        log.info("消息已保存到数据库,ID:{}", savedMessage.getId()); // 💾 持久化成功
        
        return savedMessage;
    }
    
    // 📜 获取历史消息
    public List<Message> getHistoryMessages(Long roomId, int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        
        // 🚀 先从Redis缓存获取
        String cacheKey = "chat:history:" + roomId + ":" + page;
        List<Message> cachedMessages = (List<Message>) redisTemplate.opsForValue().get(cacheKey);
        
        if (cachedMessages != null) {
            log.info("从缓存获取历史消息,房间ID:{},页码:{}", roomId, page);
            return cachedMessages;
        }
        
        // 缓存未命中,查询数据库
        List<Message> messages = messageRepository.findByRoomIdAndIsDeletedFalse(roomId, pageable);
        
        // 缓存结果,5分钟过期
        redisTemplate.opsForValue().set(cacheKey, messages, Duration.ofMinutes(5));
        
        log.info("从数据库获取历史消息,房间ID:{},消息数:{}", roomId, messages.size());
        return messages;
    }
    
    // 📁 文件消息处理
    public void handleFileMessage(MultipartFile file, Long roomId, String senderId) {
        try {
            // 🔄 文件上传到OSS(这里简化为本地存储)
            String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
            String filePath = "/uploads/" + fileName;
            
            // 保存文件
            Path targetPath = Paths.get("uploads", fileName);
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
            
            // 创建文件消息
            ChatMessage fileMessage = new ChatMessage();
            fileMessage.setSenderId(Long.parseLong(senderId));
            fileMessage.setRoomId(roomId);
            fileMessage.setType("FILE");
            fileMessage.setContent(file.getOriginalFilename()); // 显示原文件名
            fileMessage.setFileUrl(filePath);
            fileMessage.setFileSize(file.getSize());
            
            // 保存到数据库
            saveMessage(fileMessage);
            
            // 🚀 推送给房间内所有用户
            messagingTemplate.convertAndSend("/topic/room/" + roomId, fileMessage);
            
            log.info("文件消息处理完成:{}", fileName); // 📁 文件上传成功
            
        } catch (Exception e) {
            log.error("文件消息处理失败", e);
            throw new RuntimeException("文件上传失败"); // 💥 上传失败
        }
    }
    
    // 👥 获取在线用户列表
    public List<OnlineUser> getOnlineUsers() {
        Set<String> onlineUserIds = redisTemplate.opsForSet().members("online:users");
        
        return onlineUserIds.stream()
            .map(userId -> {
                User user = userRepository.findById(Long.parseLong(userId)).orElse(null);
                if (user != null) {
                    return new OnlineUser(user.getId(), user.getNickname(), user.getAvatarUrl());
                }
                return null;
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
    
    // 🔔 消息提醒处理
    public void handleMessageNotification(Message message) {
        // 获取接收者列表
        List<Long> receiverIds = getMessageReceivers(message);
        
        for (Long receiverId : receiverIds) {
            // 检查用户是否在线
            if (isUserOnline(receiverId)) {
                // 在线用户直接推送
                messagingTemplate.convertAndSendToUser(
                    receiverId.toString(), 
                    "/queue/notification",
                    new MessageNotification(message)
                );
            } else {
                // 离线用户存储未读消息计数
                String unreadKey = "unread:" + receiverId;
                redisTemplate.opsForValue().increment(unreadKey);
                
                // 可以在这里集成推送服务,发送APP推送或短信
                // pushNotificationService.sendPush(receiverId, message);
            }
        }
    }
}

🔥 集群部署:Redis+WebSocket的完美配合

  单机的WebSocket玩起来很爽,但是当你需要集群部署的时候就会发现问题了:用户A连接到服务器1,用户B连接到服务器2,他们怎么聊天?😅 这时候就需要Redis来做消息中转了!

🎪 Redis消息中转实现

// 🚀 基于Redis的集群WebSocket解决方案
@Service
public class ClusterWebSocketService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private SimpMessagingTemplate localMessagingTemplate;
    
    private final String SERVER_ID = UUID.randomUUID().toString(); // 服务器唯一标识
    
    // 🌐 跨服务器消息广播
    public void broadcastMessage(String channel, Object message) {
        // 添加服务器标识,避免重复处理
        RedisMessage redisMessage = new RedisMessage();
        redisMessage.setServerId(SERVER_ID);
        redisMessage.setChannel(channel);
        redisMessage.setPayload(message);
        redisMessage.setTimestamp(System.currentTimeMillis());
        
        // 发布到Redis
        redisTemplate.convertAndSend("websocket:" + channel, redisMessage);
        
        log.info("消息已广播到Redis频道:{}", channel); // 📡 消息广播
    }
    
    // 📨 处理来自Redis的消息
    @EventListener
    public void handleRedisMessage(RedisMessage redisMessage) {
        // 忽略自己发送的消息,避免循环
        if (SERVER_ID.equals(redisMessage.getServerId())) {
            return;
        }
        
        String channel = redisMessage.getChannel();
        Object payload = redisMessage.getPayload();
        
        // 根据频道类型处理消息
        if (channel.startsWith("room:")) {
            // 房间消息
            localMessagingTemplate.convertAndSend("/topic/" + channel, payload);
        } else if (channel.startsWith("user:")) {
            // 私聊消息
            String userId = channel.substring(5); // 去掉"user:"前缀
            localMessagingTemplate.convertAndSendToUser(userId, "/queue/private", payload);
        } else if (channel.equals("global")) {
            // 全局广播
            localMessagingTemplate.convertAndSend("/topic/public", payload);
        }
        
        log.info("处理Redis消息:{} -> {}", channel, payload); // 🔄 消息处理
    }
    
    // 👥 用户上线处理
    public void userOnline(String userId, String serverId) {
        // 记录用户在哪台服务器上
        redisTemplate.opsForHash().put("user:server:mapping", userId, serverId);
        // 加入在线用户集合
        redisTemplate.opsForSet().add("online:users", userId);
        // 设置过期时间,防止服务器异常关闭时用户状态不更新
        redisTemplate.expire("user:server:mapping", Duration.ofHours(1));
        
        log.info("用户{}在服务器{}上线", userId, serverId); // 👋 用户上线
    }
    
    // 👤 用户下线处理
    public void userOffline(String userId) {
        redisTemplate.opsForHash().delete("user:server:mapping", userId);
        redisTemplate.opsForSet().remove("online:users", userId);
        
        log.info("用户{}下线", userId); // 👋 用户下线
    }
    
    // 🎯 定向发送消息给指定用户
    public void sendToUser(String userId, Object message) {
        // 查找用户所在的服务器
        String targetServerId = (String) redisTemplate.opsForHash().get("user:server:mapping", userId);
        
        if (targetServerId == null) {
            log.warn("用户{}不在线,消息发送失败", userId); // ⚠️ 用户离线
            return;
        }
        
        if (SERVER_ID.equals(targetServerId)) {
            // 用户在当前服务器,直接发送
            localMessagingTemplate.convertAndSendToUser(userId, "/queue/private", message);
        } else {
            // 用户在其他服务器,通过Redis转发
            broadcastMessage("user:" + userId, message);
        }
    }
}

// 🎧 Redis消息监听器配置
@Configuration
public class RedisMessageListenerConfig {
    
    @Autowired
    private ClusterWebSocketService clusterWebSocketService;
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        
        // 监听WebSocket相关频道
        container.addMessageListener(new MessageListenerAdapter(clusterWebSocketService, "handleRedisMessage"), 
            new ChannelTopic("websocket:*"));
        
        return container;
    }
}

📊 连接状态管理与监控

// 📈 WebSocket连接监控服务
@Service
public class WebSocketMonitorService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private final MeterRegistry meterRegistry;
    
    // 当前服务器连接数
    private final AtomicInteger localConnections = new AtomicInteger(0);
    
    public WebSocketMonitorService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        // 注册监控指标
        Gauge.builder("websocket.connections.local")
            .description("本地WebSocket连接数")
            .register(meterRegistry, localConnections, AtomicInteger::get);
    }
    
    // 📊 连接统计
    public void onConnectionEstablished(String userId, String sessionId) {
        localConnections.incrementAndGet();
        
        // Redis中记录连接信息
        String connectionKey = "websocket:connection:" + sessionId;
        ConnectionInfo info = new ConnectionInfo(userId, sessionId, 
            InetAddress.getLocalHost().getHostName(), System.currentTimeMillis());
        
        redisTemplate.opsForValue().set(connectionKey, info, Duration.ofHours(2));
        
        // 更新全局统计
        redisTemplate.opsForValue().increment("websocket:total:connections");
        
        log.info("WebSocket连接建立 - 用户:{},会话:{},当前本地连接数:{}", 
            userId, sessionId, localConnections.get()); // 📈 连接统计
    }
    
    public void onConnectionClosed(String userId, String sessionId) {
        localConnections.decrementAndGet();
        
        // 清理连接信息
        redisTemplate.delete("websocket:connection:" + sessionId);
        redisTemplate.opsForValue().decrement("websocket:total:connections");
        
        log.info("WebSocket连接关闭 - 用户:{},会话:{},当前本地连接数:{}", 
            userId, sessionId, localConnections.get()); // 📉 连接关闭
    }
    
    // 🔍 获取全局连接统计
    public ConnectionStats getGlobalStats() {
        Long totalConnections = (Long) redisTemplate.opsForValue().get("websocket:total:connections");
        Set<String> onlineUsers = redisTemplate.opsForSet().members("online:users");
        
        return new ConnectionStats(
            totalConnections != null ? totalConnections : 0,
            onlineUsers != null ? onlineUsers.size() : 0,
            localConnections.get()
        );
    }
    
    // 🧹 定时清理过期连接
    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
    public void cleanupExpiredConnections() {
        Set<String> connectionKeys = redisTemplate.keys("websocket:connection:*");
        int cleanedCount = 0;
        
        for (String key : connectionKeys) {
            ConnectionInfo info = (ConnectionInfo) redisTemplate.opsForValue().get(key);
            if (info != null) {
                // 检查连接是否超过2小时无活动
                if (System.currentTimeMillis() - info.getLastActivity() > 7200000) {
                    redisTemplate.delete(key);
                    cleanedCount++;
                }
            }
        }
        
        if (cleanedCount > 0) {
            log.info("清理了{}个过期WebSocket连接", cleanedCount); // 🧹 清理完成
        }
    }
}

📊 监控与调试:让你的WebSocket应用稳如老狗

  WebSocket应用一旦上线,监控就变得至关重要!你需要知道有多少用户在线,消息传输是否正常,哪里可能出现瓶颈。让我们来搭建一套完整的监控体系!📈

🎪 自定义监控端点

// 📊 WebSocket监控端点
@RestController
@RequestMapping("/api/websocket/monitor")
public class WebSocketMonitorController {
    
    @Autowired
    private WebSocketMonitorService monitorService;
    
    @Autowired
    private ChatService chatService;
    
    // 📈 获取实时统计
    @GetMapping("/stats")
    public ResponseEntity<Map<String, Object>> getStats() {
        ConnectionStats stats = monitorService.getGlobalStats();
        List<OnlineUser> onlineUsers = chatService.getOnlineUsers();
        
        Map<String, Object> result = new HashMap<>();
        result.put("totalConnections", stats.getTotalConnections());
        result.put("onlineUsers", stats.getOnlineUsers());
        result.put("localConnections", stats.getLocalConnections());
        result.put("onlineUserList", onlineUsers);
        result.put("timestamp", System.currentTimeMillis());
        
        return ResponseEntity.ok(result);
    }
    
    // 🔍 获取连接详情
    @GetMapping("/connections")
    public ResponseEntity<List<ConnectionInfo>> getConnections() {
        List<ConnectionInfo> connections = monitorService.getAllConnections();
        return ResponseEntity.ok(connections);
    }
    
    // 📊 获取消息统计
    @GetMapping("/message-stats")
    public ResponseEntity<MessageStats> getMessageStats(
            @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
        
        MessageStats stats = chatService.getMessageStatsForDate(date);
        return ResponseEntity.ok(stats);
    }
    
    // 🎯 测试消息推送
    @PostMapping("/test-broadcast")
    public ResponseEntity<String> testBroadcast(@RequestParam String message) {
        ChatMessage testMessage = new ChatMessage("SYSTEM", message, MessageType.SYSTEM);
        
        // 广播测试消息
        messagingTemplate.convertAndSend("/topic/public", testMessage);
        
        return ResponseEntity.ok("测试消息已发送:" + message);
    }
    
    // 🔧 健康检查
    @GetMapping("/health")
    public ResponseEntity<Map<String, String>> healthCheck() {
        Map<String, String> health = new HashMap<>();
        
        try {
            // 检查Redis连接
            redisTemplate.opsForValue().get("health:check");
            health.put("redis", "UP");
        } catch (Exception e) {
            health.put("redis", "DOWN");
        }
        
        // 检查数据库连接
        try {
            chatService.healthCheck();
            health.put("database", "UP");
        } catch (Exception e) {
            health.put("database", "DOWN");
        }
        
        health.put("websocket", "UP");
        health.put("timestamp", LocalDateTime.now().toString());
        
        return ResponseEntity.ok(health);
    }
}

🎨 前端监控界面

// 📊 WebSocket监控前端界面
class WebSocketMonitor {
    constructor() {
        this.chartInstance = null;
        this.statsInterval = null;
        this.initChart();
        this.startMonitoring();
    }
    
    // 📈 初始化图表
    initChart() {
        const ctx = document.getElementById('connectionChart').getContext('2d');
        this.chartInstance = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '在线连接数',
                    data: [],
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    tension: 0.1
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    title: {
                        display: true,
                        text: 'WebSocket连接实时监控'
                    }
                },
                scales: {
                    y: {
                        beginAtZero: true
                    }
                }
            }
        });
    }
    
    // 🔄 开始监控
    startMonitoring() {
        this.statsInterval = setInterval(() => {
            this.fetchStats();
        }, 5000); // 每5秒更新一次
        
        // 立即执行一次
        this.fetchStats();
    }
    
    // 📊 获取统计数据
    async fetchStats() {
        try {
            const response = await fetch('/api/websocket/monitor/stats');
            const stats = await response.json();
            
            // 更新界面
            this.updateUI(stats);
            
            // 更新图表
            this.updateChart(stats);
            
        } catch (error) {
            console.error('获取监控数据失败:', error);
            this.showError('监控数据获取失败'); // ❌ 获取失败
        }
    }
    
    // 🎨 更新界面
    updateUI(stats) {
        document.getElementById('totalConnections').textContent = stats.totalConnections;
        document.getElementById('onlineUsers').textContent = stats.onlineUsers;
        document.getElementById('localConnections').textContent = stats.localConnections;
        
        // 更新在线用户列表
        const userList = document.getElementById('onlineUsersList');
        userList.innerHTML = '';
        
        stats.onlineUserList.forEach(user => {
            const userElement = document.createElement('div');
            userElement.className = 'online-user';
            userElement.innerHTML = `
                <img src="${user.avatarUrl}" alt="${user.nickname}" class="avatar">
                <span>${user.nickname}</span>
                <span class="status online">在线</span>
            `;
            userList.appendChild(userElement);
        });
    }
    
    // 📈 更新图表
    updateChart(stats) {
        const now = new Date().toLocaleTimeString();
        
        // 保持最近50个数据点
        if (this.chartInstance.data.labels.length >= 50) {
            this.chartInstance.data.labels.shift();
            this.chartInstance.data.datasets[0].data.shift();
        }
        
        this.chartInstance.data.labels.push(now);
        this.chartInstance.data.datasets[0].data.push(stats.totalConnections);
        
        this.chartInstance.update('none'); // 无动画更新,提高性能
    }
    
    // 🧪 测试功能
    async testBroadcast() {
        const message = prompt('请输入测试消息:');
        if (message) {
            try {
                const response = await fetch('/api/websocket/monitor/test-broadcast', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: `message=${encodeURIComponent(message)}`
                });
                
                if (response.ok) {
                    this.showSuccess('测试消息发送成功'); // ✅ 发送成功
                } else {
                    this.showError('测试消息发送失败'); // ❌ 发送失败
                }
            } catch (error) {
                this.showError('网络错误: ' + error.message); // 🌐 网络错误
            }
        }
    }
    
    // ⏹️ 停止监控
    stopMonitoring() {
        if (this.statsInterval) {
            clearInterval(this.statsInterval);
            this.statsInterval = null;
        }
    }
}

// 🚀 启动监控
const monitor = new WebSocketMonitor();

// 页面卸载时停止监控
window.addEventListener('beforeunload', () => {
    monitor.stopMonitoring();
});

💡 性能优化:千万级连接的终极秘籍

  当你的WebSocket应用需要支持百万甚至千万级连接时,就需要考虑更多的优化策略了!这里分享一些我在大规模部署中积累的经验!🚀

🎪 连接池与资源优化

// ⚡ 高性能WebSocket配置
@Configuration
public class HighPerformanceWebSocketConfig {
    
    // 🎯 自定义线程池配置
    @Bean("websocketTaskExecutor")
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 核心线程数 = CPU核数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 最大线程数 = CPU核数 * 2
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 队列容量
        executor.setQueueCapacity(1000);
        // 线程名前缀
        executor.setThreadNamePrefix("websocket-");
        // 拒绝策略:调用者运行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待任务完成后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        
        executor.initialize();
        return executor;
    }
    
    // 🔧 WebSocket传输配置优化
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        
        // 设置最大文本消息大小:1MB
        container.setMaxTextMessageBufferSize(1024 * 1024);
        // 设置最大二进制消息大小:10MB
        container.setMaxBinaryMessageBufferSize(10 * 1024 * 1024);
        // 设置会话空闲超时:30分钟
        container.setMaxSessionIdleTimeout(30 * 60 * 1000L);
        // 设置异步发送超时:30秒
        container.setAsyncSendTimeout(30 * 1000L);
        
        return container;
    }
}

// 🚀 高性能消息处理器
@Component
public class HighPerformanceMessageHandler extends TextWebSocketHandler {
    
    // 使用ConcurrentHashMap提高并发性能
    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    
    // 消息队列,避免阻塞
    private final BlockingQueue<MessageTask> messageQueue = new LinkedBlockingQueue<>(10000);
    
    // 异步消息处理线程池
    private final ExecutorService messageProcessor = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors() * 2,
        new ThreadFactoryBuilder().setNameFormat("msg-processor-%d").build()
    );
    
    @PostConstruct
    public void init() {
        // 启动消息处理线程
        for (int i = 0; i < Runtime.getRuntime().availableProcessors(); i++) {
            messageProcessor.submit(this::processMessages);
        }
        log.info("高性能消息处理器初始化完成"); // 🚀 初始化完成
    }
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String userId = extractUserId(session);
        sessions.put(userId, session);
        
        // 异步处理连接建立事件
        messageQueue.offer(new MessageTask(MessageTaskType.CONNECTION_ESTABLISHED, userId, null, session));
        
        log.info("用户{}连接建立,当前连接数:{}", userId, sessions.size());
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String userId = extractUserId(session);
        
        // 将消息处理任务放入队列,避免阻塞I/O线程
        messageQueue.offer(new MessageTask(MessageTaskType.TEXT_MESSAGE, userId, message.getPayload(), session));
    }
    
    // 🎯 异步消息处理主循环
    private void processMessages() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                MessageTask task = messageQueue.take(); // 阻塞等待任务
                
                switch (task.getType()) {
                    case CONNECTION_ESTABLISHED:
                        handleConnectionEstablishedAsync(task);
                        break;
                    case TEXT_MESSAGE:
                        handleTextMessageAsync(task);
                        break;
                    case CONNECTION_CLOSED:
                        handleConnectionClosedAsync(task);
                        break;
                }
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("消息处理异常", e);
            }
        }
    }
    
    // ⚡ 批量消息发送优化
    private void broadcastMessageBatch(List<String> userIds, String message) {
        // 使用并行流提高发送性能
        userIds.parallelStream().forEach(userId -> {
            WebSocketSession session = sessions.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    // 使用同步发送,避免缓冲区溢出
                    synchronized (session) {
                        session.sendMessage(new TextMessage(message));
                    }
                } catch (Exception e) {
                    log.warn("发送消息失败,用户:{}", userId, e);
                    // 清理无效连接
                    sessions.remove(userId);
                }
            }
        });
    }
}

📊 内存和连接优化策略

// 💾 内存优化的会话管理器
@Service
public class OptimizedSessionManager {
    
    // 使用弱引用避免内存泄漏
    private final Map<String, WeakReference<WebSocketSession>> sessions = new ConcurrentHashMap<>();
    
    // 连接时间戳,用于清理长时间无活动的连接
    private final Map<String, Long> lastActivity = new ConcurrentHashMap<>();
    
    // 使用Bloom Filter减少Redis查询
    private final BloomFilter<String> onlineUsersFilter = BloomFilter.create(
        Funnels.stringFunnel(Charset.defaultCharset()),
        1_000_000, // 预期元素数量:100万
        0.01 // 误报率:1%
    );
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 📝 注册会话
    public void registerSession(String userId, WebSocketSession session) {
        sessions.put(userId, new WeakReference<>(session));
        lastActivity.put(userId, System.currentTimeMillis());
        onlineUsersFilter.put(userId);
        
        // 异步更新Redis
        CompletableFuture.runAsync(() -> {
            redisTemplate.opsForSet().add("online:users", userId);
            redisTemplate.expire("online:users", Duration.ofHours(1));
        });
        
        log.debug("会话已注册:{}", userId);
    }
    
    // 🗑️ 移除会话
    public void removeSession(String userId) {
        sessions.remove(userId);
        lastActivity.remove(userId);
        
        // 异步更新Redis
        CompletableFuture.runAsync(() -> {
            redisTemplate.opsForSet().remove("online:users", userId);
        });
        
        log.debug("会话已移除:{}", userId);
    }
    
    // 🔍 检查用户是否在线(使用Bloom Filter优化)
    public boolean isUserOnline(String userId) {
        if (!onlineUsersFilter.mightContain(userId)) {
            return false; // Bloom Filter说不在,那一定不在
        }
        
        // Bloom Filter说可能在,需要进一步验证
        WeakReference<WebSocketSession> sessionRef = sessions.get(userId);
        if (sessionRef != null) {
            WebSocketSession session = sessionRef.get();
            return session != null && session.isOpen();
        }
        
        return false;
    }
    
    // 🧹 定时清理无效连接
    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
    public void cleanupInactiveSessions() {
        long currentTime = System.currentTimeMillis();
        long inactiveThreshold = 30 * 60 * 1000; // 30分钟无活动
        
        List<String> inactiveUsers = lastActivity.entrySet().parallelStream()
            .filter(entry -> currentTime - entry.getValue() > inactiveThreshold)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        
        inactiveUsers.forEach(this::removeSession);
        
        if (!inactiveUsers.isEmpty()) {
            log.info("清理了{}个无活动连接", inactiveUsers.size()); // 🧹 清理完成
        }
        
        // 清理弱引用
        sessions.entrySet().removeIf(entry -> entry.getValue().get() == null);
    }
    
    // 📊 获取连接统计
    public SessionStats getStats() {
        int activeSessions = (int) sessions.values().parallelStream()
            .filter(ref -> ref.get() != null && ref.get().isOpen())
            .count();
        
        return new SessionStats(
            sessions.size(),
            activeSessions,
            lastActivity.size()
        );
    }
}

🎊 总结:实时推送的未来趋势

  写到这里,我的手指都快敲断了!😅 回顾这一路走来,从Ajax轮询的痛苦,到WebSocket的惊喜,再到现在的大规模应用,技术的发展真是让人感慨万千!

🌟 WebSocket带来的变革

用户体验的革命: 真正的实时通信让用户体验上了一个台阶!聊天不再有延迟,游戏操作更加流畅,协作应用更加顺滑!✨

开发效率的提升: Spring Boot的WebSocket支持让我们可以快速构建实时应用,不用再为各种兼容性问题头疼!🚀

系统架构的进化: 从传统的请求-响应模式到事件驱动的架构,WebSocket推动了整个系统设计思维的转变!🔄

🚀 学习路径建议

对于想要掌握WebSocket的小伙伴们,我的建议是:

  1. 先理解协议 🤝 - WebSocket是什么,解决了什么问题
  2. 动手实践 💻 - 从简单的聊天室开始,一步步增加功能
  3. 深入原理 🔍 - 了解底层实现,掌握性能优化技巧
  4. 项目实战 🏗️ - 在真实项目中应用,积累经验
  5. 持续学习 📚 - 关注新技术发展,如WebRTC、HTTP/3等

🔮 技术趋势展望

HTTP/3和QUIC: 未来可能会有更好的传输协议,但WebSocket的编程模型依然有价值!🌐

边缘计算: WebSocket结合边缘计算,可以进一步降低延迟,提供更好的用户体验!⚡

AI集成: 智能消息路由、自动翻译、情感分析等AI功能将让实时通信更加智能!🤖

💝 最后的话

  技术的魅力在于它能解决实际问题,WebSocket不仅仅是一个协议,更是我们构建现代Web应用的重要工具!希望这篇文章能帮助大家更好地理解和应用WebSocket技术!🎉

  记住,没有完美的技术,只有合适的场景。选择技术方案时,要根据具体需求来决定,不要为了技术而技术!实用才是王道!💪

  愿每一个开发者都能在实时通信的世界里找到属于自己的乐趣!如果你有任何问题或想法,欢迎随时来找我讨论!让我们一起在技术的海洋中乘风破浪!🏄‍♂️


📚 参考资源:

  • WebSocket RFC 6455规范
  • Spring WebSocket官方文档
  • STOMP协议规范
  • Redis发布订阅机制
  • 个人大规模WebSocket部署经验

代码改变世界,实时连接你我! 🌍✨

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们一对一参考 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

✨️ Who am I?

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主及影响力最佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bug菌¹

你的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值