Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程

Posted 说好不能打脸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程相关的知识,希望对你有一定的参考价值。

1、整体调用过程

本文承接本专题上一篇文章《Spring/Boot/Cloud系列知识:HttpMessageConverter转换器使用方式》,在上一篇文章中讲解了HttpMessageConverter转换器的使用方式。本文透过上文讲解的使用方式,重点介绍SpringMVC是如何将HTTP/HTTPS请求映射成SpringMVC Controller层的具体方法,并完成调用的(本文基于的Spring版本为5.1.X,不同的版本对于相关实现有细微差异)。

1.1、关于Filter部分的说明

为了专注于介绍本文的核心内容,本文不涉及Servlet中对Filter过滤器的管理和执行过程的讨论,关于这个过程读者只需要知晓几个关键事实即可:

  • Filter过滤器并不是Spring MVC的知识点,而是Servlet容器的知识点。Spring MVC自身也有一套以GenericFilterBean为代表的Filter过滤器体系,后者实现了Servlet容器的Filter接口并将Spring中的上下文、Spring MVC上下文等关键信息封装起来便于开发人员使用。

  • 在一次HTTTP请求中,所有需要执行的Filter过滤器将形成一个调用链,并由ApplicationFilterChain类进行调用控制。这是一个典型的责任链模式,但关键点在于整个调用链的控制并不是由“循环”思路完成,而是由“递归”思路完成。这种责任链的管理方式,实际上是所有类似的生产环境中推荐的责任链模式实现方式——因为递归思路可以将责任链中节点与节点的上下文关系紧密传导,如下图:

  • 所有Filter调用完成后,ApplicationFilterChain将会把处理过程交给servlet进行后续处理,实现逻辑很简单:既是在ApplicationFilterChain中记录了一个servlet属性,该属性是一个javax.servlet.Servlet类型(interface)的属性。在正确完成所有Filter的处理后,调用这个属性的service(ServletRequest , ServletResponse)方法,关键代码片段如下所示:

public final class ApplicationFilterChain implements FilterChain 
  // ......
  private static final Class<?>[] classTypeUsedInService = new Class[]ServletRequest.class, ServletResponse.class;
  // ......
  private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException 
    // ......
    // Use potentially wrapped request from this point
    if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) 
      && Globals.IS_SECURITY_ENABLED ) 
      final ServletRequest req = request;
      final ServletResponse res = response;
      Principal principal = ((HttpServletRequest) req).getUserPrincipal();
      Object[] args = new Object[]req, res;
      // 如果是Web容器设置了安全模式,则通过这种方式完成service方法的调用
      SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
     else 
      // 普通情况下就直接调用servlet属性的service方法
      servlet.service(request, response);
    
  
  // ......

1.2、整个HTTP请求过程中需要考虑的需求问题

在ApplicationFilterChain完成上述处理过程后,剩下的工作就交由servlet来处理了。所有的servlet都必须实现javax.servlet.Servlet接口,Spring MVC为了遵循这个规则,设计了一个org.springframework.web.servlet.DispatcherServlet类,ApplicationFilterChain对象的servlet属性就引用了DispatcherServlet类的实例:

public final class ApplicationFilterChain implements FilterChain 
  // ......
  /**
   * 这个属性的引用就是DispatcherServlet类的实例
   * The servlet instance to be executed by this chain.
   */
  private Servlet servlet = null;
  // ......
  /**
   * Set the servlet that will be executed at the end of this chain.
   * @param servlet The Wrapper for the servlet to be executed
   */
  void setServlet(Servlet servlet) 
      this.servlet = servlet;
  
  // ......

DispatcherServlet类的实例的作用就是要Spring MVC组件完成与本次HTTP调用相匹配的Controller层方法,那么这个过程主要就包括几个设计需求需要着重解决

1.2.1、寻找匹配的Controller处理方法:

这个步骤需要解决的问题是:如何依据当前HTTP请求的URL信息、Head信息、Body信息等找到匹配的Controller层方法(或者匹配某种特殊的处理方式)。这个过程需要考虑的问题很多,例如同一个URL信息,根据携带信息的位置不一样匹配的controller方法可能是不一样的,并且某一个URL信息可能不对应任何的Controller层方法,而是一些特别的处理方式。例如以下示例中的多个URL信息,可能对应一个相同的或者多个不同的Controller层处理方法:

// 以下三个URL请求,可能使用同一个controller层方法进行处理
GET http://xxxxx/AAA/BBB/CCC
POST http://xxxxx/AAA/BBB/CCC 
POST http://xxxxx/AAA/BBB/CCC?param1=value1&param2=value2

// 以下两个URL请求,可能使用不同的controller层方法进行处理
GET http://aaaa/XXXX/YYYY
GET http://aaaa/XXXX/YYYY?param1=value1&param2=value2

# 也就是说,根据URL本身根本区分不出来controller方法的匹配结果

另外,由于Spring MVC组件的发展过程影响,controller层方法的定义方式也有很多种。例如现在最常使用的注解方式,开发人员可以使用“@RestController”注解、“@Controller”注解、“@PostMapping”注解、“@RequestMapping”等注解的方式定义Controller层方法;Spring MVC还可以使用单纯的XML配置文件定义controller层方法 ,例如一下就是一种XML的配置方式:

<!-- ...... -->
<bean name="userController"  class="cn.raffaello.controller.UserController">
  <property name="userIService" ref="userIService"></property>
</bean>
<!-- ...... -->

Spring MVC还提供了一个基于XML的简单配置方式,提供给开发人员使用XML配置的方式为固定的URL信息指定专门的Controller层处理方法,使用方式如下所示:

<!-- ...... -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  <property name="mappings">
    <value>
    /homepage=simpleController
    /xxxxxx=xxxxxxController
    /yyyyyy=yyyyyyController
    /zzzzzz=zzzzzzController
    ......还可以添加很多
    </value>
  </property>
</bean>
<!-- ...... -->

最后,一个URL信息也可能对应一些需要特别单独处理的场景,例如URL可能是一个欢迎页面,不需要带入开发人员书写的任何Controller方法;URL信息还可能是一些放置在Web容器中的静态资源,例如给定的是“/xxx/yyy/zzz.png”这样的URL信息。

1.2.2、为方法中的各个入参绑定实际的参数值:

为URL请求信息匹配到正确的Controller方法后,第二个需要解决的问题是:针对匹配到的方法的各种入参进行参数值转换和传入。这个场景下Spring MVC需要为多种参数绑定方式提供支持(这里只描述问题场景,并不是介绍这些注解的使用细节):

  • 开发人员可以在Controller层使用@RequestParam注解进行参数映射
    使用@RequestParam注解,可以从URL信息的QUERY部分接收参数值,使用方法如下所示:

    @GetMapping("/findSomething")
    public void findSomething(@RequestParam("some") String something , 
                              @RequestParam("other") String other) 
      // ......
    
    
  • 开发人员可以在Controller层使用@RequestAttribute注解进行参数映射

    使用@RequestAttribute注解,可以从当前请求的servlet容器的上下文中取得数据,使用方法如下所示:

    @PostMapping("/doSomeOtherthing")
    public String doSomeOtherthing(@RequestAttribute("org.springframework.session.SessionRepository.CURRENT_SESSION") HttpSession session) 
      // ......
    
    
  • 开发人员可以在Controller层使用@PathVariable注解进行参数映射

    使用@PathVariable注解,可以将URL信息中的某个地址部分映射成参数值,使用方法如下所示:

    @GetMapping("/find/param1/param2")
    public ModelAndView find(@PathVariable("param1") String param1 , @PathVariable("param2") String param2) 
      // ......
    
    
  • 开发人员可以在Controller层使用@RequestBody注解进行参数映射

    使用@RequestBody注解,可以从当前请求的Body部分映射数据,使用方法如下所示:

    @PostMapping("/doSomeOtherthing")
    public String doSomeOtherthing(@RequestBody YourBusiness yourBusiness) 
      // ......
    
    
  • 开发人员可以在Controller层使用@RequestHeader注解进行参数映射

    使用@RequestHeader注解,可以从当前请求的Head部分映射数据,使用方法如下所示:

    @GetMapping("/findSomethingFromHead")
    public void findSomethingFromHead(@RequestHeader("some") String something , 
                                      @RequestHeader("other") String other) 
      // ......
    
    
  • 开发人员还可以在不使用任何注解的情况下要求进行参数映射

    在这种情况下,Spring MVC可以根据参数名自行映射URL信息的Head部分信息,使用方法如下所示:

    @GetMapping("/findParams")
    public void findParams(String param1 , String param2) 
      // ......
    
    

    这还只是其中一部分常见的使用场景,Spring MVC甚至支持在不使用注解的情况下,为指定对象的指定属性进行参数映射(赋值)。很显然Spring MVC需要一种技术方案来支持这些参数映射的场景,甚至还应该允许开发人员基于这个技术方案自行定义参数值的接收方式。

