SpringMVC文件上傳接口設計與實現

#1 前兩篇文章的鋪墊java

#1.1 SpringMVC文件上傳源碼分析前言web

#1.2 apache fileupload源碼分析spring

#2 總體的包結構 首先看下總體的包的結構,以下圖apache

在此輸入圖片描述

總共分紅3大塊,分別以下api

##2.1 org.springframework.web.multipart數組

存放Spring定義的文件上傳接口以及異常,如tomcat

  • MultipartException對用戶拋出的解析異常(隱藏底層文件上傳解析包所拋出的異常)微信

    也就指明瞭,這個體系下只能拋出這種類型的異常,MaxUploadSizeExceededException是MultipartException它的子類,專門用於指定文件大小限制的異常。app

    用戶不該該看到底層文件上傳解析包所拋出的異常,底層採用的文件上傳解析包在解析文件上傳時也會定義本身的解析異常,這時候就須要在整合這些jar包時,須要對解析包所拋出的異常進行轉換成上述已統必定義的面向用戶的異常框架

    源碼見證下:

    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
    	String encoding = determineEncoding(request);
    	FileUpload fileUpload = prepareFileUpload(encoding);
    	try {
    		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);
    	}
    }

    FileUploadBase.SizeLimitExceededException、FileUploadException 都是底層解析包apache fileupload解析時拋出的異常,在這裏要進行try catch 處理,而後將這些異常轉化成SpringMVC自定義的異常MaxUploadSizeExceededException、MultipartException

  • MultipartFile 定義了文件解析的統一結果類型

  • MultipartResolver 定義了文件解析的處理器,不一樣的處理器不一樣的解析方式

##2.2 org.springframework.web.multipart.commons

用於整合apache fileupload的解析,對上述定義的接口進行實現,如

  • CommonsMultipartFile實現上述MultipartFile接口,即採用apache fileupload解析的結果爲CommonsMultipartFile
  • CommonsMultipartResolver實現上述MultipartResolver,待會詳細說明

##2.3 org.springframework.web.multipart.support

用於整合j2ee自帶的文件上傳的解析,對上述定義的接口進行實現,如

  • StandardMultipartFile實現上述MultipartFile接口,即採用這種方式解析的結果爲StandardMultipartFile
  • StandardServletMultipartResolver實現上述MultipartResolver,待會詳細說明

接下來詳細看看這些源碼內容

#3 SpringMVC本身的接口設計

##3.1 MultipartResolver接口的內容:

public interface MultipartResolver {
	//判斷當前的HttpServletRequest是不是文件上傳類型
	boolean isMultipart(HttpServletRequest request);
	//將HttpServletRequest轉化成MultipartHttpServletRequest
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
	//清除產生的臨時文件等
	void cleanupMultipart(MultipartHttpServletRequest request);
}

##3.2 MultipartHttpServletRequest接口內容:

MultipartHttpServletRequest 繼承了 HttpServletRequest 和 MultipartRequest,而後就具備了下面的兩個主要功能

  • 獲取文件上傳的每一部分的請求頭信息

    HttpHeaders getRequestHeaders();
    HttpHeaders getMultipartHeaders(String paramOrFileName);

    這裏的請求頭信息就是以下內容中的 Content-Disposition: form-data; name="myFile"; filename="資產型號規格模板1.xlsx" Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 等信息

    文件上傳的請求頭信息

  • 獲取文件上傳的文件內容(每一個文件信息都是MultipartFile類型)

    Iterator<String> getFileNames();
    MultipartFile getFile(String name);
    List<MultipartFile> getFiles(String name);
    Map<String, MultipartFile> getFileMap();

##3.3 整個處理流程

在SpringMVC的入口類DispatcherServlet中的doDispatch方法中,能夠看到是以下的處理流程

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HttpServletRequest processedRequest = request;
	boolean multipartRequestParsed = false;
	try {
		//略
		//步驟一
		processedRequest = checkMultipart(request);
		multipartRequestParsed = (processedRequest != request);
		//略
	}
	catch (Exception ex) {
		triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
	}
	finally {
		//略
		// Clean up any resources used by a multipart request.
		//步驟二
		if (multipartRequestParsed) {
			cleanupMultipart(processedRequest);
		}
	}
}

