SpringBoot文件上传异常之提示The temporary upload location xxx is not valid

简介: SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid目录非法

SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid目录非法,实际查看目录,结果还真没有,下面就这个问题的表现,分析下SpringBoot针对文件上传的处理过程


I. 问题分析



0. 堆栈分析


问题定位,最佳的辅助手段就是堆栈分析,首先捞出核心的堆栈信息


org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:122)
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:113)
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:86)
        at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:93)
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
复制代码


从堆栈内容来看,问题比较清晰,目录非法,根据path路径,进入目录,结果发现,没有这个目录,那么问题的关键就是没有目录为什么会导致异常了,这个目录到底有啥用

先简单描述下上面的原因,上传的文件会缓存到本地磁盘,而缓存的路径就是上面

/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT

,接着引入的疑问就是:


  • 为什么上传的文件要缓存到本地
  • 为什么临时目录会不存在
  • 什么地方实现文件缓存


1. 场景模拟


要确认上面的问题,最直观的方法就是撸源码,直接看代码就有点蛋疼了,接下来采用debug方式来层层剥离,看下根源再哪里。


首先是搭建一个简单的测试项目,进行场景复现, 首先创建一个接收文件上传的Controller,如下


@RestController
@RequestMapping(path = "/file")
public class FileUploadRest {
    /**
     * 保存上传的文件
     *
     * @param file
     * @return
     */
    private String saveFileToLocal(MultipartFile file) {
        try {
            String name = "/tmp/out_" + System.currentTimeMillis() + file.getName();
            FileOutputStream writer = new FileOutputStream(new File(name));
            writer.write(file.getBytes());
            writer.flush();
            writer.close();
            return name;
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
    @PostMapping(path = "upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        String ans = saveFileToLocal(file);
        return ans;
    }
}
复制代码


其次就是使用curl来上传文件

curl http://127.0.0.1:8080/file/upload -F "file=@/Users/user/Desktop/demo.jpg" -v
复制代码


然后在接收文件上传的方法中开启断点,注意下面红框中的 location, 就是文件上传的临时目录

image.png


2. 源码定位


上面的截图可以确认确实将上传的文件保存到了临时目录,验证方式就是进入那个目录进行查看,会看到一个tmp文件,接下来我们需要确定的是在什么地方,实现将数据缓存到本地的。


注意下图,左边红框是这次请求的完整链路,我们可以通过逆推链路,去定位可能实现文件缓存的地方


image.png

如果对spring和tomcat的源码不熟的话,也没什么特别的好办法,从上面的链路中,多打一些断点,采用传说中的二分定位方法来缩小范围。

通过最开始的request对象和后面的request对象分析,发现一个可以作为参考标准的就是上图中右边红框的request#parts属性;开始是null,文件保存之后则会有数据,下面给一个最终定位的动图


image.png


所以关键就是

org.springframework.web.filter.HiddenHttpMethodFilter#doFilterInternal 中的 String paramValue = request.getParameter(this.methodParam); 这一行代码


image.png


到这里在单步进去,主要的焦点将集中在 org.apache.catalina.connector.Request#parseParts

image.png


进入上面方法的逻辑,很容易找到具体的实现位置 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest,这个方法的实现比较有意思,有必要贴出来看一下

public List<FileItem> parseRequest(RequestContext ctx)
        throws FileUploadException {
    List<FileItem> items = new ArrayList<>();
    boolean successful = false;
    try {
        FileItemIterator iter = getItemIterator(ctx);
        // 注意这里,文件工厂类,里面保存了临时目录的地址
        // 这个对象首次是在 org.apache.catalina.connector.Request#parseParts 方法的
        FileItemFactory fac = getFileItemFactory();
        if (fac == null) {
            throw new NullPointerException("No FileItemFactory has been set.");
        }
        while (iter.hasNext()) {
            final FileItemStream item = iter.next();
            // Don't use getName() here to prevent an InvalidFileNameException.
            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(String.format("Processing of %s request failed. %s",
                                                       MULTIPART_FORM_DATA, e.getMessage()), e);
            }
            final FileItemHeaders fih = item.getHeaders();
            fileItem.setHeaders(fih);
        }
        successful = true;
        return items;
    } catch (FileUploadIOException e) {
        throw (FileUploadException) e.getCause();
    } catch (IOException e) {
        throw new FileUploadException(e.getMessage(), e);
    } finally {
        if (!successful) {
            for (FileItem fileItem : items) {
                try {
                    fileItem.delete();
                } catch (Exception ignored) {
                    // ignored TODO perhaps add to tracker delete failure list somehow?
                }
            }
        }
    }
}
复制代码


核心代码就两点,一个是文件工厂类,一个是流的拷贝;前者定义了我们的临时文件目录,也是我们解决前面问题的关键,换一个我自定义的目录永不删除,不就可以避免上面的问题了么;后面一个则是数据复用方面的


首先看下FileItemFactory的实例化位置,在org.apache.catalina.connector.Request#parseParts中,代码如下


image.png

具体的location实例化代码为

// TEMPDIR = "javax.servlet.context.tempdir";
location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));
复制代码


3. 问题review


a. 解决问题


