深入理解SpringMVC核心实现思想

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

前言

如果要用一句话概括SpringMVC实现思想,那就是谁能处理,我交给谁。

这里不讨论Tomcat,因为在SpringMVC,还没有内置Tomcat,分析他的流程暂时也用不到,但是在下一章我们分析SpringBoot时,需要对Tomcat核心有所了解。

那么对"分析他的流程暂时也用不到"这句话,可能有所疑问,不使用Tomcat,如何测试?

我们知道SpringMVC的核心之一是DispatcherServlet,在Tomcat内部会调用他的srevice方法开始处理请求,那么没有Tomcat,就不能做测试了吗?当然可以,这就是上一章我们说的通过MockHttpServletRequest构造一个请求,手动调用DispatcherServlet的service方法。

@org.springframework.stereotype.Controller
public static class UserController {
   @GetMapping("getUser")
   public String getUser(Model model) {
      return "user";
   }
}

@Test
public void test() throws Exception {
   DispatcherServlet dispatcherServlet = initServlet(UserController.class);
   MockHttpServletRequest request = new MockHttpServletRequest("GET", "/getUser");
   MockHttpServletResponse response = new MockHttpServletResponse();
   dispatcherServlet.service(request, response);
}

private DispatcherServlet initServlet(final Class<?> controllerClass) throws ServletException {
   DispatcherServlet dispatcherServlet = new DispatcherServlet() {
      @Override
      protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
         GenericWebApplicationContext wac = new GenericWebApplicationContext();
         wac.registerBeanDefinition("controller", new RootBeanDefinition(controllerClass));
         wac.refresh();
         return wac;
      }
   };
   dispatcherServlet.init(new MockServletConfig());
   return dispatcherServlet;
}
复制代码

谁能处理,我就交给谁

在此之前我们先理解开头那句话,那就是谁能处理,我交给谁,SpringMVC的核心非常简单,因为人家写的优雅,阅读起来也让人赞叹不已,一个请求每到一个时机时他会不断进行询问:"谁能处理我,谁能处理我",如果有人,那就交给他,比如到达参数解析时机,会遍历方法上所有参数,把参数信息单个拿出来询问:"谁能处理我,谁能处理我",如果有可以处理的他的对象,那就交给他,同样在返回视图的时候,也是同样的道理。

那么询问哪里?

其实就是遍历集合,这个集合中的每个类只会处理他感兴趣的东西,比如解析参数时,这个类就只会处理标有@RequestParam注解的参数,其余他都会放过。

在这里我们把遍历集合称为询问,把对象处理的结果称为应答。

而询问和应答有两种办法,一是规定一个接口,接口必须有两个方法,方式一用于标识这个类能处理哪些东西,方法二是这个类做具体的逻辑。

举个例子,在招聘时候,公司可能要求应聘者必须会说英语,那么所有人都实现了Speak这个接口,如下。

interface Speak{
    Spring getType();
    
    void handler();
}
复制代码

getType()就返回这个人会什么语言,"比如英语",那么表示他能处理有英语的这种情况,handler()方法就是做具体事,不论他是交谈还是写文章。

那么List<Speak>中就保存所有人,在询问时,需要遍历这个集合,挑选出getType()返回"英语"的对象,然后调用他的handler()即可。

第二种方法没有getType(),只有handler(),但是handler()必须有个返回值,当他不能处理的时候,就返回null,能处理的时候返回一个其他对象,到时候我们只需要调用他返回的对象即可。

这两种方法则是SpringMVC中经常用的。

RequestMappingHandlerMapping

要了解请求是如何到达一个标有@GetMapping注解方法的,需要了解RequestMappingHandlerMapping,他是核心之一,他的作用是收集RequestMappingInfo,RequestMappingHandlerMapping默认并不在Spring容器中,但他大部分又都在容器中,分情况而定,先说默认情况,默认会通过createBean创建,createBean方法可以参考以往的Spring核心文章,这个方法用于创建对象,会走Spring内部所有创建对象的流程,关键点还是在他实现的InitializingBean接口下。

InitializingBean接口有个afterPropertiesSet()方法,在Spring把对象创建完成后,就会调用他,而这个方法下会先在现有的bean集合中收集所有标有@Controller注解的类,再遍历其中所有标有@RequestMapping注解的方法,也包括@GetMapping这些,因为@GetMapping上面标有@RequestMapping(method = RequestMethod.GET)。找出这些后把这个方法封装为RequestMappingInfo,所以里面则是这个方法和注解上的信息,也就是一个RequestMappingInfo对应一个标有@RequestMapping注解的方法。

有了RequestMappingInfo,我们才能把请求映射到具体方法下。

所以DispatcherServlet也一定会持有所有RequestMappingInfo,不然无法做映射。

DispatcherServlet

使用Java做Web开发一般都离不开Servlet,而SpringMVC也离不开DispatcherServlet,他继承Servlet,用来做请求映射,就是把请求映射到对应方法上,通常他在Tomcat中url地址都是/

