從multipartResolver的一個異常到multipartResolver處理請求過程分析

記錄一下前段時間遇到的一個關於multipartResolver的異常,以及後面找出緣由的過程。java

異常分析

異常以下:web

2018-01-22 18:05:38.041 ERROR com.exception.ExceptionHandler.resolveException:22 -Could not Q multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:165) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1089) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:928) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:870) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) [servlet-api.jar:na]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [servlet-api.jar:na]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [catalina.jar:8.5.24]
    at org.apache.catalina.core.A
複製代碼

這個異常大意是說multipart/form-data傳輸的表單存在空值,沒有辦法從request的表單中讀到某個值。spring

肯定了請求自己非空值以後,去看看是否是SpringMVC接收請求並從請求中讀出參數的過程當中出了問題。apache

那麼,SpringMVC是如何處理請求傳過來的文件的呢?api

multipartResolver處理請求的過程

DispatcherServlet轉發

首先,Spring提供了對文件多路上傳的支持,只要註冊一個名爲"multipartResolver"的bean,那麼後續SpringMVC的DispatcherServlet在接收到請求的時候,會判斷請求是否是multipart文件。 若是是的話,就會調用"multipartResolver",將請求包裝成一個MultipartHttpServletRequest對象,而後後面就能夠從這個對象中取出文件來進行處理了。緩存

multipartResolver的裝載

Spring提供了一個對於MultipartResolver接口的實現:org.springframework.web.multipart.commons.CommonsMultipartResolver。看一下源碼:安全

public class CommonsMultipartResolver extends CommonsFileUploadSupport
		implements MultipartResolver, ServletContextAware {
...
}
複製代碼

CommonsFileUploadSupport是對於XML配置"multipartResolver"時的支持。 在XML配置multipartResolver時的配置以下:bash

<bean id="multipartResolver"
             class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
              <!-- 默認編碼 -->
              <property name="defaultEncoding" value="utf-8" />
              <!-- 設置multipart請求所容許的最大大小,默認不限制 -->
              <property name="maxUploadSize" value="10485760000" />
              <!-- 設置一個大小,multipart請求小於這個大小時會存到內存中,大於這個內存會存到硬盤中 -->
              <property name="maxInMemorySize" value="40960" />
       </bean>
複製代碼

這些property配置會被加載到CommonsFileUploadSupport中,而後被CommonsMultipartResolver繼承。併發

CommonsMultipartResolver的處理過程

而後就是,其實CommonsMultipartResolver依賴於Apache的jar包來實現:common-fileupload。mvc

CommonsMultipartResolver接收到請求以後,是這樣對HttpServletReques進行處理的:

(CommonsMultipartResolver文件)

@Override
	public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
		Assert.notNull(request, "Request must not be null");
        //懶加載
		if (this.resolveLazily) {
			return new DefaultMultipartHttpServletRequest(request) {
				@Override
				protected void initializeMultipart() {
					MultipartParsingResult parsingResult = parseRequest(request);
					setMultipartFiles(parsingResult.getMultipartFiles());
					setMultipartParameters(parsingResult.getMultipartParameters());
					setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
				}
			};
		}
		else {
        	 //這裏對request進行了解析
			MultipartParsingResult parsingResult = parseRequest(request);
			return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
					parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
		}
	}
複製代碼

this.resolveLazily是懶加載,若是爲true,會在initializeMultipart()被調用,即發起文檔信息獲取的時候,纔去封裝DefaultMultipartHttpServletRequest;若是爲false,當即封裝DefaultMultipartHttpServletRequest。

resolveLazily默認爲false。

而後再去看一下parseRequest(request)的解析:

(CommonsMultipartResolver文件)

	/**
	 * Parse the given servlet request, resolving its multipart elements.
	 * 對servlet請求進行處理,轉成multipart結構
	 * @param request the request to parse
	 * @return the parsing result
	 * @throws MultipartException if multipart resolution failed.
	 */
	protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
    	//從請求中讀出這個請求的編碼
		String encoding = determineEncoding(request);
        //按照請求的編碼,獲取一個FileUpload對象,裝載到CommonsFileUploadSupport的property屬性都會被裝入這個對象中
        //prepareFileUpload是繼承自CommonsFileUploadSupport的函數,會比較請求的編碼和XML中配置的編碼,若是不同,會拒絕處理
		FileUpload fileUpload = prepareFileUpload(encoding);
		try {
        	//對請求中的multipart文件進行具體的處理
			List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
			return parseFileItems(fileItems, encoding);
		}
		catch (FileUploadBase.SizeLimitExceededException ex) {
			throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
		}
		catch (FileUploadException ex) {
			throw new MultipartException("Could not parse multipart servlet request", ex);
		}
	}
