上传头像功能的实现

用户上传头像图片,涉及到的问题:图片在数据库中以何种形式存在?

文件上传保存在服务器的某个位置,只需要将该位置记录即可,即数据库中对应字段存储的是头像图片在服务器中的存储路径

在实际应用场景中,一般是将静态资源文件(图片、视频、文本文件等资源文件)存储在一台专门的电脑上,将该电脑作为一个单独的服务器使用

持久层

规划SQL语句

将用户头像存储路径更新到数据库表中的avatar字段,本质为一条更新语句

1
update t_user set avatar=?,modified_user=?,modified_time=? where uid=?;

抽象接口和方法

UserMapper接口定义对应的抽象方法

1
2
3
4
5
6
7
8
9
/**
* 更新用户图像
* @param uid 用户ide
* @param avatar 用户图像存储地址
* @param modifiedUser 信息修改者
* @param modifiedTime 信息修改时间
* @return
*/
Integer updateAvatarByUid(Integer uid,String avatar,String modifiedUser,Date modifiedTime);

抽象方法配置到映射文件UserMapper.xml

1
2
3
<update id="updateAvatarByUid">
update t_user set avatar=#{avatar},modified_user=#{modifiedUser},modified_time=#{modifiedTime} where uid=#{uid};
</update>

单元测试

1
2
3
4
5
6
7
8
@Test
public void updateAvatarByUid(){
String avatar="/userPhoto/yifei.png";
Date modifiedTime = new Date();
String modifiedUser = "管理员";
Integer rows = userMapper.updateAvatarByUid(3, avatar, modifiedUser, modifiedTime);
System.out.println("受影响行数:"+rows);
}

业务层

规划异常

  • 打开页面时,可能找不到用户的信息或用户已被删除
  • 信息插入过程中发生未知错误

前面的功能以前模块已经实现对应代码

抽象接口和方法

IUserService接口中编写对应的抽象方法

1
2
3
4
5
6
/**
* 更新用户图像信息
* @param uid 用户uid
* @param avatar 用户图像数据
*/
void alertAvatar(Integer uid,String avatar);

抽象方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void alertAvatar(Integer uid, String avatar) {
//查询当前用户是否存在
User user = userMapper.findByUid(uid);
if(user==null || user.getIsDelete()==1){
throw new UsernameNotFoundException("用户不存在");
}
//更新数据库avatar字段
Integer rows = userMapper.updateAvatarByUid(uid, avatar, user.getUsername(), new Date());
if(rows!=1){
throw new UpdateException("图像信息更新过程发生未知异常");
}
}

单元测试

1
2
3
4
5
6
@Test
public void alertAvatar(){
Integer uid = 2;
String avatar = "/upload/libai.png";
iUserService.alertAvatar(uid,avatar);
}

控制层

异常处理

业务层的两种可能异常,在以前功能模块中,控制层异常处理类中均有对应的逻辑处理

由于文件上传过程中可能由于大小、格式、类型等错误引发异常,所以需要专门规划文件上传的异常

1
2
3
4
5
FileUploadException: 泛指文件上传异常,基类,继承自RunTimeException
FileEmptyException: 文件为空异常
FileSizeException: 文件大小超出限制异常
FileTypeException: 文件类型异常
FileUploadIoException: 文件读写异常

在控制层异常处理基类中定义对应的异常逻辑(不同的异常,基于不同的状态响应码)

1
@ExceptionHandler({ServiceException.class, FileUploadException.class})
1
2
3
4
5
6
7
8
9
10
11
12
13
else if (e instanceof FileEmptyException) {
result.setState(6000);
result.setMessage("文件为空异常");
} else if (e instanceof FileSizeException) {
result.setState(6001);
result.setMessage("文件大小超出限制异常");
}else if (e instanceof FileTypeException){
result.setState(6002);
result.setMessage("文件类型异常");
} else if (e instanceof FileUploadIOException) {
result.setState(6003);
result.setMessage("文件读写异常");
}

设计请求

