文章目录
- 展示:
- 1.项目概述
- 2.项目的pom.xml和yml配置
- 3.项目结构分析:
-
-
- [3.1 分层结构](#3.1 分层结构)
- [3.2 请求与实时消息流](#3.2 请求与实时消息流)
- 3.4核心业务逻辑实现
-
- [4.1 用户与认证](#4.1 用户与认证)
- [4.2 好友关系](#4.2 好友关系)
- [4.3 会话与消息](#4.3 会话与消息)
- [4.4 在线状态与 WebSocket 生命周期](#4.4 在线状态与 WebSocket 生命周期)
- 3.5数据模型设计
-
- [5.1 实体与 DTO(model / pojo)](#5.1 实体与 DTO(model / pojo))
- [5.2 数据库表结构(由 Mapper XML 推断)](#5.2 数据库表结构(由 Mapper XML 推断))
- [3.6API 接口规范](#3.6API 接口规范)
-
- [6.1 HTTP 接口一览](#6.1 HTTP 接口一览)
- [6.2 WebSocket 接口](#6.2 WebSocket 接口)
- [3.7数据库与 Mapper 设计要点](#3.7数据库与 Mapper 设计要点)
-
- 4.关键业务流程:
- 5.部分代码分析(后端代码)
展示:
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包UserController、FriendController、SessionController、MessageController、AddFrinedController、FaviconController。
- 服务层 :
service包UserService、FriendService、SessionService、MessageService、AddFrinedService。
- 数据访问层 :
mapper包(接口)+resources/mapper/*.xml。UserMapper、FriendMapper、SessionMapper、MessageMapper、AddFriendMapper。
- 模型层 :
model、pojo- 业务实体与统一响应
Result。
- 业务实体与统一响应
- 组件层 :
componentOnlineUserManager:维护 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.login用UserMapper.selectOne按 user_name 查询,校验密码后session.setAttribute("user", user),返回用户信息(密码置空)。 - 注册 :
UserController.register接收 userName、password、confirmPassword,UserService.register校验非空与两次密码一致后UserMapper.insert;重复用户名由DuplicateKeyException捕获返回 false。 - 当前用户 :
UserController.getUserInfo→UserService.getUserInfo从HttpServletRequest.getSession(false)取 user,无会话或 user 为 null 时返回 null。
依赖:HttpSession 存储 User,未使用 JWT 等无状态方案;WebSocket 握手通过 WebSocketHandshakeInterceptor 将同一 Session 中的 user 写入 WebSocket attributes,供 WebSocketAPI 与 OnlineUserManager 使用。
4.2 好友关系
- 好友列表 :
FriendController.getFriendList→FriendService.getFriendList从 Session 取当前用户,FriendMapper.selectFriendList(userId)查询好友(从 friend 表关联 user 表得到 friendId、friendName)。 - 搜索用户(找好友) :
AddFrinedController.findFriend→AddFrinedService.findFriend,参数 name 为空则返回空列表;否则AddFriendMapper.findFriend(selfUserId, name)查询非本人且非已是好友的用户,并经由OnlineUserManager与FriendMapper.countMutualFriends补齐在线状态与共同好友数,返回SearchFriendResult列表。 - 发送好友申请 :
AddFrinedController.addFriend→AddFrinedService.addFrined,校验 Session/User 与 friendId,AddFriendMapper.addFriend写入 add_friend_request;若对方在线,通过OnlineUserManager.getSession(friendId)取得 WebSocketSession,发送 type 为 addFriendRequest 的 JSON(fromUserId、fromUserName、reason)。 - 好友申请列表 :
AddFrinedController.getFriendRequest→AddFrinedService.getFriendRequest,从 Session 取当前用户,AddFriendMapper.getFriendRequest(toUserId)查询发给自己的申请(含 fromUserId、fromUserName、reason);Session 或 User 为空时返回空列表。 - 接受好友 :
AddFrinedController.acceptFriend→AddFrinedService.acceptFriend,删除 add_friend_request 中对应记录,FriendMapper.checkFriendExists判断是否已是好友,未则AddFriendMapper.addNewFriend双向插入 friend 表;若对方在线则 WebSocket 推送 type 为 acceptFriend 的消息。 - 拒绝好友 :
AddFrinedController.rejectFriend→AddFrinedService.rejectFriend,仅AddFriendMapper.deleteFriendRequest删除申请记录。
4.3 会话与消息
- 会话列表 :
SessionController.getSessionList→SessionService.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.getMessage→MessageService.getMessage,MessageMapper.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)供推送消息与查询在线状态使用。 - WebSocketAPI :
afterConnectionEstablished中若 attributes 中有 user 则调用onlineUserManager.online;handleMessage中再次确保 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 握手完成");
}
}
}