1. 程式人生 > >關於使用webuploader上傳檔案出現的The temporary upload location is not valid

關於使用webuploader上傳檔案出現的The temporary upload location is not valid

用webUploader外掛上傳檔案的時候,經常出現如下問題:

用google瀏覽器進行除錯,console頁問題如下:

network頁問題如下:

下面我們重點看一下spring給我們提供的這幾個類裡面是如何對我這次請求進行處理的:

1、FrameWorkServlet:是一個抽象類

public abstract class FrameworkServlet extends HttpServletBean
    implements ApplicationContextAware

service方法是這樣定義的:

protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
        if(HttpMethod.PATCH == httpMethod || httpMethod == null)
            processRequest(request, response);
        else
            super.service(request, response);
    }

以上HttpMethod是一個列舉型別,resolve方法返回當前請求的method的列舉型別,如果請求型別是PATCH型別(關於patch請求型別的說明請參考:https://segmentfault.com/q/1010000005685904/為啥這樣區分,目前不太清楚)或為空的話,呼叫processRequest(request,response)方法,否則呼叫super.service(request,response)(這個方法在javax.servlet.http.HttpServlet類中)方法,我們本次請求是一個post請求,所以呼叫了後者。

2、HttpServlet:是一個抽象類

public abstract class HttpServlet extends GenericServlet

service方法是這樣定義的:

protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();
        if(method.equals("GET"))
        {
            long lastModified = getLastModified(req);
            if(lastModified == -1L)
            {
                doGet(req, resp);
            } else
            {
                long ifModifiedSince;
                try
                {
                    ifModifiedSince = req.getDateHeader("If-Modified-Since");
                }
                catch(IllegalArgumentException iae)
                {
                    ifModifiedSince = -1L;
                }
                if(ifModifiedSince < (lastModified / 1000L) * 1000L)
                {
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else
                {
                    resp.setStatus(304);
                }
            }
        } else
        if(method.equals("HEAD"))
        {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
        } else
        if(method.equals("POST"))
            doPost(req, resp);
        else
        if(method.equals("PUT"))
            doPut(req, resp);
        else
        if(method.equals("DELETE"))
            doDelete(req, resp);
        else
        if(method.equals("OPTIONS"))
            doOptions(req, resp);
        else
        if(method.equals("TRACE"))
        {
            doTrace(req, resp);
        } else
        {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object errArgs[] = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }
    }

javax.servlet.http.HttpServlet是底層Servlet對請求的處理類,以上我們看到只有get、post、put、delete、options、trace、head7種請求方式,所以我們可以猜想,FrameWorkServlet的service方法對請求進行了分別處理,在不改變javax.servlet.http.HttpServlet類的情況下,增加了對PATCH的處理,這只是我們的猜想,我們接著往下看。

又回調了FrameworkServlet中的doPost方法:

protected final void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        processRequest(request, response);
    }

我們可以看到doPost方法裡面呼叫了processRequest(request,response)方法,其實我們不妨看一下FrameworkServlet類中對其他請求型別做的處理:

都呼叫了同一個方法,那我們以上的猜想看來是正確的。

接下來我們再看processRequest()方法裡面的處理邏輯:

doService(request,response)方法是一個抽象方法,應該是要交給FrameworkServlet的子類去實現的

此處用了模板方法模式,丟擲的異常資訊只到這裡就結束了,所以我們必須找到處理該請求的FrameworkServlet的子類,也就是DispatcherServlet類中的doService方法,

我們可以看到到這裡,會解析該請求是否屬於multipartRequest請求,預設不是multipartRequest請求,我們接下來看一下checkMultipart(request)方法:

MultipartResolver是一個介面,他有兩個實現類,StandardMultipartHttpServletRequest和CommonsMultipartResolver,

StandardServletMultipartResolver:

翻譯一下大概意思就是:

該類是MultipartResolver介面的標準實現類,基於Servlet 3.0 {@link javax.servlet.http.Part} API,作為一個spring DispatcherServlet 上下文的的MultipartResolver實現類,在例項級別沒有任何額外的配置。

使用該類需要我們做如下其中的一種配置:1、在web.xml中用multipart-config模組標記對應的servlet;2、在程式中註冊該sevlet的時候用javax.servlet.MultipartConfigElement配置一下;3、如果你是自定義的servlet,你需要在你的servlet上加一個javax.servlet.annotation.MultipartConfig註解。像檔案最大值或儲存路徑這樣的相關的配置在servlet註冊的時候就要被應用,servlet3.0不允許他們在MultipartResolver級別被設定。

