/work/Tomcat/localhost/ROOT] is not valid问题分析和处理

7 篇文章 1 订阅

异常信息:

Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.5688631209104267196.27206/work/Tomcat/localhost/ROOT] is not valid

接口实现:

@Override
public Resp uploadFile(@RequestPart(value = "file") MultipartFile file) {
  // 业务处理
}

异常分析:

异常提示tomcat容器的临时工作目录被删除了

代码分析:

1.请求进入org.springframework.web.servlet.DispatcherServlet#doDispatch方法
2.验证是否是multipart请求
	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				// 验证multipart
				processedRequest = checkMultipart(request);
				// 代码省略...
3.checkMultipart方法
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
		if (this.multipartResolver != null && 
		// org.springframework.web.multipart.support.StandardServletMultipartResolver#isMultipart
		// 验证是否是post请求,Content-type是否是multipart/开头
		this.multipartResolver.isMultipart(request)) {
			if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
				logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
						"this typically results from an additional MultipartFilter in web.xml");
			}
			else if (hasMultipartException(request) ) {
				logger.debug("Multipart resolution failed for current request before - " +
						"skipping re-resolution for undisturbed error rendering");
			}
			else {
				try {
					// 解析文件
					// org.springframework.web.multipart.support.StandardServletMultipartResolver#resolveMultipart
					// 创建StandardMultipartHttpServletRequest对象,解析文本请求org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest
					return this.multipartResolver.resolveMultipart(request);
				}
				catch (MultipartException ex) {
					if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
						logger.debug("Multipart resolution failed for error dispatch", ex);
						// Keep processing error dispatch with regular request handle below
					}
					else {
						throw ex;
					}
				}
			}
		}
		// If not returned before: return original request.
		return request;
	}
4.parseRequest方法
private void parseRequest(HttpServletRequest request) {
		try {
			// 获取文本参数
			// 进入org.apache.catalina.connector.Request#getParts方法
			Collection<Part> parts = request.getParts();
			this.multipartParameterNames = new LinkedHashSet<>(parts.size());
			MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
			for (Part part : parts) {
				// 代码省略...
			}
			setMultipartFiles(files);
		}
		catch (Throwable ex) {
			handleParseFailure(ex);
		}
	}
5.getParts方法
public Collection<Part> getParts() throws IOException, IllegalStateException, ServletException {
		// 进入解析方法
        this.parseParts(true);
        if (this.partsParseException != null) {
            if (this.partsParseException instanceof IOException) {
            	// 抛出了异常提示中的IO异常
                throw (IOException)this.partsParseException;
            }

            if (this.partsParseException instanceof IllegalStateException) {
                throw (IllegalStateException)this.partsParseException;
            }

            if (this.partsParseException instanceof ServletException) {
                throw (ServletException)this.partsParseException;
            }
        }
        return this.parts;
    }
6.parseParts方法
private void parseParts(boolean explicit) {
        if (this.parts == null && this.partsParseException == null) {
            Context context = this.getContext();
            MultipartConfigElement mce = this.getWrapper().getMultipartConfigElement();
            if (mce == null) {
                if (!context.getAllowCasualMultipartParsing()) {
                    if (explicit) {
                        this.partsParseException = new IllegalStateException(sm.getString("coyoteRequest.noMultipartConfig"));
                        return;
                    }

                    this.parts = Collections.emptyList();
                    return;
                }

                mce = new MultipartConfigElement((String)null, (long)this.connector.getMaxPostSize(), (long)this.connector.getMaxPostSize(), this.connector.getMaxPostSize());
            }

            Parameters parameters = this.coyoteRequest.getParameters();
            parameters.setLimit(this.getConnector().getMaxParameterCount());
            boolean success = false;

            try {
            	// 获取本地路径,来自于@MultipartConfig的location,没有则返回空串
                String locationStr = mce.getLocation();
                File location;
                if (locationStr != null && locationStr.length() != 0) {
                    location = new File(locationStr);
                    if (!location.isAbsolute()) {
                        location = (new File((File)context.getServletContext().getAttribute("javax.servlet.context.tempdir"), locationStr)).getAbsoluteFile();
                    }
                } else {
                	// 因为没有配置location,所有会到这里取tomcat的临时文件工作目录
                    location = (File)context.getServletContext().getAttribute("javax.servlet.context.tempdir");
                }
					// 如果临时目录不存在就会抛异常
					// 上述的异常原因就是因为目录被删了,导致的异常
                if (location.isDirectory()) {
                    // 代码省略...
                }
                // 给异常提示
parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID);
                this.partsParseException = new IOException(sm.getString("coyoteRequest.uploadLocationInvalid", new Object[]{location}));
            } finally {
                if (this.partsParseException != null || !success) {
                    parameters.setParseFailedReason(FailReason.UNKNOWN);
                }
            }
        }
    }
