主頁 > 後端開發 > 精盡Spring MVC原始碼分析 - MultipartResolver 組件

精盡Spring MVC原始碼分析 - MultipartResolver 組件

2020-12-15 12:39:21 後端開發

該系列檔案是本人在學習 Spring MVC 的原始碼程序中總結下來的,可能對讀者不太友好,請結合我的原始碼注釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.2.4.RELEASE

該系列其他檔案請查看:《精盡 Spring MVC 原始碼分析 - 文章導讀》

MultipartResolver 組件,內容型別( Content-Type )為 multipart/* 的請求的決議器,主要決議檔案上傳的請求,例如,MultipartResolver 會將 HttpServletRequest 封裝成 MultipartHttpServletRequest 物件,便于獲取引數資訊以及上傳的檔案

使用方式,可以參考《MyBatis 使用手冊》中的 集成 Spring 模塊下的 spring-mvc.xml 檔案中配置 MultipartResolverCommonsMultipartResolver 實作類,然后在方法入參中用 MultipartFile 型別接收

關于在 SpringBoot 中如何使用檔案上傳可參考 Spring 官方檔案

回顧

先來回顧一下在 DispatcherServlet 中處理請求的程序中哪里使用到 MultipartResolver 組件,可以回到《一個請求的旅行程序》中的 DispatcherServletdoDispatch 方法中看看,如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // ... 省略相關代碼
    // <2> 檢測請求是否為上傳請求,如果是則通過 multipartResolver 將其封裝成 MultipartHttpServletRequest 物件
    processedRequest = checkMultipart(request);
    // ... 省略相關代碼
}

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    // 如果該請求是一個涉及到 multipart (檔案)的請求
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
        if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
            if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
                logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
            }
        }
        else if (hasMultipartException(request)) {
            logger.debug("Multipart resolution previously failed for current request - " +
                    "skipping re-resolution for undisturbed error rendering");
        }
        else {
            try {
                // 將 HttpServletRequest 請求封裝成 MultipartHttpServletRequest 物件,決議請求里面的引數以及檔案
                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;
}

<2> 處,如果該請求是一個涉及到 multipart (檔案)的請求,則通過 multipartResolverHttpServletRequest 請求封裝成 MultipartHttpServletRequest 物件,決議請求里面的引數以及檔案

MultipartResolver介面

org.springframework.web.multipart.MultipartResolver 介面,內容型別( Content-Type )為 multipart/* 的請求的決議器介面,代碼如下:

public interface MultipartResolver {
	/**
	 * 是否為 multipart 請求
	 */
	boolean isMultipart(HttpServletRequest request);
	/**
	 * 將 HttpServletRequest 請求封裝成 MultipartHttpServletRequest 物件
	 */
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

	/**
	 * 清理處理 multipart 產生的資源,例如臨時檔案
	 */
	void cleanupMultipart(MultipartHttpServletRequest request);
}

MultipartResolver 介面體系的結構如下:

一共有兩塊:

  • 上半部分,MultipartRequest 介面及其實作類
  • 下半部分,MultipartResolver 介面以及其實作類

初始化程序

DispatcherServletinitMultipartResolver(ApplicationContext context) 方法,初始化 MultipartResolver 組件,方法如下:

private void initMultipartResolver(ApplicationContext context) {
    try {
        // 從 Spring 背景關系中獲取名稱為 "multipartResolver" ,型別為 MultipartResolver 的 Bean
        this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Detected " + this.multipartResolver);
        }
        else if (logger.isDebugEnabled()) {
            logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // Default is no multipart resolver.
        this.multipartResolver = null;
        if (logger.isTraceEnabled()) {
            logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
        }
    }
}
  • 在 Spring MVC 中,multipartResolver 默認為 null【注意】,需要自己配置,例如《MyBatis 使用手冊》中的 集成 Spring 模塊下的 spring-mvc.xml 檔案中配置 MultipartResolver 為 CommonsMultipartResolver 實作類,也可以配置為 StandardServletMultipartResolver 實作類

  • 在 Spring Boot 中,multipartResolver 默認為 StandardServletMultipartResolver 實作類

