今天公司一项目突然出现无法上传文件的异常,便和同事对该问题进行了分析,通过阅读了tomcat与jetty相关功能代码,对它们实现文件上传有了一定的了解。
该项目使用SpringBoot实现,上传异常提示如下:
The temporary upload location xxx is not valid
背景:项目基于springboot开发,嵌入了tomcat插件,服务启动刚好满30天,之前文件上传功能一直是正常。
通过错误提示分析应该是一个临时上传的目录失效了,但不确定它用了哪个临时目录,干脆直接打开tomcat源代码全局搜索“The temporary upload location”字符串,找到类org.apache.catalina.connector.Rquest抛出了这个异常,具体方法代码如下:
org.apache.catalina.connector.Rquest.java private void parseParts(boolean explicit) { // Return immediately if the parts have already been parsed if (parts != null || partsParseException != null) { return; } Context context = getContext(); MultipartConfigElement mce = getWrapper().getMultipartConfigElement(); if (mce == null) { if(context.getAllowCasualMultipartParsing()) { mce = new MultipartConfigElement(null, connector.getMaxPostSize(), connector.getMaxPostSize(), connector.getMaxPostSize()); } else { if (explicit) { partsParseException = new IllegalStateException( sm.getString("coyoteRequest.noMultipartConfig")); return; } else { parts = Collections.emptyList(); return; } } } Parameters parameters = coyoteRequest.getParameters(); parameters.setLimit(getConnector().getMaxParameterCount()); boolean success = false; try { File location; String locationStr = mce.getLocation(); //当没有指定location,直接创建临时目录 if (locationStr == null || locationStr.length() == 0) { location = ((File) context.getServletContext().getAttribute( ServletContext.TEMPDIR)); } else { //当指定了location, // If relative, it is relative to TEMPDIR location = new File(locationStr); if (!location.isAbsolute()) { location = new File( (File) context.getServletContext().getAttribute(ServletContext.TEMPDIR), locationStr).getAbsoluteFile(); } } if (!location.exists() && context.getCreateUploadTargets()) { log.warn(sm.getString("coyoteRequest.uploadCreate", location.getAbsolutePath(), getMappingData().wrapper.getName())); if (!location.mkdirs()) { log.warn(sm.getString("coyoteRequest.uploadCreateFail", location.getAbsolutePath())); } } //当文件不存在或者不是目录时 if (!location.isDirectory()) { parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID); partsParseException = new IOException( //coyoteRequest.uploadLocationInvalid=The temporary upload location [{0}] is not valid sm.getString("coyoteRequest.uploadLocationInvalid", location)); return; }
通过代码分析与断点调试,总结报错的原因下:
tomcat启动时会在/tmp目录下创建的临时文件夹,文件从浏览器上传后会先保存在这个临时目录中,然后再提供给web应用使用,当这个目录被系统或者其他人删除后,有文件上传时就会报这个错。如果不设置,/tmp内的临时目录会定期被操作系统删除,我们这个项目所在的操作系统是30天删除一次。
找出原因后,解决方法就很简单了,有5种:
1. 重启tomcat,tomcat会重新创建这个临时目录
2. 不让操作系统定时删除/tmp下tomcat目录
vim /usr/lib/tmpfiles.d/tmp.conf # 添加一行 x /tmp/tomcat.*
3.bean中配置location,将tomcat文件上传使用的临时目录改成其它目录,避免被操作系统删除。
@Bean MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setLocation("/非tmp目录/tomcat"); return factory.createMultipartConfig(); }
4.通过filter等方式设置location
免费源码context.getServletContext().setAttribute(ServletContext.TEMPDIR), new File("/非tmp目录/tomcat")));
5.springboot配置文件设置
server.tomcat.basedir=/非tmp目录/tomcat
tomcat处理文件上传的方式只一种,即将上传文件先保存到临时目录,然后提供给web应用,好奇心驱使下看了一下jetty处理文件上传的代码:
org.eclipse.jetty.util.MultiPartInputStreamParser.MultiPart protected void open() throws IOException { //We will either be writing to a file, if it has a filename on the content-disposition //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we //will need to change to write to a file. if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0) { createFile(); } else { //Write to a buffer in memory until we discover we've exceed the //MultipartConfig fileSizeThreshold _out = _bout= new ByteArrayOutputStream2(); } } protected void createFile() throws IOException { Path parent = MultiPartInputStreamParser.this._tmpDir.toPath(); Path tempFile = Files.createTempFile(parent, "MultiPart", ""); _file = tempFile.toFile(); OutputStream fos = Files.newOutputStream(tempFile, StandardOpenOption.WRITE); BufferedOutputStream bos = new BufferedOutputStream(fos); if (_size > 0 && _out != null) { //already written some bytes, so need to copy them into the file _out.flush(); _bout.writeTo(bos); _out.close(); } _bout = null; _out = bos; }
可以看出jetty提供了两种方式:第一种与tomcat一样,先写入到临时目录,第二种则是直接写入到内存,这样可以提供非常好地读写速度。
当你jvm内存足够大并且客户无法忍受tomcat缓慢的上传速度时,可以采用jetty基于内存的文件上传方式,这个方式也是jetty默认使用的方式,如果你不注意控制文件上传大小,很容易出现OOM。