学习笔记 : Spring Boot之文件上传

关键字 : Servlet 3.0SpringMVCSpring Boot

简介

前天使用Sprint Boot开发了一个基于SSM框架的项目,一个简单的好友备忘录,该项目地址 : https://github.com/YUbuntu0109/SpringBoot-CURD-Memo ,在该项目中除了基本的CURD,还添加了上传用户头像的功能哟~ 以至于在写此功能时发现了Spring Boot在上传文件时不同于Spring MVC的一个细节问题 : 头像被上传到非预期路径下!

  • 注 : Spring Boot启动时会创建一个/tmp/tomcat.xxxxxx/work/Tomcat/localhost/ROOT的临时目录作为文件上传的临时目录,但是该目录会在10天之后被系统自动清理掉 !`

继而程序抛出如下异常信息 :

1
2
java.io.IOException: java.io.FileNotFoundException:
/tmp/tomcat.273391201583741210.8080/work/Tomcat/localhost/ROOT/upload/portrait/myportrait.jpg (No such file or directory)...

异常分析

upload/portrait是我用于存储头像的项目目录,而transferTo(File dest)方法预期写入的文件路径为/tmp/tomcat.273391201583741210.8080/work/Tomcat/localhost/ROOT/upload/portrait/,我们并没有创建该目录,因此会抛出此异常信息 !

问题分析

为什么会这样呢 ? 相对路径-预期路径应该是项目路径/tmp/source/,但是报错确是一个系统临时文件路径,由于是写入文件时报错,继而我们来查看一下transferTo(File dest)的源码吧 :

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
package org.apache.catalina.core;
//......

/**
* Adaptor to allow {@link FileItem} objects generated by the package renamed
* commons-upload to be used by the Servlet 3.0 upload API that expects
* {@link Part}s.
*/
public class ApplicationPart implements Part {

//......

@Override
public void write(String fileName) throws IOException {
File file = new File(fileName);
if (!file.isAbsolute()) {
file = new File(location, fileName);
}
try {
fileItem.write(file);
} catch (Exception e) {
throw new IOException(e);
}
}
}

由源码可知,在使用Servlet3.0支持的上传文件功能时,若我们没有使用绝对路径,transferTo(File dest)方法会在相对路径前添加一个location路径 ! 继而影响了SpringMVC的MultipartFile的使用 .

解决方案

使用绝对路径

通过ResourceUtils.getURL("classpath:").getPath()获取项目的绝对路径,为防止项目路径中含有空格等特殊字符继而乱码,可以通过URLDecoder.decode(String s, Charset charset)对其进行解码.

1
2
3
4
//项目下存储头像的目录
private final String uploadPath = "/static/upload/friend_portrait/";
//指定存储头像目录的完整路径(项目发布路径):若不使用绝对路径,则Spring boot会默认将上传的文件存储到临时目录中
String dirPath = URLDecoder.decode(ResourceUtils.getURL("classpath:").getPath(), StandardCharsets.UTF_8) + uploadPath;

注 : 当我们使用ClassLoader()getResource()方法获取路径时,获取到的路径是已被URLEncoder.encode(path,"utf-8")编码了的,当路径中存在中文和空格时,它会对这些字符进行转换,继而得到的往往不是我们想要的真实路径,所以我们可以调用URLDecoder.decode(String s, Charset charset)方法进行解码,以便得到原始的中文及空格路径. 发送的时候使用encode(String s, Charset charset)编码,接收的时候使用URLDecoder.decode(String s, Charset charset)解码,按指定的编码格式进行编码、解码,可以保证不会出现乱码哟 ~

1
2
3
4
5
//使用指定的编码机制将字符串转换为 application/x-www-form-urlencoded 格式
URLEncoder.encode(String s, Charset charset)

//使用指定的编码机制对 application/x-www-form-urlencoded 字符串解码
URLDecoder.decode(String s, Charset charset)

修改location的值

location可以理解为临时文件目录,可以通过配置location的值,使其指向我们的项目路径,继而来解决此问题. 只需在Spring Boot启动类中添如下代码 :

1
2
3
4
5
6
 @Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setLocation("/app/pttms/tmp");
return factory.createMultipartConfig();
}

示例程序

该上传文件的示例程序摘自我的Sping Boot-好友备忘录小项目,程序中使用绝对路径方案来解决上述问题,该程序具有参考与学习价值哟~

  1. FriendController.java : 好友信息控制器

    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
    package pers.haungyuhui.memo.controller;

    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import org.springframework.stereotype.Controller;
    import org.springframework.util.ResourceUtils;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    import pers.haungyuhui.memo.bean.Friend;
    import pers.haungyuhui.memo.service.FriendService;
    import pers.haungyuhui.memo.util.UploadFile;

    import javax.annotation.Resource;
    import java.io.FileNotFoundException;
    import java.net.URLDecoder;
    import java.nio.charset.StandardCharsets;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    /**
    * @project: memo
    * @description: 控制器-管理好友信息页面
    * @author: 黄宇辉
    * @date: 6/28/2019-8:25 PM
    * @version: 1.0
    * @website: https://yubuntu0109.github.io/
    */
    @Controller
    @RequestMapping("/memo")
    public class StudentController {

    //......

    //项目下存储头像的目录,需放在静态资源'static'目录下哟
    private final String uploadPath = "/static/upload/friend_portrait/";

    /**
    * @description: 上传头像-原理:将头像上传到项目发布目录中,通过读取数据库中的头像路径来显示头像
    * @param: photo
    * @param: request
    * @date: 2019-06-29 4:20 PM
    * @return: java.util.Map<java.lang.String, java.lang.Object>
    */
    @PostMapping("/uploadPhoto")
    @ResponseBody
    public Map<String, Object> uploadPhoto(MultipartFile photo) throws FileNotFoundException {
    //指定存储头像目录的完整路径(项目发布路径): 若不使用绝对路径,则Spring boot会默认将上传的文件存储到临时目录中
    String dirPath = URLDecoder.decode(ResourceUtils.getURL("classpath:").getPath(), StandardCharsets.UTF_8) + uploadPath;
    //返回头像的上传结果
    return UploadFile.getUploadResult(photo, dirPath, uploadPath);
    }
    }
  2. UploadFile.java : 上传文件的工具类

    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
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    package pers.haungyuhui.memo.util;

    import org.apache.commons.io.filefilter.SuffixFileFilter;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;

    /**
    * @project: memo
    * @description: 上传文件工具类
    * @author: 黄宇辉
    * @date: 6/29/2019-9:38 AM
    * @version: 1.0
    * @website: https://yubuntu0109.github.io/
    */
    public class UploadFile {

    //限制头像大小最大为20M
    private static final int MAX_SIZE = 20971520;
    //存储文件上传失败的错误信息
    private static Map<String, Object> error_result = new HashMap<>();
    //存储头像的上传结果信息
    private static Map<String, Object> upload_result = new HashMap<>();
    //指定上传文件的类型
    private static final String[] suffixs = new String[]{".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG", ".gif", ".GIF", ".bmp", ".BMP"};


    /**
    * @description: 效验所上传图片的大小及格式等信息...
    * @param: photo
    * @param: path
    * @date: 2019-06-29 9:40 AM
    * @return: java.util.Map<java.lang.String, java.lang.Object>
    */
    private static Map<String, Object> uploadPhoto(MultipartFile photo, String path) {
    //若存储文件的目录路径不存在,则创建该目录
    File filePath = new File(path);
    if (!filePath.exists()) {
    filePath.mkdirs();
    }
    //限制上传文件的大小
    if (photo.getSize() > MAX_SIZE) {
    error_result.put("success", false);
    error_result.put("msg", "上传的图片大小不能超过20M哟!");
    return error_result;
    }
    // 限制上传的文件类型
    SuffixFileFilter suffixFileFilter = new SuffixFileFilter(suffixs);
    if (!suffixFileFilter.accept(new File(path + photo.getOriginalFilename()))) {
    error_result.put("success", false);
    error_result.put("msg", "禁止上传此类型文件! 请上传图片哟!");
    return error_result;
    }
    return null;
    }

    /**
    * @description: 获取头像的上传结果信息
    * @param: photo
    * @param: dirPaht
    * @param: portraitPath
    * @date: 2019-06-29 9:44 AM
    * @return: java.util.Map<java.lang.String, java.lang.Object>
    */
    public static Map<String, Object> getUploadResult(MultipartFile photo, String dirPath, String uploadPath) {

    if (!photo.isEmpty() && photo.getSize() > 0) {
    //效验图片-error_result: 存储头像上传失败的错误信息
    Map<String, Object> error_result = uploadPhoto(photo, dirPath);
    if (error_result != null) {
    return error_result;
    }
    //使用UUID重命名图片名称(uuid__原始图片名称)
    String newPhotoName = UUID.randomUUID() + "__" + photo.getOriginalFilename();
    //将上传的图片保存到目标目录下
    try {
    photo.transferTo(new File(dirPath + newPhotoName));
    //将存储头像的目录路径返回给页面
    upload_result.put("success", true);
    upload_result.put("portrait_path", uploadPath + newPhotoName);
    } catch (IOException e) {
    e.printStackTrace();
    upload_result.put("success", false);
    upload_result.put("msg", "上传文件失败! 服务器端发生异常!");
    return upload_result;
    }

    } else {
    upload_result.put("success", false);
    upload_result.put("msg", "头像上传失败! 未找到指定图片!");
    }
    return upload_result;
    }
    }