doDispatch

从DispatcherServlet的doDispatch开始看,他是从service()下一路调用过来的,首要任务就是获取能处理这个请求的HandlerMapping

mappedHandler = getHandler(processedRequest);
复制代码

注意了,这里就是精华,上面我们说的RequestMappingHandlerMapping就是HandlerMapping的一种,他能处理的请求是从@Controller类下收集到的,也就是发起的请求地址和请求方法,正好是由@GetMapping或其他定义的才行。

但是SpringMVC怎么可能对于一个接口,只能通过@Controller、@GetMapping这样定义,他还有其他办法,其他办法定义的接口,就由其他HandlerMapping来负责,有一种不常用的叫BeanNameUrlHandlerMapping,他是根据名字来映射的,如果Spring容器中出现了一种名称以/开头的bean,并且这个bean实现了Controller接口,那么这个请求如果和bean名称一样,就交给他处理。

那么这个时候SpringMVC就开始询问了,谁能处理请求?

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
   if (this.handlerMappings != null) {
      for (HandlerMapping mapping : this.handlerMappings) {
         HandlerExecutionChain handler = mapping.getHandler(request);
         if (handler != null) {
            return handler;
         }
      }
   }
   return null;
}
复制代码

还记得上面我们说的询问应答的两种模式吗?这里Spring采用了第二种,当mapping.getHandler不能处理的时候他会返回null,能处理的时候,他会返回HandlerExecutionChain。

我们从RequestMappingHandlerMapping的getHandler开始看,因为他是最常用的,但关键是SpringMVC太抽象了,跳来跳去,所以就不看源码了,我们直接说思想。

首先就是更具请求信息匹配对应的方法,但这也比较麻烦,虽然现在已经有了所有RequestMappingInfo,但是别忘了定义路径的时候还可以加变量,这就导致匹配的工作比较复杂,但终究就是将匹配到的HandlerMethod返回,HandlerMethod是可以处理这个请求的方法。

在下面一步,开始找HandlerAdapter适配器,这个非常精华,我们知道进入标有@GetMapping注解的方法前,还有一步要做,就是根据方法所需要的参数从http请求参数中找,但并不是所有的请求都需要解析参数,如果你使用过Controller接口,那就知道这种方法定义的API,并不需要SpringMVC内部给你解析参数,他直接传你一个HttpServletRequest,你用不用参数自己决定。

那面对这种情况,SpringMVC就提供了一个HandlerAdapter,用来对请求进行其他工作,最终调用目标方法。

这也是询问的过程,SpringMVC会询问所有HandlerAdapter,谁能处理这个请求,我就交给谁。

这里的询问应答是采用的上面所说的第一种方法,也就是HandlerAdapter会提供两个方法,一个方法用来获取他能不能处理这个请求,另一个则会具体处理请求。

public interface HandlerAdapter {

   boolean supports(Object handler);

   @Nullable
   ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

}
复制代码

我们看他的一个实现类SimpleControllerHandlerAdapter,他是处理Controller接口的,具体处理的方法则是直接调用他的handleRequest,非常简单,毕竟叫SimpleController。