目前 Spring 只提供上面兩種實作類,接下來依次進行分析

StandardServletMultipartResolver

org.springframework.web.multipart.support.StandardServletMultipartResolver,實作 MultipartResolver 介面,基于 Servlet 3.0 標準的上傳檔案 API 的 MultipartResolver 實作類,代碼如下:

public class StandardServletMultipartResolver implements MultipartResolver {

   /**
    * 是否延遲決議
    */
   private boolean resolveLazily = false;

   public void setResolveLazily(boolean resolveLazily) {
      this.resolveLazily = resolveLazily;
   }


   @Override
   public boolean isMultipart(HttpServletRequest request) {
      // 請求的 Content-type 必須 multipart/ 開頭
      return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
   }

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

   @Override
   public void cleanupMultipart(MultipartHttpServletRequest request) {
      if (!(request instanceof AbstractMultipartHttpServletRequest) || ((AbstractMultipartHttpServletRequest) request).isResolved()) {
         // To be on the safe side: explicitly delete the parts,
         // but only actual file parts (for Resin compatibility)
         try {
            // 洗掉臨時的 Part
            for (Part part : request.getParts()) {
               if (request.getFile(part.getName()) != null) {
                  part.delete();
               }
            }
         }
         catch (Throwable ex) {
            LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
         }
      }
   }

}
  • isMultipart(HttpServletRequest request)方法,請求的 Content-type 是否以 multipart/ 開頭

  • resolveMultipart(HttpServletRequest request)方法,直接將 HttpServletRequest 轉換成 StandardMultipartHttpServletRequest 物件

  • cleanupMultipart(MultipartHttpServletRequest request)方法,清理資源,洗掉臨時的 javax.servlet.http.Part

StandardMultipartHttpServletRequest

org.springframework.web.multipart.support.StandardMultipartHttpServletRequest,繼承 AbstractMultipartHttpServletRequest 抽象類,基于 Servlet 3.0 的 Multipart HttpServletRequest 實作類,包含了一個 javax.servlet.http.HttpServletRequest 物件和它的 javax.servlet.http.Part 物件們,其中 Part 物件會被封裝成 StandardMultipartFile 物件

構造方法

public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
	/**
	 * 普通引數名的集合
	 */
	@Nullable
	private Set<String> multipartParameterNames;

	public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
		this(request, false);
	}

	public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
		super(request);
		// 如果不需要延遲決議
		if (!lazyParsing) {
			// 決議請求
			parseRequest(request);
		}
	}
}
  • multipartParameterNames:普通引數名的集合,非上傳檔案的引數名
  • 如果不需要延遲決議,則呼叫 parseRequest(HttpServletRequest request) 方法,直接決議請求

parseRequest

parseRequest(HttpServletRequest request) 方法,決議請求,決議 HttpServletRequest 中的 Part 物件,如果是檔案,則封裝成 StandardMultipartFile 物件,否則就是普通引數,獲取其名稱,如下:

