0%

SpringMVC 创建及请求处理过程解析

通过策略接口,Spring 框架是高度可配置的,而且包含多种视图技术,例如 JavaServer Pages(JSP)技术、Velocity、Tiles、iText 和 POI。Spring MVC 框架并不知道使用的视图,所以不会强迫开发者只使用 JSP 技术。Spring MVC 分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。

servlet-api 接口规范

Servlet 是 Server + Applet 的缩写,表示一个服务器应用。下图为 Servlet 在 servlet-api 的继承关系。

Servlet 接口

接口定义

Servlet 接口定义了 initialize a servletservice requestsremove a servlet from the server 三项内容,它们管理着整个 Servlet 的 life-cycle

  • The servlet is constructed, then initialized with the init method.
  • Any calls from clients to the service method are handled.
  • The servlet is taken out of service, then destroyed with the destroy method, then garbage collected and finalized.

除了 life-cycle 方法,还提供了获取 startup 信息的 getServletConfig 方法,以及返回 Servlet 自身基本信息的 getServletInfo 方法。

与 web.xml 配置关联

public void init(ServletConfig config) throws ServletException;

init() 方法被调用时会接收到一个 ServletConfig 类型的参数,是容器传进去的。在 web.xml 中定义 Servlet 时通过 init-param 标签配置的参数就是通过 ServletConfig 来保存的。

<web-app>
    <display-name>Application Context</display-name>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>application-context.xml</param-value>
    </context-param>
	<servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--
        在配置Servlet的时候可以设置contextConfigLocation参数来指定SpringMVC配置文件的位置,
        如果不指定就默认使用WEB-INF/[ServletName]-servlet.xml文件.
        -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:application-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
</web-app>

init() 方法在容器启动时被容器调用(当 load-on-startup 设置为负数或者不设置时会在 Servlet 第一次用到时才被调用),只会调用一次。

Tomcat 中 Servlet 的 init 方法是在 org.apache.catalina.core.StandardWrapperinitServlet() 方法中调用的,ServletConfig 传入的是 StandardWrapper(里面封装着 Servlet)自身的门面类 StandardWrapperFacade。其实这个也很容易理解,Servlet 是通过 xml 文件配置的,在解析 xml 时就会把配置参数给设置进去,这样 StandardWrapper 本身就包含配置项了,当然,并不是 StandardWrapper 的所有内容都是 Config 相关的,所以就用了其门面 Facade 类。下面是 ServletConfig 接口的定义:

public interface ServletConfig {
    //获取Servlet名称,web.xml中<servlet-name>配置
    public String getServletName();
    //返回ServletContext,即返回应用本身
    public ServletContext getServletContext();
    //获取配置项,获取web.xml中<init-param>参数
    public String getInitParameter(String name);
    public Enumeration getInitParameterNames();
}

ServletContext 其实就是 Tomcat 中 Context 的门面类 ApplicationContextFacade(具体代码参考 StandardContext)的 getServletContext 方法。一个 ServletContext 包含多个 Servlet,因为 ServletContext 代表应用(Application)本身,那么 ServletContext 里边设置的参数就可以被当前应用的所有 Servlet 共享了。

其实,除了 Servlet 级和 Context 级还有更高的一级,也就是 Tomcat 抽象层级的 Host 级。在 ServletContext 接口中的 getContext(String uripath),它可以根据路径获取到同一个站点下的别的应用的 ServletContext!当然由于安全的原因,一般会返回 null,如果想使用需要进行一些设置。

GenericServlet 抽象类

GenericServlet 是 Servlet 的抽象类,也实现了 ServletConfig 接口,可以直接调用获取配置信息。它也提供了 init() 的模版方法,这种方式可以在实现中不再关心 Config 的输入以及重载时的 super.init(config)

HttpServlet 实现类

HttpServlet 是用 HTTP 协议实现的 Servlet 基类,Spring MVC 的 DispatcherServlet 就是继承 HttpServlet。其主要实现了 service 方法来通过 HTTP Method 路由至相关的处理方法。

SpringMVC 创建过程

