今天公司一项目突然出现无法上传文件的异常,便和同事对该问题进行了分析,通过阅读了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天删除一次。
找出原因后,解决方法就很简单了,有多种:
-
重启tomcat,tomcat会重新创建这个临时目录
-
不让操作系统定时删除/tmp下tomcat目录
vim /usr/lib/tmpfiles.d/tmp.conf
# 添加一行
x /tmp/tomcat.*
-
bean中配置location,将tomcat文件上传使用的临时目录改成其它目录,避免被操作系统删除。
@Bean
MultipartConfigElement 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.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。