private void parseRequest(HttpServletRequest request) {
    try {
        // <1> 從 HttpServletRequest 中獲取 Part 們
        Collection<Part> parts = request.getParts();
        this.multipartParameterNames = new LinkedHashSet<>(parts.size());
        MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
        // <2> 遍歷 parts 陣列
        for (Part part : parts) {
            // <2.1> 獲得請求頭中的 Content-Disposition 資訊,MIME 協議的擴展
            String headerValue = https://www.cnblogs.com/lifullmoon/p/part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
            // <2.2> 對 Content-Disposition 資訊進行決議,生成 ContentDisposition 物件
            // 包含請求引數資訊,以面向“物件”的形式進行訪問
            ContentDisposition disposition = ContentDisposition.parse(headerValue);
            // <2.3> 獲得檔案名
            String filename = disposition.getFilename();
            // <2.4> 情況一,檔案名非空,說明是檔案引數,則創建 StandardMultipartFile 物件
            if (filename != null) {
                if (filename.startsWith("=?") && filename.endsWith("?=")) {
                    filename = MimeDelegate.decode(filename);
                }
                files.add(part.getName(), new StandardMultipartFile(part, filename));
            }
            // <2.5> 情況二,檔案名為空,說明是普通引數,則保存引數名稱
            else {
                this.multipartParameterNames.add(part.getName());
            }
        }
        // <3> 將上面生成的 StandardMultipartFile 檔案物件們,設定到父類的 multipartFiles 屬性中
        setMultipartFiles(files);
    }
    catch (Throwable ex) {
        handleParseFailure(ex);
    }
}
  1. 從 HttpServletRequest 中獲取 Part 們

  2. 遍歷 parts 陣列

    1. 從 Part 物件中獲得請求頭中的 Content-Disposition 資訊,MIME 協議的擴展
    2. 對 Content-Disposition 資訊進行決議,生成 ContentDisposition 物件,包含請求引數資訊,以面向“物件”的形式進行訪問
    3. ContentDisposition 物件中獲得檔案名
    4. 情況一,檔案名非空,說明是檔案引數,則創建 StandardMultipartFile 物件
    5. 情況二,檔案名為空,說明是普通引數,則保存引數名稱
  3. 將上面生成的 StandardMultipartFile 檔案物件們,設定到父類的 multipartFiles 屬性中

  4. 如果發生例外則拋出

其他方法

/** 初始化請求 */
@Override
protected void initializeMultipart() {
    parseRequest(getRequest());
}
/** 獲取請求中的引數名稱 */
@Override
public Enumeration<String> getParameterNames() {
    if (this.multipartParameterNames == null) {
        initializeMultipart();
    }
    if (this.multipartParameterNames.isEmpty()) {
        return super.getParameterNames();
    }

    // Servlet 3.0 getParameterNames() not guaranteed to include multipart form items
    // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
    Set<String> paramNames = new LinkedHashSet<>();
    Enumeration<String> paramEnum = super.getParameterNames();
    while (paramEnum.hasMoreElements()) {
        paramNames.add(paramEnum.nextElement());
    }
    paramNames.addAll(this.multipartParameterNames);
    return Collections.enumeration(paramNames);
}
/** 獲取請求中的引數,引數名和引數值的映射 */
@Override
public Map<String, String[]> getParameterMap() {
    if (this.multipartParameterNames == null) {
        initializeMultipart();
    }
    if (this.multipartParameterNames.isEmpty()) {
        return super.getParameterMap();
    }
    // Servlet 3.0 getParameterMap() not guaranteed to include multipart form items
    // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
    Map<String, String[]> paramMap = new LinkedHashMap<>(super.getParameterMap());
    for (String paramName : this.multipartParameterNames) {
        if (!paramMap.containsKey(paramName)) {
            paramMap.put(paramName, getParameterValues(paramName));
        }
    }
    return paramMap;
}
/** 獲取請求的 Content-Type 內容型別 */
@Override
public String getMultipartContentType(String paramOrFileName) {
    try {
        Part part = getPart(paramOrFileName);
        return (part != null ? part.getContentType() : null);
    }
    catch (Throwable ex) {
        throw new MultipartException("Could not access multipart servlet request", ex);
    }
}
/** 獲取請求頭資訊 */
@Override
public HttpHeaders getMultipartHeaders(String paramOrFileName) {
    try {
        Part part = getPart(paramOrFileName);
        if (part != null) {
            HttpHeaders headers = new HttpHeaders();
            for (String headerName : part.getHeaderNames()) {
                headers.put(headerName, new ArrayList<>(part.getHeaders(headerName)));
            }
            return headers;
        }
        else {
            return null;
        }
    }
    catch (Throwable ex) {
        throw new MultipartException("Could not access multipart servlet request", ex);
    }
}

StandardMultipartFile

org.springframework.web.multipart.support.StandardMultipartHttpServletRequest 的私有內部靜態類,實作了 MultipartFile 介面和 Serializable 介面,內部封裝了 javax.servlet.http.Part 物件和檔案名稱,代碼如下:

