今天公司一项目突然出现无法上传文件的异常,便和同事对该问题进行了分析,通过阅读了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.javaprivate void parseParts(boolean explicit) {// Return immediately if the parts have already been parsedif (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 TEMPDIRlocation = 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 validsm.getString("coyoteRequest.uploadLocationInvalid",location));return;}
通过代码分析与断点调试,总结报错的原因下:
tomcat启动时会在/tmp目录下创建的临时文件夹,文件从浏览器上传后会先保存在这个临时目录中,然后再提供给web应用使用,当这个目录被系统或者其他人删除后,有文件上传时就会报这个错。如果不设置,/tmp内的临时目录会定期被操作系统删除,我们这个项目所在的操作系统是30天删除一次。
找出原因后,解决方法就很简单了,有多种:
-
重启tomcat,tomcat会重新创建这个临时目录
-
不让操作系统定时删除/tmp下tomcat目录
vim /usr/lib/tmpfiles.d/tmp.conf# 添加一行x /tmp/tomcat.* -
bean中配置location,将tomcat文件上传使用的临时目录改成其它目录,避免被操作系统删除。
@BeanMultipartConfigElement multipartConfigElement() {MultipartConfigFactory factory = new MultipartConfigFactory();factory.setLocation("/非tmp目录/tomcat");return factory.createMultipartConfig();} -
通过filter等方式设置location
context.getServletContext().setAttribute(ServletContext.TEMPDIR),new File("/非tmp目录/tomcat"))); -
springboot配置文件设置
server.tomcat.basedir=/非tmp目录/tomcat
tomcat处理文件上传的方式只一种,即将上传文件先保存到临时目录,然后提供给web应用,好奇心驱使下看了一下jetty处理文件上传的代码:
org.eclipse.jetty.util.MultiPartInputStreamParser.MultiPartprotected 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。
527

被折叠的 条评论
为什么被折叠?



