1. Servlet的与文件上传相关的API
Servlet 3.0新增了对文件上传的支持,避免了使用commons-fileupload.jar工具包的麻烦
1.1. Part
Part接口用于封装form表单提交的数据;一个Part对象就代表form表单中的一个键值对;示例如下:
/**
* 要使Servlet支持文件上传,需要在Servlet类上添加@MultipartConfig注解
* 或者在web.xml文件中配置该Servlet时,在servlet标签内添加multipart-config标签来手动配置
*/
@Slf4j
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
// 通过请求的getParts()方法获取到所有表单项,然后依次处理这些表单项
// 也可以通过请求的getPart(name)方法获取到指定名称的表单项
for (Part part : request.getParts()) {
// 获取到当前表单项的所有头信息
Map<String, String> headers = new HashMap<>();
for (String headerName : part.getHeaderNames()) {
headers.put(headerName, part.getHeader(headerName));
}
// 表单项的名称,即参数名称
log.info("name: {}", part.getName());
// 表单项的内容类型,可以是字符串类型或文件流类型
log.info("contentType: {}", part.getContentType());
// 字符串或文件的字节大小
log.info("size: {}", part.getSize());
// 所有头信息
log.info("headers: {}", headers);
// 文件名称;如果是字符串类型的表单项,则为null
log.info("fileName: {}", part.getSubmittedFileName());
// 上传文件时,Servlet容器会将文件存放到临时目录
// 我们可以通过write()方法将上传的文件保存到自己的目录下(并且此时会删除临时文件)
// 这里的filename参数可以是文件名称,也可以是绝对路径
// 注意,如果是字符串类型的表单项,则write()方法不会执行写入操作
// part.write(filename);
// 获取字符串本身或文件内容的输入流
// part.getInputStream();
// 删除临时文件;如果是字符串类型的表单项,则不会做任何事
// part.delete();
}
}
}
访问该FileUploadServlet,控制台打印结果如下所示:
# 文件类型的表单项
name: file
contentType: text/plain
size: 17
headers: {Content-Disposition=form-data; name="file"; filename="1.txt", Content-Type=text/plain}
fileName: 1.txt
# 字符串类型的表单项
name: key
contentType: null
size: 5
headers: {Content-Disposition=form-data; name="key"}
fileName: null
1.2. @MultipartConfig
/**
* 该注解用于标记Servlet类,代表目标Servlet实例需要处理multipart/form-data类型的请求
* 本注解包含了与文件上传相关的配置,比如文件的保存路径、文件的最大大小等
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartConfig {
/**
* 文件的保存目录;当调用Part的write(filename)方法时,如果filename是相对路径,那么该属性将会发挥作用:
* 1. 如果指定了该属性,那么文件将会保存到这里指定的目录中
* 2. 如果没有指定,则默认保存到servletContext.getDeployment().getDeploymentInfo().getTempPath()目录中
*/
String location() default "";
/**
* 上传文件时,文件的最大大小;这里不是指单个文件,而是所有文件加起来不能超过该值;-1代表不限制
*/
long maxFileSize() default -1L;
/**
* form表单请求的最大大小;-1代表不限制
*/
long maxRequestSize() default -1L;
/**
* 保存为临时文件的大小阈值;当上传文件的大小超过该值时,Servlet容器会为上传的文件创建临时文件,否则文件内容直接保存在内存中
*/
int fileSizeThreshold() default 0;
}
2. MultipartFile
2.1. 概述
/**
* MultipartFile是SpringMVC提供的、用于表示用户所上传的文件的接口
* 上传的文件的内容可以保存在内存中或临时文件中;如果保存在临时文件中,那么当请求处理完成时,需要清除临时文件
*/
public interface MultipartFile extends InputStreamSource {
/**
* 获取表单项的参数名称,而不是文件名称
*/
String getName();
/**
* 获取上传的文件在客户端的文件系统中的原始文件名称(一般都会包含路径信息,除了Opera浏览器)
*
* 需要注意的是,这里的文件名是由客户端提供的,因此不应该被盲目地使用,因为:
* 1. 一方面,不要使用文件名称中的目录部分
* 2. 另一方面,文件名部分也有可能包含".."之类的恶意字符
*
* 如果返回空串,则代表没有上传文件;如果返回null,则代表未指定或获取不到原始名称
*/
@Nullable
String getOriginalFilename();
/**
* 获取文件的内容类型
* 如果返回null,则代表没有上传文件或未指定内容类型
*/
@Nullable
String getContentType();
/**
* 判断是否有上传的内容
* 如果未上传文件,或者上传的是空文件,则返回true
*/
boolean isEmpty();
/**
* 获取文件内容的字节大小
* 如果未上传文件,或者上传的是空文件,则返回0
*/
long getSize();
/**
* 获取文件内容
*/
byte[] getBytes() throws IOException;
/**
* 获取文件内容的输入流;用户需要自己关闭该流
*/
@Override
InputStream getInputStream() throws IOException;
/**
* 将上传的文件封装成Resource对象,方便RestTemplate和WebClient获取文件名称、内容长度和文件输入流
*/
default Resource getResource() {
return new MultipartFileResource(this);
}
/**
* 将上传的文件写入到目标文件中;如果目标文件已存在,则先删除
* 子类实现该方法时,可以对临时文件进行剪切或复制,也可以将内存中的文件内容写入到目标文件
* 由于这里没有限制不能剪切,因此该方法最好只调用一次,确保不会出现文件剪切后找不到的情况
*/
void transferTo(File dest) throws IOException, IllegalStateException;
/**
* 将上传的文件写入到目标文件中;如果目标文件已存在,则先删除
*/
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
}
}
2.2. StandardMultipartFile
/**
* 本类是StandardMultipartHttpServletRequest的静态内部类,主要负责将Servlet中的Part实例适配成MultipartFile实例
*/
private static class StandardMultipartFile implements MultipartFile, Serializable {
private final Part part;
private final String filename;
/**
* 全参构造器,需指定Servlet中的Part对象和文件的名称
*/
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));
}
}
3. MultipartHttpServletRequest
3.1. 概述
/**
* MultipartRequest用于代表上传文件的请求,该接口的所有方法都是与文件上传相关的
*/
public interface MultipartRequest {
/**
* 获取文件上传的参数集合(如["file"])的迭代器
* 注意,这里获取的是form表单项的参数名称,而不是真实的文件名称
*/
Iterator<String> getFileNames();
/**
* 获取某个form表单项参数对应的文件信息;如果没有,则返回null
* 注意,同一个参数可以用于上传多个文件;如果该参数有多个文件,则返回第一个文件
*/
@Nullable
MultipartFile getFile(String name);
/**
* 获取某个form表单项参数对应的所有文件信息;如果没有,则返回空列表
*/
List<MultipartFile> getFiles(String name);
/**
* 获取form表单项参数及其对应的文件信息的映射关系
*/
Map<String, MultipartFile> getFileMap();
/**
* 获取form表单项参数及其对应的所有文件信息的映射关系
*/
MultiValueMap<String, MultipartFile> getMultiFileMap();
/**
* 获取某个form表单项参数对应的文件的内容类型
*/
@Nullable
String getMultipartContentType(String paramOrFileName);
}
/**
* MultipartHttpServletRequest接口同时继承了HttpServletRequest接口和MultipartRequest接口
*/
public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest {
/**
* 获取Http请求方式
*/
@Nullable
HttpMethod getRequestMethod();
/**
* 获取Http请求头
*/
HttpHeaders getRequestHeaders();
/**
* 获取某个form表单项对应的文件的头部信息
*/
@Nullable
HttpHeaders getMultipartHeaders(String paramOrFileName);
}
3.2. AbstractMultipartHttpServletRequest
/**
* MultipartHttpServletRequest接口的抽象的基本实现类,主要负责管理预生成的MultipartFile实例
* 本类继承自HttpServletRequestWrapper,是HttpServletRequest的装饰器
*/
public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper
implements MultipartHttpServletRequest {
/**
* 用于缓存此次请求上传的文件信息,其中key为表单项参数名称
*/
@Nullable
private MultiValueMap<String, MultipartFile> multipartFiles;
/**
* 构造方法,需提供被装饰的HttpServletRequest对象
*/
protected AbstractMultipartHttpServletRequest(HttpServletRequest request) {
super(request);
}
/**
* 给this.multipartFiles字段进行赋值;该方法由子类在解析完请求后调用
*/
protected final void setMultipartFiles(MultiValueMap<String, MultipartFile> multipartFiles) {
this.multipartFiles =
new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles));
}
/**
* 获取this.multipartFiles字段;如果还未初始化,则先初始化(也就是说,本类支持懒初始化该字段)
*/
protected MultiValueMap<String, MultipartFile> getMultipartFiles() {
if (this.multipartFiles == null) {
initializeMultipart();
}
return this.multipartFiles;
}
/**
* 初始化this.multipartFiles字段;本方法需要被子类覆盖
*/
protected void initializeMultipart() {
throw new IllegalStateException("Multipart request not initialized");
}
/**
* 判断底层的请求是否被解析了;由于本类支持懒解析,因此需要提供该方法,方便外界判断请求是否真的解析了
*/
public boolean isResolved() {
return (this.multipartFiles != null);
}
/**
* 获取底层的ServletRequest对象;由于我们能确定它就是HttpServletRequest类型的,因此进行了强转
*/
@Override
public HttpServletRequest getRequest() {
return (HttpServletRequest) super.getRequest();
}
/**
* 获取Http请求方式
*/
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.resolve(getRequest().getMethod());
}
/**
* 获取Http请求头
*/
@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);
}
// getFiles()/getFileMap()/getMultiFileMap()方法底层都是在调用getMultipartFiles()方法,因此省略
}
3.3. StandardMultipartHttpServletRequest
/**
* 本类继承自AbstractMultipartHttpServletRequest
* 本类主要负责包装HttpServletRequest请求,并将其底层的Part对象封装成MultipartFile对象
*/
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
/**
* 用于存放普通的表单项的参数名(这些参数名对应的值是字符串,而不是文件数据)
*/
@Nullable
private Set<String> multipartParameterNames;
/**
* 构造方法1:需指定底层的HttpServletRequest对象,默认立刻解析该请求
*/
public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
this(request, false);
}
/**
* 构造方法2:需指定底层的HttpServletRequest对象,以及是否懒初始化
*/
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
// 如果不懒初始化,则立刻解析请求
if (!lazyParsing) {
parseRequest(request);
}
}
/**
* 解析请求
*/
private void parseRequest(HttpServletRequest request) {
try {
// 先获取到该请求的所有Part对象
Collection<Part> parts = request.getParts();
// 初始化this.multipartParameterNames字段,用于存放普通的表单项参数
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
// 用于存放上传文件的解析结果
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
// 对这些Part对象逐个进行解析
for (Part part : parts) {
// 获取到Content-Disposition头信息
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
// 获取到文件名称
String filename = disposition.getFilename();
// 如果文件名称不为空,说明该Part确实是上传文件的表单项
if (filename != null) {
// 这段代码不用管;好像是和JavaMail相关的
if (filename.startsWith("=?") && filename.endsWith("?=")) {
filename = MimeDelegate.decode(filename);
}
// 将该Part封装成StandardMultipartFile对象,并将该解析结果存起来
files.add(part.getName(), new StandardMultipartFile(part, filename));
// 否则,说明该Part是普通的表单项,此时将参数名称保存到this.multipartParameterNames中
} else {
this.multipartParameterNames.add(part.getName());
}
}
// 将文件解析结果保存到父类中
setMultipartFiles(files);
} catch (Throwable ex) {
// 根据ex.getMessage()来抛出对应的运行时异常
handleParseFailure(ex);
}
}
/**
* 重写父类的initializeMultipart()方法,对请求进行解析
*/
@Override
protected void initializeMultipart() {
parseRequest(getRequest());
}
/**
* 重写ServletRequestWrapper类的getParameterNames()方法
* 这里会将this.multipartParameterNames也添加到请求参数列表中
* 这是因为有的Servlet容器并不会将form表单参数添加到getParameterNames()集合中
*
* 此外,本类还重写了ServletRequestWrapper类的getParameterMap()方法,其逻辑和本方法类似,因此省略
*/
@Override
public Enumeration<String> getParameterNames() {
// 如果请求还未解析,则先解析
if (this.multipartParameterNames == null) {
initializeMultipart();
}
// 如果form表单没有提交普通参数,则还是走原来的逻辑
if (this.multipartParameterNames.isEmpty()) {
return super.getParameterNames();
}
// 获取到请求参数和form表单的普通参数,将它们合并到一起,然后转成Enumeration对象
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);
}
/**
* 获取某个form表单项参数对应的文件的内容类型
*/
@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);
}
}
/**
* 获取某个form表单项对应的文件的头部信息
*/
@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);
}
}
}
4. MultipartResolver
4.1. 概述
/**
* 解析上传文件的策略接口;本接口不依赖于ApplicationContext,可以单独使用;本接口有两个具体实现:
* 1. CommonsMultipartResolver:底层依靠commons-fileupload.jar工具包来解析上传的文件;已过时
* 2. StandardServletMultipartResolver:依靠Servlet 3.0的Part API来解析上传的文件;推荐使用
*
* DispatcherServlet默认不使用MultipartResolver组件,因为用户的应用可能会选择自己来解析上传的文件
* 如果要启用MultipartResolver组件,则必须将其注册到Mvc容器中,且Bean名称为"multipartResolver"
* DispatcherServlet会自动根据该名称从Mvc容器中获取MultipartResolver组件,并将其应用到每一个请求中
*
* 除了可以在DispatcherServlet中解析文件上传的请求之外,我们还可以使用过滤器来解析
* Spring提供了MultipartFilter过滤器来解析文件上传的请求,只需把它注册到Servlet容器中即可
* MultipartFilter会自动从根容器中获取MultipartResolver组件,一般是在不使用SpringMVC框架时使用
*
* 我们还可以在Controller中注册ByteArrayMultipartFileEditor或StringMultipartFileEditor组件来进行数据绑定
* 这两个组件会自动将上传文件的内容转成byte[]或String形式
*
* 注意,在程序代码中,我们基本不需要和MultipartResolver组件打交道
* 我们需要做的,顶多就是将文件上传的请求对象转成MultipartHttpServletRequest类型
*/
public interface MultipartResolver {
/**
* 判断目标请求是否为文件上传的请求
*/
boolean isMultipart(HttpServletRequest request);
/**
* 将原生的Http请求封装成MultipartHttpServletRequest对象,方便获取上传的文件和form表单的普通参数
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/**
* 清除MultipartHttpServletRequest请求中使用的资源,比如删除上传文件时创建的临时文件
*/
void cleanupMultipart(MultipartHttpServletRequest request);
}
4.2. StandardServletMultipartResolver
/**
* MultipartResolver接口的标准实现,基于Servlet 3.0中的Part API
*/
public class StandardServletMultipartResolver implements MultipartResolver {
/**
* 是否懒解析,默认为false;省略该字段的set方法
*/
private boolean resolveLazily = false;
/**
* 判断目标请求是否为文件上传的请求;这里只是简单地判断ContentType是否以"multipart/"开头
*/
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
/**
* 将原生的Http请求封装成MultipartHttpServletRequest对象
*/
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
/**
* 清除MultipartHttpServletRequest请求中使用的资源,比如删除上传文件时创建的临时文件
*/
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
// 如果该请求不是AbstractMultipartHttpServletRequest类型的,那么不管它是否有懒解析机制,都要尝试进行清除
// 否则,如果它确实已经解析过原始请求了,那么此时也需要执行清除操作
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved()) {
try {
// 获取到所有的Part对象,并依次调用其delete()方法
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);
}
}
}
}
5. DispatcherServlet处理文件上传
5.1. 解析文件上传的请求
我们知道,processRequest()方法主要是在doService()方法前后执行了初始化和清除工作(如初始化/恢复LocaleContext等)
而doService()方法则主要对include、forward、redirect请求进行了相应的处理,并将必要的组件存到请求域中
接下来就需要来看doDispatch()方法了;它是真正处理请求的方法:
public class DispatcherServlet extends FrameworkServlet {
/**
* 底层使用的MultipartResolver组件;该字段会在DispatcherServlet初始化时被赋值(但初始化后仍有可能为null)
*/
@Nullable
private MultipartResolver multipartResolver;
/**
* 真正处理请求,这里先省略掉无关的代码
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// processedRequest代表真正要被处理的请求,初始化为原始的request对象
// 比如,我们有可能需要对原始的request进行包装,而包装后的对象才是真正要被处理的对象
HttpServletRequest processedRequest = request;
// multipartRequestParsed代表当前请求是否为文件上传的请求,初始化为false
boolean multipartRequestParsed = false;
try {
try {
// 判断request是否为文件上传的请求,如果是,则对其进行包装,然后将包装后的对象赋值给processedRequest
processedRequest = checkMultipart(request);
// 如果processedRequest和request不相同,说明processedRequest是包装对象,也就是说,当前请求是文件上传的请求
multipartRequestParsed = (processedRequest != request);
// 通过processedRequest而不是request来查找能处理该请求的处理器
// 后面的所有HttpServletRequest传参,传的都是processedRequest对象
mappedHandler = getHandler(processedRequest);
// 省略后续的代码
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else {
// 如果当前请求是文件上传的请求,则执行清除操作
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
/**
* 判断目标请求是否为文件上传请求;如果是,则将其包装为MultipartHttpServletRequest类型
*/
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// 如果底层的MultipartResolver组件不为null,并且该组件判断当前请求确实是文件上传的请求
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
// 当前的request可能经过了层层包装,因此我们需要判断这些包装中,是否有MultipartHttpServletRequest装饰器
// 如果已经被MultipartHttpServletRequest装饰器包装了(如配置了MultipartFilter),则忽略
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
// 否则,如果该请求中有MultipartException异常,说明当前是在进行异常处理?此时也忽略
} else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
// 否则,才真正对其进行包装
} else {
try {
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;
}
}
}
}
// 执行到这里,说明MultipartResolver组件为null,或者不需要包装
return request;
}
/**
* 执行上传文件的清除操作
*/
protected void cleanupMultipart(HttpServletRequest request) {
if (this.multipartResolver != null) {
// 从request的层层包装中获取到MultipartHttpServletRequest装饰器
// 然后调用MultipartResolver组件来处理该MultipartHttpServletRequest
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
if (multipartRequest != null) {
this.multipartResolver.cleanupMultipart(multipartRequest);
}
}
}
}
5.2. 控制器方法接收上传的文件
我们可以在控制器方法中添加Part或MultipartFile类型的参数来直接接收用户上传的文件信息,如:
@RestController("/upload")
public class FileUploadController {
/**
* 这里的@RequestParam和@RequestPart注解的作用相似
* 这两个注解都可以省略,此时形参名称将作为表单项的参数名称来进行查找
* 此外,控制器方法还支持Part或MultipartFile的数组/集合形式的参数
*/
@PostMapping("/demo")
public String testPost(
@RequestParam("file") Part file1, @RequestPart("file") Part file2,
@RequestParam("file") MultipartFile file3, @RequestPart("file") MultipartFile file4
) {
System.out.println(file1 == file2); // true
System.out.println(file3 == file4); // true
return "success";
}
}
上面这两个注解分别由RequestParamMethodArgumentResolver和RequestPartMethodArgumentResolver组件来解析
我们以RequestParamMethodArgumentResolver为例来了解Part或MultipartFile类型的参数的解析过程:
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
implements UriComponentsContributor {
/**
* 判断是否支持解析目标方法参数
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 如果该参数上有@RequestParam注解
if (parameter.hasParameterAnnotation(RequestParam.class)) {
// 如果该参数是Map类型
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
// 如果@RequestParam注解指定了具体的参数名称,则支持
// 否则,说明用户想通过Map来接收所有的请求参数,此时应该由RequestParamMapMethodArgumentResolver来处理
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
// 否则,支持处理该参数
} else {
return true;
}
// 如果没有@RequestParam注解
} else {
// 如果有@RequestPart注解,则不支持
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
// 如果参数类型是Part或MultipartFile类型,或者是它们的数组/集合形式,则支持
// 因此,这些类型的参数是不需要有@RequestParam注解的
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
// 剩下的不管,可以视为返回false
} else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
} else {
return false;
}
}
}
/**
* 解析方法参数
*/
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
// 获取到HttpServletRequest对象;如果有,则尝试进行解析
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null) {
// 判断该请求是否为上传文件的请求,并且当前参数是Part或MultipartFile等类型,如果是,则提取出相应的数据
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
// 如果确实解析到了相应的数据,则直接返回
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
}
// 否则,再尝试从MultipartRequest对象中提取出对应的MultipartFile信息
Object arg = null;
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
// 如果arg为null,说明当前参数不是文件类型的,此时直接读取请求参数即可
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
}
public final class MultipartResolutionDelegate {
/**
* 解析控制器方法中的文件类型的参数
*/
@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception {
// 判断当前请求是否为上传文件的请求
// 如果能获取到MultipartHttpServletRequest装饰器,或者ContentType以"multipart/"开头,则是上传文件的请求
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
boolean isMultipart = (multipartRequest != null || isMultipartContent(request));
// 如果参数是MultipartFile类型
if (MultipartFile.class == parameter.getNestedParameterType()) {
// 如果当前不是上传文件的请求,则返回null
if (!isMultipart) {
return null;
}
// 必要的话,将该请求封装成StandardMultipartHttpServletRequest
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
// 通过StandardMultipartHttpServletRequest对象来获取相应的MultipartFile对象
return multipartRequest.getFile(name);
// 如果参数是Collection<MultipartFile>或List<MultipartFile>等类型
} else if (isMultipartFileCollection(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files : null);
// 如果参数是MultipartFile[]类型
} else if (isMultipartFileArray(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
// 如果参数是Part类型
} else if (Part.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
// 直接通过getPart(name)方法来进行解析
return request.getPart(name);
// 如果参数是Collection<Part>或List<Part>等类型
} else if (isPartCollection(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts : null);
// 如果参数是Part[]类型
} else if (isPartArray(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
// 否则,说明是普通类型,这种情况不归当前类管,因此返回UNRESOLVABLE
} else {
return UNRESOLVABLE;
}
}
/**
* 遍历所有Part对象,并将名称与name匹配的Part对象保存下来并返回
*/
private static List<Part> resolvePartList(HttpServletRequest request, String name) throws Exception {
Collection<Part> parts = request.getParts();
List<Part> result = new ArrayList<>(parts.size());
for (Part part : parts) {
if (part.getName().equals(name)) {
result.add(part);
}
}
return result;
}
}