Spring MVC真正如何工作

How Spring MVC Really Works

Image title

这是Spring Web MVC的强大功能和内部工作的深入研究,它是Spring框架的一部分。

本文的源代码可从GitHub上获得。

项目设置

在整篇文章中,我们将使用最新,最出色的Spring Framework5。我们在这里重点介绍Spring的经典Web堆栈,该框架的第一版已经提供了该框架,但仍然是构建Web应用程序的主要方式 与Spring。

对于初学者来说,要设置测试项目,您将使用Spring Boot及其一些初学者依赖项。 您还需要定义父级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.M5</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

请注意,为了使用Spring 5,您还需要使用Spring Boot2.x。 在撰写本文时,这是一个里程碑版本,可在Spring Milestone存储库中获得。 让我们将此存储库添加到您的Maven项目中:

1
2
3
4
5
6
7
8
9
10
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

您可以在Maven Central上签出Spring Boot的当前版本。

样例项目

为了了解Spring Web MVC的工作原理,您将实现一个带有登录页面的简单应用程序。 要显示登录页面,请创建一个@Controller注释的类InternalController,它具有上下文根的GET映射。

hello()方法是无参数的。 它返回一个String,该字符串由Spring MVC解释为视图名称(在我们的示例中为login.html模板):

1
2
3
4
5
6
import org.springframework.web.bind.annotation.GetMapping;

@GetMapping("/")
public String hello() {
    return"login";
}

要处理用户登录,请创建另一个方法来处理带有登录数据的POST请求。 然后,根据结果将用户重定向到成功页面或失败页面。

请注意,login()方法接收域对象作为参数,并返回ModelAndView对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

@PostMapping("/login")
public ModelAndView login(LoginData loginData) {
    if (LOGIN.equals(loginData.getLogin())
      && PASSWORD.equals(loginData.getPassword())) {
        return new ModelAndView("success",
          Collections.singletonMap("login", loginData.getLogin()));
    } else {
        return new ModelAndView("failure",
          Collections.singletonMap("login", loginData.getLogin()));
    }
}