能夠看到這裏主要有兩個步驟

  • 步驟一 檢查是不是文件上傳類型,若是是則進行解析,而後將HttpServletRequest request封裝成MultipartHttpServletRequest
  • 步驟二 若是是文件上傳,則進行資源清理,如刪除上傳的臨時文件等

下面分別來講

###3.3.1 判斷並解析HttpServletRequest成MultipartHttpServletRequest:

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
		if (request instanceof MultipartHttpServletRequest) {
			logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
					"this typically results from an additional MultipartFilter in web.xml");
		}
		else if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) instanceof MultipartException) {
			logger.debug("Multipart resolution failed for current request before - " +
					"skipping re-resolution for undisturbed error rendering");
		}
		else {
			return this.multipartResolver.resolveMultipart(request);
		}
	}
	// If not returned before: return original request.
	return request;
}
  • 首先看看DispatcherServlet的multipartResolver屬性是否有值,而咱們在xml文件中以下的配置就是向DispatcherServlet注入multipartResolver屬性

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">  
    	<property name="defaultEncoding" value="UTF-8" />  
    </bean>

    DispatcherServlet在初始化的時候,會去尋找id爲"multipartResolver"而且類型爲MultipartResolver的bean,因此id必須爲MULTIPART_RESOLVER_BEAN_NAME即"multipartResolver",以下

    private void initMultipartResolver(ApplicationContext context) {
    try {
    	this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
    	if (logger.isDebugEnabled()) {
    		logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
    	}
    }
    catch (NoSuchBeanDefinitionException ex) {
    	// Default is no multipart resolver.
    	this.multipartResolver = null;
    	if (logger.isDebugEnabled()) {
    		logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME +
    				"': no multipart request handling provided");
    	}
    }

    }

  • 當multipartResolver屬性有值的時候,先調用它的boolean isMultipart(HttpServletRequest request)方法,判斷當前的request是不是符合文件上傳類型,若是符合則調用它的MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)方法將當前的request進行解析而且封裝成MultipartHttpServletRequest類型。有了MultipartHttpServletRequest,咱們就能獲取上傳的文件信息了。

    而後咱們就能夠經過2中途徑來獲取上傳的文件。

    • 途徑1 直接使用MultipartHttpServletRequest request做爲參數,以下

      @RequestMapping(value="/test/file",method=RequestMethod.POST)
      @ResponseBody
      public String fileUpload(MultipartHttpServletRequest request){
      	Map<String, MultipartFile> files=request.getFileMap();
      	//使用files
      	return "success";
      }
    • 途徑2 使用@RequestParam("myFile") 來獲取文件(RequestParam裏面的"myFile"是input標籤的name的值而不是文件名),以下

      @RequestMapping(value="/test/file",method=RequestMethod.POST)
      @ResponseBody
      public String fileUpload(@RequestParam("myFile") MultipartFile file){
      	//使用file
      	return "success";
      }

    對於途徑1很好理解,對於途徑2,爲何呢?

    這裏簡單提下,對於@RequestParam註解是由RequestParamMethodArgumentResolver來進行處理的,是它進行了特殊處理,當@RequestParam修飾的類型爲MultipartFile或者javax.servlet.http.Part(後面再詳細說此Part)時進行特殊處理,以下

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) 
    throws Exception {
    	Object arg;
    
    	HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
    	MultipartHttpServletRequest multipartRequest =
    		WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
    
    	if (MultipartFile.class.equals(parameter.getParameterType())) {
    		assertIsMultipartRequest(servletRequest);
    		Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: 
    			is a MultipartResolver configured?");
    		arg = multipartRequest.getFile(name);
    	}
    	else if (isMultipartFileCollection(parameter)) {
    		assertIsMultipartRequest(servletRequest);
    		Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: 
    			is a MultipartResolver configured?");
    		arg = multipartRequest.getFiles(name);
    	}
    	else if(isMultipartFileArray(parameter)) {
    		assertIsMultipartRequest(servletRequest);
    		Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: 
    			is a MultipartResolver configured?");
    		arg = multipartRequest.getFiles(name).toArray(new MultipartFile[0]);
    	}
    	else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {
    		assertIsMultipartRequest(servletRequest);
    		arg = servletRequest.getPart(name);
    	}
    	else if (isPartCollection(parameter)) {
    		assertIsMultipartRequest(servletRequest);
    		arg = new ArrayList<Object>(servletRequest.getParts());
    	}
    	else if (isPartArray(parameter)) {
    		assertIsMultipartRequest(servletRequest);
    		arg = RequestPartResolver.resolvePart(servletRequest);
    	}
    	else {
    		arg = null;
    		if (multipartRequest != null) {
    			List<MultipartFile> files = multipartRequest.getFiles(name);
    			if (!files.isEmpty()) {
    				arg = (files.size() == 1 ? files.get(0) : files);
    			}
    		}
    		if (arg == null) {
    			String[] paramValues = webRequest.getParameterValues(name);
    			if (paramValues != null) {
    				arg = paramValues.length == 1 ? paramValues[0] : paramValues;
    			}
    		}
    	}
    
    	return arg;
    }

    咱們這裏能夠看到,其實也是經過MultipartHttpServletRequest的getFile等方法來獲取的,同時支持數組、集合形式的參數