1
2
3
4
request url: /user/alert_avatar
request method: POST(原因:GET最大允许提交数据量为2K)
request params: HttpSession,MultiPartFile File //(SpringMVC提供的文件上传对象)
response data: JsonResult<String> //(页面切换,需要时刻保存头像路径,否则再次切换头像页面无法显示)

处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/** 设置上传文件的最大值 10MB */
public static final int MAX_AVATAR_SIZE = 10*1024*1024;
/** 设置允许接收的文件类型 */
public static final List<String> AVATAR_TYPE = new ArrayList<>();
static {
AVATAR_TYPE.add("image/jpeg");
AVATAR_TYPE.add("image/png");
AVATAR_TYPE.add("image/bmp");
AVATAR_TYPE.add("image/gif");
}


/**
* MultiPartFile是SpringMVC提供的一个接口,这个接口为我们包装了获取文件类型的数据,任何类型的File都可以接受
* SpringBoot整合了SpringMVC,只需要在处理请求的方法参数列表上申明一个MultiPartFile的参数
* SpringBoot会自定将接受的文件数据赋值给这个参数
* @param file
* @param session
* @return
*/
@RequestMapping("/alert_avatar")
public JsonResult<String> alertAvatar(MultipartFile file,HttpSession session){
if(file==null){
System.out.println("=============");
throw new FileEmptyException("文件为空异常");
}
if(file.getSize()>MAX_AVATAR_SIZE){
throw new FileSizeException("文件超出大小限制");
}
if(!AVATAR_TYPE.contains(file.getContentType())){
throw new FileTypeException("文件类型错误");
}
//规定文件存储路径 .../upload/xxx.xx
String parent = session.getServletContext().getRealPath("upload");
File dir = new File(parent);
//parent文件夹不存在则创建
if(!dir.exists()){
dir.mkdirs();
}
//获取文件名称
String fileName = file.getOriginalFilename();
String[] splits = fileName.split("\\.");
//文件后缀
String suffix = splits[splits.length-1];
//生成随机的文件名(为避免不同用户文件名重复导致数据被覆盖丢失
String uuid = UUID.randomUUID().toString().toUpperCase();
String newFileName = uuid+"."+suffix; //新的文件名
System.out.println(newFileName);
//存储路径
File dest = new File(dir,newFileName);
try {
file.transferTo(dest);
} catch (IOException e) {
throw new FileUploadIOException("文件读写错误");
}

Integer uid = getUidFromSession(session);
System.out.println(dest.getPath());
String avatar = "/upload/"+newFileName;
userService.alertAvatar(uid,avatar);

userService.alertAvatar(uid,avatar);
return new JsonResult<>(OK,"用户头像修改成功",avatar);
}

前端页面

直接通过表单发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--上传头像表单开始-->
<form class="form-horizontal" role="form" action="/user/alert_avatar" method="post"
enctype="multipart/form-data">
<div class="form-group">
<label class="col-md-2 control-label">选择头像:</label>
<div class="col-md-5">
<img id="img-avatar" src="../images/index/user.jpg" class="img-responsive" />
</div>
<div class="clearfix"></div>
<div class="col-md-offset-2 col-md-4">
<input type="file" name="file">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="submit" class="btn-primary" value="上传" />
</div>
</div>
</form>

部分功能优化和Bug解决

更改SpringMVC默认文件大小

方式一:在配置文件application.yaml里修改

1
2
3
4
5
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 15MB