複製代碼

上面的((ServletFileUpload) fileUpload).parseRequest(request)解析實現以下:

(FileUploadBase文件)

    /**
     * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
     * compliant <code>multipart/form-data</code> stream.
     *
     * @param ctx The context for the request to be parsed.
     *
     * @return A list of <code>FileItem</code> instances parsed from the
     *         request, in the order that they were transmitted.
     *
     * @throws FileUploadException if there are problems reading/parsing
     *                             the request or storing files.
     */
    public List<FileItem> parseRequest(RequestContext ctx)
            throws FileUploadException {
        List<FileItem> items = new ArrayList<FileItem>();
        boolean successful = false;
        try {
        	//從請求中取出multipart文件
            FileItemFactoryFactoryFactoryator iter = getItemIterator(ctx);
            //得到FileItemFactory工廠,實現類爲DiskFileItemFactory
            FileItemFactory fac = getFileItemFactory();
            if (fac == null) {
                throw new NullPointerException("No FileItemFactory has been set.");
            }
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();
                // Don't use getName() here to prevent an InvalidFileNameException. final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name; //工廠模式,獲取FileItem對象,實現類是DiskFileItem FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); items.add(fileItem); try { Streams.copy(item.openStream(), fileItem.getOutputStream(), true); } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { //咱們遇到的異常就是在這裏拋出的 throw new IOFileUploadException(format("Processing of %s request failed. %s", MULTIPART_FORM_DATA, e.getMessage()), e); } final FileItemHeaders fih = item.getHeaders(); fileItem.setHeaders(fih); } successful = true; return items; } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { throw new FileUploadException(e.getMessage(), e); } finally { if (!successful) { for (FileItem fileItem : items) { try { fileItem.delete(); } catch (Throwable e) { // ignore it } } } } } 複製代碼

咱們遇到的異常就是在這個位置拋出的,後面找錯誤會在這裏深刻,可是咱們仍是先把整個請求流轉的流程走完。

到此,List對象就處理完返回了,而後再繼續看對List的處理

(CommonsFileUploadSupport文件)

	/**
	 * Parse the given List of Commons FileItems into a Spring MultipartParsingResult,
	 * containing Spring MultipartFile instances and a Map of multipart parameter.
	 * @param fileItems the Commons FileIterms to parse
	 * @param encoding the encoding to use for form fields
	 * @return the Spring MultipartParsingResult
	 * @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem)
	 */
	protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
		MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
		Map<String, String[]> multipartParameters = new HashMap<String, String[]>();
		Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();

		// Extract multipart files and multipart parameters.
		for (FileItem fileItem : fileItems) {
        	//若是fileItem是一個表單
			if (fileItem.isFormField()) {
				String value;
				String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
				if (partEncoding != null) {
					try {
						value = fileItem.getString(partEncoding);
					}
					catch (UnsupportedEncodingException ex) {
						if (logger.isWarnEnabled()) {
							logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
									"' with encoding '" + partEncoding + "': using platform default");
						}
						value = fileItem.getString();
					}
				}
				else {
					value = fileItem.getString();
				}
				String[] curParam = multipartParameters.get(fileItem.getFieldName());
				if (curParam == null) {
					// simple form field
					multipartParameters.put(fileItem.getFieldName(), new String[] {value});
				}
				else {
					// array of simple form fields
					String[] newParam = StringUtils.addStringToArray(curParam, value);
					multipartParameters.put(fileItem.getFieldName(), newParam);
				}
				multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
			}
            //若是fileItem是一個multipart文件
			else {
				// multipart file field
				CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
				multipartFiles.add(file.getName(), file);
				if (logger.isDebugEnabled()) {
					logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
							" bytes with original filename [" + file.getOriginalFilename() + "], stored " +
							file.getStorageDescription());
				}
			}
		}
		return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
	}
複製代碼

到此,MultipartParsingResult的處理就結束並返回了,而後CommonsMultipartResolver中的resolveMultipart就將其裝到DefaultMultipartHttpServletRequest中並返回,處理完了。

DefaultMultipartHttpServletRequest是MultipartHttpServletRequest的實現類。

關於maxInMemorySize

前面已經說過,maxInMemorySize的做用是 「設置一個大小,multipart請求小於這個大小時會存到內存中,大於這個內存會存到硬盤中」。 再看一下maxInMemorySize被set到對象中的過程:

