异常信息:
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) {
// 业务处理
}
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临时目录会被删除?
8.怎样解决问题?
方法一:修改Linux配置文件
测试目录是否生效很简单,从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);
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();
