全栈实时聊天室(java项目)

文章目录

展示:

http://115.159.197.143:8080/login.html
该项目的超链接(可以试用)

登入页面:

注册页面:

cilent.html:


添加好友页面:

...等其他功能可自行探索

1.项目概述

这是一个基于 Spring Boot 3.5.6 + Java 17 开发的实时在线聊天室系统,采用前后端分离架构,支持用户注册登录、好友管理、即时消息发送等功能。

技术栈:

后端:Spring Boot 3.5.6、Java 17、MyBatis-Plus 3.5.6、MySQL、WebSocket

前端:HTML5、CSS3、JavaScript(使用 jQuery 3.7.1)

架构:RESTful API + WebSocket 实时通信

后端技术栈选型

类别 技术选型 版本/说明
基础框架 Spring Boot 3.5.6
语言 Java 17
Web spring-boot-starter-web 内置
实时通信 spring-boot-starter-websocket 内置
持久层 MyBatis + MyBatis-Plus MyBatis 3.5.16,MyBatis-Plus 3.5.6(boot-starter)
数据库 MySQL 驱动 mysql-connector-j,库名 java_chatroom
工具库 Lombok 1.18.30
JSON Jackson Spring 默认集成

选型要点:Spring Boot 3.x 统一技术栈;WebSocket 用于即时消息与好友申请/接受通知;MyBatis-Plus 与 XML 并存(UserMapper 继承 BaseMapper,其余 Mapper 以 XML 为主);MySQL 存储用户、好友、会话、消息及好友申请数据。

2.项目的pom.xml和yml配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.6</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>chatroom_</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>chatroom</name>
    <description>chatroom_</description>
    <properties>
        <java.version>17</java.version>
        <!-- 统一版本号,方便维护 -->
        <lombok.version>1.18.30</lombok.version>
        <mybatis-plus.version>4.0.0</mybatis-plus.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Web核心依赖(包含webSocket相关基础) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot WebSocket(Spring Boot 3.x无需单独引spring-websocket,starter已包含) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- 保留在dependencies标签内,替换原有MyBatis/MyBatis-Plus相关依赖 -->
        <!-- MyBatis-Plus核心依赖(适配Spring Boot 3.x) -->
        <!-- 替换原有MyBatis-Plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.6</version> <!-- 适配Spring Boot 3.x的稳定版 -->
        </dependency>

        <!-- 显式引入MyBatis核心依赖(兜底,解决注解包找不到问题) -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.16</version>
        </dependency>
        <!-- 可选:手动显式引入MyBatis核心依赖(防止依赖传递异常) -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.16</version> <!-- 适配Spring Boot 3.x的版本 -->
            <scope>compile</scope>
        </dependency>

        <!-- MyBatis与Spring整合依赖(MyBatis-Plus starter已包含,可省略,仅兜底) -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok(统一配置,仅保留一个) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.5</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <!-- 新增:指定打包后的jar包名,避免多余横杠,和服务器端保持一致 -->
        <finalName>chatroom-0.0.1-SNAPSHOT</finalName>
        <plugins>
            <!-- Maven编译插件(适配Lombok,你的原有配置,完全保留) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <!-- Spring Boot打包插件(补充executions核心配置,保留原有excludes) -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
                <!-- 新增:核心执行配置,生成可执行jar并写入主类清单 -->
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
java 复制代码
# 数据库连接配置
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_chatroom?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 
#    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  # 禁用静态资源缓存(开发环境)
  web:
    resources:
      cache:
        period: 0
      chain:
        cache: false
