Spring MVC - MultipartResolver組件

MultipartResolver

/**
 * 依照<a href="https://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>,對於二進制文件上傳的策略接口。
 * 在一個應用程序上下文和單機模式,都是可使用的。
 *
 * 對於Spring 3.1來說,有兩個具體的實現類:
 * {@link org.springframework.web.multipart.commons.CommonsMultipartResolver}
 * for Apache Commons FileUpload
 * {@link org.springframework.web.multipart.support.StandardServletMultipartResolver}
 * for the Servlet 3.0+ Part API
 *
 * 對於Spring的DispatcherServlet沒有默認的解析器實現,因爲一個應用程序可能自己選擇去分析它的二進制請求。
 * 爲了定義一個實現類,創建一個id爲“multipartResolver”在DispatcherServlet中的application context裏。
 * 這樣的一個解析器會應用到DispatchServlet的所有請求中
 *
 * 如果一個DispatcherServlet發現了一個二進制請求,會通過配置的MultipartResolver進行解析並且傳遞一個包裝的HttpServletRequest。
 * 控制器這時候可以將給定的請求轉換成MultipartHttpServletRequest接口,這個接口允許訪問任何MultipartFile。
 * 注意:這個轉換僅僅是在一個二進制請求中得到支持。
 *
 * <pre class="code">
 * public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
 *   MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
 *   MultipartFile multipartFile = multipartRequest.getFile("image");
 *   ...
 * }</pre>
 *
 * 沒有直接訪問,命令或者控制器可以註冊一個ByteArrayMultipartFileEditor或者StringMultipartFileEditor帶有它們的數據綁定器
 * 自動將二進制內容轉換形成bean屬性。
 *
 * 作爲使用一個帶有DispatcherServlet的MultipartResolver的一個選項,MultipartFilter可以被註冊到web.xml中。
 * 它委派給root application context中的MultipartResolver類型的bean。這個主要是針對的是沒有Spring MVC框架的應用程序。
 *
 * 注意:從來也不需要從代碼層面去訪問MultipartResolver。它簡單地在幕後工作,使MultipartHttpServletRequest對於控制器可用。
 */
public interface MultipartResolver {

	/**
	 * 確定給定的請求是否包含二進制內容。
	 * 將會檢查內容類型“multipart/form-data”,但是實際接收的請求可能依賴於解析器的實現類。
	 */
	boolean isMultipart(HttpServletRequest request);

	/**
	 * 將給定的http請求解析成二進制文件和參數,然後包裝請求成一個MultipartHttpServletRequest對象。
	 * 這個對象,提供對文件描述符的訪問和使內含的參數可以訪問,通過標準的ServletRequest方法。
	 */
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

	/**
	 * 清理二進制處理的任何資源,比如上傳文件的一個存儲。
	 */
	void cleanupMultipart(MultipartHttpServletRequest request);

}

StandardServletMultipartResolver

/**
 * MultipartResolver的標準實現接口,基於Servlet 3.0的 {@link javax.servlet.http.Part} API。
 * 在一個Spring DispatcherServlet上下文中,添加"multipartResolver"bean,沒有任何額外的代碼級別的配置。
 *
 * 注意:爲了使用基於二進制解析的Servlet 3.0,你需要標記在web.xml中“multipart-config”部分的受到影響的servlet,
 * 或者使用以編程式的servlet註冊的MultipartConfigElement或者在servlet class中的使用MultipartConfig註解。
 * 例如最大的文件大小或者存儲位置的配置設置使用的是servlet註冊的級別;Servlet3.0不允許以MultipartResolver中設置它們。
 *
 * public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
 *	 // ...
 *	 @Override
 *	 protected void customizeRegistration(ServletRegistration.Dynamic registration) {
 *     // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
 *     registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
 *   }
 * }
 */
public class StandardServletMultipartResolver implements MultipartResolver {

	private boolean resolveLazily = false;


	/**
	 * 設置是否在訪問文件或者參數的時候,延遲解析二進制請求。
	 * 默認是false。
	 */
	public void setResolveLazily(boolean resolveLazily) {
		this.resolveLazily = resolveLazily;
	}


	@Override
	public boolean isMultipart(HttpServletRequest request) {
		// 內容類型以"multipart/"開頭
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}

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

	/**
	 * 實際刪除的是Part
	 */
	@Override
	public void cleanupMultipart(MultipartHttpServletRequest request) {
		if (!(request instanceof AbstractMultipartHttpServletRequest) ||
			    // MultiValueMap<String, MultipartFile> != null
				((AbstractMultipartHttpServletRequest) request).isResolved()) {
			// To be on the safe side: explicitly delete the parts,
			// but only actual file parts (for Resin compatibility)
			try {
				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);
			}
		}
	}

}

CommonsMultipartResolver

暫時放置,以後會更新的。

demo

/**
 * @Description:    批量上傳文件
 * @Author:         apple
 * @CreateDate:     2019/7/9
 * @UpdateUser:     apple
 * @UpdateDate:     2019/7/9
 */
@Slf4j
@Controller
@RequestMapping("/file")
public class UploadFileController {

    /** 文件上傳路徑 **/
    @Value("${spring.servlet.multipart.location}")
    private String uploadLocation;

    /** 用於記錄當前上傳的次數 **/
    private AtomicLong uploadCount = new AtomicLong();

    @GetMapping(value = {"/upload/page", ""})
    public String uploadPage() {
        return "/upload.html";
    }

