其实文件上传这块松哥之前和大家聊过很屡次了,这次因为最近正在进行 SpringMVC 的源码剖析,所以又再次把这个话题拉进去“鞭尸”,不过这次松哥想从源码角度来聊聊这个话题。

了解源码的前提是先会用,所以咱们还是先来看看用法,而后再来剖析源码。

1.两种文件解析计划

对于上传文件的申请,SpringMVC 中目前共有两种不同的解析计划:

  • StandardServletMultipartResolver
  • CommonsMultipartResolver

StandardServletMultipartResolver 反对 Servlet3.0 中规范的文件上传计划,应用非常简单;CommonsMultipartResolver 则须要联合 Apache Commons fileupload 组件一起应用,这种形式兼容低版本的 Servlet。

StandardServletMultipartResolver

先来回顾下 StandardServletMultipartResolver 的用法。

应用 StandardServletMultipartResolver,能够间接通过 HttpServletRequest 自带的 getPart 办法获取上传文件并保留,这是一种规范的操作形式,这种形式也不必增加任何额定的依赖,只须要确保 Servlet 的版本在 3.0 之上即可。

首先咱们须要为 Servlet 配置 multipart-config,哪个 Servlet 负责解决上传文件,就为哪个 Servlet 配置 multipart-config。在 SpringMVC 中,咱们的申请都是通过 DispatcherServlet 进行散发的,所以咱们就为 DispatcherServlet 配置 multipart-config

配置形式如下:

<servlet>    <servlet-name>springmvc</servlet-name>    <servlet-class>org.springframework.web.servlet.DispatcherServlet</serv    <init-param>        <param-name>contextConfigLocation</param-name>        <param-value>classpath:spring-servlet.xml</param-value>    </init-param>    <multipart-config>        <location>/tmp</location>        <max-file-size>1024</max-file-size>        <max-request-size>10240</max-request-size>    </multipart-config></servlet><servlet-mapping>    <servlet-name>springmvc</servlet-name>    <url-pattern>/</url-pattern></servlet-mapping>

而后在 SpringMVC 的配置文件中提供一个 StandardServletMultipartResolver 实例,留神该实例的 id 必须为 multipartResolver(具体起因参见:SpringMVC 初始化流程剖析一文)。

<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver"></bean>

配置实现后,咱们就能够开发一个文件上传接口了,如下:

@RestControllerpublic class FileUploadController {    SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");    @PostMapping("/upload")    public String fileUpload(MultipartFile file, HttpServletRequest req) {        String format = sdf.format(new Date());        String realPath = req.getServletContext().getRealPath("/img") + format;        File folder = new File(realPath);        if (!folder.exists()) {            folder.mkdirs();        }        String oldName = file.getOriginalFilename();        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));        try {            file.transferTo(new File(folder, newName));            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;        } catch (IOException e) {            e.printStackTrace();        }        return "error";    }    @PostMapping("/upload2")    public String fileUpload2(HttpServletRequest req) throws IOException, ServletException {        StandardServletMultipartResolver resolver = new StandardServletMultipartResolver();        MultipartFile file = resolver.resolveMultipart(req).getFile("file");        String format = sdf.format(new Date());        String realPath = req.getServletContext().getRealPath("/img") + format;        File folder = new File(realPath);        if (!folder.exists()) {            folder.mkdirs();        }        String oldName = file.getOriginalFilename();        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));        try {            file.transferTo(new File(folder, newName));            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;        } catch (IOException e) {            e.printStackTrace();        }        return "error";    }    @PostMapping("/upload3")    public String fileUpload3(HttpServletRequest req) throws IOException, ServletException {        String other_param = req.getParameter("other_param");        System.out.println("other_param = " + other_param);        String format = sdf.format(new Date());        String realPath = req.getServletContext().getRealPath("/img") + format;        File folder = new File(realPath);        if (!folder.exists()) {            folder.mkdirs();        }        Part filePart = req.getPart("file");        String oldName = filePart.getSubmittedFileName();        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));        try {            filePart.write(realPath + newName);            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;        } catch (IOException e) {            e.printStackTrace();        }        return "error";    }}

我这里一共提供了三个文件上传接口,其实最终都是通过 StandardServletMultipartResolver 进行解决的。