到上面,基本上就捞到了最终的问题,先看如何解决这个问题


方法1

  • 应用重启


方法2

  • 增加服务配置,自定义baseDir
server.tomcat.basedir=/tmp/tomcat
复制代码


方法3

  • 注入bean,手动配置临时目录
@Bean
MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setLocation("/tmp/tomcat");
    return factory.createMultipartConfig();
}
复制代码


方法4

  • 配置不删除tmp目录下的tomcat
vim /usr/lib/tmpfiles.d/tmp.conf
# 添加一行
x /tmp/tomcat.*
复制代码


b. 流拷贝


tomcat中实现流的拷贝代码如下,org.apache.tomcat.util.http.fileupload.util.Streams#copy(java.io.InputStream, java.io.OutputStream, boolean, byte[]) , 看下面的实现,直观影响就是写得真特么严谨


public static long copy(InputStream inputStream,
            OutputStream outputStream, boolean closeOutputStream,
            byte[] buffer)
    throws IOException {
    OutputStream out = outputStream;
    InputStream in = inputStream;
    try {
        long total = 0;
        for (;;) {
            int res = in.read(buffer);
            if (res == -1) {
                break;
            }
            if (res > 0) {
                total += res;
                if (out != null) {
                    out.write(buffer, 0, res);
                }
            }
        }
        if (out != null) {
            if (closeOutputStream) {
                out.close();
            } else {
                out.flush();
            }
            out = null;
        }
        in.close();
        in = null;
        return total;
    } finally {
        IOUtils.closeQuietly(in);
        if (closeOutputStream) {
            IOUtils.closeQuietly(out);
        }
    }
}
复制代码


c. 自问自答



前面提出了几个问题,现在给一个简单的回答,因为篇幅问题,后面会单开一文,进行详细说明


什么地方缓存文件


上面的定位过程给出答案,具体实现逻辑在 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest


为什么目录会不存在


springboot启动时会创建一个/tmp/tomcat.*/work/Tomcat/localhost/ROOT的临时目录作为文件上传的临时目录,但是该目录会在n天之后被系统自动清理掉,这个清理是由linux操作系统完成的,具体的配置如下 vim /usr/lib/tmpfiles.d/tmp.conf


#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
# See tmpfiles.d(5) for details
# Clear tmp directories separately, to make them easier to override
v /tmp 1777 root root 10d
v /var/tmp 1777 root root 30d
# Exclude namespace mountpoints created with PrivateTmp=yes
x /tmp/systemd-private-%b-*
X /tmp/systemd-private-%b-*/tmp
x /var/tmp/systemd-private-%b-*
X /var/tmp/systemd-private-%b-*/tmp
复制代码


为什么要缓存文件


因为流取一次消费之后,后面无法再从流中获取数据,所以缓存方便后续复用;这一块后面详细说明


4. 小结



定位这个问题的感觉,就是对SpringBoot和tomcat的底层,实在是不太熟悉,作为一个以Spring和tomcat吃饭的码农而言,发现问题就需要改正,列入todo列表,后续需要深入一下



相关文章
|
9月前
|
XML Java Maven
springboot-多环境配置文件
本文介绍了如何创建开发和生产环境的配置文件,并在IDEA和Maven中进行配置。开发环境中,通过设置profile为`dev`来指定配置文件;生产环境中,使用Maven命令参数`-Pprod`打包并指定配置文件。公共配置可放在`application.yml`中统一管理。日志配置需确保`logback-spring.xml`中的profile正确,以保证日志正常输出。
566 4
springboot-多环境配置文件
|
10月前
|
存储 前端开发 Java
Springboot静态资源映射及文件映射
在Spring Boot项目中,为了解决前端访问后端存储的图片问题,起初尝试通过静态资源映射实现,但发现这种方式仅能访问打包时已存在的文件。对于动态上传的图片(如头像),需采用资源映射配置,将特定路径映射到服务器上的文件夹,确保新上传的图片能即时访问。例如,通过`addResourceHandler(&quot;/img/**&quot;).addResourceLocations(&quot;file:E:\\myProject\\forum_server\\&quot;)`配置,使前端可通过URL直接访问图片。
646 0
Springboot静态资源映射及文件映射
|
9月前
|
前端开发 Cloud Native Java
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
|
9月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
1226 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
9月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
440 0
|
Java 应用服务中间件
SpringBoot获取项目文件的绝对路径和相对路径
SpringBoot获取项目文件的绝对路径和相对路径
716 1
SpringBoot获取项目文件的绝对路径和相对路径
|
XML Java Kotlin
springboot + minio + kkfile实现文件预览
本文介绍了如何在容器中安装和启动kkfileviewer,并通过Spring Boot集成MinIO实现文件上传与预览功能。首先,通过下载kkfileviewer源码并构建Docker镜像来部署文件预览服务。接着,在Spring Boot项目中添加MinIO依赖,配置MinIO客户端,并实现文件上传与获取预览链接的接口。最后,通过测试验证文件上传和预览功能的正确性。
1316 4
springboot + minio + kkfile实现文件预览
|
网络协议 Java
springboot配置hosts文件
springboot配置hosts文件
215 11
|
存储 前端开发 JavaScript

热门文章

最新文章