Spring+SpringMVC实现文件上传&文件上传源码解析&文件上传原理总结
前言
本文章主要目的在于解析SpringMVC文件上传的CommonsMultipartFileResolver解析器及MultipartFile封装过程
一、Spirng集成SpringMVC实现文件上传
如果大家记得SpringMVC上传文件的代码步骤,那就忽略该环节,直接先第二个。源码解析
环境搭建
- 第一步:添加maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SpringCommonsMultifile</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Spring整合SpringMVC文件上传</name>
<dependencies>
<!-- Spring依赖 -->
<!-- 1.Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- 2.Spring dao依赖 -->
<!-- spring-jdbc包括了一些如jdbcTemplate的工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- 3.Spring web依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- 4.Spring test依赖:方便做单元测试和集成测试 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- spring mvc 框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!-- jsp/jstl/core 页面标签 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- SLF4J API -->
<!-- SLF4J 是一个日志抽象层,允许你使用任何一个日志系统,并且可以随时切换还不需要动到已经写好的程序 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
<!-- Log4j 日志系统(最常用) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<!-- jackson -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
</dependencies>
</project>
- 第二步:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.spring">
<!-- 扫描时跳过 @Controller 注解的JAVA类(控制器) -->
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
- spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.spring">
<!-- 扫描时跳过 @Controller 注解的JAVA类(控制器) -->
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
- web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Spring整合SpringMVC文件上传</display-name>
<context-param>
<param-name>contextConfigLocation </param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
- fileUpload.html上传页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>spring集成SpringMVC实现文件上传</title>
</head>
<body>
<form method="post" action="/sys/commons/file/upload" enctype="multipart/form-data">
<input type="file" name="file" formenctype="multipart/form-data"/>
<input type="submit"/>
</form>
</body>
</html>
- Controller
package com.spring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("sys/commons/file")
public class SysFileCommonsController {
@RequestMapping("upload")
@ResponseBody
public Map<String,Object> upload(@RequestParam("file") MultipartFile file){
Map<String,Object> result = new HashMap<String, Object>();
File newFile = new File("C:\\Users\\dell\\Desktop\\test.jpg");
try {
file.transferTo(newFile);
result.put("code",200);
result.put("filePath","C:\\Users\\dell\\Desktop\\test.jpg");
} catch (IOException e) {
e.printStackTrace();
result.put("code",500);
result.put("Error",e.getMessage());
}
return result;
}
@RequestMapping("test")
@ResponseBody
public Map<String,Object> test(){
Map<String,Object> result = new HashMap<String, Object>();
result.put("msg","你好,Spring老兄,我们又见面了");
return result;
}
}
最终能结果如下图:
二、CommonsMultipartResolver原理及源码解析
老规矩,鲁迅先生曾经说过:分析原理不画结构图,那是麻绳提豆腐一别提了。
好了,废话少说:先上图
结合上面的结构图我们来一步一步讲解起源
- 节点一:当进入DispathcherServlet中后会调用doDispatch方法,代码如下
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
/**
*这里调用了checkMultipart方法,并且返回一个HttpServletRequest对象
*返回的这个对像就是已经封装好文件数据的对象
*/
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
在doDispatch方法中我们可以看到,它将request交给了checkMultipart方法,并且返回了一个HttpServletRequest对象,其实这个返回的对象中就已经包含了上传的文件信息。那么它是怎么将文件信息封装进来的呢?继续看第二个节点
- 节点二
节点二就是checkMultipart方法内部所做的事情
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
this.logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, this typically results from an additional MultipartFilter in web.xml");
} else if (this.hasMultipartException(request)) {
this.logger.debug("Multipart resolution failed for current request before - skipping re-resolution for undisturbed error rendering");
} else {
try {
return this.multipartResolver.resolveMultipart(request);
} catch (MultipartException var3) {
if (request.getAttribute("javax.servlet.error.exception") == null) {
throw var3;
}
}
this.logger.debug("Multipart resolution failed for error dispatch", var3);
}
}
return request;
}
在进入到checkMultipart方法后,首先进行了判断。
1、判断容器中是否存在id为multipartResolver的上传文件解析器commonsMutpartResolver的bean实例
2、判断请求方式是否是POST请求,并且是参数类型是否是以"multipart/"开头的
到这里其实也就是进入到了节点三
具体源码如下
- 节点三
isMultipart方法
@Override
public boolean isMultipart(HttpServletRequest request) {
return (request != null && ServletFileUpload.isMultipartContent(request));
}
目的:request请求不为空的情况下继续判断请求是否符合带有文件的请求
isMultipartContent方法如下
public static final boolean isMultipartContent(HttpServletRequest request) {
return !"POST".equalsIgnoreCase(request.getMethod()) ? false : FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}
isMultipartContent方法如下
public static final boolean isMultipartContent(RequestContext ctx) {
String contentType = ctx.getContentType();
if (contentType == null) {
return false;
} else {
return contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/");
}
}
可以看上面的方法先判断了请求是否是POST请求,然后又判断了请求的内容类型是否是以"multipart/"开头
经过以上三个方法的层层调用,最终节点三返回了一个布尔值
- 节点四
当节点返回的是true是,才会继续进入第四个节点
并且调用CommonsMultipartResolver对象的resolveMultipart方法,这时候才会真正的返回一个带有上传文件信息的HttpServletRequest对象
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
if (this.resolveLazily) {
/**
*执行到这里是才真正的开始创建DefaultMultipartHttpServletRequest对象,
*并且复写了的它的初始化方法,进而径文件信息封装到了这个对象中
*/
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中,并且将其返回给DispathcherServlet对象
看到这里可能有同学要问了,resolveMultipart方法返回的是DefaultMultipartHttpServletRequest对象,数据怎么跑到DispathcherServlet去了。
别慌,看一下下面的集成关系图你就懂了
怎么样,够清楚了吧。ServletRequest是它的老祖先,拿他点数据还不行吗,难道他还有意见不成!
总结
SpringMVC文件上传原理
当客户端发送了带有文件信息并且ContentType是以"multipart/"开头的请求时,DispathcherServlet前端控制器会将请求委托给CommonsMutipartResolver文件解析器,文件解析器分析请求信息是否为POST请求并且请求中是否含有ContentType以"multipart/"开头的数据,当它们都为true时,前端控制器才会继续委托CommonsMultipart文件解析器将请求中的文件数据封装到DefaultMultipartHttpServletRequest对象中,并且最终返回给DispathcherServlet