private static class StandardMultipartFile implements MultipartFile, Serializable {

    private final Part part;

    private final String filename;

    public StandardMultipartFile(Part part, String filename) {
        this.part = part;
        this.filename = filename;
    }

    @Override
    public String getName() {
        return this.part.getName();
    }

    @Override
    public String getOriginalFilename() {
        return this.filename;
    }

    @Override
    public String getContentType() {
        return this.part.getContentType();
    }

    @Override
    public boolean isEmpty() {
        return (this.part.getSize() == 0);
    }

    @Override
    public long getSize() {
        return this.part.getSize();
    }

    @Override
    public byte[] getBytes() throws IOException {
        return FileCopyUtils.copyToByteArray(this.part.getInputStream());
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return this.part.getInputStream();
    }

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        this.part.write(dest.getPath());
        if (dest.isAbsolute() && !dest.exists()) {
            // Servlet 3.0 Part.write is not guaranteed to support absolute file paths:
            // may translate the given path to a relative location within a temp dir
            // (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths).
            // At least we offloaded the file from memory storage; it'll get deleted
            // from the temp dir eventually in any case. And for our user's purposes,
            // we can manually copy it to the requested location as a fallback.
            FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
        }
    }

    @Override
    public void transferTo(Path dest) throws IOException, IllegalStateException {
        FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
    }
}

這個類封裝了 Servlet 3.0 的 Part 物件,也就是我們常用到的 MultipartFile 物件,支持對檔案的操作,內部其實都是呼叫 javax.servlet.http.Part 的方法

AbstractMultipartHttpServletRequest

org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest 抽象類,繼承了 HttpServletRequestWrapper 類,實作了 MultipartHttpServletRequest介面

該類是 StandardMultipartHttpServletRequestDefaultMultipartHttpServletRequest 的父類,實作了一些公共的方法,代碼如下:

public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper implements MultipartHttpServletRequest {
    /**
     * 請求中的檔案資訊
     */
	@Nullable
	private MultiValueMap<String, MultipartFile> multipartFiles;

	protected AbstractMultipartHttpServletRequest(HttpServletRequest request) {
		super(request);
	}

	@Override
	public HttpServletRequest getRequest() {
		return (HttpServletRequest) super.getRequest();
	}

	@Override
	public HttpMethod getRequestMethod() {
		return HttpMethod.resolve(getRequest().getMethod());
	}

    /** 獲取請求頭資訊 */
	@Override
	public HttpHeaders getRequestHeaders() {
		HttpHeaders headers = new HttpHeaders();
		Enumeration<String> headerNames = getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String headerName = headerNames.nextElement();
			headers.put(headerName, Collections.list(getHeaders(headerName)));
		}
		return headers;
	}

    /** 獲取檔案名稱串列 */
	@Override
	public Iterator<String> getFileNames() {
		return getMultipartFiles().keySet().iterator();
	}

    /** 獲取指定檔案名的單個檔案 */
	@Override
	public MultipartFile getFile(String name) {
		return getMultipartFiles().getFirst(name);
	}

    /** 獲取指定檔案名的多個檔案 */
	@Override
	public List<MultipartFile> getFiles(String name) {
		List<MultipartFile> multipartFiles = getMultipartFiles().get(name);
		if (multipartFiles != null) {
			return multipartFiles;
		}
		else {
			return Collections.emptyList();
		}
	}

	@Override
	public Map<String, MultipartFile> getFileMap() {
		return getMultipartFiles().toSingleValueMap();
	}

	@Override
	public MultiValueMap<String, MultipartFile> getMultiFileMap() {
		return getMultipartFiles();
	}

	public boolean isResolved() {
		return (this.multipartFiles != null);
	}

	protected final void setMultipartFiles(MultiValueMap<String, MultipartFile> multipartFiles) {
		this.multipartFiles = new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles));
	}

	protected MultiValueMap<String, MultipartFile> getMultipartFiles() {
		if (this.multipartFiles == null) {
			initializeMultipart();
		}
		return this.multipartFiles;
	}

	/** 交由子類實作 */
	protected void initializeMultipart() {
		throw new IllegalStateException("Multipart request not initialized");
	}
}