###3.3.2 清理佔用的資源,如臨時文件

protected void cleanupMultipart(HttpServletRequest servletRequest) {
	MultipartHttpServletRequest req = WebUtils.getNativeRequest(
		servletRequest, MultipartHttpServletRequest.class);
	if (req != null) {
		this.multipartResolver.cleanupMultipart(req);
	}
}

這裏其實就是調用MultipartResolver接口的void cleanupMultipart(MultipartHttpServletRequest request)方法

至此SpringMVC已經完成了本身的文件上傳框架體系,即底層無論採用何種文件解析包都是走這樣的一個流程。這樣的一個流程其實就是對實際業務的抽象過程。咱們在寫代碼的時候,常常就缺乏抽象的能力,即不多抽象出各類業務邏輯的共同點。

#4 整合apache fileupload對文件上傳的解析 剛纔說了整個文件上傳的處理流程,而後咱們就來看下apache fileupload是如何整合進來的。即CommonsMultipartResolver是如何實現的

##4.1 判斷一個request是不是multipart形式的

@Override
	public boolean isMultipart(HttpServletRequest request) {
		return (request != null && ServletFileUpload.isMultipartContent(request));
	}

這裏就是使用apache fileupload本身的ServletFileUpload.isMultipartContent判斷方法,上一篇文章已經講述了,這裏再也不說明了。

這裏咱們能夠再多想一下,功能的職責劃分問題(雖然問題很簡單,主要是想引導你們在寫代碼的時候多去思考)。

由於目前判斷一個request是不是multipart形式,都是同樣的,無論你是哪一種解析包,爲何SpringMVC不統一進行判斷,而是採用解析包的判斷?

若是SpringMVC本身進行統一的判斷,彷佛也沒什麼問題。站在apache fileupload的角度來講,判斷request是不是multipart形式 的確應該是它的一個功能,而不是等待外界來判斷。

SpringMVC既然採用第三方的解析包,就要遵照人家解析包的判斷邏輯,而不是自行判斷,雖然他們目前的判斷邏輯是同樣的。萬一後來又出來一個解析包,判斷邏輯不同呢?若是流程體系仍是採用SpringMVC本身的判斷,可能就無法正常解析了

##4.2 將HttpServletRequest解析成DefaultMultipartHttpServletRequest

一旦上述判斷經過了,則就須要執行解析過程(能夠當即解析,也能夠延遲解析),看下具體的解析過程

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 {
		MultipartParsingResult parsingResult = parseRequest(request);
		return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
				parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
	}
}

這裏大體說下過程,詳細的內容去看源代碼。

  • 使用apache fileupload的ServletFileUpload對request進行解析,解析結果爲List<FileItem>,代碼以下:

    List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
  • FileItem爲apache fileupload本身的解析結果,須要轉化爲SpringMVC本身定義的MultipartFile

    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) {
    		if (fileItem.isFormField()) {
    			String value;
    			String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
    			if (partEncoding != null) {
    				try {
    					value = fileItem.getString(partEncoding);
    				}
    				catch (UnsupportedEncodingException ex) {
    					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());
    		}
    		else {
    			// multipart file field
    			CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
    			multipartFiles.add(file.getName(), file);
    		}
    	}
    	return new MultipartParsingResult(multipartFiles, multipartParameters, 
    				multipartParameterContentTypes);
    }

