关于我在实现用户头像更换时遇到的图片上传和保存的问题
目录
前言
前端更换头像
后端处理
文件系统存储图片
数据库存储图片
处理图片文件
生成图片名
保存图片
将图片路径存储到数据库
完整代码
总结
前言
最近在实现一个用户头像更换的功能,但是因为之前并没有处理过图片的上传和保存,所以就开始到处查阅资料。
前端更换头像
发现只需要给图片加个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"来决定是使用绝对路径还是相对路径