本文主要讲一下,一个HTTP请求在后端服务的流转路径,Tomcat等一众servlet容器如何定义了Web应用的基础样貌,后来的MVC框架又是如何弱化了servlet的存在,改为自己实现请求派发的。
前些日子我写了十几篇文章来介绍Tomcat的架构,并汇总了一个文章目录:Tomcat文章目录,如果你对Tomcat也感兴趣,可以翻看一下。Tomcat整体架构分为两大块:连接器与容器。连接器负责接收HTTP请求并将其解析为Request对象,然后连接器将Request对象传给容器,四大容器会层层向下寻址子容器,直到找到该请求对应的servlet。
在不谈MVC框架的情况下,一个HTTP请求在Tomcat中是这样流转的
- 连接器收到HTTP请求,从"处理线程"的线程池中挑一个线程,将该请求对应的socket连接抛给该线程进行接下来的处理,而连接器主线程则继续等待下一个请求。
- 处理线程将HTTP请求拼装成容器需要的Request与Response对象,将两个对象抛给容器进行接下来的处理。
- Tomcat中的容器都是一对多的关系,每个父容器都有自己的策略,可以根据request请求来定位到某个子容器。从Engine容器接收到请求开始,就开始层层往下寻找子容器,直到找到一个Wrapper容器,该Wrapper容器包含了能处理该request请求的servlet,接着就调用该servlet的service方法来执行用户自定义逻辑处理该request请求,并拿到结果数据。最后将返回结果层层上抛,处理线程拿到容器给的结果后,回传给客户端,一个HTTP请求的一生就结束了。
所以按照这个流程来说,一个Web应用中应该存在很多个不同的servlet来处理对应的HTTP请求,(这里我暂且叫这种模式为多servlet模式),在servlet编程时代,确实也是这个样子。
但是当MVC框架出来后,这个模式发生了一些改变,MVC框架放弃了多servlet模式,转而用一个servlet来接收所有的HTTP请求,在自己的框架内部再进行请求的分派处理。
MVC为什么这么做呢?我理解MVC这么做是为了减少与Tomcat的耦合度,转为在自身框架内部实现高度的定制化;而且使用单servlet天然的就将请求入口统一了,如果要对请求做一些统一处理就变得很方便。
MVC框架有哪些呢?最初大家使用Struts比较多,在Struts漏洞暴雷后,现在大家基本上都在使用SpringMVC,而且SpringBoot普及后,SpringMVC就成了一个理所应当的选择。接下来就简单看下SpringMVC的设计。
DispatcherServlet
在SpringMVC中那个入口servlet就是DispatcherServlet,它负责配合Tomcat来接收请求。
HandlerMethod
SpringMVC内部怎么表达一个Controller方法呢?答案就是用一个HandlerMethod对象来表示一个Controller方法。
例如下面这个Controller,testGet 与 testPost 两个方法在SpringMVC内部就会被表示成两个 HandlerMethod 对象
java
@RestController
public class TestController {
@GetMapping("/test")
public User testGet(Long userId) {
User user = new User();
user.setUserId(userId);
user.setName("张三");
user.setAge(18);
return user;
}
@PostMapping("/test")
public User testPost(Long userId) {
User user = new User();
user.setUserId(userId);
user.setName("张三");
user.setAge(18);
return user;
}
@Data
private static class User{
private Long userId;
private String name;
private Integer age;
}
}
HandlerMethod中存放了该方法的名字,所在类,参数信息,对应的请求uri,以及支持的请求方式(get、post...)等信息。
SpringMVC在启动时就将所有的 HandlerMethod 对象创建好,并为其建立一个索引 <uri,HandlerMethod对象> ,我们将这个索引称为 HandlerMapping。通过 HandlerMapping 就可以根据请求找到指定的 HandlerMethod 。
找到了 HandlerMethod 怎么执行它代表的方法呢?老套路,使用反射,即 method.invoke() 方法。这个反射执行逻辑被放在了一个叫 HandlerAdapter 的组件里。
加入拦截器
在反射执行 HandlerMethod 的方法前,会找到项目中配置的所有与这个请求有关的拦截器,然后在反射调用 method 前后,执行拦截器中的 preHandle、postHandle 及 afterCompletion 方法,拦截器的工作模式就是这样。拦截器的接口定义为 HandlerInterceptor 。
另外,Tomcat传给 DispatcherServlet 的信息为 HttpServletRequest 与 HttpServletResponse两个对象,SpringMVC内部拥有将 HttpServletRequest 转化为 method 的参数对象的组件,也有将method 的返回结果转化为 json 等结构的组件。(这里的method指的就是HandlerMethod对应的method)。
现在,我们再来看下Tomcat与SpringMVC配合时,一个HTTP请求的一生。
HTTP请求在Tomcat中的流转路径不变,最后找到 DispatcherServlet 这个servlet,接下来就进入到了SpringMVC的框架范围了。
SpringMVC根据请求的 HttpServletRequest 对象,找到对应的 HandlerMethod, 再找到会拦截该HanderMethod的拦截器,执行对应拦截器的 preHandle 方法后,再反射调用 HandlerMethod 对应的 method 方法,拿到反射调用的结果后,再按照程序安排执行拦截器的 postHandle 、afterCompletion 方法,拿到最终结果后再抛给Tomcat,Tomcat将结果返回给客户端。
另外在SpringMVC中还存在全局异常处理等机制,代码入口在 DispatcherServlet 的 processDispatchResult 方法中,通过 @RestControllerAdvice 配置的自定义全局异常处理机制就会在这个方法里被触发,感兴趣的自己去翻一下吧。