方式二:采用Java代码修改上传文件大小限制,在主类中进行配置,可以定义一个方法,用@Bean修饰,在类的前面添加@Configuration修饰,该方法返回值类型`MultiPartConfigElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@SpringBootApplication
//指明当前项目中mapper接口的路径,项目启动会自动加载对应接口文件
@MapperScan("com.bang.store.mapper")
public class StoreApplication {

public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}

@Bean
public MultipartConfigElement getMultipartConfigElement(){
//创建配置类工厂类对象
MultipartConfigFactory factory = new MultipartConfigFactory();
//设置需要创建对象相关信息
//10MB
factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
factory.setMaxRequestSize(DataSize.of(15,DataUnit.MEGABYTES));
//通过工厂类创建MultiPartConfigElement对象
return factory.createMultipartConfig();
}
}

页面图像显示

通过ajax发送请求,解析数据,设置到image对应标签进行数据展示

前端表单数据映射

  • $(“#表单id”).serialize()
    • 可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的空间数据(比如:text\password\radio\checkbox等)
  • new FormData($(“#表单id”)[index])

    • FormData类,将表单中数据保持原有结构进行数据的发送
  • ajax默认处理数据时按照字符串的形式进行处理,以及默认采用字符串的形式提交数据,关闭这两个默认功能

    • ```javascript
      processData: false //处理数据的形式,false为关闭以字符串的形式处理数据
      contentType: false // 关闭默认的数据提交格式
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27

      ```javascript
      <script>
      $("#btn-change-avatar").click(function (){
      $.ajax({
      url: "/user/alert_avatar"
      ,type: "POST"
      ,data: new FormData($("#form-horizontal")[0])
      ,processData: false //处理数据的形式,false为关闭以字符串的形式处理数据
      ,contentType: false // 关闭默认的数据提交格式
      ,dataType: "JSON"
      ,success: function (data){
      if(data.state == 200){
      //图像显示在页面
      //attr(key,val) 给标签对应属性设置对应值
      $("#img-avatar").attr("src",data.data);
      alert("图像修改成功");
      }else{
      alert("图像上传失败 "+data.message);
      }
      }
      ,error:function (xmh){
      alert("图像上传发生未知异常"+xmh.status);
      }
      });
      });
      </script>

页面跳转图像消失解决办法

从其他页面再次回到当前页面或者登陆时显示图像

  • 图像上传成功后,可以将图像路径保存在cookie对象,然后每次检测用户打开上传图像页面,在该页面中通过$(document).ready()方法自动检测读取cookie中图像并设置到image的src属性

  • 此逻辑应该写在登陆页面

    • 前端query中cookie的使用

      1
      2
      3
      4
      5
      //1.导入cookie js文件
      <script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>
      //2.调用cookie方法
      //三个参数,key,avlue为键值对,time为cookie的存活时间,单位为天
      $.cookie(key,value,time);
    • login.html页面新增逻辑

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <script>
      $("#btn-login").click(function (){
      $.ajax({
      url: "/user/login"
      ,type: "POST"
      ,data: $("#form-login").serialize()
      ,dataType: "JSON"
      ,success: function (data){
      if(data.state == 200){
      alert("登录成功");
      //图像路径设置到cookie对象
      $.cookie("avatar",data.data.avatar, {expires:1});
      //跳转到对应页面
      //相对路径指定对应页面位置
      location.href="index.html"
      }else{
      alert("登陆失败 "+data.message);
      }
      }
      ,error:function (xmh){
      alert("登陆失败"+xmh.status);
      }
      });
      });
      </script>
    • upload.html里面检测加载cookie的逻辑代码

      1
      2
      3
      4
      5
      6
      $(document).ready(function () {
      //获取cookie数据
      let avatar = $.cookie("avatar");
      alert(avatar);
      $("#img-avatar").attr("src",avatar);
      })
    • 重新上传图像,需要覆盖原始cookie里的对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      $("#btn-change-avatar").click(function (){
      $.ajax({
      url: "/user/alert_avatar"
      ,type: "POST"
      ,data: new FormData($("#form-horizontal")[0])
      ,processData: false //处理数据的形式,false为关闭以字符串的形式处理数据
      ,contentType: false // 关闭默认的数据提交格式
      ,dataType: "JSON"
      ,success: function (data){
      if(data.state == 200){
      //图像显示在页面
      //attr(key,val) 给标签对应属性设置对应值
      $("#img-avatar").attr("src",data.data);
      //覆盖原来的cookie
      $.cookie("avatar",data.data, {expires:1});
      alert("图像修改成功");
      }else{
      alert("图像上传失败 "+data.message);
      }
      }
      ,error:function (xmh){
      alert("图像上传发生未知异常"+xmh.status);
      }
      });
      });