(CommonsFileUploadSupport文件)

	/**
	 * Set the maximum allowed size (in bytes) before uploads are written to disk.
	 * Uploaded files will still be received past this amount, but they will not be
	 * stored in memory. Default is 10240, according to Commons FileUpload.
	 * @param maxInMemorySize the maximum in memory size allowed
	 * @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold
	 */
	public void setMaxInMemorySize(int maxInMemorySize) {
		this.fileItemFactory.setSizeThreshold(maxInMemorySize);
	}
複製代碼

CommonsFileUploadSupport中有一個fileItemFactory對象,maxInMemorySize就被set到了這個工廠類的屬性SizeThreshold裏。

這個fileItemFactory工廠類,會在生成fileItem對象的時候用到。 生成這個對象的過程當中,會根據maxInMemorySize來判斷,是將其存到內存中,仍是存到硬盤中。

存儲的過程在前面已經提過了:

...
		try {
           		Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
                } catch (FileUploadIOException e) {
                    throw (FileUploadException) e.getCause();
                } catch (IOException e) {
                    throw new IOFileUploadException(format("Processing of %s request failed. %s",
                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
                }
                final FileItemHeaders fih = item.getHeaders();
複製代碼

進入fileItem.getOutputStream()看看:

/**
     * Returns an {@link java.io.OutputStream OutputStream} that can
     * be used for storing the contents of the file.
     *
     * @return An {@link java.io.OutputStream OutputStream} that can be used
     *         for storing the contensts of the file.
     *
     * @throws IOException if an error occurs.
     */
    public OutputStream getOutputStream()
        throws IOException {
        if (dfos == null) {
            File outputFile = getTempFile();
            dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
        }
        return dfos;
    }
複製代碼

再進去getTempFile():

/**
     * Creates and returns a {@link java.io.File File} representing a uniquely
     * named temporary file in the configured repository path. The lifetime of
     * the file is tied to the lifetime of the <code>FileItem</code> instance;
     * the file will be deleted when the instance is garbage collected.
     *
     * @return The {@link java.io.File File} to be used for temporary storage.
     */
    protected File getTempFile() {
        if (tempFile == null) {
            File tempDir = repository;
            if (tempDir == null) {
                tempDir = new File(System.getProperty("java.io.tmpdir"));
            }

            String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());

            tempFile = new File(tempDir, tempFileName);
        }
        return tempFile;
    }
複製代碼

當沒有設置uploadTempDir屬性,也就是FileItemFactory中的repository的時候會自動選擇一個緩存路徑System.getProperty("java.io.tmpdir"),將上傳請求落到硬盤上的這個位置。

注意看這個註釋:the file will be deleted when the instance is garbage collected. 這裏說了FileItem的實例聲明週期,當GC的時候,存在內存裏的FileItem會被GC回收掉。因此這就是爲何沒有辦法讀到multipart/form-data對象。

bug緣由和解決方案

  1. 解決頻繁GC的問題。太過頻繁的GC明顯是出了問題了,致使請求中的文件被回收掉,報空指針。(這也是我這邊解決問題的方案)
  2. 設置好maxInMemorySize和uploadTempDir兩個屬性,保證上傳文件緩存到硬盤上,普通請求在內存中就能夠了。若是涉及大量的文件上傳,這個是頗有必要的,否則併發高的時候,內存會被文件給佔滿。而後會觸發GC,FileItem被回收掉以後,後面就會再去讀取,就被出現咱們異常中的空指針錯誤。
  3. 還有一種可能性,就是multipartResolver配置的時候,沒有設置uploadTempDir屬性。按理說這個是沒有問題的,由於會默認幫你設爲系統的緩存路徑,這個路徑一般是/tmp,這個目錄全部用戶都有權限讀取。可是若是是生產環境,這個系統默認的緩存路徑極可能會被修改過,修改了位置,或者權限。這也是爲了安全的方面考慮,可是這在咱們所講的流程中,就會形成後面讀取的時候,出現空指針的錯誤。

這些異常都不容易排查,因此須要對整個流程都清晰了以後,才容易找到問題的所在。單單看本身的代碼是不能看出來的,例如權限的問題,在實際生產環境中才會遇到,也比較無奈。

我爲何要把這個問題寫的這麼複雜

把這個問題寫了這麼多,最後的解決方案卻寫的不多,看起來多是很傻,可是是有緣由的:

  1. 這個異常在網上沒有找到有用的解決方案,也沒有看到講明白緣由的
  2. 把流程理一遍,有利於後面遇到相似問題的時候,更好地解決。例如說不是報空值,而是報其餘問題的時候。
  3. 最重要的一點,因爲運行環境的緣由,沒有辦法復現,也沒有找到當時的堆棧信息,因此不得不整個流程都走了一遍......
相關文章
相關標籤/搜索