关于我在实现用户头像更换时遇到的图片上传和保存的问题
目录
前言
前端更换头像
后端处理
文件系统存储图片
数据库存储图片
处理图片文件
生成图片名
保存图片
将图片路径存储到数据库
完整代码
总结
前言
最近在实现一个用户头像更换的功能,但是因为之前并没有处理过图片的上传和保存,所以就开始到处查阅资料。
前端更换头像
发现只需要给图片加个click事件就能打开图片文件夹选择图片,但是显然只有一个click事件,只能选择图片(给图片赋值)并没有我期望的更换图片功能。
之后我使用change函数,来处理这个点击事件。
change()函数用于侦听表单元素(如文本框、选择框和复选框)的值发生变化的事件,并在这些变化发生时触发相应的事件,之后只需要在change函数里完成前端逻辑就行。
然后利用FileReader()读取图片信息并编码成DataURL格式。
FileReader()是一个JavaScript内置对象,提供了一种将文件内容读取到内存中以供处理的方式。通过使用FileReader对象,可以读取不同类型的文件,例如文本文件、图像文件等
FileReader的onload方法可以在FileReader()读取文件成功时触发,就可以利用这个完成图片的更换
前端头像的更替就这样完成了,过程还是挺轻松的,紧接着就要考虑如何把图片信息传到服务器并把图片储存起来,关于这个问题,我当时依然是一头雾水,继续到处查资料。
之后了解到spring框架提供了一个MultipartFile的接口MultipartFile,主要用于处理文件上传的场景。当你在Web应用中需要让用户通过表单上传文件时(如图片、文档等),Spring框架可以自动将这些文件封装成MultipartFile对象。
所以只需要通过表单将图片发送给服务器,使用这个MultipartFile接受就可以对图片文件进行我们想要的处理了
var formData = new FormData();formData.append('file', avatarUrlEl);$.ajax({url: 'user/modifyAvatarUrl',type: 'post',data: formData,processData: false,contentType: false,success: function (body) {if(body.code == 0){//返回响应成功,更换图片//$('#settings_avatar').attr("src",body.message);$('#settings_avatar').css('background-image', 'url(' + body.message + ')');location.assign("index.html");}else {//提示信息$.toast({heading: '警告',text: result.message,icon: 'warning'});}},error: function () {//提示信息$.toast({heading: '失败',text: '访问出错',icon: 'error'});}});
后端处理
经过我不断的百度,找到了两种比较靠谱的存储图片的方法
一种是文件系统存储,一种是数据库存储
文件系统存储图片
- 在文件系统中选择一个存储头像图片的文件夹
- 上传图片:处理图片请求把图片保存到刚刚的文件路径下,小编这里一开始是想着存储在项目目录下的static中的image文件里,但是之后遇到一些问题。之后会详细介绍
- 保存路径,将图片路径存储在数据库中(小编这里是存储在项目库下的user表里的头像路径字段)
这种方法就相当于,将图片存储在本地文件,然后给客户端提供一个文件路径,这样客户端在需要显示头像的地方直接查询数据库里的头像路径就行
这个方法的优点是,简单,速度快,缺点就是文件数量过多时,会导致文件系统性能下降,需要定期维护
数据库存储图片
就是直接将图片存储在数据库中,数据库会以BLOB格式存储到数据库字段中
但是这样做会增加数据库的负担,所以我并没有选择这种方法
处理图片文件
知道了怎么保存图片,接下来就是考虑怎么处理文件,刚刚有提到,我们直接使用spring的MultipartFile接口来接收前端传来的图片文件,所以也就利用这个接口来处理文件。
在详细了解MultipartFile接口和查阅了一些资料后,小编也是完成了这部分的功能
生成图片名
为避免图片名重复导致文件访问错误,小编这里使用UUID生成一个32位的随机字符串来当图片名字,接着还要知道图片的类型也就是图片后缀,可以利用MultipartFile接口的getOriginalFilename()方法
//获取图片类型String imageType = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//文件名(随机生成字符串)String imgName = UUIDUtil.getUUID_32() + imageType;
getOriginalFilename()作用是获取上传文件的原始名称,然后截取前面的字符串只留最后的后缀就可以获得图片的类型。
lastIndexOf()方法的主要作用是返回指定元素在数组或字符串中最后一次出现的位置
getUUID_32是小编自己封装的方法
保存图片
依然直接使用MultipartFile的transferTo方法,这个方法可以将上传的文件保存到指定的位路径,需要传一个File类型的数据。
//创建图片文件try{file.transferTo(new File("/image/"+imgName));} catch (Exception e){return AppResult.failed(ResultCode.FAILED);}
这里小编最开始是想利用相对路径存储在项目下的image文件里,但是在最后发现这样根本不行,并没有将图片成功保存,但是我换成绝对路径就可以。这个方法应该只能传入绝对路径,在查阅资料的过程中小编也有看到这个方法好像确实不能使用相对路径。(小编的猜测)
但是倔强的小编偏要使用相对路径,所以就使用getCanonicalPath()先获取绝对路径在拼接图片名
//创建图片文件File dir = new File(relativePath);String realPath = dir.getCanonicalPath();//获得绝对路径System.out.println("当前工作目录: " + realPath);try{file.transferTo(new File(realPath+"/"+imgName));} catch (Exception e){return AppResult.failed(ResultCode.FAILED);}
//relativePath是小编配置文件中的一个自定义配置,就是image文件的相对路径
将图片路径存储到数据库
这个就是一个简单的数据库插入,关键是imgUrl变量,这个就是存储在数据库中的路径也是后面客户端从数据库拿,请求图片信息的路径。
获取用户信息小编这里用的是一个JWT令牌根据客户端的token字符串来获取userId,使用session也是一样。
//将图片地址保存到数据库,这个也是之后前端要请求的路径String imgUrl = "/image/" + imgName;//修改头像//通过token获取idString token = request.getHeader(Contants.USER_TOKEN_HANDER);Long userId = JwtUtil.getIdByToken(token);if (userId == null || StringUtil.isEmpty(token)) {//用户未登录//打印信息log.warn(ResultCode.FAILED_FORBIDDEN.toString());return AppResult.failed(ResultCode.FAILED_FORBIDDEN);}System.out.println("imgUrl: " + imgUrl);int row = userService.updateAvatarUrl(userId,imgUrl);if(row != 1){return AppResult.failed(ResultCode.FAILED);}return AppResult.success(imgUrl);
完整代码
@ApiOperation("更新头像")@PostMapping("/modifyAvatarUrl")public AppResult modifyAvatarUrl(HttpServletRequest request, MultipartFile file) throws IOException {//获取图片类型String imageType = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//文件名(随机生成字符串)String imgName = UUIDUtil.getUUID_32() + imageType;//创建图片文件File dir = new File(relativePath);String realPath = dir.getCanonicalPath();//获得绝对路径System.out.println("当前工作目录: " + realPath);try{file.transferTo(new File(realPath+"/"+imgName));} catch (Exception e){return AppResult.failed(ResultCode.FAILED);}//将图片地址保存到数据库,这个也是之后前端要请求的路径String imgUrl = "/image/" + imgName;//修改头像//通过token获取idString token = request.getHeader(Contants.USER_TOKEN_HANDER);Long userId = JwtUtil.getIdByToken(token);if (userId == null || StringUtil.isEmpty(token)) {//用户未登录//打印信息log.warn(ResultCode.FAILED_FORBIDDEN.toString());return AppResult.failed(ResultCode.FAILED_FORBIDDEN);}System.out.println("imgUrl: " + imgUrl);int row = userService.updateAvatarUrl(userId,imgUrl);if(row != 1){return AppResult.failed(ResultCode.FAILED);}return AppResult.success(imgUrl);}
完成之后小编欣喜若狂的启动服务器发现头像修改完之后,客户端并没有显示图片,并表示没有获取到图片
正在小编以为是代码错误时打开数据库一看,图片路径成功存储没有错误,前端访问的路径也没错,而且文件里也成功生成了这个图片并是一个32位的随机字符串命名。一切都没有出错那为什么会访问不到图片?小编带着疑惑重启了服务器,结果客户端的图片就可以显示了也成功访问到,试了好几次都是这样,于是小编又开始了不断的查阅资料。最后终于找到问题。
SpringBoot在启动时将静态资源(比如图片)加载到classpath目录下,并在打包时将这些资源保存在JAR包中,当上传新的图片时,这些图片并没有被包含进启动的项目中,因此无法访问,只有重启服务器后,新的jar包被创建后才能将新的图片加载。
解决方法也很简单,只需要配置一个静态资源映射来实现就行,创建一个配置类实现WebMvcConfigurer的addResourceHandlers方法就行,指定图片的存储路径和访问路径
//realPath也是配置文件里的一个自定义配置,表示存储图片的文件的绝对路径
这个方法就是将客户端向服务器的带有image请求的访问路径映射到其他地方。这就是为什么客户端的访问路径要是image开头,就是为了这里做映射。addResourceLocations()里的路径就是图片实际存储的路径。
这里小编踩了一个坑,我最开始用的是相对路径,发现没有作用然后换成绝对路径才行,我以为这个方法也不能使用相对路径,了解详细信息后知道了,路径前面的"file:"是协议表示后面的路径是一个绝对路径,当使用"file:"时就会映射到一个绝对路径,如果使用相对路径就会找不到要映射的路径。
//如果想要使用相对路径需要把协议换成"classpath:"才行,并且不能直接使用/image/,至少要确定到/static/image/才行(这里我也不知道为什么,至少就自己测试的来看是这样的)
加上这个图片路径映射就可以完成用户头像更换的功能了
上线到云服务器
在刚开始我还没有搞懂整个流程是什么样的,就上线到服务器时,出了很多bug,客户端一直访问不到图片(因为映射路径和图片存储路径的问题),详细了解后,将图片存储到/root/image/forum_image路径下,使用绝对路径将所有/image/**的请求也映射到整个路径下,客户端就可以正常请求到图片了
总结
整个流程涉及到三个路径(实际上是两个,一个数据库存储的要被映射的路径,一个实际存储图片的路径)
- 图片的存储路径:这个路径因为transferTo的原因只能使用绝对路径,所以需要提前选择一个文件用来存储图片
- 图片的访问路径:这个就是客户端发起图片请求响应时的路径,同时也是数据库里存储的路径(在写代码时这两个路径必须一样,因为服务器要根据这个路径来映射到刚刚的图片存储路径,博主这里就是一个image开头的路径,就是将所以image开头的请求都映射到图片存储路径上,而且记得拦截器要排除这个路径)
- 图片映射路径:也就是addResourceLocations()里传的路径,和图片的存储路径是一样的,但是这里可以通过改变路径的协议是"file"还是"classpath"来决定是使用绝对路径还是相对路径
相关文章:

关于我在实现用户头像更换时遇到的图片上传和保存的问题
目录 前言 前端更换头像 后端处理 文件系统存储图片 数据库存储图片 处理图片文件 生成图片名 保存图片 将图片路径存储到数据库 完整代码 总结 前言 最近在实现一个用户头像更换的功能,但是因为之前并没有处理过图片的上传和保存,所以就开始…...

10.二叉搜索树中第k小的元素(medium)
1.题目链接: 230. 二叉搜索树中第 K 小的元素 - 力扣(LeetCode)230. 二叉搜索树中第 K 小的元素 - 给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数…...

AlimaLinux设置静态IP
通过nmcli命令来操作 步骤 1:确认当前活动的网络接口名称 首先,需要确认当前系统中可用的网络接口名称。可以使用以下命令查看: nmcli device步骤 2:修改配置以匹配正确的接口名称 sudo nmcli connection modify ens160 ipv4.…...

滑动窗口——将x减到0的最小操作数
题目: 这个题如果我们直接去思考方法是很困难的,因为我们不知道下一步是在数组的左还是右操作才能使其最小。正难则反,思考一下,无论是怎么样的,最终这个数组都会分成三个部分左中右,而左右的组合就是我们…...

基于SpringBoot的抽奖系统测试报告
一、编写目的 本报告为抽奖系统测试报告,本项目可用于团体抽奖活动,包括了用户注册,用户登录,修改奖项以及抽奖等功能。 二、项目背景 抽奖系统采用前后端分离的方法来实现,同时使用了数据库来存储相关的数据&…...

服务器mysql连接我碰到的错误
搞了2个下午,总算成功了 我在服务器上使用docker部署了java项目与mysql,但mysql连接一直出现问题 1.首先,我使用的是localhost连接,心想反正都在服务器上吧。 jdbc:mysql://localhost:3306/fly-bird?useSSLfalse&serverTime…...

【Part 2安卓原生360°VR播放器开发实战】第四节|安卓VR播放器性能优化与设备适配
《VR 360全景视频开发》专栏 将带你深入探索从全景视频制作到Unity眼镜端应用开发的全流程技术。专栏内容涵盖安卓原生VR播放器开发、Unity VR视频渲染与手势交互、360全景视频制作与优化,以及高分辨率视频性能优化等实战技巧。 📝 希望通过这个专栏&am…...
TIME - MoE 模型代码 4——Time-MoE-main/run_eval.py
源码:https://github.com/Time-MoE/Time-MoE 这段代码是一个用于评估 Time-MoE 模型性能的脚本,它支持分布式环境下的模型评估,通过计算 MSE 和 MAE 等指标来衡量模型在时间序列预测任务上的表现。代码的核心功能包括:模型加载、…...
数字孪生概念
数字孪生(Digital Twin) 是指通过数字技术对物理实体(如设备、系统、流程或环境)进行高保真建模和实时动态映射,实现虚实交互、仿真预测和优化决策的技术体系。它是工业4.0、智慧城市和数字化转型的核心技术之一。 1. …...

从知识图谱到精准决策:基于MCP的招投标货物比对溯源系统实践
前言 从最初对人工智能的懵懂认知,到逐渐踏入Prompt工程的世界,我们一路探索,从私有化部署的实际场景,到对DeepSeek技术的全面解读,再逐步深入到NL2SQL、知识图谱构建、RAG知识库设计,以及ChatBI这些高阶应…...
DAMA车轮图
DAMA车轮图是国际数据管理协会(DAMA International)提出的数据管理知识体系(DMBOK)的图形化表示,它以车轮(同心圆)的形式展示了数据管理的核心领域及其相互关系。以下是基于用户提供的关键词对D…...

图形化编程革命:iVX携手AI 原生开发范式
一、技术核心:图形化编程的底层架构解析 1. 图形化开发的效率优势:代码量减少 72% 的秘密 传统文本编程存在显著的信息密度瓶颈。以 "按钮点击→条件判断→调用接口→弹窗反馈" 流程为例,Python 实现需定义函数、处理缩进并编写 …...
线程池使用ThreadLocal注意事项
ThreadLocal和线程池都是Java中处理多线程的重要工具,但它们在结合使用时需要特别注意一些问题。 ThreadLocal简介 ThreadLocal提供了线程局部变量,每个线程都有自己独立的变量副本,互不干扰。 基本用法: private static fina…...

JAVA EE_网络原理_网络层
晨雾散尽,花影清晰。 ----------陳長生. ❀主页:陳長生.-CSDN博客❀ 📕上一篇:数据库Mysql_联…...

森林生态学研究深度解析:R语言入门、生物多样性分析、机器学习建模与群落稳定性评估
在生态学研究中,森林生态系统的结构、功能与稳定性是核心研究内容之一。这些方面不仅关系到森林动态变化和物种多样性,还直接影响森林提供的生态服务功能及其应对环境变化的能力。森林生态系统的结构主要包括物种组成、树种多样性、树木的空间分布与密度…...

AI大模型学习十八、利用Dify+deepseekR1 +本地部署Stable Diffusion搭建 AI 图片生成应用
一、说明 最近在学习Dify工作流的一些玩法,下面将介绍一下Dify Stable Diffusion实现文生图工作流的应用方法 Dify与Stable Diffusion的协同价值 Dify作为低代码AI开发平台的优势:可视化编排、API快速集成 Stable Diffusion的核心能力:高效…...

关于chatshare.xyz激活码使用说明和渠道指南!
chatshare.xyz和chatshare.biz是两个被比较的平台,分别在其功能特性和获取渠道有所不同。 本文旨在探讨它们的差异,以及提供如何获取并使用的平台信息。此外,还提及其他一些相关资源和模板推荐以满足用户需求。 主要区分关键点 1、chatshar…...
【Python-Day 12】Python列表进阶:玩转添加、删除、排序与列表推导式
Langchain系列文章目录 01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…...
RAII是什么?
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C编程中的一项非常重要且经典的设计思想,也是现代C资源管理的基石。它主要解决资源的自动管理与释放问题,从而帮助程序员避免资源泄漏、悬空指针等常…...

Qt开发经验 --- 避坑指南(14)
文章目录 [toc]1 linux下使用linuxdeploy打包2 Qt源码下载3 QtCreator配置github copilot实现AI编程4 使用其它编程AI辅助开发Qt5 Qt开源UI库6 QT6.8以后版本安装QtWebEngine7 清除QtCreator配置 更多精彩内容👉内容导航 👈👉Qt开发经验 &…...
JavaScript 循环语句全解析:选择最适合的遍历方式
循环是编程中处理重复任务的核心工具。JavaScript 提供了多种循环语句,每种都有其适用场景和独特优势。本文将深入解析 JavaScript 的 6 种核心循环语句,通过实际示例帮助你精准选择合适的循环方案。 一、基础循环三剑客 1. for 循环 经典索引控制 ja…...

MIT 6.S081 2020 Lab3 page tables 个人全流程
文章目录 零、写在前面1、关于页表2、RISC-V Rv39页表机制3、虚拟地址设计4、页表项设计5、访存流程6、xv6 的页表切换7、页表遍历 一、Print a page table1.1 说明1.2 实现 二、A kernel page table per process2.1 说明2.2 初始化 / 映射相关2.3 用户内核页表的创建和回收2.4…...
Oracle 通过 ROWID 批量更新表
Oracle 通过 ROWID 批量更新表 在 Oracle 数据库中,使用 ROWID 进行批量更新是一种高效的更新方法,因为它直接定位到物理行位置,避免了通过索引查找的开销。 ROWID 基本概念 ROWID 是 Oracle 数据库中每一行的唯一物理地址标识符ÿ…...
webpack 的工作流程
Webpack 的工作流程可以分为以下几个核心步骤,我将结合代码示例详细说明每个阶段的工作原理: 1. 初始化配置 Webpack 首先会读取配置文件(默认 webpack.config.js),合并命令行参数和默认配置。 // webpack.config.js…...
tcpdump 的用法
tcpdump 是一款强大的命令行网络抓包工具,用于捕获和分析网络流量。以下是其核心用法指南: 一、基础命令格式 sudo tcpdump [选项] [过滤表达式]权限要求:需 root 权限(使用 sudo) 二、常用选项 选项说明-i <接口…...
Agent杂货铺
零散记录一些Agent相关的内容。不成体系,看情况是否整理 ReAct ReAct 是一种实践代理模型的高级框架,通过将大语言模型(LLMs)的推理和执行行动的能力结合起来,增强了它们在处理复杂任务时的决策能力、适应性和与外部…...

【Redis】Redis的主从复制
文章目录 1. 单点问题2. 主从模式2.1 建立复制2.2 断开复制 3. 拓扑结构3.1 三种结构3.2 数据同步3.3 复制流程3.3.1 psync运行流程3.3.2 全量复制3.3.3 部分复制3.3.4 实时复制 1. 单点问题 单点问题:某个服务器程序,只有一个节点(只搞一个…...

第04章—技术突击篇:如何根据求职意向进行快速提升与复盘
经过上一讲的内容阐述后,咱们定好了一个与自身最匹配的期望薪资,接着又该如何准备呢? 很多人在准备时,通常会选择背面试八股文,这种做法效率的确很高,毕竟能在“八股文”上出现的题,也绝对是面…...

Quantum convolutional nerual network
一些问答 1.Convolution: Translationally Invariant Quasilocal Unitaries 理解? Convolution(卷积): 在量子信息或量子多体系统中,"卷积"通常指一种分层、局部操作的结构,类似于经典卷积神经网…...

RL之ppo训练
又是一篇之前沉在草稿箱的文章,放出来^V^ PPO原理部分这两篇就够了: 图解大模型RLHF系列之:人人都能看懂的PPO原理与源码解读人人都能看懂的RL-PPO理论知识 那些你或多或少听过的名词 actor-critic: actor表示策略,critic表示价值…...