#  配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:
  mapper-locations: classpath:mapper/**.xml
  #配置驼峰⾃动转换
  configuration:
    map-underscore-to-camel-case: true
mybatis-plus:
  configuration: # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.项目结构分析:

src/

main/

java/

com/

example/

chatroom/

.idea/

├── misc.xml (177 B)

├── modules.xml (263 B)

└── workspace.xml (2.16 KB)

Utils/

├── ChatroomException.java (338 B)

├── ExceptionAdvice.java (2.18 KB)

└── ResposeAdvice.java (1.48 KB)

api/

├── TextWebSocketAPI.java (1.49 KB)

└── WebSocketAPI.java (5.17 KB)

component/

└── OnlineUserManager.java (1.98 KB)

config/

├── WebMvcConfig.java (818 B)

├── WebSocketConfig.java (1.19 KB)

└── WebSocketHandshakeInterceptor.java (2.77 KB)

constant/

└── Constant.java (211 B)

controller/

├── AddFrinedController.java (1.9 KB)

├── FaviconController.java (679 B)

├── FriendController.java (635 B)

├── MessageController.java (605 B)

├── SessionController.java (1013 B)

└── UserController.java (1.54 KB)

mapper/

├── AddFriendMapper.java (708 B)

├── FriendMapper.java (422 B)

├── MessageMapper.java (349 B)

├── SessionMapper.java (782 B)

└── UserMapper.java (426 B)

model/

├── AddFriendRequest.java (876 B)

├── Friend.java (471 B)

├── Message.java (1.13 KB)

├── MessageRequest.java (664 B)

├── MessageResponse.java (1.01 KB)

├── MessageSession.java (740 B)

├── MessageSessionUserItem.java (494 B)

├── SearchFriendResult.java (1.05 KB)

└── User.java (982 B)

pojo/

└── Result.java (1.2 KB)

service/

├── AddFrinedService.java (9.06 KB)

├── FriendService.java (1.53 KB)

├── MessageService.java (718 B)

├── SessionService.java (3.95 KB)

└── UserService.java (2.73 KB)

├── chatroom.iml (466 B)

└── ChatroomApplication.java (387 B)

resources/

mapper/

├── AddFriendMapper.xml (1.45 KB)

├── FriendMapper.xml (1.06 KB)

├── MessageMapper.xml (834 B)

├── SessionMapper.xml (1.05 KB)

└── UserMapper.xml (334 B)

static/

css/

├── addfirned.css (8.06 KB)

├── client.css (48.15 KB)

├── common.css (17.83 KB)

├── login.css (12.71 KB)

└── register.css (10.84 KB)

image/

├── 好友.png (6.37 KB)

├── 聊天会话.png (9.57 KB)

├── 聊天会话1.png (8.91 KB)

├── 微信图片_20240723235959.jpg (208.38 KB)

├── 用户好友-copy.png (7.4 KB)

├── 用户好友-copy1.png (7.83 KB)

├── img.png (8.76 KB)

└── search.png (5.35 KB)

js/

└── client.js (44.6 KB)

├── client.html (7.82 KB)

├── favicon.ico (4.19 KB)

├── favicon.svg (753 B)

├── login.html (7.27 KB)

├── README_FAVICON.md (1.28 KB)

├── register.html (11.61 KB)

└── test.html (1.1 KB)

├── application.properties (33 B)

└── application.yml (774 B)

test/

java/

com/

example/

chatroom/

└── ChatroomApplicationTests.java (223 B)

3.1 分层结构

  • 入口层ChatroomApplication(@SpringBootApplication)。
  • 配置层config
    • WebMvcConfig:静态资源与 favicon 映射。
    • WebSocketConfig:注册 WebSocket 路径与拦截器。
    • WebSocketHandshakeInterceptor:握手时从 HttpSession 取 User 写入 WebSocket attributes。
  • 控制层controller
    • UserControllerFriendControllerSessionControllerMessageControllerAddFrinedControllerFaviconController
  • 服务层service
    • UserServiceFriendServiceSessionServiceMessageServiceAddFrinedService
  • 数据访问层mapper 包(接口)+ resources/mapper/*.xml
    • UserMapperFriendMapperSessionMapperMessageMapperAddFriendMapper
  • 模型层modelpojo
    • 业务实体与统一响应 Result
  • 组件层component
    • OnlineUserManager:维护 userId 与 WebSocketSession 映射。
  • WebSocket 处理api
    • WebSocketAPI(业务消息)、TextWebSocketAPI(备用,未注册到当前路径)。
  • 全局处理Utils
    • ResposeAdvice(统一响应封装)、ExceptionAdvice(统一异常)、ChatroomException(业务异常)。

3.2 请求与实时消息流

  • HTTP 请求 :浏览器 → DispatcherServlet → Controller → Service → Mapper → MySQL;返回经 ResposeAdvice 包装为 Result(code/message/data)。
  • WebSocket :浏览器连接 /message,经 WebSocketHandshakeInterceptor 握手,由 WebSocketAPI 处理连接建立、消息收发、连接关闭;在线状态由 OnlineUserManager 与 Session 属性中的 User 绑定。

3.4核心业务逻辑实现

4.1 用户与认证

  • 登录UserController.login 接收 User(userName、password),UserService.loginUserMapper.selectOne 按 user_name 查询,校验密码后 session.setAttribute("user", user),返回用户信息(密码置空)。
  • 注册UserController.register 接收 userName、password、confirmPassword,UserService.register 校验非空与两次密码一致后 UserMapper.insert;重复用户名由 DuplicateKeyException 捕获返回 false。
  • 当前用户UserController.getUserInfoUserService.getUserInfoHttpServletRequest.getSession(false) 取 user,无会话或 user 为 null 时返回 null。

依赖:HttpSession 存储 User,未使用 JWT 等无状态方案;WebSocket 握手通过 WebSocketHandshakeInterceptor 将同一 Session 中的 user 写入 WebSocket attributes,供 WebSocketAPIOnlineUserManager 使用。

4.2 好友关系

  • 好友列表FriendController.getFriendListFriendService.getFriendList 从 Session 取当前用户,FriendMapper.selectFriendList(userId) 查询好友(从 friend 表关联 user 表得到 friendId、friendName)。
  • 搜索用户(找好友)AddFrinedController.findFriendAddFrinedService.findFriend,参数 name 为空则返回空列表;否则 AddFriendMapper.findFriend(selfUserId, name) 查询非本人且非已是好友的用户,并经由 OnlineUserManagerFriendMapper.countMutualFriends 补齐在线状态与共同好友数,返回 SearchFriendResult 列表。
  • 发送好友申请AddFrinedController.addFriendAddFrinedService.addFrined,校验 Session/User 与 friendId,AddFriendMapper.addFriend 写入 add_friend_request;若对方在线,通过 OnlineUserManager.getSession(friendId) 取得 WebSocketSession,发送 type 为 addFriendRequest 的 JSON(fromUserId、fromUserName、reason)。
  • 好友申请列表AddFrinedController.getFriendRequestAddFrinedService.getFriendRequest,从 Session 取当前用户,AddFriendMapper.getFriendRequest(toUserId) 查询发给自己的申请(含 fromUserId、fromUserName、reason);Session 或 User 为空时返回空列表。
  • 接受好友AddFrinedController.acceptFriendAddFrinedService.acceptFriend,删除 add_friend_request 中对应记录,FriendMapper.checkFriendExists 判断是否已是好友,未则 AddFriendMapper.addNewFriend 双向插入 friend 表;若对方在线则 WebSocket 推送 type 为 acceptFriend 的消息。
  • 拒绝好友AddFrinedController.rejectFriendAddFrinedService.rejectFriend,仅 AddFriendMapper.deleteFriendRequest 删除申请记录。

4.3 会话与消息

  • 会话列表SessionController.getSessionListSessionService.getSessionList,从 Session 取当前用户,SessionMapper.getSessionIdsByUserId 按 lastTime 降序得到会话 ID 列表,对每个 sessionId 用 SessionMapper.getFriendListBySessionId 取对方好友信息、MessageMapper.getPostTimeBySessionId 取最后一条消息内容,组装为 MessageSession(sessionId、friends、lastMessage)。
  • 创建会话SessionController.addMessageSession(路径 /session)→ SessionService.addMessageSession,参数为对方 userId;SessionMapper.addMessageSession 插入 message_session 获自增 sessionId,再 SessionMapper.addMessageSessionUser 插入两条 message_session_user(当前用户与对方),返回 sessionId。
  • 历史消息MessageController.getMessageMessageService.getMessageMessageMapper.getMessage(sessionId) 按会话查消息(按 postTime 降序,limit 100),Service 层 Collections.reverse 转为时间升序供前端展示。
  • 发送消息(实时) :前端通过 WebSocket 发送 JSON:type、sessionId、content。WebSocketAPI.handleMessage 解析为 MessageRequest,调用 transmessage:构造 MessageResponse(type、sessionId、fromId、fromName、content),根据 sessionId 与当前用户用 SessionMapper.getFriendListBySessionId 得到会话内用户(含自己),对每个用户通过 OnlineUserManager.getSession 若在线则推送同一 MessageResponse JSON;同时 MessageMapper.addMessage 将消息落库(Message 中 session 字段名为 sessinoId,与 XML 中部分使用 sessionId 需在实现上对应)。

4.4 在线状态与 WebSocket 生命周期

  • OnlineUserManager :使用 ConcurrentHashMap<Integer, WebSocketSession> 维护 userId 与 WebSocketSession。online(userId, session) 在连接建立或发消息时注册/更新;offline(userId, session) 在连接关闭或异常时移除;getSession(userId) 供推送消息与查询在线状态使用。
  • WebSocketAPIafterConnectionEstablished 中若 attributes 中有 user 则调用 onlineUserManager.onlinehandleMessage 中再次确保 online 并解析 MessageRequest 执行 transmessage;handleTransportError / afterConnectionClosed 中若 user 非空则 onlineUserManager.offline
  • WebSocketHandshakeInterceptor :beforeHandshake 中从 ServletRequest 取 HttpSession,若有 user 则 attributes.put("user", user);当前实现为即使用户为空也允许握手通过,仅不注册在线。

3.5数据模型设计

5.1 实体与 DTO(model / pojo)

  • User(表 user):userId(自增)、userName、password;MyBatis-Plus 注解 @TableName("user")、@TableId、@TableField。
  • Friend:逻辑模型,无对应单表;friendId、friendName,多来自 user 表或联表查询。
  • Message:messageId、sessinoId、fromId、fromName、content(库表字段含 sessionId、postTime 等,fromName 由联表得到)。
  • MessageRequest:WebSocket 入参,type、sessionId、content。
  • MessageResponse:WebSocket 出参,type、sessionId、fromId、fromName、content。
  • MessageSession:sessionId、friends(List)、LastMessage。
  • MessageSessionUserItem:userId、sessionId,对应 message_session_user 表。
  • AddFriendRequest:type、fromUserId、fromUserName、reason;既作 WebSocket 推送结构,也作 getFriendRequest 查询结果映射。
  • SearchFriendResult:friendId、friendName、online、mutualFriendCount,专用于搜索好友结果。
  • Result<T>(pojo):code、message、data;成功 200(Constant.RESULT_SUCCESS),失败 -1(Constant.RESULT_FAIl),未登录 0(Constant.RESULT_UNLOGIN)。

5.2 数据库表结构(由 Mapper XML 推断)

  • user:主键自增,含 user_name、password 等。
  • friend:userId、friendId,双向好友关系(A-B 存两条)。
  • message_session:session_id(自增)、lastTime(用于会话列表排序)。
  • message_session_user:session_id、user_id,会话与用户多对多。
  • message:message_id、session_id、from_id、content、post_time 等。
  • add_friend_request:fromUserId、toUserId、reason,好友申请单。

字段命名在 XML 中多为下划线(如 session_id、user_id),通过 MyBatis 的 map-underscore-to-camel-case 与 resultType/模型属性对应。


3.6API 接口规范

6.1 HTTP 接口一览

方法 路径 控制器方法 说明
POST /login UserController.login Body: User(userName,password);返回用户信息或错误信息
请求 /register UserController.register 参数: userName, password, confirmPassword
GET /getUserInfo UserController.getUserInfo 从 Session 取当前用户
GET /getFriendList FriendController.getFriendList 当前用户好友列表
GET /getSessionList SessionController.getSessionList 当前用户会话列表
GET /session SessionController.addMessageSession 参数: userId;创建会话,返回 sessionId
GET /getMessage MessageController.getMessage 参数: sessionId;历史消息列表
GET /findFriend AddFrinedController.findFriend 参数: name;搜索可添加用户
GET /addFriend AddFrinedController.addFriend 参数: friendId, reason;发送好友申请
GET /getFriendRequest AddFrinedController.getFriendRequest 当前用户收到的好友申请列表
GET /acceptFriend AddFrinedController.acceptFriend 参数: friendId;接受申请并加好友
GET /rejectFriend AddFrinedController.rejectFriend 参数: friendId;拒绝申请
GET /favicon.ico FaviconController.favicon 204 No Content,避免 404

说明:除登录为 POST + Body,其余多为 GET + 查询参数;实际返回均由 ResposeAdvice 包装为 Result(code、message、data)。

6.2 WebSocket 接口

  • 路径/message(由 WebSocketConfig 注册,Handler 为 WebSocketAPI,拦截器为 WebSocketHandshakeInterceptor)。
  • 客户端 → 服务端 :JSON,如 {"type":"message","sessionId":xxx,"content":"..."},仅 type 为 message 时被处理。
  • 服务端 → 客户端
    • 聊天消息:MessageResponse(type、sessionId、fromId、fromName、content)。
    • 好友申请通知:type=addFriendRequest,fromUserId、fromUserName、reason。
    • 好友接受通知:type=acceptFriend,fromUserId、fromUserName、reason。

3.7数据库与 Mapper 设计要点

  • UserMapper :继承 MyBatis-Plus BaseMapper<User>,XML 中仅定义 insert(user 表)。
  • FriendMapper:selectFriendList(通过 friend 表关联 user)、countMutualFriends(共同好友)、checkFriendExists(是否已是好友)。
  • SessionMapper:addMessageSession(插入 message_session,useGeneratedKeys 回填 sessionId)、addMessageSessionUser、getSessionIdsByUserId(按 lastTime 降序)、getFriendListBySessionId(会话内除自己外的用户)。
  • MessageMapper:addMessage(注意 Java 模型为 sessinoId)、getPostTimeBySessionId(最后一条 content)、getMessage(按 sessionId、postTime 降序 limit 100,联表 user 取 fromName)。
  • AddFriendMapper:findFriend(排除本人与已有好友,按 friendName 模糊查询)、addFriend(插入 add_friend_request)、getFriendRequest、deleteFriendRequest、addNewFriend(双向插入 friend)。

事务:SessionService.addMessageSession 使用 @Transactional,保证创建会话与插入两条 message_session_user 的原子性。

4.关键业务流程:


5.部分代码分析(后端代码)

5.1用户模块

块分为控制器层(UserController)和服务层(UserService),遵循「Controller 接收请求→Service 处理业务逻辑→Mapper 操作数据库」的分层设计思想

  • /login :用户登录接口
  • 接收方式: @RequestBody User ,接收JSON 格式的用户名和密码
  • 处理逻辑:调用 UserService.login() 进行验证
  • 返回结果:登录成功返回用户信息,失败返回错误信息
  • /register :用户注册接口
  • 接收方式:普通参数接收用户名、密码、确认密码
  • 处理逻辑:调用 UserService.register() 进行注册
  • 返回结果:固定返回 true
  • /getUserInfo :获取用户信息接口
  • 接收方式:从 HttpServletRequest 获取会信息
  • 处理逻辑:调用 UserService.getUserInfo() 获取用户信息
  • 返回结果:返回当前登录用户信息

作为用户模块的核心业务逻辑层,负责处理登录、注册、获取用户信息的所有业务规则:参数校验、数据库查询 / 插入、Session 状态维护、异常处理等,是整个模块的 "大脑",控制器层仅做转发,持久层(Mapper)仅做数据库操作,所有业务判断都集中在此。

java 复制代码
public Object login(String userName, String password, HttpServletRequest servletRequest){
    // 1. 构造MyBatis-Plus查询条件:按user_name等值查询
    QueryWrapper<User>queryWrapper=new QueryWrapper<>();
    queryWrapper.eq("user_name",userName);
    // 2. 调用Mapper查询数据库,获取用户信息
    User user=userMapper.selectOne(queryWrapper);
    System.out.println("login user="+user);
    // 3. 校验:用户不存在 或 密码不匹配 → 返回错误提示
    if(user==null||!user.getPassword().equals(password)){
        return "账号或密码错误";
    }
    // 4. 验证通过:创建/获取Session,将用户信息存入Session
    HttpSession session = servletRequest.getSession(true);
    session.setAttribute("user",user);
    // 5. 隐藏密码,避免返回前端泄露
    user.setPassword("");
    // 6. 验证通过 → 返回用户信息
    return user;
}

login(String userName, String password, HttpServletRequest servletRequest) :

  • 业务逻辑:根据用户名查询用户 → 验证密码 → 创建会话 → 返回用户信息
  • 技术实现:使用 QueryWrapper 构建查询条件,通过 UserMapper 查询数据库
  • 会话管理:使用 HttpSession 存储用户信息
java 复制代码
public Object register(String userName,String password,String confirmPassword){
    // 1. 非空校验:用户名/密码/确认密码不能为空
    if(!StringUtils.hasLength(userName)){return "用户名不能为空";}
    if(!StringUtils.hasLength(password)){return "密码不能为空";}
    if(!StringUtils.hasLength(confirmPassword)){return "确认密码不能为空";}
    // 2. 两次密码一致性校验
    if(!password.equals(confirmPassword)){return "两次密码不同";}
    try {
       // 3. 调用Mapper插入用户信息    userMapper.insert(userName,password);
        return true; // 注册成功
    }catch (org.springframework.dao.DuplicateKeyException e){
        // 4. 捕获唯一键异常(用户名重复,数据库user_name字段应设为唯一索引)
        return false;
    }
    catch (Exception e){
        // 5. 捕获其他未知异常(如数据库连接失败)
        System.out.println("插入数据时发生异常"+e.getMessage());
        return false;
    }
}

register(String userName, String password, String confirmPassword) :

  • 业务逻辑:参数验证 → 插入用户数据 → 处理异常
  • 技术实现:使用 StringUtils.hasLength() 验证参数,调用 UserMapper.insert() 插入数据
  • 异常处理:捕获 DuplicateKeyException 处理用户名重复情况
java 复制代码
public Object getUserInfo(HttpServletRequest httpServletRequest) {
    User user=null;
    try {
        // 1. 获取Session:false表示「无Session则返回null,不创建新Session」(关键)
        HttpSession session = httpServletRequest.getSession(false);
         // 2. 从Session中取用户信息
         user = (User) session.getAttribute("user");
         // 3. 隐藏密码
         user.setPassword("");
    }catch (NullPointerException e){
        // 捕获空指针异常(Session为null 或 Session中无user)
        return null;
    }
    return user;
}

getUserInfo(HttpServletRequest httpServletRequest) :

  • 业务逻辑:从会话中获取用户信息 → 清除密码 → 返回用户信息
  • 技术实现: 使用httpServletRequest.getSession(false) 获取会话,避免创建新会话
  • 异常处理:捕获 NullPointerException 处理会话不存在情况
sql 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.mapper.UserMapper">
    <insert id="insert">
        insert into user values (null,#{userName},#{password});
    </insert>
</mapper>

5.2会话模块

核心实现获取当前登录用户的聊天会话列表、创建双人聊天会话两个核心业务,是聊天室中会话维度的核心逻辑层。

java 复制代码
package com.example.chatroom.service;

import com.example.chatroom.mapper.MessageMapper;
import com.example.chatroom.mapper.SessionMapper;
import com.example.chatroom.model.Friend;
import com.example.chatroom.model.MessageSession;
import com.example.chatroom.model.MessageSessionUserItem;
import com.example.chatroom.model.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import net.sf.jsqlparser.expression.operators.relational.OldOracleJoinBinaryExpression;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;

import javax.print.DocFlavor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

@Service
public class SessionService {
    @Autowired
    private SessionMapper sessionMapper;
    @Autowired
    private MessageMapper messageMapper;

    public Object getSessionList(HttpServletRequest httpServletRequest) {
        List<MessageSession> messageSessionList = new ArrayList<>();
        try {
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null) {
                System.out.println("Message-Session为null");
                return messageSessionList;
            }
//        获取当前用户Id
            User user = (User) session.getAttribute("user");
            if (user == null) {
                System.out.println("User为null,用户未登录");
                return messageSessionList;
            }
            List<Integer> sessionIds = sessionMapper.getSessionIdsByUserId(user.getUserId());
            if (sessionIds == null) {
                return messageSessionList;
            }
            for (Integer sessionId : sessionIds) {
                System.out.println(sessionId);
                MessageSession messageSession = new MessageSession();
                messageSession.setSessionId(sessionId);
                List<Friend> friendList = sessionMapper.getFriendListBySessionId(sessionId, user.getUserId());
                for(Friend friend:friendList){
                    System.out.println(friend.getFriendName());
                }
                messageSession.setFriends(friendList);

                String lastContent = messageMapper.getPostTimeBySessionId(sessionId);
                if(lastContent==null){
                    messageSession.setLastMessage("");
                }else messageSession.setLastMessage(lastContent);

                messageSessionList.add(messageSession);
            }
            return messageSessionList;
        }catch (Exception e){
            e.printStackTrace();
        }
        return messageSessionList;
    }
@Transactional  //进行事务回滚
    public Object addMessageSession(Integer userId,HttpServletRequest httpServletRequest){
    HttpSession session = httpServletRequest.getSession();
    User user = (User) session.getAttribute("user");
        // 通过addMessageSession方法创建一个新会话并获取自增主键的id
        MessageSession messageSession=new MessageSession();
        sessionMapper.addMessageSession(messageSession);
        int sessionId =messageSession.getSessionId();
//
        MessageSessionUserItem messageSessionUserItem1=new MessageSessionUserItem();
        messageSessionUserItem1.setUserId(user.getUserId());
        messageSessionUserItem1.setSessionId(sessionId);
        sessionMapper.addMessageSessionUser(messageSessionUserItem1);

        MessageSessionUserItem messageSessionUserItem2=new MessageSessionUserItem();
        messageSessionUserItem2.setUserId(userId);
        messageSessionUserItem2.setSessionId(sessionId);
        sessionMapper.addMessageSessionUser(messageSessionUserItem2);
        return sessionId;
    }
}
  • getSessionList(HttpServletRequest httpServletRequest) :
  • 技术实现:从 HttpSession 获取当前用户信息,调用SessionMapper,MessageMapper 查询数据
  • 异常处理:捕获所有异常,打印堆栈信息
    addMessageSession(IntegeruserId,HttpServletRequesthttpServletRequest)
  • 技术实现:使用 @Transactional 注解确保事务一致性,调用SessionMapper 插入数据
  • 事务管理:使用 @Transactional 注解,确保会话创建和参与者添加的原子性
sql 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.mapper.SessionMapper">
    <insert id="addMessageSession" keyProperty="sessionId" useGeneratedKeys="true">
     insert into message_session values (null,now());
    </insert>
    <insert id="addMessageSessionUser">
     insert into message_session_user values (#{sessionId},#{userId});
    </insert>
    <select id="getSessionIdsByUserId" resultType="java.lang.Integer">
        select session_id from message_session where session_id in
        ( select sessionId from message_session_user where userId=#{userId})
         order by lastTime desc;
    </select>
    <select id="getFriendListBySessionId" resultType="com.example.chatroom.model.Friend">
    select user_id as friendId,user_name as friendName from user where user_id in
    (select userId from message_session_user where sessionId=#{sessionId} and userId !=#{userSelfId});
    </select>
</mapper>

5.3好友模块

好友列表模块

好友模块是聊天系统的重要组成部分,负责管理用户的好友关系,提供获取好友列表的功能。该模块为用户提供了好友管理的基础功能,是添加好友和发起会话的前提条件。

getFriendList(HttpServletRequest httpServletRequest) :

  • 业务逻辑:验证会话 → 获取当前用户信息 → 查询好友列表 → 返回结果
  • 技术实现:从 HttpSession 获取当前用户信息,调用FriendMapper.selectFriendList() 查询数据
  • 异常处理:捕获所有异常,记录错误日志,返回空列表
  • 日志记录:使用 @Slf4j 注解,记录关键操作和异常信息
sql 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.mapper.FriendMapper">
    <select id="selectFriendList" resultType="com.example.chatroom.model.Friend">
        select user_id as friendId ,user_name as friendName from user where user_id in
        (select friendId from friend where userId=#{userid}  );
    </select>
<!--    最多可以查100条,可以调整-->

    <!-- 共同好友数:friend 表为双向记录(A->B 与 B->A),这里统计交集即可 -->
    <select id="countMutualFriends" resultType="java.lang.Integer">
        select count(*) from friend f1
        inner join friend f2 on f1.friendId = f2.friendId
        where f1.userId = #{userId} and f2.userId = #{otherUserId}
    </select>

    <!-- 检查好友关系是否存在 -->
    <select id="checkFriendExists" resultType="java.lang.Integer">
        select count(*) from friend where userId = #{userId} and friendId = #{friendId}
    </select>

</mapper>

添加好友模块

好友搜索、发送申请、查看申请、通过申请、拒绝申请五大核心功能

五大核心接口功能 & 执行流程:

1. 模糊搜索好友:/findFriend(GET)

java 复制代码
    public Object findFriend(String name, HttpServletRequest httpServletRequest) {
        HttpSession session = httpServletRequest.getSession(false);
        if (session == null) {
            System.out.println("Session 为 null,用户未登录");
            return new ArrayList<>();
        }
        User user = (User)session.getAttribute("user");
        if (user == null) {
            System.out.println("User 为 null,用户未登录");
            return new ArrayList<>();
        }
        // 如果name为null或空字符串,返回空列表,防止返回所有结果
        if(name==null || name.trim().isEmpty()){
            return new ArrayList<>();
        }
        List<SearchFriendResult> friends = addFriendMapper.findFriend(user.getUserId(), name.trim());
        // 补齐:在线状态 + 共同好友数(用于前端展示/排序)
        for (SearchFriendResult friend : friends) {
            Integer friendId = friend.getFriendId();
            friend.setOnline(onlineUserManager.getSession(friendId) != null);
            Integer mutual = friendMapper.countMutualFriends(user.getUserId(), friendId);
            friend.setMutualFriendCount(mutual == null ? 0 : mutual);
        }
        return friends;
    }

1.登录态校验(Session+User 非空),失败返回空列表;

2.校验搜索关键词:若为null/空字符串/纯空格,直接返回空列表(防全表查询);

3.调用 Mapper 模糊查询排除自身的可添加用户(SearchFriendResult列表);

4.对每个搜索结果做前端体验优化:

通过OnlineUserManager判断该用户在线状态(有 WebSocket 会话则为在线);

调用 Mapper 统计与当前用户的共同好友数(为 null 则置 0);

5.返回补充了「在线状态、共同好友数」的搜索结果列表

2. 发送好友申请:/addFriend(GET)

java 复制代码
 public Object addFrined(Integer friendId, String reason, HttpServletRequest httpServletRequest) throws IOException {
        HttpSession session = httpServletRequest.getSession(false);
        if (session == null) {
            System.out.println("Session 为 null,用户未登录");
            throw new RuntimeException("用户未登录");
        }
        User user=(User) session.getAttribute("user");
        if (user == null) {
            System.out.println("User 为 null,用户未登录");
            throw new RuntimeException("用户未登录");
        }
        if (friendId == null || friendId <= 0) {
            System.out.println("friendId 无效: " + friendId);
            throw new RuntimeException("好友ID无效");
        }
        if (objectMapper == null) {
            objectMapper = new ObjectMapper();
        }
        try {
//            写入数据库
            addFriendMapper.addFriend(user.getUserId(),friendId,reason);
        } catch (Exception e) {
            System.out.println("添加好友请求失败(可能已存在): " + e.getMessage());
            // 如果是重复添加,不抛出异常,继续尝试发送WebSocket通知
        }
//        发送webSocket响应
        WebSocketSession webSocketSession = onlineUserManager.getSession(friendId);
        System.out.println("发送好友申请通知 - friendId: " + friendId + ", session: " + (webSocketSession != null ? "存在" : "不存在"));
        if(webSocketSession!=null){
            if(!webSocketSession.isOpen()){
                System.out.println("WebSocket session 已关闭,无法发送通知");
            } else {
                AddFriendRequest addFriendRequest=new AddFriendRequest();
                addFriendRequest.setType("addFriendRequest");
                addFriendRequest.setFromUserId(user.getUserId());
                addFriendRequest.setFromUserName(user.getUserName());
                addFriendRequest.setReason(reason);
                String json = objectMapper.writeValueAsString(addFriendRequest);
                System.out.println("发送 WebSocket 消息: " + json);
                webSocketSession.sendMessage(new TextMessage(json));
                System.out.println("WebSocket 消息发送成功");
            }
        } else {
            System.out.println("目标用户不在线,无法发送 WebSocket 通知");
        }
        return "";
    }

1.登录态校验,失败抛出运行时异常(区别于其他接口的空返回);

2.参数校验:friendId为null/≤0则抛出运行时异常;

3.数据库操作:调用 Mapper 写入好友申请记录,捕获所有异常(处理重复申请场景),仅打印日志不抛异常;WebSocket 实时通知:

通过OnlineUserManager获取被申请人的 4.WebSocket 会话;

若会话存在且为打开状态,封装AddFriendRequest通知对象(含申请人 ID / 用户名 / 申请理由),序列化为 JSON 后发送文本消息;

若会话不存在 / 已关闭,打印日志(目标用户不在线);

3. 查看收到的好友申请:/getFriendRequest(GET)

java 复制代码
 public Object getFriendRequest(HttpServletRequest httpServletRequest) {
        List<AddFriendRequest> list=new ArrayList<>();
        try {
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null) {
                System.out.println("Session 为 null,用户未登录");
                return list;
            }
            User user = (User)session.getAttribute("user");
            if (user == null) {
                System.out.println("User 为 null,用户未登录");
                return list;
            }
            list=addFriendMapper.getFriendRequest(user.getUserId());
        } catch (NullPointerException e) {
            System.out.println("获取好友请求时发生异常: " + e.getMessage());
            e.printStackTrace();
        }
        return list;
    }

1.初始化空的申请列表,作为异常 / 未登录的兜底返回值;

2.登录态校验(Session+User 非空),失败返回初始化的空列表;

3.调用 Mapper 查询以当前用户为被申请人的所有未处理好友申请;

4.捕获空指针异常,打印堆栈信息,返回空列表;

5.返回查询到的好友申请列表(AddFriendRequest)。

4. 通过好友申请:/acceptFriend(GET)

java 复制代码
 public Object acceptFriend(Integer friendId, HttpServletRequest httpServletRequest) throws JsonProcessingException {
    try {
        HttpSession session = httpServletRequest.getSession(false);
        if (session == null) {
            System.out.println("Session 为 null,用户未登录");
            throw new RuntimeException("用户未登录");
        }
        User user=(User)session.getAttribute("user");
        if (user == null) {
            System.out.println("User 为 null,用户未登录");
            throw new RuntimeException("用户未登录");
        }
        if (friendId == null || friendId <= 0) {
            System.out.println("friendId 无效: " + friendId);
            throw new RuntimeException("好友ID无效");
        }
        if (objectMapper == null) {
            objectMapper = new ObjectMapper();
        }
//        删除好友申请的缓存
        addFriendMapper.deleteFriendRequest(friendId,user.getUserId());
        System.out.println("接受好友申请 - friendId: " + friendId + ", userId: " + user.getUserId());
//        检查好友关系是否已存在
        Integer exists = friendMapper.checkFriendExists(user.getUserId(), friendId);
        if (exists != null && exists > 0) {
            System.out.println("好友关系已存在,跳过添加");
        } else {
//            在好友表中添加新数据
            addFriendMapper.addNewFriend(friendId,user.getUserId());
            System.out.println("好友关系添加成功");
        }
//        通过webSecket来通知对方刷新好友列表
        WebSocketSession webSocketSession = onlineUserManager.getSession(friendId);
        System.out.println("发送好友接受通知 - friendId: " + friendId + ", session: " + (webSocketSession != null ? "存在" : "不存在"));
        if (webSocketSession!=null){
            if(!webSocketSession.isOpen()){
                System.out.println("WebSocket session 已关闭,无法发送接受通知");
            } else {
                AddFriendRequest addFriendRequest=new AddFriendRequest();
                addFriendRequest.setType("acceptFriend");
                addFriendRequest.setFromUserId(user.getUserId());
                addFriendRequest.setFromUserName(user.getUserName());
                addFriendRequest.setReason("");
                String json = objectMapper.writeValueAsString(addFriendRequest);
                System.out.println("发送 WebSocket 接受消息: " + json);
                webSocketSession.sendMessage(new TextMessage(json));
                System.out.println("WebSocket 接受消息发送成功");
            }
        } else {
            System.out.println("目标用户不在线,无法发送接受通知");
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return "";
    }

1.登录态校验,失败抛出运行时异常;

2.参数校验:friendId为null/≤0则抛出运行时异常;

3.数据库操作 1:调用 Mapper删除该条好友申请记录(未处理申请变为已处理);

4.数据库操作 2:先校验好友关系是否已存在,若不存在则调用 Mapper建立双向好友关系;

5.WebSocket 实时通知:

通过OnlineUserManager获取申请人的 WebSocket 会话;

若会话存在且打开,封装AddFriendRequest通知对象(类型为acceptFriend、含被申请人 ID / 用户名),序列化为 JSON 发送;

若会话不存在 / 已关闭,打印日志;

6.捕获 IO 异常并转为运行时异常,返回空字符串。

5. 拒绝好友申请:/rejectFriend(GET)

java 复制代码
  public Object rejectFriend(Integer friendId, HttpServletRequest httpServletRequest) {
        try{
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null) {
                System.out.println("Session 为 null,用户未登录");
                return "";
            }
            User user =(User)session.getAttribute("user");
            if (user == null) {
                System.out.println("User 为 null,用户未登录");
                return "";
            }
            addFriendMapper.deleteFriendRequest(friendId, user.getUserId());
        }catch (NullPointerException e){
            System.out.println("拒绝好友请求时发生异常: " + e.getMessage());
            e.printStackTrace();
        }
        return "";
    }

1.登录态校验(Session+User 非空),失败返回空字符串;

2.调用 Mapper删除该条好友申请记录(未处理申请变为已处理);

3.捕获空指针异常,打印堆栈信息,返回空列表;

sql 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.mapper.AddFriendMapper">
    <insert id="addFriend">
        insert into add_friend_request values (#{fromUserId},#{toUserId},#{reason});
    </insert>
    <insert id="addNewFriend">
        insert into friend values (#{userId},#{friendId}),(#{friendId},#{userId});
    </insert>
    <delete id="deleteFriendRequest">
        delete from add_friend_request
        where fromUserId=#{friendId} and toUserId=#{userId};
    </delete>

    <select id="findFriend" resultType="com.example.chatroom.model.SearchFriendResult">
        select user_id as friendId,user_name as friendName from user where
        user_id !=#{selfUserId} 
        and user_id not in (select friendId from friend where userId = #{selfUserId})
        <if test="friendName != null and friendName != ''">
            and CHAR_LENGTH(TRIM(#{friendName})) > 0
            and user_name like CONCAT('%', #{friendName}, '%')
        </if>
        <if test="friendName == null or friendName == ''">
            and 1=0
        </if>
    </select>
    <select id="getFriendRequest" resultType="com.example.chatroom.model.AddFriendRequest">
        select a.fromUserId,u.user_name as fromUserName,a.reason from
        add_friend_request a,user u where a.toUserId=#{toUserId} and a.fromUserId=u.user_id;
    </select>
</mapper>

5.4消息模块

java 复制代码
    public Object getMessage(Integer sessionId) {
      List<Message> messageList = messageMapper.getMessage(sessionId);
//    在数据库中查询出来的信息结果按照时间降序,前端页面展示需求按时间升序
        Collections.reverse(messageList);
      return messageList;
    }

getMessage(Integer sessionId) :

  • 业务逻辑:根据会话 ID 查询消息列表 → 反转列表顺序 → 返回结果
  • 技术实现-调用MessageMapper.getMessage() 查询数据,使用 Collections.reverse() 反转列表顺序
sql 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.mapper.MessageMapper">
    <insert id="addMessage">
        insert into message values(null,#{sessinoId},#{fromId},#{content},now());
    </insert>

    <select id="getPostTimeBySessionId" resultType="java.lang.String">
        select content from message where sessionId=#{sessionId} order by postTime
desc limit 1;
    </select>
    <select id="getMessage" resultType="com.example.chatroom.model.Message">
        select messageId,fromId,content,user_name as fromName from
        message,user where sessionId=#{sessionId} and
        message.fromId=user.user_id order by postTime desc limit 100;
    </select>
</mapper>

5.5WebSocket模块

模块 1:基础测试处理器 - TextWebSocketAPI

重写TextWebSocketHandler的 4 个核心方法,仅做日志打印和简单的消息回显,是典型的 WebSocket 入门测试代码,无业务价值:

afterConnectionEstablished:WebSocket 握手成功、连接建立后触发,打印「连接成功」;handleTextMessage:接收前端文本消息后触发,打印接收的消息,同时将消息原封不动回显给前端,无意义调用父类空实现super.handleTextMessage()handleTransportError:连接发生网络异常(如断网、超时)时触发,打印「发生异常」;afterConnectionClosed:连接正常断开(如前端主动关闭)时触发,打印「连接断开」。

模块 2:核心业务处理器 - WebSocketAPI

这是代码的核心,重写了 5 个方法(含 1 个自定义业务方法),实现聊天室核心逻辑,按执行流程拆解更易理解:

阶段 1:连接建立 - afterConnectionEstablished

WebSocket 握手成功后触发,核心做用户上线注册:打印连接成功日志和 Session 中的属性;

从WebSocketSession的属性中获取User对象(需提前通过握手拦截器放入,否则为null);若用户信息非空,调用onlineUserManager.online()将用户 ID 与当前 Session 绑定,完成上线注册;若为空,打印空日志和属性键名,无后续处理。

阶段 2:接收消息 - handleMessage

注意:重写的是父类AbstractWebSocketHandler的通用消息处理方法,非TextWebSocketHandler专为文本消息设计的handleTextMessage,核心做消息接收和初步解析:

打印日志

再次获取 Session 中的 User 对象,为空则直接返回,不处理消息;冗余调用用户上线方法onlineUserManager.online()(连接建立时已调用,重复执行会导致在线管理异常);校验消息类型为TextMessage,避免非文本消息导致类型转换错误;

通过getPayload()获取消息体字符串,用ObjectMapper解析为业务请求实体MessageRequest;调用自定义方法transmessage()处理消息转发和持久化。

阶段 3:消息核心处理 - transmessage(自定义方法)

这是聊天室的核心业务方法,实现消息构建、转发、持久化三大功能,是代码的核心逻辑:

构建转发消息:实例化MessageResponse,设置发送人 ID / 名称、会话 ID、消息内容,序列化为 JSON 字符串;

获取会话内用户:通过sessionMapper.getFriendListBySessionId()根据会话 ID 查询当前用户的会话好友列表(排除自己);追加自己:手动创建当前用户的 Friend 对象并加入好友列表,实现「自己发的消息自己也能看到;遍历转发消息:根据好友 ID 从onlineUserManager获取对应的 WebSocketSession,若用户在线则发送 JSON 消息,离线则跳过;消息持久化:实例化数据库实体Message,设置会话 ID、消息内容、发送人 ID,调用messageMapper.addMessage()插入数据库。

阶段 4:异常处理 - handleTransportError

WebSocket 连接发生底层传输异常(如网络中断、客户端强制关闭连接)时触发:

打印异常日志和异常信息;获取 User 对象,非空则调用onlineUserManager.offline()将用户下线,清理 Session 绑定关系。

阶段 5:连接断开 - afterConnectionClosed

WebSocket 连接正常关闭(如前端调用close()、页面刷新)时触发:

打印断开日志和关闭状态;与异常处理逻辑一致,非空则调用offline()完成用户下线。

java 复制代码
public class TextWebSocketAPI extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//       此方法在WebSocket连接成功后  被调用
        System.out.println("TextAPI 连接成功!");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
//        收到消息后被调用
        System.out.println("TextAPI 接收消息:"+message.toString());
        session.sendMessage(message);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
//      连接发生异常时  此方法被调用
        System.out.println("TextAPI 发生异常!");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
//        连接正常断开时方法被调用
        System.out.println("TextAPI 连接断开!");
    }
}
java 复制代码
package com.example.chatroom.api;
//package com.example.chatroom.model.MessageResponse;
import com.example.chatroom.component.OnlineUserManager;
import com.example.chatroom.mapper.MessageMapper;
import com.example.chatroom.mapper.SessionMapper;
import com.example.chatroom.model.*;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.List;

@Component
public class WebSocketAPI extends TextWebSocketHandler {
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private SessionMapper sessionMapper;

    @Autowired
    private MessageMapper messageMapper;
    private ObjectMapper objectMapper=new ObjectMapper();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("WebSocketAPI 连接成功");
        System.out.println("Session attributes: " + session.getAttributes());
        // 连接建立时立即注册用户为在线状态
        User user = (User) session.getAttributes().get("user");
        if (user != null) {
            onlineUserManager.online(user.getUserId(), session);
            System.out.println("用户上线: " + user.getUserName() + " (ID: " + user.getUserId() + ")");
        } else {
            System.out.println("WebSocket 连接但用户信息为空,attributes keys: " + session.getAttributes().keySet());
        }
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        System.out.println("WebSocketAPI 发送信息成功:"+message.toString());
        User user = (User) session.getAttributes().get("user");
        if(user==null){
            return;
        }
        onlineUserManager.online(user.getUserId(),session);
        System.out.println("用户Id:"+user.getUserId());
//      Textmessage通过getPayload方法获取内容
        // 1. 先判断消息类型为TextMessage(避免非文本消息报错)
        if (message instanceof TextMessage textMessage) {
            // 2. 获取String类型的消息体(无需强转)
            String jsonPayload = textMessage.getPayload();
            // 3. 直接用String解析为MessageRequest
            MessageRequest req = objectMapper.readValue(jsonPayload, MessageRequest.class);
            transmessage(user, req);
        }
//        进行消息
    }

    private void transmessage(User user, MessageRequest req) throws IOException {
        MessageResponse messageResponse=new MessageResponse();
        messageResponse.setFromId(user.getUserId());
        messageResponse.setFromName(user.getUserName());
        messageResponse.setSessionId(req.getSessionId());
        messageResponse.setContent(req.getContent());
//        将java字符串转换为json
        String messageResponseJson = objectMapper.writeValueAsString(messageResponse);

//        通过sessionId获取会话其他用户信息
      List<Friend> friends= sessionMapper.getFriendListBySessionId(req.getSessionId(), user.getUserId());
//     将自己也加上
        Friend friendMyself=new Friend();
        friendMyself.setFriendId(user.getUserId());
        friendMyself.setFriendName(user.getUserName());
        friends.add(friendMyself);
//        根据每个userId找到对应的webSocket
        for (Friend friend: friends){
            WebSocketSession session = onlineUserManager.getSession(friend.getFriendId());
            if(session==null){
//                用户不在线继续
                continue;
            }
            session.sendMessage(new TextMessage(messageResponseJson));
        }
//        将数据存储在数据库中
        Message message=new Message();
        message.setSessinoId(req.getSessionId());
        message.setContent(req.getContent());
        message.setFromId(user.getUserId());
        messageMapper.addMessage(message);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("WebSocketAPI 连接产生异常:"+exception.toString());
        User user=(User) session.getAttributes().get("user");
        if(user==null){
            return;
        }
        onlineUserManager.offline(user.getUserId(),session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("WebSocketAPI 正常断开连接:"+status.toString());
        User user=(User) session.getAttributes().get("user");
        if(user==null){
            return;
        }
        onlineUserManager.offline(user.getUserId(),session);
    }
}

5.6拦截器模块

一、WebMvcConfig:WebMVC 静态资源配置类

核心定位:

实现 Spring 的WebMvcConfigurer接口,自定义 WebMVC 的静态资源映射规则,解决前端静态资源访问和favicon.ico(网站图标)404问题。

关键代码分析:

1.注解与接口:@Configuration标记为配置类,Spring 会自动加载;实现WebMvcConfigurer是 Spring 推荐的非侵入式WebMVC 自定义方式(替代旧的 WebMvcConfigurerAdapter)。

2.核心方法addResourceHandlers:

registry.addResourceHandler("/**").addResourceLocations("classpath:/static/"):将所有前端请求路径映射到项目resources/static/目录(Spring Boot 默认静态资源目录),显式配置让规则更清晰,覆盖默认的松散映射。

registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/static/"):单独映射网站图标请求,避免因静态资源匹配规则问题导致favicon.ico返回 404。

设计细节:

该配置是显式增强:Spring Boot 默认已支持/static/的静态资源访问,此处显式配置是为了明确化规则,方便后续扩展(比如添加自定义静态资源目录)。

优先级说明:控制器的@RequestMapping请求会优先于静态资源映射,因此不会出现/message(WebSocket 路径)被静态资源拦截的问题。

二、WebSocketConfig:WebSocket 核心启用与处理器注册配置类

核心定位:

开启 Spring 的 WebSocket 功能,将自定义的 WebSocket 处理器注册到指定路径,配置跨域、添加握手拦截器,是 WebSocket 长连接的入口配置。

关键代码分析

1.核心注解:

@Configuration:标记为配置类;

@EnableWebSocket:开启 Spring 对 WebSocket 的自动配置支持,是使用 Spring WebSocket 的必备注解。

2.接口与方法:实现WebSocketConfigurer接口,重写registerWebSocketHandlers(WebSocket 处理器注册的核心方法)。

3.核心逻辑:

注入自定义 WebSocket 处理器TextWebSocketAPI(测试用,已注释)和WebSocketAPI(实际业务用);

为/message路径注册webSocketAPI处理器,前端通过ws://ip:port/messageaddInterceptors(newWebSocketHandshakeInterceptor()):为该 WebSocket 路径添加握手拦截器,在连接建立前执行身份提取、参数校验等逻辑;

setAllowedOriginPatterns(""):解决 WebSocket 跨域问题,替代旧的setAllowedOrigins(" ")------ 旧方法在前端携带 Cookie / 凭证时会失效,新方法支持跨域且兼容凭证传递,是 Spring 5.3 + 的推荐写法。

设计细节:

注释的/test路径是开发测试用的临时配置,上线前建议清理,避免无用接口暴露;未配置withSockJS():说明该项目使用原生 WebSocket 协议(ws/wss),而非 SockJS 的降级兼容方案,适合现代浏览器环境(无需兼容低版本浏览器)。

三、WebSocketHandshakeInterceptor:WebSocket 握手拦截器

核心定位:

实现 Spring 的HandshakeInterceptor接口,在WebSocket 握手阶段(连接建立前 / 后)执行自定义逻辑,核心作用是从 HTTP Session 中提取登录用户信息,共享到 WebSocket 的属性中,让后续的 WebSocket 处理器能获取当前连接的用户身份;同时添加了详细的调试日志,方便排错。

关键代码分析

1.接口方法:HandshakeInterceptor包含两个核心方法,执行时机为握手阶段(早于 WebSocket 连接建立):beforeHandshake:握手前执行(核心),返回boolean------true允许握手,false拒绝握手;afterHandshake:握手后执行,仅做回调通知(无返回值),可处理握手后的收尾 / 日志。

2.beforeHandshake核心逻辑:

类型转换:将ServerHttpRequest转为ServletServerHttpRequest,从而获取 Servlet 原生的HttpServletRequest和HttpSession;

调试日志:打印请求 URI、Cookie、SessionID、用户信息等,是排错的关键(比如跨域时 Cookie 未传递、Session 中无用户的问题能快速定位);安全获取 Session:使用getSession(false)而非getSession()------避免为未登录用户创建空的 HTTP Session,减少服务器资源消耗,是 Web 开发的最佳实践;提取用户信息:从 Session 中获取User对象,若存在则放入 WebSocket 的attributes(键值对 Map),让后续的WebSocketAPI处理器能通过WebSocketSession获取该用户;宽松的握手策略:即使 Session 中无用户信息,仍返回true允许握手 ------避免前端因握手失败而无限重连,同时在日志中标记 "用户未登录",兼顾体验和调试。

3.afterHandshake逻辑:仅打印握手结果(成功 / 异常),简单且无侵入性。

设计细节:

无状态拦截:拦截器仅做信息提取和传递,不做业务逻辑处理,符合 "单一职责" 原则;异常友好:捕获并打印握手异常,避免因拦截器报错导致整个 WebSocket 功能崩溃;身份解耦:将用户身份从 HTTP Session 共享到 WebSocket,实现HTTP 会话与WebSocket 会话的身份统一,无需让前端重复传参认证。

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 确保静态资源正确映射
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/");
        
        // 明确处理favicon请求
        registry.addResourceHandler("/favicon.ico")
                .addResourceLocations("classpath:/static/");
    }
}
java 复制代码
@Configuration
@EnableWebSocket //调用WebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TextWebSocketAPI textWebSocketAPI;
    @Autowired
    private WebSocketAPI webSocketAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//        通过此方法将刚创建的Handler类注册到具体的路径下
//        此时浏览器调用/text路径时就会调用textWebSocketAPI中的方法
//        registry.addHandler(textWebSocketAPI,"/test");
        registry.addHandler(webSocketAPI,"/message")
                .addInterceptors(new WebSocketHandshakeInterceptor())
                // 使用 setAllowedOriginPatterns 替代 setAllowedOrigins("*")
                // 这样可以在携带 Cookie 的情况下正常工作
                .setAllowedOriginPatterns("*");
    }
}
java 复制代码
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("WebSocket 握手开始...");
        System.out.println("请求 URI: " + request.getURI());

        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;

            // 打印 Cookie 信息用于调试
            String cookies = servletRequest.getServletRequest().getHeader("Cookie");
            System.out.println("请求 Cookie: " + cookies);

            HttpSession httpSession = servletRequest.getServletRequest().getSession(false);

            System.out.println("HTTP Session: " + (httpSession != null ? httpSession.getId() : "null"));

            if (httpSession != null) {
                User user = (User) httpSession.getAttribute("user");
                System.out.println("HTTP Session 中的用户: " + (user != null ? user.getUserName() + " (ID: " + user.getUserId() + ")" : "null"));

                if (user != null) {
                    attributes.put("user", user);
                    System.out.println("用户信息已复制到 WebSocket attributes");
                    return true;
                } else {
                    System.out.println("HTTP Session 中没有用户信息");
                }
            } else {
                System.out.println("HTTP Session 不存在(可能是跨域或 Cookie 未发送)");
            }
        }

        // 即使没有用户信息也允许连接,但不注册用户
        // 这样可以避免连接一直失败重试
        System.out.println("允许 WebSocket 连接,但用户未登录");
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception exception) {
        if (exception != null) {
            System.out.println("WebSocket 握手异常: " + exception.getMessage());
        } else {
            System.out.println("WebSocket 握手完成");
        }
    }
}
相关推荐
1104.北光c°2 小时前
【从零开始学Redis | 第一篇】Redis常用数据结构与基础
java·开发语言·spring boot·redis·笔记·spring·nosql
阿猿收手吧!2 小时前
【C++】volatile与线程安全:核心区别解析
java·c++·安全
Hui Baby2 小时前
Java SPI 与 Spring SPI
java·python·spring
摇滚侠2 小时前
Maven 教程,Maven 安装及使用,5 小时上手 Maven 又快又稳
java·maven
倔强菜鸟2 小时前
2026.2.2--Jenkins的基本使用
java·运维·jenkins
hai74252 小时前
在 Eclipse 的 JSP 项目中引入 MySQL 驱动
java·mysql·eclipse
瑞雪兆丰年兮3 小时前
[从0开始学Java|第十一天]学生管理系统
java·开发语言
看世界的小gui3 小时前
Jeecgboot通过Maxkey实现单点登录完整方案
java·spring boot·jeecgboot
Arvin6273 小时前
IntelliJ IDEA:无法读取**.properties
java·intellij-idea