上面的方法都比較簡單,用于獲取請求中的檔案物件

MultiValueMap<String, MultipartFile> multipartFiles屬性,保存由子類決議出請求中的 Part 物件所封裝成的 MultipartFile 物件

CommonsMultipartResolver

org.springframework.web.multipart.commons.CommonsMultipartResolver,實作 MultipartResolver、ServletContextAware 介面,繼承 CommonsFileUploadSupport 抽象類,基于 Apache Commons FileUpload 的 MultipartResolver 實作類

如果需要使用這個 MultipartResolver 實作類,需要引入 commons-fileuploadcommons-iocommons-codec 組件,例如:

<dependencies>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.8.0</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.15</version>
    </dependency>
</dependencies>

注意,如果 Spring Boot 專案中需要使用 CommonsMultipartResolver,需要在 application.yml 中添加如下配置,排除其默認的配置,如下:

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

構造方法

public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {
	/**
	 * 是否延遲決議
	 */
	private boolean resolveLazily = false;

	public CommonsMultipartResolver() {
		super();
	}

	public CommonsMultipartResolver(ServletContext servletContext) {
		this();
		setServletContext(servletContext);
	}
}

isMultipart

@Override
public boolean isMultipart(HttpServletRequest request) {
    // 必須是 POST 請求,且 Content-Type 為 multipart/ 開頭
    return ServletFileUpload.isMultipartContent(request);
}

判斷是否為 multipart 請求,必須是 POST 請求,且 Content-Type 為 multipart/ 開頭

resolveMultipart

@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 {
        // 決議請求,獲取檔案、引數資訊
        MultipartParsingResult parsingResult = parseRequest(request);
        return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
                parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
    }
}

將 HttpServletRequest 轉換成 DefaultMultipartHttpServletRequest 物件

如果開啟了延遲決議,則重寫該物件的 initializeMultipart() 方法,用于決議請求

否則直接呼叫 parseRequest(HttpServletRequest request) 方法決議請求,回傳 MultipartParsingResult 物件,包含 MultipartFile 物件和普通引數資訊

parseRequest

parseRequest(HttpServletRequest request)方法,用于決議請求,回傳 MultipartParsingResult 物件,包含 MultipartFile 物件、普通引數資訊以及引數的 Content-Type 資訊,方法如下:

protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
    // <1> 獲取請求中的編碼
    String encoding = determineEncoding(request);
    // <2> 獲取 ServletFileUpload 物件
    FileUpload fileUpload = prepareFileUpload(encoding);
    try {
        // <3> 獲取請求中的流資料
        List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
        // <4> 將這些流資料轉換成 MultipartParsingResult,包含 CommonsMultipartFile、引數資訊、Content-type
        return parseFileItems(fileItems, encoding);
    }
    catch (FileUploadBase.SizeLimitExceededException ex) {
        throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
    }
    catch (FileUploadBase.FileSizeLimitExceededException ex) {
        throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
    }
    catch (FileUploadException ex) {
        throw new MultipartException("Failed to parse multipart servlet request", ex);
    }
}
  1. 獲取請求中的編碼

  2. 根據編碼獲取到 ServletFileUpload 物件( commons-fileupload 中的類),在 newFileUpload(FileItemFactory fileItemFactory) 方法中回傳的就是 ServletFileUpload 物件,可以看到父類 CommonsFileUploadSupport 的構造方法,如下:

    // org.springframework.web.multipart.commons.CommonsFileUploadSupport.java
    
    public CommonsFileUploadSupport() {
        this.fileItemFactory = newFileItemFactory();
        // 由子類實作
        this.fileUpload = newFileUpload(getFileItemFactory());
    }
    

    具體細節就不講述了

  3. 通過 ServletFileUpload 物件決議請求,回傳流資料 List<FileItem> fileItems

  4. 呼叫父類 CommonsFileUploadSupport 的 parseFileItems(List<FileItem> fileItems, String encoding) 方法,將這些流資料轉換成 MultipartParsingResult 物件

    // org.springframework.web.multipart.commons.CommonsFileUploadSupport.java
    
    protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
        MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<>();
        Map<String, String[]> multipartParameters = new HashMap<>();
        Map<String, String> multipartParameterContentTypes = new HashMap<>();
    
        // Extract multipart files and multipart parameters.
        for (FileItem fileItem : fileItems) {
            if (fileItem.isFormField()) {
                String value;
                String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
                try {
                    value = https://www.cnblogs.com/lifullmoon/p/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 = https://www.cnblogs.com/lifullmoon/p/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 = createMultipartFile(fileItem);
                multipartFiles.add(file.getName(), file);
                LogFormatUtils.traceDebug(logger, traceOn ->"Part '" + file.getName() + "', size " + file.getSize() +
                                " bytes, filename='" + file.getOriginalFilename() + "'" +
                                (traceOn ? ", storage=" + file.getStorageDescription() : "")
                );
            }
        }
        return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
    }
    

    大致就是遍歷 fileItems 集合,如果是一個簡單的表單欄位,那么就是一個普通的引數,將引數名和值保存起來

    否則就是檔案,將其封裝成 CommonsMultipartFile 保存起來