public class SimpleControllerHandlerAdapter implements HandlerAdapter {
   @Override
   public boolean supports(Object handler) {
      return (handler instanceof Controller);
   }
   @Override
   @Nullable
   public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
         throws Exception {
      return ((Controller) handler).handleRequest(request, response);
   }
复制代码

在来看今天的主角RequestMappingHandlerAdapter,他所支持的类型条件之一必须实现HandlerMethod。

@Override
public final boolean supports(Object handler) {
   return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
复制代码

HandlerMethod是不是很熟悉?在上面说过,标有@RequestMapping的方法,就可以理解为一个HandlerMethod的实例。

那么下一步就要进入RequestMappingHandlerAdapter的handle开始处理请求了。

但是还有一步,那就是拦截器,在正式处理请求前,会遍历所有拦截器,如果有个拦截器返回false,那么这个请求就不会往下走了,虽然默认有两个,但是那两个都不会拦截任何请求,拦截器的收集通常在WebMvcConfigurationSupport下,如果了解WebMvcConfigurationSupport源码的人可以发现,他内部有好多@Bean,其中之一是RequestMappingHandlerMapping,那就有一个问题,上面我们不是说RequestMappingHandlerMapping是用过createBean创建的吗?其实也分两种情况,第一是容器中没有WebMvcConfigurationSupport的时候,他会通过createBean创建,但是如果已经有了,那么就直接拿来用。

而我们通过继承的方式重写了addInterceptors,就可以添加自己的拦截器,拦截器需要实现HandlerInterceptor接口,但内部还是会把他封装为MappedInterceptor,因为一个拦截器通常有三个参数,拦截路径、排除路径、拦截器接口本身。

如果没有任何拦截器拦截本请求,那么就会进入Adapter中,还是拿RequestMappingHandlerAdapter来说,他适配的任务是参数转换,同样的道理,SpringMVC有众多的参数转换器,有的是处理@RequestParam的,有的是处理参数是ServletRequest类型的,在初学SpringMVC时,你可能有这样的感觉,不论参数怎么写,好像SpringMVC都能给你找到,你需要url中的参数,那就写@RequestParam,你需要Model,那就是Model,你需要参数是Map对象,那就写Map,感觉无所不能,那是因为SpringMVC给我们提供了30多个参数转换器,这30多个转换器覆盖了绝大部分,也可以说所有,如果你觉得还不行,SpringMVC强大的扩展能力也允许你自定义。

那剩下就是询问了,谁可以处理这个参数?

这个询问应答也是使用上述第一种方法,提供一个接口,如下,supportsParameter用来判断这个参数支不支持解析,resolveArgument则是具体参数转换,可以看到resolveArgument的参数已经涵盖了所有信息,能用的用不上的都有。

public interface HandlerMethodArgumentResolver {
   boolean supportsParameter(MethodParameter parameter);

   @Nullable
   Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
复制代码

拿RequestParamMethodArgumentResolver来说,他所支持的类型则是参数上有@RequestParam注解。

public boolean supportsParameter(MethodParameter parameter) {
   if (parameter.hasParameterAnnotation(RequestParam.class)) {
      if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
         RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
         return (requestParam != null && StringUtils.hasText(requestParam.name()));
      }
      else {
         return true;
      }
   }
   else {
      if (parameter.hasParameterAnnotation(RequestPart.class)) {
         return false;
      }
      parameter = parameter.nestedIfOptional();
      if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
         return true;
      }
      else if (this.useDefaultResolution) {
         return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
      }
      else {
         return false;
      }
   }
}
复制代码

具体解析过程也是通过原生的getParameterValues方法,比较简单。

接下来进行结果处理,同样在初学SpringMVC的时候,不管结果怎么写,SpringMVC都能给你返回,这也就是HandlerMethodReturnValueHandler的原因,有的是处理Map的,有的是处理Stream的,还有的是处理HttpHeaders的,参数转换器和结果转换器应该是我们比较用的多的了。

比如方法上标记上@ResponseBody,那么最终就会使用RequestResponseBodyMethodProcessor,里面还会设计到一个HttpMessageConverter,用来做值转换,这里的值是方法返回的结果,转换用来把结果转换成json格式或者xml,SpringMVC也提供了大量的HttpMessageConverter,也可以自定义。

返回视图

在这里就结束了,但还有最后一步,就是根据Model生成html返回,但这不是必须的,如果没有加入@ResponseBody,那么这时候才会进入这一步。

但是这时候你会发现,这一步是在请求完成后进入的,也就是下面 这个方法。

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
复制代码

也就是说,不管有没有加入@ResponseBody,这个方法都会进入,那么肯定有一个标识,在前面已经标识过了,意思是我已经处理了,并且返回给客户端了,后面视图解析的工作请不要处理。

事实也就是如此,看下面这句话,注释中也很明确,意思是handler处理器返回是否要渲染这个视图?如果要渲染那么mv变量肯定不为空,(mv是ModelAndView)

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
   render(mv, request, response);
   if (errorView) {
      WebUtils.clearErrorRequestAttributes(request);
   }
}
复制代码

但这个还不是主要的,主要的是下面这个,位于ModelAndViewContainer下,ModelAndViewContainer会伴随着所有请求过程,在RequestResponseBodyMethodProcessor下,表示这个方法标有@ResponseBody,那么在处理途中,把下面requestHandled变量设置为true,后续视图解析的时候就直接跳过了。

/**
 * Whether the request has been handled fully within the handler.
 */
public boolean isRequestHandled() {
   return this.requestHandled;
}
复制代码

具体在RequestMappingHandlerAdapter的getModelAndView下。

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
      ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
   modelFactory.updateModel(webRequest, mavContainer);
   //如果已经处理了,则跳过
   if (mavContainer.isRequestHandled()) {
      return null;
   }
   ModelMap model = mavContainer.getModel();
   ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
   return mav;
}
复制代码

我们可以自定义视图解析,只要继承ViewResolver,放在容器中就行。

public  class ViewTest implements ViewResolver {
   @Override
   public View resolveViewName(String viewName, Locale locale) throws Exception {
      return new View() {
         @Override
         public String getContentType() {
            return "text/html";
         }
         @Override
         public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().append("<h1>111</h1>");
         }
      };
   }
}
复制代码

可以有多个ViewResolver,但是SpringMVC在内部遍历的时候,只要resolveViewName不返回null,那么就调用View的render方法开始渲染,在SpringBoot中也可以这样使用。

ViewResolver解析器的作用就是把Model中的属性都拿出来,根据标签渲染成html,这里的标签比如jsp中的<c:forEach>、Thymeleaf中的th:each。

猜你喜欢

转载自juejin.im/post/7066250146756952077