7.为什么javax.servlet.context.tempdir临时目录会被删除?

原因是因为在Linux系统中/tmp目录下的文件默认10天清理一次

cat /usr/lib/tmpfiles.d/tmp.conf

在这里插入图片描述

8.怎样解决问题?
方法一:修改Linux配置文件

在/usr/lib/tmpfiles.d/tmp.conf中添加临时目录清除排除规则

x /tmp/tomcat.*
方法二:项目中指定basedir路径
// Tomcat base directory. If not specified, a temporary directory is used.
server.tomcat.basedir=/data/file

注意:配置的前提是Spring Boot 的web项目且使用的是tomcat容器,配置是针对tomcat容器的,basedir会在org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer#customize代码中设置的,并且server.tomcat.basedir配置不是实时生效的,需要重启项目,tomcat会重新加载配置,替换临时目录。
测试目录是否生效很简单,从tomcat的上下文中读取一下看看就可以了:

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    private HttpServletRequest httpServletRequest;

    @GetMapping("/ok")
    public String testOk() {
        // javax.servlet.context.tempdir
        Object attribute = httpServletRequest.getServletContext().getAttribute(ServletContext.TEMPDIR);
        return attribute == null ? "tmpdir is blank" : attribute.toString();
    }
}

org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer#customize代码中会设置basedir

@Override
	public void customize(ConfigurableTomcatWebServerFactory factory) {
		ServerProperties properties = this.serverProperties;
		ServerProperties.Tomcat tomcatProperties = properties.getTomcat();
		PropertyMapper propertyMapper = PropertyMapper.get();
		// 会设置org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#baseDirectory字段的值
		propertyMapper.from(tomcatProperties::getBasedir).whenNonNull()
				.to(factory::setBaseDirectory);

设置basedir之后的webserver初始化会使用:
org.springframework.context.support.AbstractApplicationContext#refresh
org.springframework.context.support.AbstractApplicationContext#onRefresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		Tomcat tomcat = new Tomcat();
		// 这个地方就会更改临时文件目录,如果没有配置就会创建默认的临时文件目录
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());

默认的临时文件目录创建:

/**
	 * Return the absolute temp dir for given web server.
	 * @param prefix server name
	 * @return the temp dir for given server.
	 */
	protected final File createTempDir(String prefix) {
		try {
			File tempDir = File.createTempFile(prefix + ".", "." + getPort());
			tempDir.delete();
			tempDir.mkdir();
			tempDir.deleteOnExit();
			return tempDir;
		}
		catch (IOException ex) {
			throw new WebServerException(
					"Unable to create tempDir. java.io.tmpdir is set to "
							+ System.getProperty("java.io.tmpdir"),
					ex);
		}
	}

创建临时文件

public static File createTempFile(String prefix, String suffix, File directory) throws IOException
    {
        if (prefix.length() < 3)
            throw new IllegalArgumentException("Prefix string too short");
        if (suffix == null)
            suffix = ".tmp";
		// TempDirectory.location()获取的就是java.io.tmpdir目录,Linux中就是/tmp
		// private static final File tmpdir = new File(AccessController.doPrivileged(new GetPropertyAction("java.io.tmpdir")));
        File tmpdir = (directory != null) ? directory
                                          : TempDirectory.location();

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值