ModelAndView是两个不同对象的持有人:

  • 模型–用于呈现页面的数据的键值映射

  • 视图–页面的模板,其中填充了来自模型的数据

  • 为了方便起见,将它们结合在一起,以便控制器方法可以一次返回它们。

    要渲染HTML页面,请使用Thymeleaf作为视图模板引擎,该引擎与Spring紧密集成在一起。

    Servlet作为Java Web应用程序的基础

    因此,当您在浏览器中键入http:// localhost:8080 /并按Enter,请求到达Web服务器时,实际上会发生什么? 您如何从该请求到在浏览器中查看网络表单,

    鉴于该项目是一个简单的Spring Boot应用程序,您将能够通过Spring5Application运行它。

    Spring Boot默认使用Apache Tomcat。 因此,运行该应用程序,您可能会在日志中看到以下信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    2017-10-16 20:36:11.626  INFO 57414 --- [main]
      o.s.b.w.embedded.tomcat.TomcatWebServer  :
      Tomcat initialized with port(s): 8080 (http)

    2017-10-16 20:36:11.634  INFO 57414 --- [main]
      o.apache.catalina.core.StandardService   :
      Starting service [Tomcat]

    2017-10-16 20:36:11.635  INFO 57414 --- [main]
      org.apache.catalina.core.StandardEngine  :
      Starting Servlet Engine: Apache Tomcat/8.5.23

    由于Tomcat是Servlet容器,因此发送到Tomcat Web服务器的每个HTTP请求自然都由Java Servlet处理。 因此,毫无疑问,Spring Web应用程序的入口点是servlet。

    简而言之,servlet是任何Java Web应用程序的核心组件。 它是低级的,并且不会以特定的编程模式(例如MVC)强加太多。

    HTTP Servlet只能接收HTTP请求,以某种方式处理它,然后将响应发送回去。

    并且,从Servlet 3.0 API开始,您现在可以超越XML配置并开始利用Java配置(有轻微的限制)。

    DispatcherServlet是Spring MVC的心脏

    作为Web应用程序开发人员,我们真正想要做的是抽象出以下乏味和样板任务,并专注于有用的业务逻辑:

  • 将HTTP请求映射到某种处理方法

  • 将HTTP请求数据和标头解析为数据传输对象(DTO)或域对象

  • 模型-视图-控制器交互

  • 从DTO,域对象等生成响应

  • Spring DispatcherServlet正是提供了这一点。 它是Spring Web MVC框架的核心。 该核心组件接收对您的应用程序的所有请求。

    正如您将看到的,DispatcherServlet是非常可扩展的。 例如,它允许您插入不同的现有适配器或新适配器来完成许多任务:

  • 将请求映射到应该处理该请求的类或方法(HandlerMappinginterface的实现)

  • 使用特定的模式来处理请求,例如常规的servlet,更复杂的MVC工作流程或只是POJO bean中的方法(HandlerAdapter接口的实现)

  • 通过名称解析视图,允许您使用不同的模板引擎,XML,XSLT或任何其他视图技术(ViewResolver接口的实现)

  • 通过使用默认的Apache Commons文件上载实现或编写自己的MultipartResolver来解析多部分请求

  • 使用任何LocaleResolver实现来解析语言环境,包括cookie,会话,Accept HTTP标头或任何其他确定用户期望的语言环境的方法

  • HTTP请求的处理

    首先,让我们跟踪对简单HTTP请求的处理,以处理您的控制器层中的方法并返回到浏览器/客户端。

    DispatcherServlet具有很长的继承层次结构。 有必要自上而下地逐一了解这些方面。 请求处理方法将使我们最感兴趣。

    https://lh3.googleusercontent.com/4ahYReme6gkGU8NIU--JzvxgCckXNYzCBa_Fi7Xk6DwqNctyNj0pOB_UPU4Euboy66vfURfovJK8VUgJMTz0ms2yQtFnqEhw9iJnWd_pCCpWN5tNXIYhUFtkCxakO-GTyuKlHoqs

    在标准开发过程中的本地以及远程了解HTTP请求是理解MVC体系结构的关键部分。

    通用Servlet

    GenericServlet是Servlet规范的一部分,不直接关注HTTP。 它定义了service()方法,该方法接收传入的请求并生成响应。

    注意ServletRequest和ServletResponse方法参数如何不与HTTP协议绑定:

    1
    2
    public abstract void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException;

    这是最终对服务器的任何请求(包括简单的GET请求)调用的方法。

    HttpServlet的

    顾名思义,HttpServlet类是HTTP定义的Servlet实现,也由规范定义。

    用更实际的术语来说,HttpServlet是带有service()方法实现的抽象类,该实现按HTTP方法类型拆分请求,并且大致如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();
        if (method.equals(METHOD_GET)) {
            // ...
            doGet(req, resp);
        } else if (method.equals(METHOD_HEAD)) {
            // ...
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            // ...
        }

    HttpServletBean

    接下来,HttpServletBean是层次结构中第一个可识别Spring的类。 它使用从web.xml或WebApplicationInitializer接收到的servlet init-param值注入bean的属性。

    如果是对您的应用程序的请求,则针对那些特定的HTTP请求调用doGet(),doPost()等方法。

    框架Servlet

    FrameworkServlet将Servlet功能与Web应用程序上下文集成在一起,实现了ApplicationContextAware接口。 但是它也能够自行创建Web应用程序上下文。

    如您所见,HttpServletBean超类将init-params注入为bean属性。 因此,如果在servlet的contextClass init-param中提供了上下文类名称,则将创建此类的实例作为应用程序上下文。 否则,将使用默认的XmlWebApplicationContext类。

    由于当今XML配置已经过时,因此Spring Boot默认使用AnnotationConfigWebApplicationContext配置DispatcherServlet。 但是您可以轻松更改它。

    例如,如果您需要使用基于Groovy的应用程序上下文来配置Spring Web MVC应用程序,则可以在web.xml文件中使用以下DispatcherServlet的配置:

    1
    2
    3
    4
    5
        dispatcherServlet
            org.springframework.web.servlet.DispatcherServlet

            contextClass
            org.springframework.web.context.support.GroovyWebApplicationContext

    可以使用WebApplicationInitializer类以更现代的基于Java的方式完成相同的配置。

    DispatcherServlet:统一请求处理

    HttpServlet.service()实现通过HTTP谓词的类型路由请求,在低级servlet的上下文中非常有意义。 但是,在Spring MVC的抽象级别上,方法类型只是可用于将请求映射到其处理程序的参数之一。

    因此,FrameworkServlet类的另一个主要功能是将处理逻辑重新加入到单个processRequest()方法中,该方法又调用doService()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    protected final void doGet(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
        processRequest(request, response);
    }

    @Override
    protected final void doPost(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
        processRequest(request, response);
    }

    // …

    DispatcherServlet:丰富请求

    最后,DispatcherServlet实现了doService()方法。 在这里,它向请求中添加了一些有用的对象,这些对象可能会在处理管道中派上用场:Web应用程序上下文,语言环境解析器,主题解析器,主题源等:

    1
    2
    3
    4
    5
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE,
      getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    此外,doService()方法还准备输入和输出闪存映射。 Flash映射基本上是一种将参数从一个请求传递到紧随其后的另一个请求的模式。 这在重定向过程中可能非常有用(例如在重定向后向用户显示一键式信息消息):

    1
    2
    3
    4
    5
    6
    7
    FlashMap inputFlashMap = this.flashMapManager
      .retrieveAndUpdate(request, response);
    if (inputFlashMap != null) {
        request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE,
          Collections.unmodifiableMap(inputFlashMap));
    }
    request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());

    然后,doService()方法调用负责请求分派的doDispatch()方法。

    DispatcherServlet:分派请求

    dispatch()方法的主要目的是为请求找到合适的处理程序,并向其提供请求/响应参数。 处理程序基本上是任何种类的对象,并且不限于特定的接口。 这也意味着Spring需要为此处理程序找到一个适配器,该适配器知道如何与处理程序"对话"。

    为了找到与请求匹配的处理程序,Spring会通过HandlerMapping接口的注册实现。 有许多不同的实现方式可以满足您的需求。

    SimpleUrlHandlerMapping允许通过URL将请求映射到某个处理Bean。 例如,可以通过将其mappings属性注入类似于以下内容的java.util.Properties实例来进行配置:

    1
    2
    /welcome.html=ticketController
    /show.html=ticketController

    处理程序映射最广泛使用的类可能是RequestMappingHandlerMapping,它将请求映射到@Controller类的@RequestMapping注释方法。 这正是将调度程序与控制器的hello()和login()方法连接的映射。

    请注意,您的Spring感知方法分别以@GetMapping和@PostMapping进行注释。 这些注解又用@RequestMapping元注解标记。

    dispatch()方法还负责其他一些HTTP特定的任务:

  • 在未修改资源的情况下,GET请求的短路处理

  • 将多部分解析器应用于相应的请求

  • 如果处理程序选择异步处理请求,则对请求进行短路处理

  • 处理请求

    现在,Spring确定了请求的处理程序和处理程序的适配器,是时候最终处理请求了。 这是HandlerAdapter.handle()方法的签名。 请务必注意,处理程序可以选择如何处理请求:

  • 自行将数据写入响应对象并返回null

  • 返回要由DispatcherServlet呈现的ModelAndView对象

  • 1
    2
    3
    4
    @Nullable
    ModelAndView handle(HttpServletRequest request,
                        HttpServletResponse response,
                        Object handler) throws Exception;

    提供了几种类型的处理程序。 这是SimpleControllerHandlerAdapter处理Spring MVC控制器实例的方式(不要将其与@Controller注释的POJO混淆)。

    请注意,控制器处理程序如何返回ModelAndView对象,而不如何单独呈现视图:

    1
    2
    3
    4
    public ModelAndView handle(HttpServletRequest request,
      HttpServletResponse response, Object handler) throws Exception {
        return ((Controller) handler).handleRequest(request, response);
    }

    第二个是SimpleServletHandlerAdapter,它将常规Servlet用作请求处理程序。

    Servlet对ModelAndView一无所知,只是简单地自己处理请求,将结果呈现到响应对象中。 因此,此适配器仅返回null而不是ModelAndView:

    1
    2
    3
    4
    5
    public ModelAndView handle(HttpServletRequest request,
      HttpServletResponse response, Object handler) throws Exception {
        ((Servlet) handler).service(request, response);
        return null;
    }

    在您的情况下,控制器是带有几个@RequestMapping批注的POJO,因此任何处理程序基本上都是包装在HandlerMethod实例中的此类的方法。 为了适应这种处理程序类型,Spring使用RequestMappingHandlerAdapter类。

    处理参数和处理程序方法的返回值

    请注意,控制器方法通常不采用HttpServletRequest和HttpServletResponsearguments,而是接收和返回许多不同类型的数据,例如域对象,路径参数等。

    另外,请注意,您无需从控制器方法返回ModelAndView实例。 您可以返回视图名称,或将被转换为JSON响应的ResponseEntity或POJO。

    RequestMappingHandlerAdapter确保从HttpServletRequest解析方法的参数。 同样,它从方法的返回值创建ModelAndView对象。

    RequestMappingHandlerAdapter中有一段重要的代码可确保发生所有转换魔术:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ServletInvocableHandlerMethod invocableMethod
      = createInvocableHandlerMethod(handlerMethod);
    if (this.argumentResolvers != null) {
        invocableMethod.setHandlerMethodArgumentResolvers(
          this.argumentResolvers);
    }
    if (this.returnValueHandlers != null) {
        invocableMethod.setHandlerMethodReturnValueHandlers(
          this.returnValueHandlers);
    }

    argumentsResolvers对象是不同HandlerMethodArgumentResolver实例的组合。

    有30多种不同的参数解析器实现。 它们允许从请求中提取任何种类的信息,并将其作为方法参数提供。 这包括URL路径变量,请求主体参数,请求标头,cookie,会话数据等。

    returnValueHandlers对象是HandlerMethodReturnValueHandler对象的组合。 还有许多不同的值处理程序,可以处理方法的结果以创建适配器期望的ModelAndViewobject。

    例如,当您从hello()方法返回一个字符串时,ViewNameMethodReturnValueHandler处理该值。 但是,当您从login()方法返回就绪的ModelAndView时,Spring将使用ModelAndViewMethodReturnValueHandler。

    渲染视图

    到目前为止,Spring已经处理了HTTP请求并收到了ModelAndView对象,因此它必须呈现用户将在浏览器中看到的HTML页面。 它基于模型和封装在ModelAndView对象中的选定视图来执行此操作。

    还要注意,您可以呈现JSON对象,XML或可以通过HTTP协议传输的任何其他数据格式。 我们将在此处即将进行的REST重点部分中详细介绍这一点。

    让我们回到DispatcherServlet。 render()方法首先使用提供的LocaleResolver实例设置响应区域设置。 假设您的现代浏览器正确设置了Accept标头,并且默认情况下使用了AcceptHeaderLocaleResolver。

    在渲染期间,ModelAndView对象可能已经包含对选定视图的引用,或者仅包含视图名称,或者如果控制器依赖于默认视图,则根本不包含任何内容。

    由于hello()和login()方法都将所需的视图指定为String名称,因此必须使用此名称进行查找。 因此,这是viewResolvers列表起作用的地方:

    1
    2
    3
    4
    5
    6
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            return view;
        }
    }

    这是ViewResolver实例的列表,包括thymeleaf-spring5集成库提供的ThymeleafViewResolver。 该解析器知道在哪里搜索视图,并提供相应的视图实例。

    调用视图的render()方法后,Spring最终通过将HTML页面发送到用户的浏览器来完成请求处理:

    REST支持

    除了典型的MVC场景之外,我们还可以使用该框架来创建REST Web服务。

    简而言之,您可以接受资源作为输入,将POJO指定为方法参数,并使用@RequestBody对其进行注释。 您还可以使用@ResponseBody注释方法本身,以指定其结果必须直接转换为HTTP响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.ResponseBody;

    @ResponseBody
    @PostMapping("/message")
    public MyOutputResource sendMessage(
      @RequestBody MyInputResource inputResource) {

        return new MyOutputResource("Received:"
          + inputResource.getRequestMessage());
    }

    由于Spring MVC的可扩展性,这也是可能的。

    为了将内部DTO编组为REST表示,该框架利用HttpMessageConverter基础结构。 例如,实现之一是MappingJackson2HttpMessageConverter,它能够使用Jackson库将模型对象与JSON相互转换。

    为了进一步简化REST API的创建,Spring引入了@RestController批注。 在默认情况下假设@ResponseBody语义很方便,并且避免在每个REST控制器上显式设置它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class RestfulWebServiceController {

        @GetMapping("/message")
        public MyOutputResource getMessage() {
            return new MyOutputResource("Hello!");
        }
    }

    结论

    在本文中,您已经详细介绍了Spring MVC框架中的请求处理。 您已经看到了框架的不同扩展如何协同工作,以提供所有魔力,并使您不必处理HTTP协议的难处。