Springboot上传踩坑

原因是公司部分架构换成Springboot,为了兼容老项目的业务逻辑,就把上传模块做迁移处理,结果版本兼容踩了好多坑.

SpringBoot 上传文件

配置application.yml文件

spring: 
  # 文件上传
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 1024MB

单文件上传

    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return "上传失败,请选择文件";
        }

        String fileName = file.getOriginalFilename();
        String filePath = "D:/upload/";
        File dest = new File(filePath + fileName);
        try {
            file.transferTo(dest);
            log.info("上传成功");
            return "上传成功";
        } catch (IOException e) {
            log.error(e.toString(), e);
        }
        return "上传失败!";
    }

这种情况是只有一个表单,选择文件, form 的 enctype 为 multipart/form-data

多文件上传

    @PostMapping(value = "upload", consumes = "multipart/form-data")
    @ResponseBody
    public UnifyInfo uploadTest(@RequestParam("file") MultipartFile[] files) throws Exception {
        UnifyInfo unifyInfo = new UnifyInfo();

        File filePath = new File("D:/upload/");
        if (!filePath.exists()) {
            filePath.mkdirs();
        }

        if (files == null) {
            unifyInfo.setCode(ErrorCode.FAILURE.getCode());
            return unifyInfo;
        }

        for (MultipartFile file : files) {
            if (file != null) {
                try {
                    file.transferTo(new File(filePath + File.separator + file.getOriginalFilename()));
                } catch (IOException e) {
                    e.printStackTrace();
                    unifyInfo.setCode(ErrorCode.FAILURE.getCode());
                    return unifyInfo;
                }
            }
        }
        unifyInfo.setCode(ErrorCode.SUCCESS.getCode());
        return unifyInfo;
    }

项目中使用的是Apache的fileupload组件上传的,想着和Springboot的上传做兼容的,因为参数MultipartFile也有对应FormField.结果使用 ServletFileUpload上传文件失败,upload.parseRequest(request)为空

尝试强转 CommonsMultipartFile 来获取 FileItem ,因为 SpringBoot 的 MultipartFile 的默认实现是 StandardMultipartFile ,不满足一些业务使用的判断规则.

StandardMultipartFile 不能转换为 CommonsMultipartFile 的问题

使用此方式获取

需要配置下Bean

/**
 * @author Allen
 * @Date: 2019/05/22 14:50
 * @Description: BeanConfig
 * @Version 1.0
 **/
@Configuration
public class BeanConfig {

    /**
     * Description : 配置上传控件为CommonsMultipartFile解析器
     * @date 2019/5/22 14:53
     * @param
     * @return org.springframework.web.multipart.commons.CommonsMultipartResolver
     */
    @Bean(name = "multipartResolver")
    public CommonsMultipartResolver getCommonsMultipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(1073741824L);
        multipartResolver.setMaxInMemorySize(1048576);
        return multipartResolver;
    }

}
    CommonsMultipartFile cmf = (CommonsMultipartFile) file;
    FileItem item = cmf.getFileItem();

但是此 FileItem 查看源码虽然也是使用的Apache的组件里的 FileItem 类,可是转换过来后,发现字段都没有设置上, FieldName 对应不了表单传的值, isFormField 明明是表单也为false, String也都获取不到.

SpringBoot获取request的几种方式

  • 通过静态方法获取
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
  • 通过参数直接获取,只要在你的方法上加上参数,Springboot就会帮你绑定,你可以直接使用。如果你的方法有其他参数,把这两个加到后面即可。
@GetMapping(value = "")
public String center(HttpServletRequest request,HttpServletResponse response) {
    //...
}
  • 注入到类,这样就不用每个方法都写了
@Autowired
private HttpServletRequest request;

@Autowired
private HttpServletResponse response;

调试时发现 request 中有值,遂通过 request 来获取 MultipartFile 值


    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;    
            List<MultipartFile> fileList = multipartRequest.getFiles("file");
    
    // 简易写法
    List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");

行不通,最后还是想着使用原来的上传方式吧.但是要面对前面的 parseRequest 转换为空的问题 ,即 List<FileItem> items = upload.parseRequest(request);得到的 size=0 ,也就是根本没有得到文件数据。

网上一般有两种说法

  • (1) 原因在于 Spring 的配置文件中已经配置了MultipartResolver ,导致文件上传请求已经被预处理过了,所以此处解析文件列表为空,对应的做法是删除该段配置.

  • (2) 认为是structs的过滤器导致请求已被预处理,所以也要修改对应过滤器的配置.

