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

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

找出原因后,解决方法就很简单了,有多种:

  1. 重启tomcat,tomcat会重新创建这个临时目录

  2. 不让操作系统定时删除/tmp下tomcat目录

    vim /usr/lib/tmpfiles.d/tmp.conf# 添加一行x /tmp/tomcat.*
  3. bean中配置location,将tomcat文件上传使用的临时目录改成其它目录,避免被操作系统删除。

    @BeanMultipartConfigElement 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.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。​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值