  1. 第一个接口是咱们在 SpringMVC 框架中常见的一种文件上传解决形式,间接在参数中写上 MultipartFile,这个 MultipartFile 其实就是从以后申请中解析进去的,具体负责参数解析工作的就是 RequestParamMethodArgumentResolver。
  2. 第二个接口其实是一种古老的文件上传实现计划,参数就是一般的 HttpServletRequest,而后在参数里边,咱们再手动利用 StandardServletMultipartResolver 实例进行解析(这种状况能够不必本人 new 一个 StandardServletMultipartResolver 实例,间接将 Spring 容器中的注入进来即可)。
  3. 第三个接口咱们就利用了 Servlet3.0 的 API,调用 getPart 获取文件,而后再调用对象的 write 办法将文件写出去即可。

大抵上一看,感觉方法还挺多,其实认真看,万变不离其宗,一会咱们看完源码,置信小伙伴们还能变动出更多写法。

CommonsMultipartResolver

CommonsMultipartResolver 预计很多人都比拟相熟,这个兼容性很好,就是有点过期了。应用 CommonsMultipartResolver 须要咱们首先引入 commons-fileupload 依赖:

<dependency>    <groupId>commons-fileupload</groupId>    <artifactId>commons-fileupload</artifactId>    <version>1.4</version></dependency>

而后在 SpringMVC 的配置文件中提供 CommonsMultipartResolver 实例,如下:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">--></bean>

接下来开发文件上传接口就行了:

@PostMapping("/upload")public String fileUpload(MultipartFile file, HttpServletRequest req) {    String format = sdf.format(new Date());    String realPath = req.getServletContext().getRealPath("/img") + format;    File folder = new File(realPath);    if (!folder.exists()) {        folder.mkdirs();    }    String oldName = file.getOriginalFilename();    String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));    try {        file.transferTo(new File(folder, newName));        return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;    } catch (IOException e) {        e.printStackTrace();    }    return "error";}

这个就没啥好说,比拟容易。

文件上传这块松哥之前在视频中也和大家分享过,公号后盾回复 ssm 能够查看视频详情。

用法把握了,接下来咱们来看原理。

2.StandardServletMultipartResolver

不废话,间接来看看源码:

public class StandardServletMultipartResolver implements MultipartResolver {    private boolean resolveLazily = false;    public void setResolveLazily(boolean resolveLazily) {        this.resolveLazily = resolveLazily;    }    @Override    public boolean isMultipart(HttpServletRequest request) {        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()) {            try {                for (Part part : request.getParts()) {                    if (request.getFile(part.getName()) != null) {                        part.delete();                    }                }            }            catch (Throwable ex) {            }        }    }}

这里满打满算就四个办法,其中一个还是 set 办法,咱们来看另外三个功能性办法:

  1. isMultipart:这个办法次要是用来判断以后申请是不是文件上传申请,这里的判断思路很简略,就看申请的 content-type 是不是以 multipart/ 结尾,如果是,则这就是一个文件上传申请,否则就不是文件上传申请。
  2. resolveMultipart:这个办法负责将以后申请封装一个 StandardMultipartHttpServletRequest 对象而后返回。
  3. cleanupMultipart:这个办法负责善后,次要实现了缓存的清理工作。

在这个过程中波及到 StandardMultipartHttpServletRequest 对象,咱们也来略微说一下:

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)        throws MultipartException {    super(request);    if (!lazyParsing) {        parseRequest(request);    }}private void parseRequest(HttpServletRequest request) {    try {        Collection<Part> parts = request.getParts();        this.multipartParameterNames = new LinkedHashSet<>(parts.size());        MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());        for (Part part : parts) {            String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);            ContentDisposition disposition = ContentDisposition.parse(headerValue);            String filename = disposition.getFilename();            if (filename != null) {                if (filename.startsWith("=?") && filename.endsWith("?=")) {                    filename = MimeDelegate.decode(filename);                }                files.add(part.getName(), new StandardMultipartFile(part, filename));            }            else {                this.multipartParameterNames.add(part.getName());            }        }        setMultipartFiles(files);    }    catch (Throwable ex) {        handleParseFailure(ex);    }}

StandardMultipartHttpServletRequest 对象在构建的过程中,会主动进行申请解析,调用 getParts 办法获取所有的项,而后进行判断,将文件和一般参数别离保留下来备用。

这块的逻辑比较简单。

3.CommonsMultipartResolver

再来看 CommonsMultipartResolver。

先来看它的 isMultipart 办法:

@Overridepublic boolean isMultipart(HttpServletRequest request) {    return ServletFileUpload.isMultipartContent(request);}public static final boolean isMultipartContent(        HttpServletRequest request) {    if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {        return false;    }    return FileUploadBase.isMultipartContent(new ServletRequestContext(request));}

ServletFileUpload.isMultipartContent 办法其实就在咱们引入的 commons-fileupload 包中。它的判断逻辑分两步:首先查看是不是 POST 申请,而后查看 content-type 是不是以 multipart/ 开始。