    /**
     * 上傳文件
     * (對於HttpServletRequest類型的解析)
     *
     * @param request 上傳請求
     * @return 處理結果
     */
    @PostMapping("/upload/request")
    @ResponseBody
    public Map<String, Object> uploadRequest(HttpServletRequest request) {
        MultipartHttpServletRequest multiRequest = null;
        if (request instanceof MultipartHttpServletRequest) {
            multiRequest = (MultipartHttpServletRequest) request;
        } else {
            dealResultMap(false, generateFailResolve());
        }
        Assert.notNull(multiRequest, "Request can not be null.");
        // (注:"file"是前端定義的input標籤的name,批量上傳的name要保持一致)
        List<MultipartFile> multipartFiles = multiRequest.getFiles("file");
        Assert.notNull(multipartFiles, "MultipartFile list is null. Fail to resolve files.");
        Map<String, Object> finalMap = new HashMap<>(multipartFiles.size());
        Map<String, Object> resultMap;
        if (multipartFiles.size() > 0) {
            for (MultipartFile multipartFile : multipartFiles) {
                Assert.notNull(multipartFile, "MultipartFile can not be null");
                resultMap = transferTo(multipartFile);
                if (resultMap != null) {
                    finalMap.put("No." + uploadCount.incrementAndGet() + " uploads", resultMap);
                }
            }
        }
        return finalMap;
    }

    /**
     * 上傳文件
     * (對於MultipartFile類型的解析)
     *
     * @param multipartFiles MultipartFile類型的文件組
     * @return 處理結果
     */
    @PostMapping("/upload/multipart")
    @ResponseBody
    public Map<String, Object> uploadMultipartFile(@RequestParam("file") MultipartFile[] multipartFiles) {
        Map<String, Object> finalMap = new HashMap<>(multipartFiles.length);
        Map<String, Object> resultMap;
        for (MultipartFile multipartFile : multipartFiles) {
            try {
                resultMap = transferTo(multipartFile);
                if (resultMap != null) {
                    finalMap.put("No." + uploadCount.incrementAndGet() + " uploads", resultMap);
                }
            } catch (Exception e) {
                if (log.isErrorEnabled()) {
                    log.error(multipartFile.getOriginalFilename() + " fail to upload");
                }
            }
        }
        return finalMap;
    }

    /**
     * 上傳文件
     *
     * @param multipartFile MultipartFile類型的文件
     * @return 處理後的信息map
     */
    private Map<String, Object> transferTo(MultipartFile multipartFile) {
        String fileName = multipartFile.getOriginalFilename();
        Assert.notNull(fileName, "File name must not be null");
        fileName = generateWithTimeMillis(replaceStr(fileName));
        File file = new File(fileName);
        try {
            // 上傳文件(核心語句)
            multipartFile.transferTo(file);
        } catch (IOException e) {
            // 如果上傳失敗
            if (log.isErrorEnabled()) {
                log.error(generateUploadMsg(false, fileName), e.getMessage());
            }
            return dealResultMap(false, generateUploadMsg(false, fileName));
        }
        // 上傳成功
        if (log.isInfoEnabled()) {
            log.info(generateUploadMsg(true, fileName));
        }
        return dealResultMap(true, generateUploadMsg(true, fileName));
    }

    /**
     * 對文件名加時間數字處理
     *
     * @param fileName 文件名
     * @return 返回生成帶有時間數字的文件名
     */
    private String generateWithTimeMillis(String fileName) {
        String suffix = "";
        String fileRealName;
        final String symbol = ".";
        int index;
        if ((index = fileName.lastIndexOf(symbol)) != -1) {
            suffix = fileName.substring(index);
            fileRealName = fileName.substring(0, index);
        } else {
            fileRealName = fileName;
        }
        return fileRealName +
                "-" +
                System.currentTimeMillis() +
                suffix;
    }

    /**
     * 生成上傳處理後的信息
     * (無論成功與否)
     *
     * @param success  是否上傳成功
     * @param fileName 上傳的文件名
     * @return 處理後的信息
     */
    private String generateUploadMsg(boolean success, String fileName) {
        StringBuilder builder = new StringBuilder();
        builder.append("File-[");
        builder.append(fileName);
        if (success) {
            builder.append("] succeed to upload, to ").append(uploadLocation);
        } else {
            builder.append("] fail to upload");
        }
        return builder.toString();
    }

    /**
     * 生成解析失敗的信息
     *
     * @return 解析失敗的信息
     */
    private String generateFailResolve() {
        return "Fail to resolve files";
    }


    /**
     * 定義返回的信息格式
     *
     * @param success 處理成功
     * @param msg 處理後的信息
     * @return 信息map
     */
    private Map<String, Object> dealResultMap(boolean success, String msg) {
        Map<String, Object> resultMap = new HashMap<>(2);
        resultMap.put("isSuccessful", success);
        resultMap.put("msg", msg);
        return resultMap;
    }

    /**
     * 對於文件路徑帶有"/"的,全部轉換成"\\"格式
     * 然後截取最後一個"\\"後的文件名
     *
     * @param fileName 文件路徑名
     * @return 符合格式的文件名
     */
    private String replaceStr(String fileName) {
        String replaceStr = "\\";
        fileName = fileName.replaceAll("/", replaceStr);
        if (fileName.lastIndexOf(replaceStr) != -1) {
            fileName = fileName.substring(fileName.lastIndexOf("\\"));
        }
        return fileName;
    }

}

Spring Boot的相關配置

application.properties

是否開啓二進制上傳

spring.servlet.multipart.enabled=true

文件上傳的路徑

spring.servlet.multipart.location=/usr/local/file

最大的文件大小

spring.servlet.multipart.maxFileSize=100MB

最大的請求大小

spring.servlet.multipart.maxRequestSize=1GB

寫入磁盤的閾值

spring.servlet.multipart.fileSizeThreshold=1TB

延遲解析二進制請求,只有當訪問參數或者文件時纔去解析

spring.servlet.multipart.resolverLazily=false
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章