HttpServletBean、FrameworkServlet 以及 DispatcherServlet 直接实现三个接口:EnvironmentCapable、EnvironmentAware 和 ApplicationContextAware。Aware 接口可以通过 set 方法将对应的资源注入 Bean 中。Capable 接口可以通过 get 方法在 Bean 中获取对应的资源。

实际上在 HttpServletBean 中 Environment 使用的是 StandardServletEnvironment(在 createEnvironment 方法中创建),这里确实封装 ServletContext,同时还封装了 ServletConfig、JndiProperty、系统环境变量和系统属性,这些都封装到了其 propertySources 属性下。

HttpServletBean

从 Servlet 中可知,HttpServletBean 的初始化入口方法应该是 init 方法。

@Override
public final void init() throws ServletException {

   //将Servlet中配置的参数封装到pvs变量中,requiredProperties为必需参数,如果没配置将报异常
   PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
   if (!pvs.isEmpty()) {
       BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
       ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
       bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
       //模板方法,可以在子类调用,做一些初始化工作。bw就是代表DispatcherServlet
       initBeanWrapper(bw);
       //将配置的初始化值(如contextConfigLocation)设置到DispatcherServlet
       bw.setPropertyValues(pvs, true);
   }

   //模板方法,子类初始化的入口方法
   initServletBean();
}

在 HttpServletBean 的 init 中,首先将 Servlet 中配置的参数使用 BeanWrapper 设置到 DispatcherServlet 的相关属性,然后调用模板方法 initServletBean,子类就通过这个方法初始化。

FrameworkServlet

从 HttpServletBean 中可知,FrameworkServlet 的初始化入口方法应该是 initServletBean。

protected final void initServletBean() throws ServletException {
      //初始化 WebApplicationContext
      this.webApplicationContext = initWebApplicationContext();
      //模板方法,初始化FrameworkServlet,子类并未使用
      initFrameworkServlet();
}

在设置WebApplicationContext时会根据情况调用onRefresh方法

第一种方法是在构造方法中已经传递 webApplicationContext 参数,这时只需要对其进行一些设置即可。这种方法主要用于 Servlet3.0 以后的环境中,Servlet3.0 之后可以在程序中使用 ServletContext.addServlet 方式注册 Servlet,这时就可以在新建 FrameworkServlet 和其子类的时候通过构成方法传递已经准备好的 webApplicationContext。

第二种方法是 webApplicationContext 已经在 ServletContext 中了。这时只需要在配置 Servlet <init-param> 的时候将 ServletContext 中的 webApplicationContext 的 name 配置到 contextAttribute 属性就可以了。

第三种方法是在前面两种方式都无效的情况下自己创建一个。正常情况下就是使用的这种方式。创建过程在 createWebApplicationContext 方法中,createWebApplicationContext 内部又调用了 configureAndRefreshWebApplicationContext 方法。

当使用第三种方法初始化时已经 refresh,不需要再调用 onRefresh。同样在第一种方式中也调用了 configureAndRefreshWebApplicationContext 方法,也 refresh 过,所以只有使用第二种方式初始化 webApplicationContext 的时候才会在这里调用 onRefresh 方法。不过不管用哪种方式调用,onRefresh 最终肯定会而且只会调用一次,而且 DispatcherServlet 正是通过重写这个模板方法来实现初始化的。

DispatcherServlet

onRefresh 方法是 DispatcherServlet 的入口方法。onRefresh 中简单地调用了 initStrategies,在 initStrategies 中调用了其 9 大组件的初始化方法:

@Override
protected void onRefresh(ApplicationContext context) {
   initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
   initMultipartResolver(context);
   initLocaleResolver(context);
   initThemeResolver(context);
   initHandlerMappings(context);
   initHandlerAdapters(context);
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   initViewResolvers(context);
   initFlashMapManager(context);
}