這裏有普通字段的處理和文件字段的處理。還記得上文講的org.springframework.web.multipart.commons包的CommonsMultipartFile嗎?能夠看到經過new CommonsMultipartFile(fileItem),就將FileItem結果轉化爲了MultipartFile結果。

至此就將HttpServletRequest解析成了DefaultMultipartHttpServletRequest,因此咱們在使用request時,它的類型其實就是DefaultMultipartHttpServletRequest類型,咱們能夠經過它來獲取各類上傳的文件信息。

##4.3 清理臨時文件

其實就是對全部的CommonsMultipartFile中的FileItem進行刪除臨時文件的操做,這個刪除操做是apache fileupload本身定義的,以下

protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
	for (List<MultipartFile> files : multipartFiles.values()) {
		for (MultipartFile file : files) {
			if (file instanceof CommonsMultipartFile) {
				CommonsMultipartFile cmf = (CommonsMultipartFile) file;
				cmf.getFileItem().delete();
			}
		}
	}
}

至此,SpringMVC與apache fileupload的整合完成了,其餘的整合也是相似的操做。

#5 整合j2ee自帶的文件上傳的解析

這個再也不詳細說明,主要引出來 javax.servlet.http.Part 這個對象是j2ee內置的文件上傳解析結果,相似apache fileupload的FileItem解析結果,從Servlet3.0才加入進來的。

和apache fileupload同樣的步驟,來看下具體源碼內容:

##5.1 判斷一個request是不是multipart形式的

@Override
public boolean isMultipart(HttpServletRequest request) {
	// Same check as in Commons FileUpload...
	if (!"post".equals(request.getMethod().toLowerCase())) {
		return false;
	}
	String contentType = request.getContentType();
	return (contentType != null && contentType.toLowerCase().startsWith("multipart/"));
}

一樣是這兩個條件,post和"multipart/"開頭。

##5.2 將HttpServletRequest解析成StandardMultipartHttpServletRequest

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

在建立StandardMultipartHttpServletRequest的時候進行解析,解析過程和apache fileupload很是相似,只不過用Part替代了apache fileupload的FileItem,以下

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 filename = extractFilename(part.getHeader(CONTENT_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);
	}
}

遍歷全部的Part,把每個Part轉化成StandardMultipartFile,而apache fileupload則是轉化成CommonsMultipartFile。再也不詳細說明,具體的能夠去看源碼。

##5.3 遇到的一些問題 這裏還有不少小插曲。

  • 我以前導入的一直是

    <dependency>
    	<groupId>javax.servlet</groupId>
    	<artifactId>servlet-api</artifactId>
    	<version>2.5</version>
    	<scope>provided</scope>
    </dependency>

    以後把它換成3點多的版本,仍是沒找到javax.servlet.http.Part,最後才發現導入的是下面的形式

    <dependency>
    	<groupId>javax.servlet</groupId>
    	<artifactId>javax.servlet-api</artifactId>
    	<version>3.0.1</version>
    	<scope>provided</scope>
    </dependency>

    這裏的scope是provided,即再也不加入運行環境,直接使用tomcat容器自身的servlet-api。目前個人tomcat7中servlet-api.jar包是含有這個javax.servlet.http.Part對象的,因此是能夠的

  • 而後我就替換掉apache fileupload,使用Servlet3自帶的Part功能,來使用文件上傳,發現不行,沒有獲得解析結果,就想嘗試調試下,然而運行到Collection<Part> parts = request.getParts()這裏的時候,就不能查看源文件了,這裏的request是org.apache.catalina.connector.RequestFacade類型,沒有關聯到源文件,通過一番尋找,最終找到tomcat的maven依賴

    <dependency>
    	<groupId>org.apache.tomcat</groupId>
    	<artifactId>tomcat-catalina</artifactId>
    	<version>7.0.55</version>
    	<scope>provided</scope>
    </dependency>

    有了它,咱們就能夠在調試的時候,查看tomcat內部的運行狀況了

  • 而後一路跟蹤,定位到結果爲 須要將org.apache.catalina.core.StandardContext的allowCasualMultipartParsing屬性設置爲true,即容許進行文件解析,默認爲false。須要在server.xml中修改工程配置,而後就大功告成了。

    <Context ... allowCasualMultipartParsing="true"/>

歡迎關注微信公衆號:乒乓狂魔

微信公衆號

相關文章
相關標籤/搜索