还有说查看源代码说是框架对 request 已经进行过预转了,就是下面的.

List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);    

但是,我自己查看源码发现不一样,是正常的封装返回

    FileItemIterator iter = getItemIterator(ctx);
    FileItemFactory fac = getFileItemFactory();
    if (fac == null) {
        throw new NullPointerException("No FileItemFactory has been set.");
    }
    while (iter.hasNext()) {
        final FileItemStream item = iter.next();
        final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
        FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
                                           item.isFormField(), fileName);
        items.add(fileItem);
        try {
            Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
        } catch (FileUploadIOException e) {
            throw (FileUploadException) e.getCause();
        } catch (IOException e) {
            throw new IOFileUploadException(format("Processing of %s request failed. %s",
                                                   MULTIPART_FORM_DATA, e.getMessage()), e);
        }
        final FileItemHeaders fih = item.getHeaders();
        fileItem.setHeaders(fih);
    }

还是不行,最后忽然想到 SpringBoot 默认的上传组件会不会影响这个转换呢?

修改配置,

SpringBoot 2.x 使用 servlet 配置

  servlet:
    multipart:
      enabled: false

SpringBoot 1.x 使用 http 配置

  http:
    multipart:
      enabled: false

发现终于成功获取到了.

最后贴下 Apache 上传组件代码使用的部分逻辑

    @RequestMapping(value = "/test/file")
    @ResponseBody
    public String file (HttpServletRequest request,HttpServletResponse response) throws IOException{
        response.setContentType("application/json;charset=utf-8");
        try{
            //使用Apache文件上传组件处理文件上传步骤:
            //1、创建一个DiskFileItemFactory工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
            factory.setSizeThreshold(1024*100);//设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB
            /*//设置上传时生成的临时文件的保存目录
            factory.setRepository(tmpFile);*/
            //2、创建一个文件上传解析器
            ServletFileUpload upload = new ServletFileUpload(factory);
            //监听文件上传进度
            upload.setProgressListener(new ProgressListener(){
                public void update(long pBytesRead, long pContentLength, int arg2) {
                    System.out.println("文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead);
                }
            });
            //解决上传文件名的中文乱码
            upload.setHeaderEncoding("UTF-8");
            //3、判断提交上来的数据是否是上传表单的数据
            if(!ServletFileUpload.isMultipartContent(request)){
                //按照传统方式获取数据
                return;
            }

            //设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB
            upload.setFileSizeMax(1024*1024*10);
            //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB
            upload.setSizeMax(1024*1024*30);
            //4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                //如果fileitem中封装的是普通输入项的数据
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解决普通输入项的数据的中文乱码问题
                    String value = item.getString("UTF-8");
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name + "=" + value);
                }else{//如果fileitem中封装的是上传文件
                    //得到上传的文件名称,
                    String filename = item.getName();
                    System.out.println(filename);
                    if(filename==null || filename.trim().equals("")){
                        continue;
                    }
                    //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
                    //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                    filename = filename.substring(filename.lastIndexOf("\\")+1);
                    //得到上传文件的扩展名
                    String fileExtName = filename.substring(filename.lastIndexOf(".")+1);
                    filename = filename.substring(0,filename.lastIndexOf("."));
                    //如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法
                    System.out.println("上传的文件的扩展名是:"+fileExtName);
                    //保存文件
                    FileManager fileManager = new FileManager();
                    if(FileManager.ERROR.equals(fileManager.save(filename,fileExtName,"song",item.getInputStream()))){
                        return JsonUtil.statusResponse(1,"上传文件失败",null);
                    }
                }
            }
        }catch (FileUploadBase.FileSizeLimitExceededException e) {
            e.printStackTrace();
            return JsonUtil.statusResponse(1,"单个文件超出最大值!!!",null);
        }catch (FileUploadBase.SizeLimitExceededException e) {
            e.printStackTrace();
            return JsonUtil.statusResponse(1,"上传文件的总的大小超出限制的最大值!!!",null);
        }catch (Exception e) {
            e.printStackTrace();
            return JsonUtil.statusResponse(1,"其他异常,上传失败!!!",null);
        }
        return JsonUtil.statusResponse(0,"上传文件成功",fileManager.getFileURI(filename,fileExtName));
    }
最后由 不一样的少年 编辑于2021年12月03日 16:47