可能有读者不理解为什么要这么写,为什么不将 initStrategies 的具体实现直接写到 onRefresh 中呢?initStrategies 方法不是多余的吗?其实这主要是分层的原因,onRefresh 是用来刷新容器的,initStrategies 用来初始化一些策略组件。如果把 initStrategies 里面的代码直接写到 onRefresh 里面,对于程序的运行也没有影响,不过这样一来,如果在 onRefresh 中想再添加别的功能,就会没有将其单独写一个方法出来逻辑清晰,不过这并不是最重要的,更重要的是,如果在别的地方也需要调用 initStrategies 方法(如需要修改一些策略后进行热部署),但 initStrategies 没独立出来,就只能调用 onRefresh,那样在 onRefresh 增加了新功能的时候就麻烦了。另外单独将 initStrategies 写出来还可以被子类覆盖,使用新的模式进行初始化。

初始化方式分两步:首先通过 context.getBean 在容器里面按注册时的名称或类型进行查找,所以在 SpringMVC 的配置文件中只需要配置相应类型的组件,容器就可以自动找到。如果找不到就调用 getDefaultStrategy 按照类型获取默认的组件。需要注意的是,这里的 context 指的是 FrameworkServlet 中创建的 WebApplicationContext,而不是 ServletContext。

XML 配置解析原理

在 Spring 的 XML 文件中通过命名空间配置的标签是怎么解析的?对于一个具体的命名空间,Spring 是怎么找到解析它的类的呢?其实在 Spring 中是把解析标签的类都放到了相应的 META-INF 目录下的 spring.handlers 文件中。

解析配置的接口是 org.springframework.beans.factory.xmI.NamespaceHandler,它的继承结构如下

NamespaceHandler 里一共定义了三个方法:init、parse 和 decorate。init 是用来初始化自己的;parse 用于将配置的标签转换成 spring 所需要的 BeanDefinition;decorate 用于对所在的 BeanDefinition 进行一些修改。

NamespaceHandler 的实现类主要有三个:NamespaceHandlerSupport、SimpleConstructorNamespaceHandler、SimplePropertyNamespaceHandler。其中 NamespaceHandlerSupport 是 NamespaceHandler 的默认实现,SimpleConstructorNamespaceHandler 用于统一对通过 c: 配置的构造方法进行解析,SimplePropertyNamespaceHandler 用于统一对通过 p: 配置的参数进行解析。

NamespaceHandlerSupport 并没有做具体的解析工作,而是定义了三个处理器 parsers、decorators、attributeDecorators,分别用于处理解析工作、处理标签类型、处理属性类型的装饰。接口的 parse 和 decorate 方法的执行方式是先找到相应的处理器,然后进行处理。具体的处理器由子类实现,然后注册到 NamespaceHandlerSupport 上面。所以要定义一个命名空间的解析器,只需要在 init 中定义相应的 parsers、decorators、attributeDecorators 并注册到 NamespaceHandlerSupport 上面。

SpringMVC 处理请求

SpringMVC 处理请求的过程中用到两个 Servlet:

  • FrameworkServlet:将不同类型的请求合并到 processRequest 方法统一处理,其主要处理过程如下
    • 将当前请求的 LocalContext 和 ServletRequestAttribute 在处理请求前设置到了 LocaleContextHolder 和 RequestContextHolder,并在请求处理完成后恢复。
    • 调用了 doService 模版方法具体处理请求。
    • 请求处理完成后发布 ServletRequestHandledEvent 消息。
  • DispatcherServlet:doService 给 request 设置属性并将请求交给 doDispatch 方法具体处理

HttpServletBean

HttpServletBean 主要参与了创建工作,并没有涉及请求的处理。

FrameworkServlet

Servlet 的处理过程:首先是从 Servlet 接口的 service 方法开始,然后在 HttpServlet 的 service 方法中根据请求的类型不同将请求路由到了 doGet、doHead、doPost、doPut、doDelete、doOptions 和 doTrace 七个方法,并且做了 doHead、doOptions 和 doTrace 的默认实现。其中 doHead 调用 doGet,然后返回只有 header 没有 body 的 response。

