写点什么

聊聊 SpringMVC 是如何工作的?

  • 2023-01-13
    湖南
  • 本文字数:8049 字

    阅读完需:约 26 分钟

聊聊  SpringMVC  是如何工作的?

SpringMVC 的作用毋庸置疑,虽然我们现在都是用 SpringBoot,但是 SpringBoot 中仍然是在使用 SpringMVC 来处理请求。


我们在使用 SpringMVC 时,传统的方式是通过定义 web.xml,比如:


<web-app>


<servlet>  <servlet-name>app</servlet-name>  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  <init-param>    <param-name>contextConfigLocation</param-name>    <param-value>/WEB-INF/spring.xml</param-value>  </init-param>  <load-on-startup>1</load-on-startup></servlet>
<servlet-mapping> <servlet-name>app</servlet-name> <url-pattern>/app/*</url-pattern></servlet-mapping>
复制代码


</web-app>复制代码我们只要定义这样的一个 web.xml,然后启动 Tomcat,那么我们就能正常使用 SpringMVC 了。


SpringMVC 中,最为核心的就是 DispatcherServlet,在启动 Tomcat 的过程中:


Tomcat 会先创建 DispatcherServlet 对象然后调用 DispatcherServlet 对象的 init()而在 init()方法中,会创建一个 Spring 容器,并且添加一个 ContextRefreshListener 监听器,该监听器会监听 ContextRefreshedEvent 事件(Spring 容器启动完成后就会发布这个事件),也就是说 Spring 容器启动完成后,就会执行 ContextRefreshListener 中的 onApplicationEvent()方法,从而最终会执行 DispatcherServlet 中的 initStrategies(),这个方法中会初始化更多内容:


protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);


initHandlerMappings(context);initHandlerAdapters(context);
initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);
复制代码


}复制代码其中最为核心的就是 HandlerMapping 和 HandlerAdapter。


什么是 Handler?Handler 表示请求处理器,在 SpringMVC 中有四种 Handler:


实现了 Controller 接口的 Bean 对象实现了 HttpRequestHandler 接口的 Bean 对象添加了 @RequestMapping 注解的方法一个 HandlerFunction 对象比如实现了 Controller 接口的 Bean 对象:


@Component("/test")public class HoellerBeanNameController implements Controller {


@Overridepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {  System.out.println("Hoeller");  return new ModelAndView();}
复制代码


}复制代码实现了 HttpRequestHandler 接口的 Bean 对象:


@Component("/test")public class HoellerBeanNameController implements HttpRequestHandler {


@Overridepublic void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  System.out.println("Hoeller");}
复制代码


}复制代码添加了 @RequestMapping 注解的方法:


@RequestMapping@Componentpublic class HoellerController {


@Autowiredprivate HoellerService hoellerService;
@RequestMapping(method = RequestMethod.GET, path = "/test")@ResponseBodypublic String test(String username) { return "Hoeller";}
复制代码


}复制代码一个 HandlerFunction 对象(以下代码中有两个):


@ComponentScan("com.hoeller")@Configurationpublic class AppConfig {


@Beanpublic RouterFunction<ServerResponse> person() {  return route()      .GET("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello GET"))      .POST("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello POST"))      .build();}
复制代码


}复制代码什么是 HandlerMapping?HandlerMapping 负责去寻找 Handler,并且保存路径和 Handler 之间的映射关系。


因为有不同类型的 Handler,所以在 SpringMVC 中会由不同的 HandlerMapping 来负责寻找 Handler,比如:


BeanNameUrlHandlerMapping:负责 Controller 接口和 HttpRequestHandler 接口 RequestMappingHandlerMapping:负责 @RequestMapping 的方法 RouterFunctionMapping:负责 RouterFunction 以及其中的 HandlerFunctionBeanNameUrlHandlerMapping 的寻找流程:


找出 Spring 容器中所有的 beanName 判断 beanName 是不是以“/”开头如果是,则把它当作一个 Handler,并把 beanName 作为 key,bean 对象作为 value 存入 handlerMap 中 handlerMap 就是一个 MapRequestMappingHandlerMapping 的寻找流程:


找出 Spring 容器中所有 beanType 判断 beanType 是不是有 @Controller 注解,或者是不是有 @RequestMapping 注解判断成功则继续找 beanType 中加了 @RequestMapping 的 Method 并解析 @RequestMapping 中的内容,比如 method、path,封装为一个 RequestMappingInfo 对象最后把 RequestMappingInfo 对象做为 key,Method 对象封装为 HandlerMethod 对象后作为 value,存入 registry 中 registry 就是一个 MapRouterFunctionMapping 的寻找流程会有些区别,但是大体是差不多的,相当于是一个 path 对应一个 HandlerFunction。


各个 HandlerMapping 除开负责寻找 Handler 并记录映射关系之外,自然还需要根据请求路径找到对应的 Handler,在源码中这三个 HandlerMapping 有一个共同的父类 AbstractHandlerMapping


AbstractHandlerMapping 实现了 HandlerMapping 接口,并实现了 getHandler(HttpServletRequest request)方法。


AbstractHandlerMapping 会负责调用子类的 getHandlerInternal(HttpServletRequest request)方法从而找到请求对应的 Handler,然后 AbstractHandlerMapping 负责将 Handler 和应用中所配置的 HandlerInterceptor 整合成为一个 HandlerExecutionChain 对象。


所以寻找 Handler 的源码实现在各个 HandlerMapping 子类中的 getHandlerInternal()中,根据请求路径找到 Handler 的过程并不复杂,因为路径和 Handler 的映射关系已经存在 Map 中了。


比较困难的点在于,当 DispatcherServlet 接收到一个请求时,该利用哪个 HandlerMapping 来寻找 Handler 呢?看源码:


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;}复制代码很简单,就是遍历,找到就返回,默认顺序为:


所以 BeanNameUrlHandlerMapping 的优先级最高,比如:


@Component("/test")public class HoellerBeanNameController implements Controller {


@Overridepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {  System.out.println("Hello Hoeller");  return new ModelAndView();}
复制代码


}复制代码 @RequestMapping(method = RequestMethod.GET, path = "/test")@ResponseBodypublic String test(String username) {return "Hi Hoeller";}复制代码请求路径都是/test,但是最终是 Controller 接口的会生效。


什么是 HandlerAdapter?找到了 Handler 之后,接下来就该去执行了,比如执行下面这个 test()


@RequestMapping(method = RequestMethod.GET, path = "/test")@ResponseBodypublic String test(String username) {return "Hoeller";}复制代码但是由于有不同种类的 Handler,所以执行方式是不一样的,再来总结一下 Handler 的类型:


实现了 Controller 接口的 Bean 对象,执行的是 Bean 对象中的 handleRequest()实现了 HttpRequestHandler 接口的 Bean 对象,执行的是 Bean 对象中的 handleRequest()添加了 @RequestMapping 注解的方法,具体为一个 HandlerMethod,执行的就是当前加了注解的方法一个 HandlerFunction 对象,执行的是 HandlerFunction 对象中的 handle()所以,按逻辑来说,找到 Handler 之后,我们得判断它的类型,比如代码可能是这样的:


Object handler = mappedHandler.getHandler();if (handler instanceof Controller) {((Controller)handler).handleRequest(request, response);} else if (handler instanceof HttpRequestHandler) {((HttpRequestHandler)handler).handleRequest(request, response);} else if (handler instanceof HandlerMethod) {((HandlerMethod)handler).getMethod().invoke(...);} else if (handler instanceof HandlerFunction) {((HandlerFunction)handler).handle(...);}复制代码但是 SpringMVC 并不是这么写的,还是采用的适配模式,把不同种类的 Handler 适配成一个 HandlerAdapter,后续再执行 HandlerAdapter 的 handle()方法就能执行不同种类 Hanlder 对应的方法。


针对不同的 Handler,会有不同的适配器:


HttpRequestHandlerAdapterSimpleControllerHandlerAdapterRequestMappingHandlerAdapterHandlerFunctionAdapter 适配逻辑为:


protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {if (this.handlerAdapters != null) {for (HandlerAdapter adapter : this.handlerAdapters) {if (adapter.supports(handler)) {return adapter;}}}throw new ServletException("No adapter for handler [" + handler +"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");}复制代码传入 handler,遍历上面四个 Adapter,谁支持就返回谁,比如判断的代码依次为:


public boolean supports(Object handler) {return (handler instanceof HttpRequestHandler);}


public boolean supports(Object handler) {return (handler instanceof Controller);}


public final boolean supports(Object handler) {return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));}


public boolean supports(Object handler) {return handler instanceof HandlerFunction;}复制代码根据 Handler 适配出了对应的 HandlerAdapter 后,就执行具体 HandlerAdapter 对象的 handle()方法了,比如:


HttpRequestHandlerAdapter 的 handle():


public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {((HttpRequestHandler) handler).handleRequest(request, response);return null;}复制代码 SimpleControllerHandlerAdapter 的 handle():


public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return ((Controller) handler).handleRequest(request, response);}复制代码 HandlerFunctionAdapter 的 handle():


HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;serverResponse = handlerFunction.handle(serverRequest);复制代码因为这三个接收的直接就是 Requeset 对象,不用 SpringMVC 做额外的解析,所以比较简单,比较复杂的是 RequestMappingHandlerAdapter,它执行的是加了 @RequestMapping 的方法,而这种方法的写法可以是多种多样,SpringMVC 需要根据方法的定义去解析 Request 对象,从请求中获取出对应的数据然后传递给方法,并执行。


@RequestMapping 方法参数解析当 SpringMVC 接收到请求,并找到了对应的 Method 之后,就要执行该方法了,不过在执行之前需要根据方法定义的参数信息,从请求中获取出对应的数据,然后将数据传给方法并执行。


一个 HttpServletRequest 通常有:


request parameterrequest attributerequest sessionreqeust headerreqeust body 比如如下几个方法:


public String test(String username) {return "Hoeller";}复制代码表示要从 request parameter 中获取 key 为 username 的 value


public String test(@RequestParam("uname") String username) {return "Hoeller";}复制代码表示要从 request parameter 中获取 key 为 uname 的 value


public String test(@RequestAttribute String username) {return "Hoeller";}复制代码表示要从 request attribute 中获取 key 为 username 的 value


public String test(@SessionAttribute String username) {return "Hoeller";}复制代码表示要从 request session 中获取 key 为 username 的 value


public String test(@RequestHeader String username) {return "Hoeller";}复制代码表示要从 request header 中获取 key 为 username 的 value


public String test(@RequestBody String username) {return "Hoeller";}复制代码表示获取整个请求体


所以,我们发现 SpringMVC 要去解析方法参数,看该参数到底是要获取请求中的哪些信息。


而这个过程,源码中是通过 HandlerMethodArgumentResolver 来实现的,比如:


RequestParamMethodArgumentResolver:负责处理 @RequestParamRequestHeaderMethodArgumentResolver:负责处理 @RequestHeaderSessionAttributeMethodArgumentResolver:负责处理 @SessionAttributeRequestAttributeMethodArgumentResolver:负责处理 @RequestAttributeRequestResponseBodyMethodProcessor:负责处理 @RequestBody 还有很多其他的...而在判断某个参数该由哪个 HandlerMethodArgumentResolver 处理时,也是很粗暴:


private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {


HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);if (result == null) {  for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {      if (resolver.supportsParameter(parameter)) {          result = resolver;          this.argumentResolverCache.put(parameter, result);          break;      }  }}return result;
复制代码


}复制代码就是遍历所有的 HandlerMethodArgumentResolver,哪个能支持处理当前这个参数就由哪个处理。


比如:


@RequestMapping(method = RequestMethod.GET, path = "/test")@ResponseBodypublic String test(@RequestParam @SessionAttribute String username) {System.out.println(username);return "Hoeller";}复制代码以上代码的 username 将对应 RequestParam 中的 username,而不是 session 中的,因为在源码中 RequestParamMethodArgumentResolver 更靠前。


当然 HandlerMethodArgumentResolver 也会负责从 request 中获取对应的数据,对应的是 resolveArgument()方法。


比如 RequestParamMethodArgumentResolver:


protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);


if (servletRequest != null) {    Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);    if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {        return mpArg;    }}
Object arg = null;MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);if (multipartRequest != null) { List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { arg = (files.size() == 1 ? files.get(0) : files); }}if (arg == null) { String[] paramValues = request.getParameterValues(name); if (paramValues != null) { arg = (paramValues.length == 1 ? paramValues[0] : paramValues); }}return arg;
复制代码


}


复制代码核心是:


if (arg == null) {String[] paramValues = request.getParameterValues(name);if (paramValues != null) {arg = (paramValues.length == 1 ? paramValues[0] : paramValues);}}复制代码按同样的思路,可以找到方法中每个参数所要求的值,从而执行方法,得到方法的返回值。


@RequestMapping 方法返回值解析而方法返回值,也会分为不同的情况。比如有没有加 @ResponseBody 注解,如果方法返回一个 String:


加了 @ResponseBody 注解:表示直接将这个 String 返回给浏览器没有加 @ResponseBody 注解:表示应该根据这个 String 找到对应的页面,把页面返回给浏览器在 SpringMVC 中,会利用 HandlerMethodReturnValueHandler 来处理返回值:


RequestResponseBodyMethodProcessor:处理加了 @ResponseBody 注解的情况 ViewNameMethodReturnValueHandler:处理没有加 @ResponseBody 注解并且返回值类型为 String 的情况 ModelMethodProcessor:处理返回值是 Model 类型的情况还有很多其他的...我们这里只讲 RequestResponseBodyMethodProcessor,因为它会处理加了 @ResponseBody 注解的情况,也是目前我们用得最多的情况。


RequestResponseBodyMethodProcessor 相当于会把方法返回的对象直接响应给浏览器,如果返回的是一个字符串,那么好说,直接把字符串响应给浏览器,那如果返回的是一个 Map 呢?是一个 User 对象呢?该怎么把这些复杂对象响应给浏览器呢?


处理这块,SpringMVC 会利用 HttpMessageConverter 来处理,比如默认情况下,SpringMVC 会有 4 个 HttpMessageConverter:


ByteArrayHttpMessageConverter:处理返回值为字节数组的情况,把字节数组返回给浏览器 StringHttpMessageConverter:处理返回值为字符串的情况,把字符串按指定的编码序列号后返回给浏览器 SourceHttpMessageConverter:处理返回值为 XML 对象的情况,比如把 DOMSource 对象返回给浏览器 AllEncompassingFormHttpMessageConverter:处理返回值为 MultiValueMap 对象的情况 StringHttpMessageConverter 的源码也比较简单:


protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {HttpHeaders headers = outputMessage.getHeaders();if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {headers.setAcceptCharset(getAcceptedCharsets());}Charset charset = getContentTypeCharset(headers.getContentType());StreamUtils.copy(str, charset, outputMessage.getBody());}复制代码先看有没有设置 Content-Type,如果没有设置则取默认的,默认为 ISO-8859-1,所以默认情况下返回中文会乱码,可以通过以下来中方式来解决:


@RequestMapping(method = RequestMethod.GET, path = "/test", produces = {"application/json;charset=UTF-8"})@ResponseBodypublic String test() {return "公众号 Hoeller";}复制代码 @ComponentScan("com.hoeller")@Configuration@EnableWebMvcpublic class AppConfig implements WebMvcConfigurer {


@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {  StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();  messageConverter.setDefaultCharset(StandardCharsets.UTF_8);  converters.add(messageConverter);}
复制代码


}复制代码不过以上四个 Converter 是不能处理 Map 对象或 User 对象的,所以如果返回的是 Map 或 User 对象,那么得单独配置一个 Converter,比如 MappingJackson2HttpMessageConverter,这个 Converter 比较强大,能把 String、Map、User 对象等等都能转化成 JSON 格式。


@ComponentScan("com.hoeller")@Configuration@EnableWebMvcpublic class AppConfig implements WebMvcConfigurer {


@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {  MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();  messageConverter.setDefaultCharset(StandardCharsets.UTF_8);  converters.add(messageConverter);}
复制代码


}复制代码具体转化的逻辑就是 Jackson2 的转化逻辑。


总结以上就是整个 SpringMVC 从启动到处理请求,从接收请求到执行方法的整体流程,如果大家喜欢,麻烦点赞转发一下,后续我就分享更多细节内容。

用户头像

欢迎关注公众号:灵风的架构笔记 2022-10-21 加入

Java后端架构领域丨面题面经丨学习路线丨职业规划丨专业知识丨互联网资讯等分享

评论

发布
暂无评论
聊聊  SpringMVC  是如何工作的?_程序员_风铃架构日知录_InfoQ写作社区