1.2.3、进行实际的方法调用,并处理返回值:

找到了正确的Controller层方法并完成了对应参数值的转换后,Controller层方法就可以被正式调用了。无论Controller层方法的调用过程是否成功(业务逻辑的处理是很可能出现错误的),HTTP请求的调用结果信息,如何转换成适当的HTTP响应信息返回给调用者,又成为一个新的需要解决的问题。要知道Controller方法根据业务处理要求,会有很多形式的返回值:

  • Controller层方法没有返回值的情况:

    开发人员完全可以定义一个没有任何返回值的Controller方法,如下所示:

    // 该Controller层方法没有任何返回值
    @PostMapping("/findx")
    public void findx(YourBusiness queryBusiness) 
      // ......
    
    

    但是方法没有任何返回值(或者返回值为void)不代表开发人员放任处理过程返回“白页”。Spring MVC组件在这种情况下需要支持一种“默认”值的返回要求,例如帮助所有返回值为void的Controller层方法一段统一的、默认的json信息:

  • 使用ModelAndView对象进行调用信息返回:

    Controller层方法可以返回一个ModelAndView对象,该对象帮助Spring MVC组件描述了本次HTTP请求处理完成后需要向调用者呈现的视图信息和模型信息,例如一个JSP页面资源:

    @GetMapping("/find/aaaa/bbbb")
    public ModelAndView find() 
      return new ModelAndView("/uuuu");
    
    

    以上代码中实例化的ModelAndView对象,描述了一个JSP页面资源的名称。当然JSP资源在当下流行的开发模式下已经不经常使用了,但这种返回值的场景仍然是Spring MVC组件组要支持的。

  • 使用自定义对象进行调用信息返回:

    开发人员可以自行定义一个类作为Controller层方法的返回信息:

    // 该Controller层方法
    @PostMapping("/createYourBusiness")
    public DynamicTaskSchedulerVo createYourBusiness() 
      // ......
    
    

    Spring MVC一旦发现这样的情况,就应该支持将这些自定义的返回对象转换为适当的能够被HTTP响应携带的信息。以上内容所描述的Spring MVC组件需要支持的返回值场景,只是其中一部分场景,实际上需要考虑的场景还有很多,例如当Controller层方法处理过程出现异常时,进行影响信息返回。

以上描述中本文详细阐述了Spring MVC为了支持一次完成的HTTP请求操作,需要考虑的主要问题以及这些问题的示例情况。包括,如何找到HTTP请求对应的处理方式、如何为匹配的Controller层方法进行参数转换和正式调用、如何处理Controller层方法的各种返回值,等等。看来Spring MVC组件确实需要多种完善的设计方式来解决这些问题,下一篇文章将为读者进行详细的讲解。

(接下文)

以上是关于Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程的主要内容,如果未能解决你的问题,请参考以下文章

Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)

Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)

Spring/Boot/Cloud系列知识:SpringMVC 传参详解(下)

Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程

Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程

Spring/Boot/Cloud系列知识:SpringMVC进行HTTP信息接收和发送的过程