CommonsMultipartResolver:

 翻譯一下:

MultipartResolver的實現類,基於Apache Commons FileUpload1.2或以上版本(官方網址:http://commons.apache.org/proper/commons-fileupload),提供maxUploadSize, maxInMemorySize, defaultEncoding等屬性的配置(繼承自CommonsFileUploadSupport),見ServletFileUpload / DiskFileItemFactory(“sizeMax”,“sizeThreshold”,*“headerEncoding”)有關預設值和可接受值的詳細資訊。儲存臨時檔案到servlet容器的臨時路徑,需要通過應用程式上下文<i>或帶有ServletContext的建構函式(用於獨立使用)去初始化。

如果以上翻譯看不懂沒關係,翻譯的不好,磕磕絆絆的,我們先接著往下看。

我們現在用的是StandardServletMultipartResolver(這也是springBoot預設例項化的MultipartResolver物件)這個實現類裡面的resolveMultipart方法,

我們可以通過spring.http.multipart.enabled=false將預設方式關閉。

@Override
	public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
		return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
	}

StandardMultipartHttpServletRequest:

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
		super(request);
		if (!lazyParsing) {
			parseRequest(request);
		}
	}

parseRequest(request);

private void parseRequest(HttpServletRequest request) {
		try {
			Collection<Part> parts = request.getParts();
			this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
			MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
			for (Part part : parts) {
				String disposition = part.getHeader(CONTENT_DISPOSITION);
				String filename = extractFilename(disposition);
				if (filename == null) {
					filename = extractFilenameWithCharset(disposition);
				}
				if (filename != null) {
					files.add(part.getName(), new StandardMultipartFile(part, filename));
				}
				else {
					this.multipartParameterNames.add(part.getName());
				}
			}
			setMultipartFiles(files);
		}
		catch (Exception ex) {
			throw new MultipartException("Could not parse multipart servlet request", ex);
		}
	}

我們的異常就是在這個方法裡面丟擲來的,現在我們仔細來分析一下這個方法:這個方法主要是用來解析本次請求中上傳了幾個檔案,將所有上傳的檔案用filename,file的形式儲存起來,並設定為不可修改集合。

我們先來看一下request.getParts()這個方法:

跳轉到org.apache.catalina.connector.Request這個類中的getParts() ---->  parseParts(true),這個方法才開始真正對本次請求進行解析,其中

先從包裝器wrapper中獲取我們啟動時註冊好的MultipartConfigElement物件,獲取裡面的location,也就是我們自定義的檔案上傳路徑,如果我們沒有設定的話就將ServletContext上下文中的預設路徑javax.servlet.context.tempdir的值賦給我們的location,也就是我們在response中看到的那個invalid的路徑了,服務在linux上會將該臨時目錄建立在tmp路徑下,但是linux會定期清除tmp目錄下的檔案,所以服務執行一段時間上傳檔案時就會出現上述錯誤,因此我們可以在application.yml中加上location配置:

spring:
  http:
    multipart:
      enabled: true
      max-file-size: 200MB
      max-request-size: 200MB
      file-size-threshold: 200MB
      location: /home/developer/project/triber/tosift/upload/tmp

這樣再部署到伺服器上,觀察一段時間發現錯誤已經不會再發生了,但是最近又出現該類錯誤,而且我並沒有改動任何檔案,有點無語了。

springBoot的版本更新了,配置方式也變了。把我們配置的http變成servlet,還要加上如下配置才行:

@Configuration
public class MultipartConfig {
	
	/**
	 * 檔案上傳臨時路徑
	 * */
	@Bean
	MultipartConfigElement multipartConfigElement(
			@Value("${spring.servlet.multipart.max-file-size}") String maxFileSize,
			@Value("${spring.servlet.multipart.max-request-size}") String maxRequestSize, 
			@Value("${spring.servlet.multipart.file-size-threshold}") String fileSizeThreshold,
			@Value("${spring.servlet.multipart.location}") String location){
		MultipartConfigFactory multipartConfigFactory = new MultipartConfigFactory();
		//單個檔案大小
		multipartConfigFactory.setMaxFileSize(maxFileSize);
		//單次請求檔案大小
		multipartConfigFactory.setMaxRequestSize(maxRequestSize);
		//
		multipartConfigFactory.setFileSizeThreshold(fileSizeThreshold);
		//上傳路徑
//		multipartConfigFactory.setLocation(location);
		return multipartConfigFactory.createMultipartConfig();
	}