cleanupMultipart

cleanupMultipart(MultipartHttpServletRequest request)方法,清理檔案產生的臨時資源,如下:

// CommonsMultipartResolver.java
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
    if (!(request instanceof AbstractMultipartHttpServletRequest) ||
            ((AbstractMultipartHttpServletRequest) request).isResolved()) {
        try {
            cleanupFileItems(request.getMultiFileMap());
        }
        catch (Throwable ex) {
            logger.warn("Failed to perform multipart cleanup for servlet request", ex);
        }
    }
}
// CommonsFileUploadSupport.java
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();
                LogFormatUtils.traceDebug(logger, traceOn -> "Cleaning up part '..."));
            }
        }
    }
}

DefaultMultipartHttpServletRequest

org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest,繼承 AbstractMultipartHttpServletRequest 抽象類,MultipartHttpServletRequest 的默認實作類,代碼如下:

public class DefaultMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {

	private static final String CONTENT_TYPE = "Content-Type";

	@Nullable
	private Map<String, String[]> multipartParameters;

	@Nullable
	private Map<String, String> multipartParameterContentTypes;

	public DefaultMultipartHttpServletRequest(HttpServletRequest request, MultiValueMap<String, MultipartFile> mpFiles,
			Map<String, String[]> mpParams, Map<String, String> mpParamContentTypes) {

		super(request);
		setMultipartFiles(mpFiles);
		setMultipartParameters(mpParams);
		setMultipartParameterContentTypes(mpParamContentTypes);
	}

	public DefaultMultipartHttpServletRequest(HttpServletRequest request) {
		super(request);
	}

	@Override
	@Nullable
	public String getParameter(String name) {
		String[] values = getMultipartParameters().get(name);
		if (values != null) {
			return (values.length > 0 ? values[0] : null);
		}
		return super.getParameter(name);
	}

	@Override
	public String[] getParameterValues(String name) {
		String[] parameterValues = super.getParameterValues(name);
		String[] mpValues = getMultipartParameters().get(name);
		if (mpValues == null) {
			return parameterValues;
		}
		if (parameterValues == null || getQueryString() == null) {
			return mpValues;
		}
		else {
			String[] result = new String[mpValues.length + parameterValues.length];
			System.arraycopy(mpValues, 0, result, 0, mpValues.length);
			System.arraycopy(parameterValues, 0, result, mpValues.length, parameterValues.length);
			return result;
		}
	}

	@Override
	public Enumeration<String> getParameterNames() {
		Map<String, String[]> multipartParameters = getMultipartParameters();
		if (multipartParameters.isEmpty()) {
			return super.getParameterNames();
		}

		Set<String> paramNames = new LinkedHashSet<>();
		paramNames.addAll(Collections.list(super.getParameterNames()));
		paramNames.addAll(multipartParameters.keySet());
		return Collections.enumeration(paramNames);
	}