在 FrameworkServlet 中重写了 service、doGet、doPost、doPut、doDelete、doOptions、doTrace 方法(除了doHead 的所有处理请求的方法)。在 service 方法中增加了对 PATCH 类型请求的处理,其他类型的请求直接交给了父类进行处理 doOptions 和 doTrace 方法可以通过设置 dispatchOptionsRequest 和 dispatchTraceRequest 参数决定是自己处理还是交给父类处理(默认都是交给父类处理,doOptions 会在父类的处理结果中增加 PATCH 类型);doGet、doPost、doPut 和 doDelete 都是自己处理。所有需要自己处理的请求都交给了processRequest方法进行统一处理。

我们发现这里所做的事情跟 HttpServlet 里将不同类型的请求路由到不同方法进行处理的思路正好相反,这里又将所有的请求合并到了 processRequest 方法。当然并不是说 SpringMVC 中就不对 request 的类型进行分类,而全部执行相同的操作了,恰恰相反,Spring MVC 中对不同类型请求的支持非常好,不过它是通过另外一种方式进行处理的,它将不同类型的请求用不同的 Handler 进行处理,后面再详细分析。

可能有的读者会想,直接覆盖了 service 不是就可以了吗?HttpServlet 是在 service 方法中将请求路由到不同的方法的,如果在 service 中不再调用 super.service(),而是直接将请求交给 processRequest 处理不是更简单吗?从现在的结构来看确实如此,不过那么做其实存在着一些问题。比如,我们为了某种特殊需求需要在 Post 请求处理前对 request 做一些处理,这时可能会新建一个继承自 DispatcherServlet 的类,然后覆盖 doPost 方法,在里面先对 request 做处理,然后再调用 super.doPost(),但是父类根本就没调用 doPost,所以这时候就会出问题了。虽然
这个问题的解决方法也很简单,但是按正常的逻辑,调用 doPost 应该可以完成才合理,而且一般情况下开发者并不需要对 Spring MVC 内部的结构非常了解,所以 Spring MVC 的这种做法虽然看起来有点笨拙但是是必要的。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

   long startTime = System.currentTimeMillis();
   Throwable failureCause = null;

   //获取LocaleContextHolder中原来保存的LocaleContext
   LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
   //获取当前请求的LocaleContext
   LocaleContext localeContext = buildLocaleContext(request);
   //获取RequestContextHolder中原来保存的RequestAttributes
   RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
   //获取当前请求的ServletRequestAttributes
   ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
   asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
   //将当前请求的LocaleContext和ServletRequestAttributes设置到LocaleContextHolder
   initContextHolders(request, localeContext, requestAttributes);

   try {
      //实际请求请求入口
      doService(request, response);
   }
   catch (ServletException | IOException ex) {
      failureCause = ex;
      throw ex;
   }
   catch (Throwable ex) {
      failureCause = ex;
      throw new NestedServletException("Request processing failed", ex);
   }
   //在方法最后的finally中调用resetContextHolders方法将原来的LocaleContext和RequestAttributes
   //又恢复了。这是因为在Sevlet外面可能还有别的操作,如Filter(Spring-MVC自己的HandlerInterceptor
   //是在doService内部的)等,为了不影响那些操作,所以需要进行恢复。

   finally {
      resetContextHolders(request, previousLocaleContext, previousAttributes);
      if (requestAttributes != null) {
         requestAttributes.requestCompleted();
      }
      logResult(request, response, failureCause, asyncManager);
      publishRequestHandledEvent(request, response, startTime, failureCause);
   }
}

processRequest 方法中的核心语句是 doService(request,response),这是一个模板方法,在 DispatcherServlet 中具体实现。在 doService 前后还做了一些事情(也就是大家熟悉的装饰模式):首先获取了 LocaleContextHolder 和 RequestContextHolder 中原来保存的 LocaleContext 和 RequestAttributes 并设置到 previousLocaleContext 和 previousAttributes 临时属性,然后调用 buildLocaleContext 和 buildRequestAttributes 方法获取到当前请求的 LocaleContext 和 RequestAttributes,并通过 initContextHolders 方法将它们设置到 LocaleContextHolder 和 RequestContextHolder 中(处理完请求后再恢复到原来的值),接着使用 request 拿到异步处理管理器并设置了拦截器,做完这些后执行了 doService 方法,执行完后,最后(finally 中)通过 resetContextHolders 方法将原来的 previousLocaleContext 和 previousAttributes 恢复到 LocaleContextHolder 和 RequestContextHolder 中,并调用 publishRequestHandledEvent 方法发布了一个 ServletRequestHandledEvent 类型的消息。

