✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯 你正在阅读「Java项目-轻聊」系列文章 🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
✨ 靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:

文章目录:
- 用户管理模块
-
- [1. 模块职责](#1. 模块职责)
- 2、获取用户信息
-
- [2.1 约定前后端交互的接口](#2.1 约定前后端交互的接口)
- [2.2 一图理清业务逻辑](#2.2 一图理清业务逻辑)
- [2.3 后端实现](#2.3 后端实现)
-
- [2.3.1 控制层实现](#2.3.1 控制层实现)
- [2.3.2 服务层实现](#2.3.2 服务层实现)
- [2.3.3 持久层实现](#2.3.3 持久层实现)
- [2.3.4 使用Postman测试](#2.3.4 使用Postman测试)
- [2.4 前端实现](#2.4 前端实现)
- [2.5 测试](#2.5 测试)
- 3、上传头像
-
- [3.1 约定前后端交互接口](#3.1 约定前后端交互接口)
- [3.2 一图理清业务逻辑](#3.2 一图理清业务逻辑)
- [3.3 后端实现](#3.3 后端实现)
-
- [3.3.1 配置 `application.yml`](#3.3.1 配置
application.yml) - [3.3.2 文件配置类的实现](#3.3.2 文件配置类的实现)
- [3.3.3 解释配置文件和配置类关系](#3.3.3 解释配置文件和配置类关系)
- [3.3.4 实现控制层](#3.3.4 实现控制层)
- [3.3.5 实现服务层](#3.3.5 实现服务层)
- [3.3.6 实行持久层](#3.3.6 实行持久层)
- [3.3.7 测试](#3.3.7 测试)
- [3.3.1 配置 `application.yml`](#3.3.1 配置
- [3.4 前端实现](#3.4 前端实现)
- 4、获取头像并显示
-
- [4.1 约定前后端交互接口](#4.1 约定前后端交互接口)
-
- [方式 A:静态路径(直接访问服务器上的静态资源)](#方式 A:静态路径(直接访问服务器上的静态资源))
- [方式 B:接口兜底](#方式 B:接口兜底)
- [4.2 一图理清业务逻辑](#4.2 一图理清业务逻辑)
- [4.3 后端实现](#4.3 后端实现)
-
- [4.3.1 配置类实现-直接访问静态资源](#4.3.1 配置类实现-直接访问静态资源)
- [4.3.2 控制层实现](#4.3.2 控制层实现)
- [4.3.3 服务层实现](#4.3.3 服务层实现)
- [4.3.4 持久层实现](#4.3.4 持久层实现)
- [4.3.5 使用Postman进行接口测试](#4.3.5 使用Postman进行接口测试)
- [4.4 前端实现](#4.4 前端实现)
- [4.5 测试](#4.5 测试)
用户管理模块
1. 模块职责
我们首先来梳理一下我们的用户管理模块需要实现哪一些功能?
| 能力 | 后端 | 前端 |
|---|---|---|
| 注册 | POST /register |
register.html |
| 登录 | POST /login |
login.html |
| 当前用户 | GET /userInfo |
client.js → getUserInfo() |
| 退出 | POST /logout |
client.js → initLogoutButton() |
| 上传头像 | POST /user/uploadAvatar |
#avatar-file-input + initAvatarUpload() |
| 读头像 | GET /user/getAvatar 或 /avatars/** |
avatarSrc() |
| 搜用户 | GET /search/user |
searchUser() |
那么上述的功能中,对于注册和登录我们已经实现过了,我们本期就是主要实现的是剩余的头像上传获取以及用户信息3个功能,其中退出登录和我们的搜索用户我们等整个项目做完毕差不多的时候再去实现。
登录态 :HttpSession,属性名 "user",值为 User 对象。浏览器自动携带 JSESSIONID Cookie。
无全局登录拦截器 :各接口自行读 Session;client.html 靠 /userInfo 的 userId 是否为 0 判断是否跳转登录页。注意这个点我们在之前的登录和注册中提到过的。
2、获取用户信息
2.1 约定前后端交互的接口
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /userInfo |
| 是否需要登录 | 需要 Session(浏览器自动带 Cookie) |
| 请求参数 | 无 |
| 成功响应示例 | { "userId": 1, "username": "张三", "password": "", "avatarPath": "/avatars/abc.jpg" } |
| 未登录响应 | { "userId": 0, "username": "", "password": "", "avatarPath": null } |
2.2 一图理清业务逻辑

2.3 后端实现
由于没有请求参数,我们就不需要param实体类了,返回对象我们直接使用的是Object,我们本项目暂时不做统一数据返回格式。
2.3.1 控制层实现
java
@GetMapping("/userInfo") // 获取当前登录用户资料(含在线状态等扩展字段)
public Object getUserInfo(HttpServletRequest req) { // 依赖 Session 中的 user
HttpSession session = req.getSession(false); // 不自动创建会话
if (session == null) { // 未登录
System.out.println("[getUserInfo]当前获取不到用户对象"); // 调试日志
return new User(); // 空用户表示未登录
}
User user = (User) session.getAttribute("user"); // 读取会话中的登录用户
if (user == null) { // 会话存在但未写入 user(异常或过期残留)
System.out.println("[getUserInfo]当前获取不到用户对象"); // 调试日志
return new User(); // 同样返回空用户
}
return userService.getUserInfo(user); // 补充在线状态、头像路径等业务字段后返回
}

2.3.2 服务层实现
java
@Override // 实现获取用户信息
public User getUserInfo(User sessionUser) {
if (sessionUser == null) { // 会话用户为空
return new User(); // 返回空用户对象
}
User dbUser = userMapper.selectById(sessionUser.getUserId()); // 从数据库查询最新用户数据
if (dbUser != null) { // 数据库中存在该用户
sessionUser.setAvatarPath(dbUser.getAvatarPath()); // 同步最新头像路径到会话用户
}
sessionUser.setPassword(""); // 清空密码,防止泄露
return sessionUser; // 返回更新后的会话用户
}

2.3.3 持久层实现
java
/**
* 按主键查用户(头像、消息推送等)
*
* @param userId 登录表单传入的用户 id
* @return 存在则返回完整 User(含 password);不存在返回 null
*/
User selectById(int userId);
<select id="selectById" resultType="com.zhongge.web_chatroom.dao.dataobject.User">
select userId, username, password, avatar_path as avatarPath
from user where userId = #{userId}
</select>
2.3.4 使用Postman测试
首先先登录,使得后端返回sessionid让Postman存起来

然后再测试获取用户数据看是否和我们的接口文档中结果一致

2.4 前端实现
前端我们主要就是实现ajax请求前后端交互接口
首先在页面一加载就去请求后端获取用户信息

js
//////////////////////////////////////////////////
/// 用户信息与列表加载
//////////////////////////////////////////////////
// 页面加载时拉取当前登录用户,成功则初始化 WebSocket 与列表
function getUserInfo() {
$.ajax({
method: 'get',
url: '/userInfo', // Session 中的用户信息
success: function(body) {
if (body.userId && body.userId > 0) {
let userBar = document.querySelector('.main .left .user');
userBar.querySelector('.user-name').textContent = body.username;
} else {
alert("当前用户未登录");
location.assign("/login.html"); // 未登录跳转
}
}
});
}

2.5 测试

点击确定之后将会跳到登录页面

3、上传头像
3.1 约定前后端交互接口
| 项目 | 内容 |
|---|---|
| 方法 | POST |
| 路径 | /user/uploadAvatar |
| Content-Type | multipart/form-data |
| 字段 | file(图片) |
| 成功 | { "ok": true, "avatarPath": "/avatars/xxx.jpg", "message": "头像上传成功" } |
| 失败 | { "ok": false, "message": "仅支持 jpg、jpeg、png 格式" } 等 |
3.2 一图理清业务逻辑

3.3 后端实现
3.3.1 配置 application.yml
这些配置文件,我们只需要直接粘贴就行了
bash
# ===================== 头像上传自定义配置 =====================
# 项目自定义:用户头像上传相关配置
avatar:
# 头像文件本地存储路径(项目根目录 upload/avatars,不放入resources避免打包冲突)
upload-dir: upload/avatars
# 单个头像文件最大大小(单位:MB)
max-size-mb: 2
# 用户未设置头像时,默认显示的头像访问路径
default-web-path: /img/default-avatar.svg
# ===================== Spring 核心配置 =====================
spring:
# 文件上传配置(SpringMVC)
servlet:
multipart:
# 单个文件最大限制(必须和上面avatar.max-size-mb保持一致)
max-file-size: 2MB
# 单次请求最大上传总大小
max-request-size: 2MB
3.3.2 文件配置类的实现

java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 这个类的作用:
* 专门管理【头像上传的所有配置】
* 比如:头像存在哪里?最大能传多大?默认头像用哪张?
* 相当于:头像配置的总管
*/
@Component // 交给 Spring 管理,别的地方可以直接拿来用
@ConfigurationProperties(prefix = "avatar") // 从 application.yml 读取以 avatar 开头的配置
public class AvatarProperties {
// ====================== 1. 配置项 ======================
// 头像保存的文件夹(默认:项目目录下的 upload/avatars)
private String uploadDir = "upload/avatars";
// 头像最大大小(默认 2MB)
private int maxSizeMb = 2;
// 用户没传头像时,显示的默认头像
private String defaultWebPath = "/img/default-avatar.svg";
// ====================== 2. 内部使用的路径 ======================
// 最终解析好的【绝对文件夹路径】
private Path avatarDir;
// ====================== 3. 项目启动时自动执行 ======================
/**
* 项目一启动,就自动执行这个方法:
* 1. 把配置的路径转成 绝对路径
* 2. 自动创建文件夹(不存在就新建)
*/
@PostConstruct
public void initStorageDir() throws IOException {
// 解析真实路径
avatarDir = resolveUploadDir();
// 自动创建文件夹(如果没有就新建)
Files.createDirectories(avatarDir);
}
// ====================== 4. 解析路径(相对 → 绝对) ======================
/**
* 把配置里写的相对路径,变成电脑能识别的绝对路径
* 比如:upload/avatars → D:/项目/upload/avatars
*/
public Path resolveUploadDir() {
Path configured = Paths.get(uploadDir);
// 如果是绝对路径,直接返回
if (configured.isAbsolute()) {
return configured.normalize();
}
// 如果是相对路径,拼接成:项目运行目录 + 配置目录
return Paths.get(System.getProperty("user.dir"), uploadDir)
.toAbsolutePath()
.normalize();
}
// ====================== 5. 给别人用的 Get/Set ======================
// 获取头像存储的绝对路径
public Path getAvatarDir() {
if (avatarDir == null) {
avatarDir = resolveUploadDir();
}
return avatarDir;
}
// 配置文件读取用
public String getUploadDir() {
return uploadDir;
}
public void setUploadDir(String uploadDir) {
this.uploadDir = uploadDir;
}
// 头像大小限制
public int getMaxSizeMb() {
return maxSizeMb;
}
public void setMaxSizeMb(int maxSizeMb) {
this.maxSizeMb = maxSizeMb;
}
// 默认头像
public String getDefaultWebPath() {
return defaultWebPath;
}
public void setDefaultWebPath(String defaultWebPath) {
this.defaultWebPath = defaultWebPath;
}
}
3.3.3 解释配置文件和配置类关系
为什么在application.yml写了对应的值,而在我们的配置类中还要写呢?

首先我们需要了解一下这和配置类是干什么的?
主要做 4 件事:
-
读取配置
从
application.yml读取:- 头像存在哪个文件夹
- 最大多大
- 默认头像路径
-
自动创建文件夹
项目一启动,自动创建
upload/avatars目录,不用你手动建。 -
把相对路径 → 绝对路径
比如:
upload/avatars变成
D:/idea项目/chatroom/upload/avatars为什么要将相对路径变为绝对路径呢?原因如下:
相对路径 = 不知道在哪
绝对路径 = 精准知道在哪
Spring 保存文件、读取文件、映射网址访问文件,必须用绝对路径!
-
给整个项目提供统一配置
上传、显示、配置都用这一个类,不乱。
那么为什么private String uploadDir = "upload/avatars";
你这个不是自己赋值么?为什么读取配置文件呀
答:作用就是 【有配置就读配置,没配置就用默认】 ,同时注意,他之所以可以读取配置文件是因为,他有@ConfigurationProperties(prefix = "avatar")这个代码,所以他才可以读取配置文件的。而且还要满足一下两点,才能读取配置文件:


所以我们在后面写了,对应的get和set方法,不管有没有用,先加上再说

代码解释

3.3.4 实现控制层
首先你要上传头像,那么你就得知道我们谁可以上传我们的头像?毫无疑问我们只有用户自己才可以去上传我们的头像,就这个而言,我们就需要指定你要上传的文件,同时要指定哪一个用户,他们使用表单的形式上传。
首先,我们控制层的每一个接口都是要从请求体中获取sessionid根据他看我们的session对象中是否有用户对象,那么每一个类都这么写,会比较麻烦,所以我们就将这个获取用户对象的行为封装为一个方法供给控制层复用
java
/**
* 封装控制层复用用户获取是否该用户对象
* @param req 请求体对象
* @return
*/
private User getLoginUser(HttpServletRequest req) { // 控制器内复用:从 Session 取登录用户
HttpSession session = req.getSession(false); // 不创建新 Session
if (session == null) { // 无会话即未登录
return null; // 调用方按 null 处理
}
return (User) session.getAttribute("user"); // 可能为 null(会话无效)
}
控制层的实现思路如下:

java
/**
* 上传并更新用户头像
* @param file 文件
* @param userId 用户id
* @param req 请求体
* @return
*/
@PostMapping("user/uploadAvatar")
public Object uploadAvatar(@RequestParam("file") MultipartFile file, //我们上传的头像文件
@RequestParam("userId") int userId, //哪一个用户上传的
HttpServletRequest req){ //看里面的session是否是该用户
//首先我们得看这个用户是否已经登录过了
User loginUser = getLoginUser(req);
if (loginUser == null) {
//如果你的用户是空的,那么你就没有登录过,我就告诉你用户未登录
//此时我返回的数据是无用数据,即使你黑客调用我这个接口,那么你得到的数据也是无效数据
Map<String, Object> resp = new HashMap<>();
resp.put("ok", false);
resp.put("msg", "未登录");
return resp;//将这个无效数据返回给前端,前端根据提供的这个线索来让用户去登录
}
//如果用户已经登录过了,那么我就看这个用户的用户id是否和session对象里面的userid一致
//因为需要保证我们用户自己只允许修改自己的头像
if (loginUser.getUserId() != userId) {
Map<String, Object> resp = new HashMap<>();
resp.put("ok", false);
resp.put("msg", "只能修改自己的头像");
return resp;
}
//到此处,我们就去调用我们的服务层实现具体的上传头像的逻辑
return userService.uploadAvatar(file, userId);
}
3.3.5 实现服务层
接口定义:
java
/**
* 上传用户头像
*
* @param file 头像文件
* @param userId 用户id
* @return 前端需要的响应体
*/
Map<String, Object> uploadAvatar(MultipartFile file, int userId);
服务层实现逻辑:
1.参数校验:控制层给我的文件对象和用户id
2.校验文件的后缀是否符合
3.校验文件的大小是否符合
4.将文件保存到服务器中
5.将文件相对路径保存到数据库中
6.构造响应数据
我只允许上传的文件类型:
java
// 允许上传的头像文件扩展名白名单
private static final Set<String> ALLOWED_EXT =
new HashSet<>(
Arrays.asList("jpg", "jpeg", "png")
);
要将文件配置类导入进来,我们会用到里面的方法
java
@Resource
private AvatarProperties avatarProperties;
文件存到磁盘的时候,需要的是绝对路径,在文件配置类中,这个绝对路径我们项目一启动就已经获取到了,此时我们就直接从配置类中获取即可。

那么我们后面将文件存于磁盘的时候,我们会先将原来的就文件删除,怎么删除呢?会先获取到用户对象中的头像路径,这个头像路径是相对路径(我们不会存绝对路径的,因为扩展性不好),但是此时我们存相对路径而我们删除头像需要的是绝对路径,所以此时就需要一个方法将相对路径变为绝对路径,如下所示:
java
/**
* 将相对路径变为绝对路径
* @param avatarPath 相对路径
* @return 绝对路径
*/
private Path resolveAvatarFile(String avatarPath) {
if (avatarPath == null || avatarPath.trim().isEmpty()) { // 路径为空
return null; // 无头像
}
String normalized = avatarPath.trim(); // 去除首尾空白
String fileName; // 实际文件名
if (normalized.startsWith("/avatars/")) { // Web 路径格式 /avatars/xxx.png
fileName = normalized.substring("/avatars/".length()); // 截取文件名部分
} else { // 其他路径格式
fileName = Paths.get(normalized).getFileName().toString(); // 取路径最后一段作为文件名
}
Path disk = avatarDir().resolve(fileName); // 拼接上传目录下的完整路径
if (Files.isRegularFile(disk)) { // 上传目录中存在该文件
return disk; // 返回磁盘路径
}
try {
ClassPathResource cp = new ClassPathResource("static/avatars/" + fileName); // 尝试从 classpath 静态资源查找
if (cp.exists()) { // 静态资源中存在
return cp.getFile().toPath(); // 返回 classpath 文件路径
}
} catch (IOException ignored) { // 读取 classpath 资源失败则忽略
}
return null; // 未找到头像文件
}
服务层代码:
java
@Override//实现头像上传
public Map<String, Object> uploadAvatar(MultipartFile file, int userId) {
Map<String, Object> resp = new HashMap<>();
/**
* 1.参数校验:控制层给我的文件对象和用户id
*/
//为了程序的健壮性,那么我们首先会看你控制层给我的这个文件是否为空
if (file == null || file.isEmpty()) {
resp.put("ok", false);
resp.put("message", "请选择要上传的图片");
return resp;
}
//你控制层给我一个userId,但是我得校验这个userId是否存在,此时怎么办呢?
//此时我就会将你控制层给我的这个userId拿去数据库中查询,看是否有这个用户存在,如果存在我就返回
//我们读取到的这个用户,到时候用于获取他里面的用户头像
User user = userMapper.selectById(userId);
if (user == null) { //用户不存在
resp.put("ok", false);
resp.put("message", "用户不存在");
return resp;
}
/**
* 2.校验文件的后缀是否符合
*/
//获取原始的文件名
String originalFilename = file.getOriginalFilename();
//将这个原始的文件名 按照 . 进行分割 获取到我们的文件扩展名
String ext = resolveExtension(originalFilename);
if (ext == null || !ALLOWED_EXT.contains(ext)) {
resp.put("ok", false);
resp.put("message", "仅支持 jpg、jpeg、png 格式的图片");
return resp;
}
/**
* 3.校验文件的大小是否符合 在文件配置类中去获取一下对应的文件大小
*/
long maxBytes = avatarProperties.getMaxSizeMb() * 1024L * 1024L; // 计算最大允许字节数
if (file.getSize() > maxBytes) { // 文件超出大小限制
resp.put("ok", false); // 标记失败
resp.put("message", "图片大小不能超过 " + avatarProperties.getMaxSizeMb() + "MB"); // 提示大小限制
return resp; // 提前返回
}
/**
* 4.将文件保存到服务器中
*/
//获取唯一的文件对象
String fileName = UUID.randomUUID()//获取UUID对象
.toString()//将这个对象变为字符串(含有"-")
.replace("-", "") + "." + ext; // 将"-"变为"" 并加扩展名
Path path = avatarDir();
//获取到绝对路径之后,我们得将这个文件名给拼接到我们的绝对路径上
Path target = path.resolve(fileName); // 拼接文件名
try {
//得确保我们的文件目录是存在的,不存在我就创建
Files.createDirectories(target.getParent()); // 确保头像目录存在
file.transferTo(target.toFile()); // 将上传文件写入磁盘
} catch (IOException e) { // 保存文件失败
resp.put("ok", false); // 标记失败
resp.put("message", "保存图片失败:" + e.getMessage()); // 返回 IO 错误信息
return resp; // 提前返回
}
/**
* 5.将文件相对路径保存到数据库中
*/
String webPath = "/avatars/" + fileName;//相对路径从我们的/avatars/开始(这个具体是看项目需求的)
deleteOldAvatarFile(user.getAvatarPath()); // 删除旧头像文件,避免磁盘冗余
userMapper.updateAvatarPath(userId, webPath); // 更新数据库中的头像路径
/**
* 6.构造响应数据
*/
resp.put("ok", true); // 标记上传成功
resp.put("avatarPath", webPath); // 返回头像路径
resp.put("avatarUrl", webPath); // 返回头像 URL(与路径相同)
resp.put("message", "头像上传成功"); // 成功提示
return resp; // 返回完整响应
}
/**
* 删除旧头像文件
* @param oldAvatarPath 旧头像路径
*/
private void deleteOldAvatarFile(String oldAvatarPath) {
Path old = resolveAvatarFile(oldAvatarPath); // 解析旧头像路径
if (old != null && Files.isRegularFile(old)) { // 旧文件存在
try {
Files.delete(old); // 删除旧头像文件
} catch (IOException ignored) { // 删除失败不影响新头像上传
}
}
}
/**
* 将相对路径变为绝对路径
* @param avatarPath 相对路径
* @return 绝对路径
*/
private Path resolveAvatarFile(String avatarPath) {
if (avatarPath == null || avatarPath.trim().isEmpty()) { // 路径为空
return null; // 无头像
}
String normalized = avatarPath.trim(); // 去除首尾空白
String fileName; // 实际文件名
if (normalized.startsWith("/avatars/")) { // Web 路径格式 /avatars/xxx.png
fileName = normalized.substring("/avatars/".length()); // 截取文件名部分
} else { // 其他路径格式
fileName = Paths.get(normalized).getFileName().toString(); // 取路径最后一段作为文件名
}
Path disk = avatarDir().resolve(fileName); // 拼接上传目录下的完整路径
if (Files.isRegularFile(disk)) { // 上传目录中存在该文件
return disk; // 返回磁盘路径
}
try {
ClassPathResource cp = new ClassPathResource("static/avatars/" + fileName); // 尝试从 classpath 静态资源查找
if (cp.exists()) { // 静态资源中存在
return cp.getFile().toPath(); // 返回 classpath 文件路径
}
} catch (IOException ignored) { // 读取 classpath 资源失败则忽略
}
return null; // 未找到头像文件
}
/**
* 获取文件扩展名
* @param originalFilename 原始文件的名字
* @return 文件扩展名
*/
private String resolveExtension(String originalFilename) {
//校验
if (originalFilename == null
|| !originalFilename.contains(".")) {
return null;
}
return originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
}
/**
* 获取头像存储目录
* @return 返回的是路径对象
*/
private Path avatarDir() {
return avatarProperties.getAvatarDir(); // 从配置读取头像根目录
}
3.3.6 实行持久层
java
/**
* 更新用户头像路径
* @param userId 用户 id
* @param avatarPath 头像在 web 服务器上的路径 即 相对路径
*/
int updateAvatarPath(@Param("userId") int userId, @Param("avatarPath") String avatarPath);
xml
<update id="updateAvatarPath">
update user set avatar_path = #{avatarPath} where userId = #{userId}
</update>
3.3.7 测试



3.4 前端实现
你要知道,我们前端是需要上传用户id的,核心目的就是为了我们的用户只能操作自己的头像。
那么问题就是你这个用户id从哪里获取到呢?
还记得我们本期的第一个功能么?就是获取用户信息,那么在用户信息中就有我们的userId,OK此时我们核心要考虑的就是你和这个用户id怎么存储起来。
那么方法也很简单,就是在我们的获取用户信息的时候,就在我们的元素的属性中多加一个属性,用户存储我们后端返回的userId,如下所示:
js
//将用户id存起来,以便于后续我们的使用:比如上传头像或者消息未读取等等
userBar.setAttribute('user-id', body.userId);


前端代码:
js
/**
* 头像上传:选择图片后调用 /user/uploadAvatar
*/
// 选择图片后 multipart 上传头像
function initAvatarUpload() {
let fileInput = document.querySelector('#avatar-file-input'); // 隐藏 file 输入
fileInput.onchange = function() {
let file = fileInput.files[0]; // 用户选中的第一张图
if (!file) {
return; // 取消选择
}
let userBar = document.querySelector('#user-bar');
let userId = userBar.getAttribute('user-id');
if (!userId) {
alert('请先登录');
return;
}
let formData = new FormData(); // multipart 表单
formData.append('file', file); // 文件字段
formData.append('userId', userId); // 用户 ID 字段
$.ajax({
url: '/user/uploadAvatar',
method: 'post',
data: formData,
processData: false, // 不序列化 FormData
contentType: false, // 让浏览器设置 multipart 边界
success: function(body) {
if (body.ok) {
alert(body.message || '头像上传成功');
} else {
alert(body.message || '上传失败');
}
},
error: function() {
alert('头像上传失败,请稍后重试');
}
});
fileInput.value = ''; // 允许再次选同一文件
};
}
// DOM 就绪后初始化各模块 -- 记得调用函数否则不生效
$(document).ready(function() {
initSwitchTab(); // 会话/好友 标签Tab切换
getUserInfo();//获取用户信息
initAvatarUpload(); // 头像上传
});

4、获取头像并显示
4.1 约定前后端交互接口
方式 A:静态路径(直接访问服务器上的静态资源)
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /avatars/{文件名} |
| 说明 | 不走 Controller,由 WebMvcConfig 映射磁盘文件 |
方式 B:接口兜底
| 项目 | 内容 |
|---|---|
| 方法 | GET |
| 路径 | /user/getAvatar?userId=1 |
| 响应 | 图片二进制(Content-Type: image/jpeg 等) |
| 无头像时 | 返回 default-avatar.svg |
4.2 一图理清业务逻辑

4.3 后端实现
目前我们取头像显示头像,它的一个逻辑有两处关键要点:
- 第一处获取用户信息的时候,我们当初后端是返回了一个图像的相对路径的,那我们在此处可以将相对路径保存在我们的标签属性中,然后呢通过这个相对路径,直接去后端里面请求我们的头像。也就是说获取用户信息的时候,同时会刷新我们的头像。
- 第2处就是我们的头像上传成功之后,我们会返回用户头像的相对路径,那我们上传成功之后,应该自动显示你上传成功之后的头像。
4.3.1 配置类实现-直接访问静态资源

代码:
java
import org.springframework.context.annotation.Configuration; // 配置类注解
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; // 静态资源 URL 与磁盘路径映射
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // MVC 定制接口
/**
* 头像静态资源访问配置类
* 作用:把本地磁盘上的【头像文件夹】映射成可以通过浏览器 URL 直接访问的路径
* 例如:浏览器访问 http://localhost:8080/avatars/123.jpg
* 就会去本地磁盘的 avatarDir 目录下找 123.jpg 这个文件
*/
@Configuration
// 标记这是一个 Spring 配置类,项目启动时 Spring 会自动加载这个类
public class WebMvcConfig implements WebMvcConfigurer {
// 实现 WebMvcConfigurer 接口:用于自定义 SpringMVC 的配置
// 这里用来添加【静态资源映射】,访问直接走资源处理器,不经过 Controller
// 头像配置属性类
// 从 application.yml 中读取配置:头像存储路径、文件大小限制等
private final AvatarProperties avatarProperties;
/**
* 构造方法注入 AvatarProperties
* Spring 推荐:构造器注入(比 @Autowired 更安全、更规范)
*/
public WebMvcConfig(AvatarProperties avatarProperties) {
this.avatarProperties = avatarProperties;
}
/**
* 重写:添加静态资源处理器(核心方法)
* 作用:把 URL 路径 和 本地磁盘路径 做映射绑定
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// ===================== 步骤1:拿到本地头像目录,转成 Spring 能识别的格式 =====================
// avatarProperties.getAvatarDir() → 返回的是 Path 对象(Java NIO 文件路径)
// .toUri().toString() → 转成 Spring 识别的路径格式:file:/D:/project/upload/avatars/
String location = avatarProperties.getAvatarDir().toUri().toString();
// Spring 静态资源映射 强制要求:本地目录路径必须以 / 结尾
// 如果配置的路径末尾没有 /,手动补上,否则会找不到文件
if (!location.endsWith("/")) {
location = location + "/";
}
// ===================== 步骤2:注册映射规则 =====================
registry
// 1. 对外暴露的 URL 访问规则
// 浏览器访问:/avatars/xxx、/avatars/a/b/xxx 都会进入这个映射
.addResourceHandler("/avatars/**")
// 2. 映射到本地磁盘的真实路径
// 访问 /avatars/** → 去上面拼接的 location 磁盘目录找文件
.addResourceLocations(location);
}
}
我们为什么要做这个配置类呢?因为是我们获取用户信息或者是上传头像成功之后,后端都会返回一个头像的相对路径,那此时你就可以直接通过这个相对路径去我们的后端,访问我们头像的静态资源。
比如后端返回静态头像相对路径:/avatars/bd14bc28a7d947d49aa27d77cffff827.jpg
那么你直接拿这个相对路径拼接在我们的url上面,然后呢去后端访问图片的资源,可是我们后端它静态静态资源得用绝对路径来访问。
此时涉及的这个相对路径转变为我们绝对路径,就用这个配置来实现。
他最大的一个作用就是你不用去我们的控制层,不用通过用户id去数据库里面访问出它的一个静态路径,然后呢,再通过这个静态路径去拼接出我们的绝对路径,再把我们的图像返回给前端。
4.3.2 控制层实现
上述我们实现的是前端直接通过相对路径来访问后端的头像资源。
接下来我们实现如果前端没有相对路径的话,我们得通过控制层Controller,然后前端根据用户id我们会从数据库中查询出对用户的头像的相对路径,然后再通过相对路径,把它转变为我们头像所在的绝对路径,然后把这个头像文件作为一个流给前端。【注意你的前端的img标签里面,它的src属性,你传头像的路径或者是头像本身这个数据它都是可以显示的。】
大概的业务逻辑如下:

了解完业务逻辑之后,我们就直接上手代码:
代码:
java
/**
* 根据用户 ID 获取头像(二进制流)
* @param userId 用户 ID
* @return 头像信息(二进制流)
*/
@GetMapping("/user/getAvatar")
public ResponseEntity<org.springframework.core.io.Resource> getAvatar(@RequestParam("userId") int userId) { // 无头像时返回默认图
return userService.getAvatarResponse(userId); // 构造带 Content-Type 的文件响应
}
解释:
ResponseEntity<Resource>这两个类有什么作用?




4.3.3 服务层实现
代码
java
/****************************** 获取头像相关 ***************************/
/**
* 获取头像
* @param userId 用户 ID
* @return 返回头像二进制数据信息(给浏览器直接显示)
*/
@Override
public ResponseEntity<Resource> getAvatarResponse(int userId) {
// 1. 校验用户ID是否合法:小于等于0 属于无效ID
if (userId <= 0) {
// 无效用户 → 直接返回默认头像
return buildDefaultAvatarResponse();
}
// 2. 根据用户ID查询数据库,拿到用户信息
User user = userMapper.selectById(userId);
// 3. 解析头像路径:
// 如果用户存在 → 用用户的avatarPath
// 如果用户不存在 → 传null
// resolveAvatarFile:把数据库存的相对路径 → 转为服务器磁盘绝对路径
Path imagePath = resolveAvatarFile(user != null ? user.getAvatarPath() : null);
// 4. 判断解析出来的路径是否有效,且磁盘上确实存在这个文件
if (imagePath != null && Files.isRegularFile(imagePath)) {
// 文件存在 → 返回用户真实头像
return buildFileResponse(imagePath);
}
// 5. 走到这里说明:
// 用户没设置头像 / 头像文件被删了 / 用户不存在
// 统一返回默认头像
return buildDefaultAvatarResponse();
}
// ===================== 构建【默认头像】的HTTP响应 =====================
// 作用:用户没有头像时,返回项目里自带的默认SVG头像
private ResponseEntity<Resource> buildDefaultAvatarResponse() {
try {
// 从项目的 resources/static/img/ 目录下加载默认头像文件
// ClassPathResource:专门读取项目内部资源文件
ClassPathResource defaultRes = new ClassPathResource("static/img/default-avatar.svg");
// 判断默认头像文件是否存在
if (!defaultRes.exists()) {
// 不存在 → 返回404
return ResponseEntity.notFound().build();
}
// 构造HTTP响应,返回默认头像
return ResponseEntity.ok() // HTTP 200 成功
.header(HttpHeaders.CACHE_CONTROL, "max-age=3600") // 缓存1小时,默认头像不常换
.contentType(MediaType.parseMediaType("image/svg+xml")) // 告诉浏览器这是SVG图片
.body(defaultRes); // 把默认头像文件放进响应体
} catch (Exception e) {
// 读取文件发生异常 → 返回404
return ResponseEntity.notFound().build();
}
}
// ===================== 构建【用户自定义头像】的HTTP响应 =====================
// 作用:返回用户上传到磁盘的真实头像
private ResponseEntity<Resource> buildFileResponse(Path imagePath) {
// 把磁盘路径包装成Spring能识别的文件资源
FileSystemResource resource = new FileSystemResource(imagePath.toFile());
// 构造HTTP响应返回头像
return ResponseEntity.ok() // HTTP 200 成功
.header(HttpHeaders.CACHE_CONTROL, "max-age=300") // 缓存5分钟,方便更换头像后刷新
.contentType(probeMediaType(imagePath)) // 自动识别图片格式:jpg/png/svg
.body(resource); // 把图片文件流放进响应体,返回给前端
}
// ===================== 根据文件后缀自动识别图片类型 =====================
private MediaType probeMediaType(Path path) {
// 获取文件名并转为小写,避免大小写问题
String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".png")) { // 文件是PNG
return MediaType.IMAGE_PNG;
}
if (name.endsWith(".svg")) { // 文件是SVG矢量图
return MediaType.parseMediaType("image/svg+xml");
}
// 其他都按 JPG/JPEG 处理
return MediaType.IMAGE_JPEG;
}
/****************************** 获取头像结束 ***************************/
4.3.4 持久层实现
java
/**
* 按主键查用户(头像、消息推送等)
*
* @param userId 登录表单传入的用户 id
* @return 存在则返回完整 User(含 password);不存在返回 null
*/
User selectById(int userId);
xml
<select id="selectById" resultType="com.zhongge.web_chatroom.dao.dataobject.User">
select userId, username, password, avatar_path as avatarPath
from user where userId = #{userId}
</select>
4.3.5 使用Postman进行接口测试
首先模拟前端有相对路径,然后我们不走Controller,直接通过webconfig类,将相对路径变为绝对路径,访问我们的头像资源。

其次就是没有相对路径的时候,那么我们就得走Controller

4.4 前端实现
前端的实现的话,其实就是补充显示头像,这样的一个业务逻辑。
核心就是我们在获取用户数据的时候以及上传完头像之后,你得把相对路径给存到节点的属性之中,之后呢,我们得把用户的id以及我们的头像的相对路径给他传递到一个方法中,这个方法主要做什么?
这个方法的作用就是给我们去填充头像标签中的src属性的,如果你有相对路径的话,我直接就给你填充进去,如果你没有相对路径,我就去请求后端来获取到我们的图像数据。
直接文字描述显得很复杂,我们边上手代码,变解释
1、在获取用户信息中新增以下代码:
js
if (body.avatarPath) {
userBar.setAttribute('data-avatar-path', body.avatarPath);
}
refreshMyAvatar(body.userId, body.avatarPath, false);

2、在我们的上传头像中新增以下代码:
js
if (body.avatarPath) {
userBar.setAttribute('data-avatar-path', body.avatarPath); // 供发送消息带头像
}
refreshMyAvatar(userId, body.avatarPath, true); // 顶栏立即更新

3、头像显示的代码方法:
js
/** 前端默认头像(加载失败时兜底) */
const DEFAULT_AVATAR_SRC = '/img/default-avatar.svg'; // 静态默认头像路径
/**
* 刷新页面顶部「我的头像」图片显示
* @param userId 用户ID(传给后端获取头像用)
* @param avatarPath 数据库里存的头像路径(/avatars/xxx.jpg)
* @param bustCache 是否强制破除缓存(上传新头像后必须传 true)
*/
function refreshMyAvatar(userId, avatarPath, bustCache) {
// 1. 找到页面上 id="my-avatar" 的头像 <img> 标签
let img = document.querySelector('#my-avatar');
// 2. 如果页面上没有这个头像元素(DOM 不存在),直接退出,不执行后面逻辑
if (!img) {
return;
}
// 3. 调用 avatarSrc() 方法,计算出最终要显示的头像 URL
let url = avatarSrc(userId, avatarPath);
// 4. 如果需要破缓存(刚上传完头像时 bustCache = true)
if (bustCache) {
// 给 URL 拼接时间戳:?t=1735689123456
// 如果 URL 已经有 ? 就用 & 拼接,没有就用 ? 拼接
url += (url.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now();
}
// 5. 把新的图片地址赋值给 img 标签,页面立刻刷新显示新头像
img.src = url;
}
/**
* 【核心逻辑】计算头像的最终显示地址(两套方案自动切换)
* 方案1:有头像路径 → 直接用静态地址 /avatars/xxx.jpg
* 方案2:无路径 → 走后端接口 /user/getAvatar?userId=xxx
* @param userId 用户ID
* @param avatarPath 数据库存储的头像相对路径
* @return 最终可直接赋值给 img.src 的地址
*/
function avatarSrc(userId, avatarPath) {
// ===================== 方案1:优先使用数据库里存的头像路径 =====================
// 如果 avatarPath 有值,并且不是空字符串
if (avatarPath && String(avatarPath).trim()) {
// 直接返回数据库里的路径:/avatars/123.jpg
// 这个地址会被你之前的 WebMvcConfig 映射到磁盘文件
return String(avatarPath).trim();
}
// ===================== 方案2:没有自定义头像,走后端接口获取 =====================
// 规范化用户ID(转成合法数字)
let id = normalizeUserId(userId);
// 如果用户ID不合法(null/0/负数)
if (!id) {
// 返回系统默认头像(常量,一般是 default-avatar.svg)
return DEFAULT_AVATAR_SRC;
}
// ID 合法 → 调用后端头像接口:/user/getAvatar?userId=xxx
// 这个接口就是你刚才那堆 Java 代码:getAvatarResponse()
return '/user/getAvatar?userId=' + id;
}
/**
* 用户ID 规范化工具方法
* 作用:把任意类型的 userId 转成合法的正整数
* @param userId 原始用户ID
* @return 合法正整数 → 返回ID;不合法 → 返回 null
*/
function normalizeUserId(userId) {
// 强制按 10 进制解析成整数(过滤字符串、小数等非法内容)
let id = parseInt(userId, 10);
// 只有大于 0 的数字才是有效ID,否则返回 null
return (id > 0) ? id : null;
}
解释是否需要缓存:

解释头像显示逻辑:

4.5 测试
启动服务器:
我们从上传头像到显示头像进行测试:
生成头像

点击确定之后,将会显示我们的头像

OK,最后,到此我们本期就结束了,下一期我们将实现好友管理,来获取我们的好友列表,老铁们我花了很久书写的,如果对你有用,记得给我一个免费的关注和点赞,感谢老铁的支持。
兄弟们,干货持续更新,记得点赞👍关注🌟收藏⭐,追更不迷路~