再来看它的 resolveMultipart 办法:

@Overridepublic MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {    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());    }}

依据 resolveLazily 属性值,抉择两种不同的策略将以后对象从新构建成一个 DefaultMultipartHttpServletRequest 对象。如果 resolveLazily 为 true,则在 initializeMultipart 办法中进行申请解析,否则先解析,再构建 DefaultMultipartHttpServletRequest 对象。

具体的解析办法如下:

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) {        //...    }}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<>();    for (FileItem fileItem : fileItems) {        if (fileItem.isFormField()) {            String value;            String partEncoding = determineEncoding(fileItem.getContentType(), encoding);            try {                value = fileItem.getString(partEncoding);            }            catch (UnsupportedEncodingException ex) {                value = fileItem.getString();            }            String[] curParam = multipartParameters.get(fileItem.getFieldName());            if (curParam == null) {                multipartParameters.put(fileItem.getFieldName(), new String[] {value});            }            else {                String[] newParam = StringUtils.addStringToArray(curParam, value);                multipartParameters.put(fileItem.getFieldName(), newParam);            }            multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());        }        else {            CommonsMultipartFile file = createMultipartFile(fileItem);            multipartFiles.add(file.getName(), file);        }    }    return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);}

这里的解析就是首先获取到 FileItem 汇合,而后调用 parseFileItems 办法进行进一步的解析。在进一步的解析中,会首先判断这是文件还是一般参数,如果是一般参数,则保留到 multipartParameters 中,具体保留过程中还会判断是否为数组,而后再将参数的 ContentType 保留到 multipartParameterContentTypes 中,文件则保留到 multipartFiles 中,最初由三个 Map 形成一个 MultipartParsingResult 对象并返回。

至此,StandardServletMultipartResolver 和 CommonsMultipartResolver 源码就和大家说完了,能够看到,还是比拟容易的。

4.解析流程

最初,咱们再来梳理一下解析流程。

以如下接口为例(因为在理论开发中个别都是通过如下形式上传文件):

@PostMapping("/upload")public String fileUpload(MultipartFile file, HttpServletRequest req) {    String format = sdf.format(new Date());    String realPath = req.getServletContext().getRealPath("/img") + format;    File folder = new File(realPath);    if (!folder.exists()) {        folder.mkdirs();    }    String oldName = file.getOriginalFilename();    String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));    try {        file.transferTo(new File(folder, newName));        return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;    } catch (IOException e) {        e.printStackTrace();    }    return "error";}

这里 MultipartFile 对象次要就是在参数解析器中获取的,对于参数解析器,大家能够参考:深入分析 SpringMVC 参数解析器 一文,这里波及到的参数解析器是 RequestParamMethodArgumentResolver。

在 RequestParamMethodArgumentResolver#resolveName 办法中有如下一行代码:

if (servletRequest != null) {    Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);    if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {        return mpArg;    }}

这个办法会进行申请解析,返回 MultipartFile 对象或者 MultipartFile 数组。

@Nullablepublic static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)        throws Exception {    MultipartHttpServletRequest multipartRequest =            WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);    boolean isMultipart = (multipartRequest != null || isMultipartContent(request));    if (MultipartFile.class == parameter.getNestedParameterType()) {        if (!isMultipart) {            return null;        }        if (multipartRequest == null) {            multipartRequest = new StandardMultipartHttpServletRequest(request);        }        return multipartRequest.getFile(name);    }    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);    }    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);    }    else if (Part.class == parameter.getNestedParameterType()) {        if (!isMultipart) {            return null;        }        return request.getPart(name);    }    else if (isPartCollection(parameter)) {        if (!isMultipart) {            return null;        }        List<Part> parts = resolvePartList(request, name);        return (!parts.isEmpty() ? parts : null);    }    else if (isPartArray(parameter)) {        if (!isMultipart) {            return null;        }        List<Part> parts = resolvePartList(request, name);        return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);    }    else {        return UNRESOLVABLE;    }}

首先获取 multipartRequest 对象,而后再从中获取文件或者文件数组。如果咱们应用 StandardServletMultipartResolver 做文件上传,这里获取到的 multipartRequest 就是 StandardMultipartHttpServletRequest;如果咱们应用 CommonsMultipartResolver 做文件上传,这里获取到的 multipartRequest 就是 DefaultMultipartHttpServletRequest。

5.小结

好啦,明天就和大家简略剖析了下两个文件上传组件的源码,至此,SpringMVC 九大组件咱们曾经剖析了 7 个啦,还剩两个,行将完结,完结后松哥会整顿成 pdf 分享给大家。