当 publishEvents 设置为 true 时,请求处理结束后就会发出这个消息,无论请求处理成功与否都会发布。publishEvents 可以在 web.xml 文件中配置 Spring MVC 的 Servlet 时配置,默认为 true。通过以下方法便可以简单的监听日志。

@Component
public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> {
    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        //do something
    }
}

总结一下 processRequest 方法,在 processRequest 里面主要的处理逻辑交给了 doService,这是一个模板方法,在子类具体实现,另外就是对使用当前 request 获取到的 LocaleConext 和 RequestAttributes 进行了保存,以及处理完之后的恢复,在最后发布了 ServletRequestHandledEvent 事件。

DispatcherServlet

处理请求

DispatcherServlet 是 Spring MVC 最核心的类,整个处理过程的顶层设计都在这里面。DispatcherServlet 执行处理的入口方法是 doService,而 doService 则将具体的处理交给了 doDispatch。

doDispatch 中最核心的代码只要 4 句,

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  // Determine handler for the current request.
  // 根据 request 找到 handler, 拦截器链式处理请求
  mappedHandler = getHandler(processedRequest);
  // Determine handler adapter for the current request.
  // 根据 handler 找到对应的 HandlerAdapter. 解析参数后
  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  // Actually invoke the handler.
  // 用 HandlerAdapter 处理 Handler,执行 Controller
  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  // 调用 processDispatchResult 进行页面渲染
  processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

}

在这里需要区分一下以上提到的三个组件:Handler、HandlerMapping、HandlerAdapter,以及 ViewResolver。

Handler:也就是处理器,它直接对应着 MVC 中的 C 也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法,如果你能想到别的表现形式也可以使用,它的类型是 Object。Spring 中标注了 @RequestMapping 的所有方法都可以看成一个 Handler。只要可以实际处理请求就可以是 Handler。

HandlerMapping:是用来查找 Handler 的,在 Spring MVC 中会处理很多请求,每个请求都需要一个 Handler 来处理,具体接收到一个请求后使用哪个 Handler 来处理就是 HandlerMapping 要做的事情。

HandlerAdapter:因为 Spring MVC 中的 Handler 可以是任意的形式,只要能处理请求就 OK,但是 Servlet 需要的处理方法的结构却是固定的,都是以 request 和 response 为参数的方法(如 doService 方法)。HandlerAdapter 主要是让固定的 Servlet 处理方法调用灵活的 Handler。

在初始化时,HandlerAdapter 会加载多个 HandlerMethodArgumentResolver,默认的 Handler 通过 RequestMappingHandlerAdapter.getDefaultArgumentResolvers 进行加载,并最终在 org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues 进行实际的解析。

如想要拥有扩展能力,可先实现 HandlerMethodArgumentResolver 接口,并通过 WebMvcConfigurerAdapter 中的 addArgumentResolvers 进行注入。

渲染页面

最后调用的 processDispatchResult 主要用来渲染页面,其主要是调用 render 来完成正向流程的渲染过程的。

render(mv, request, response);

以下是 render 的简化逻辑

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  // Determine locale for request and apply it to the response.
  // 对 response 设置 Local,过程中使用到了 LocaleResolver
  Locale locale =
    (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
  response.setLocale(locale);

  // 获取实际的 View
  View view;
  String viewName = mv.getViewName();
  if (viewName != null) {
    // We need to resolve the view name.
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

  }
  else {
    // No need to lookup: the ModelAndView object contains the actual View object.
    view = mv.getView();
  }

  // Delegate to the View object for rendering.
  // 调用 View 的 render 进行具体的渲染,渲染的过程使用了 ThemeResolver。
  view.render(mv.getModelInternal(), request, response); 
}

