知客网

jetty与tomcat实现文件上传的区别(源码分析)

2021-10-23 11:23:00

今天公司一项目突然出现无法上传文件的异常,便和同事对该问题进行了分析,通过阅读了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。


上一篇:

下一篇:

Copyright© 2015-2020 知客网版权所有