	@Override
	public Map<String, String[]> getParameterMap() {
		Map<String, String[]> result = new LinkedHashMap<>();
		Enumeration<String> names = getParameterNames();
		while (names.hasMoreElements()) {
			String name = names.nextElement();
			result.put(name, getParameterValues(name));
		}
		return result;
	}

	@Override
	public String getMultipartContentType(String paramOrFileName) {
		MultipartFile file = getFile(paramOrFileName);
		if (file != null) {
			return file.getContentType();
		}
		else {
			return getMultipartParameterContentTypes().get(paramOrFileName);
		}
	}

	@Override
	public HttpHeaders getMultipartHeaders(String paramOrFileName) {
		String contentType = getMultipartContentType(paramOrFileName);
		if (contentType != null) {
			HttpHeaders headers = new HttpHeaders();
			headers.add(CONTENT_TYPE, contentType);
			return headers;
		}
		else {
			return null;
		}
	}

	protected final void setMultipartParameters(Map<String, String[]> multipartParameters) {
		this.multipartParameters = multipartParameters;
	}

	protected Map<String, String[]> getMultipartParameters() {
		if (this.multipartParameters == null) {
			initializeMultipart();
		}
		return this.multipartParameters;
	}

	protected final void setMultipartParameterContentTypes(Map<String, String> multipartParameterContentTypes) {
		this.multipartParameterContentTypes = multipartParameterContentTypes;
	}

	protected Map<String, String> getMultipartParameterContentTypes() {
		if (this.multipartParameterContentTypes == null) {
			initializeMultipart();
		}
		return this.multipartParameterContentTypes;
	}
}

代碼并不復雜,稍微閱讀一下就理解了??

總結

本文對 Spring MVC 處理請求的程序中使用到的 MultipartResolver 組件進行了分析,如果請求的 Content-Typemultipart/*,涉及到檔案上傳,所以處理請求的第一步需要通過 MultipartResolver 組件對請求進行轉換處理,會將 HttpServletRequest 請求物件封裝成 MultipartHttpServletRequest 物件,便于獲取引數資訊和操作上傳的檔案(MultipartFile 物件),

MultipartResolver 組件的實作類有兩種:

  • org.springframework.web.multipart.support.StandardServletMultipartResolver:實作 MultipartResolver 介面,基于 Servlet 3.0 標準的上傳檔案 API 的 MultipartResolver 實作類
  • org.springframework.web.multipart.commons.CommonsMultipartResolver:實作 MultipartResolver 介面,基于 Apache Commons FileUpload 的 MultipartResolver 實作類

兩者的區別:

  • StandardServletMultipartResolver 會將 HttpServletRequest 封裝成 StandardMultipartHttpServletRequest 物件,由 Servlet 3.0 提供 API 獲取請求中的 javax.servlet.http.Part 物件,然后進行決議,檔案會封裝成 StandardMultipartFile 物件

  • CommonsMultipartResolver 會將 HttpServletRequest 封裝成 DefaultMultipartHttpServletRequest 物件,由 Apache 的 Commons FileUpload 組件來實作,通過 org.apache.commons.fileupload.servlet.ServletFileUpload 物件獲取請求中的 org.apache.commons.fileupload.FileItem 物件,然后進行決議,檔案會封裝成 CommonsMultipartFile 物件,如何使用可以參考上面的 CommonsMultipartResolver 小節

注意事項:

  • 在 Spring MVC 中,multipartResolver 默認為 null,需要自己配置,例如《MyBatis 使用手冊》中的 集成 Spring 模塊下的 spring-mvc.xml 檔案中配置 MultipartResolver 為 CommonsMultipartResolver 實作類,也可以配置為 StandardServletMultipartResolver 實作類
  • 在 Spring Boot 中,multipartResolver 默認為 StandardServletMultipartResolver 實作類

參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/234989.html

標籤:Java

上一篇:spring事務相關

下一篇:Java生成微信小程式二維碼

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more