另外 View 和 ViewResolver 的原理与 Handler 和 HandlerMapping 的原理类似。View 是用来展示数据的,而 ViewResolver 用来查找 View。View 就是页面所需要的模板,内容就是 Model 中的数据,ViewResolver 就是用来选择使用哪个模板的选择器。

那么如何将请求与视图对应起来呢?可以通过 RequestToViewNameTranslator 实现所有 Request 到 ViewName 的转换规则,这些都要在一个 Translator 里面全部实现(因为 SpringMVC 中只能配置一个 RequestToViewNameTranslator)。

支持文件上传

MultipartResolver 用于处理上传请求,处理方法是将普通的 request 包装成 Multipart-HttpServletRequest,后者可以直接调用 getFile 方法获取到 File,如果上传多个文件,还可以调用 getFileMap 得到 FileName→File 结构的 Map,这样就使得上传请求的处理变得非常简单。当然,这里做的其实是锦上添花的事情,如果上传的请求不用 MultipartResolver 封装成 MultipartHttpServletRequest,直接用原来的 request 也是可以的。

/**
 *
 * <p>There are two concrete implementations included in Spring, as of Spring 3.1:
 * <ul>
 * <li>{@link org.springframework.web.multipart.commons.CommonsMultipartResolver}
 * for Apache Commons FileUpload
 * <li>{@link org.springframework.web.multipart.support.StandardServletMultipartResolver}
 * for the Servlet 3.0+ Part API
 * </ul>
 *
 * <p>There is no default resolver implementation used for Spring
 * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlets},
 * as an application might choose to parse its multipart requests itself. To define
 * an implementation, create a bean with the id "multipartResolver" in a
 * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet's}
 * application context. Such a resolver gets applied to all requests handled
 * by that {@link org.springframework.web.servlet.DispatcherServlet}.
 *
 * <p>If a {@link org.springframework.web.servlet.DispatcherServlet} detects a
 * multipart request, it will resolve it via the configured {@link MultipartResolver}
 * and pass on a wrapped {@link javax.servlet.http.HttpServletRequest}. Controllers
 * can then cast their given request to the {@link MultipartHttpServletRequest}
 * interface, which allows for access to any {@link MultipartFile MultipartFiles}.
 * Note that this cast is only supported in case of an actual multipart request.
 *
 * <pre class="code">
 * public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
 *   MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
 *   MultipartFile multipartFile = multipartRequest.getFile("image");
 *   ...
 * }</pre>
 *
 * Instead of direct access, command or form controllers can register a
 * {@link org.springframework.web.multipart.support.ByteArrayMultipartFileEditor}
 * or {@link org.springframework.web.multipart.support.StringMultipartFileEditor}
 * with their data binder, to automatically apply multipart content to form
 * bean properties.
 *
 * <p>As an alternative to using a {@link MultipartResolver} with a
 * {@link org.springframework.web.servlet.DispatcherServlet},
 * a {@link org.springframework.web.multipart.support.MultipartFilter} can be
 * registered in {@code web.xml}. It will delegate to a corresponding
 * {@link MultipartResolver} bean in the root application context. This is mainly
 * intended for applications that do not use Spring's own web MVC framework.
 *
 * <p>Note: There is hardly ever a need to access the {@link MultipartResolver}
 * itself from application code. It will simply do its work behind the scenes,
 * making {@link MultipartHttpServletRequest MultipartHttpServletRequests}
 * available to controllers.
 *
 * @see MultipartHttpServletRequest
 * @see MultipartFile
 * @see org.springframework.web.multipart.commons.CommonsMultipartResolver
 * @see org.springframework.web.multipart.support.ByteArrayMultipartFileEditor
 * @see org.springframework.web.multipart.support.StringMultipartFileEditor
 * @see org.springframework.web.servlet.DispatcherServlet
 */
public interface MultipartResolver {

   // 判断是否为上传请求
   boolean isMultipart(HttpServletRequest request);
   // 将 request 包装为 MultipartHttpServletRequest
   MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
   // 清理上传过程中产生的临时资源
   void cleanupMultipart(MultipartHttpServletRequest request);
}