You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 导读5分钟轻松了解一个HTTP请求的处理过程
你好,我是傅健。
上一章节我们学习了自动注入、AOP 等 Spring 核心知识运用上的常见错误案例。然而,我们**使用 Spring 大多还是为了开发一个 Web 应用程序**所以从这节课开始我们将学习Spring Web 的常见错误案例。
在这之前,我想有必要先给你简单介绍一下 Spring Web 最核心的流程,这可以让我们后面的学习进展更加顺利一些。
那什么是 Spring Web 最核心的流程呢?无非就是一个 HTTP 请求的处理过程。这里我以 Spring Boot 的使用为例,以尽量简单的方式带你梳理下。
首先,回顾下我们是怎么添加一个 HTTP 接口的,示例如下:
```
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld";
};
}
```
这是我们最喜闻乐见的一个程序,但是对于很多程序员而言,其实完全不知道为什么这样就工作起来了。毕竟,不知道原理,它也能工作起来。
但是,假设你是一个严谨且有追求的人,你大概率是有好奇心去了解它的。而且相信我,这个问题面试也可能会问到。我们一起来看看它背后的故事。
其实仔细看这段程序,你会发现一些**关键的“元素”**
1. 请求的 Path: hi
2. 请求的方法Get
3. 对应方法的执行hi()
那么,假设让你自己去实现 HTTP 的请求处理,你可能会写出这样一段伪代码:
```
public class HttpRequestHandler{
Map<RequestKey, Method> mapper = new HashMap<>();
public Object handle(HttpRequest httpRequest){
RequestKey requestKey = getRequestKey(httpRequest);
Method method = this.mapper.getValue(requestKey);
Object[] args = resolveArgsAccordingToMethod(httpRequest, method);
return method.invoke(controllerObject, args);
};
}
```
那么现在需要哪些组件来完成一个请求的对应和执行呢?
1. 需要有一个地方(例如 Map去维护从 HTTP path/method 到具体执行方法的映射;
2. 当一个请求来临时,根据请求的关键信息来获取对应的需要执行的方法;
3. 根据方法定义解析出调用方法的参数值,然后通过反射调用方法,获取返回结果。
除此之外,你还需要一个东西,就是利用底层通信层来解析出你的 HTTP 请求。只有解析出请求了,才能知道 path/method 等信息,才有后续的执行,否则也是“巧妇难为无米之炊”了。
所以综合来看,你大体上需要这些过程才能完成一个请求的解析和处理。那么接下来我们就按照处理顺序分别看下 Spring Boot 是如何实现的,对应的一些关键实现又长什么样。
首先,解析 HTTP 请求。对于 Spring 而言它本身并不提供通信层的支持它是依赖于Tomcat、Jetty等容器来完成通信层的支持例如当我们引入Spring Boot时我们就间接依赖了Tomcat。依赖关系图如下
![](https://static001.geekbang.org/resource/image/bf/71/bf28efcd2d8dc920dddbe4dabaeefb71.png)
另外正是这种自由组合的关系让我们可以做到直接置换容器而不影响功能。例如我们可以通过下面的配置从默认的Tomcat切换到Jetty
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>-
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
```
依赖了Tomcat后Spring Boot在启动的时候就会把Tomcat启动起来做好接收连接的准备。
关于Tomcat如何被启动你可以通过下面的调用栈来大致了解下它的过程
![](https://static001.geekbang.org/resource/image/45/44/456dc47793b0f99c9c2d193027f0ed44.png)
说白了就是调用下述代码行就会启动Tomcat
```
SpringApplication.run(Application.class, args);
```
那为什么使用的是Tomcat你可以看下面这个类或许就明白了
```
//org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration
class ServletWebServerFactoryConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(
//省略非关键代码
return factory;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyServletWebServerFactory JettyServletWebServerFactory(
ObjectProvider<JettyServerCustomizer> serverCustomizers) {
//省略非关键代码
return factory;
}
}
//省略其他容器配置
}
```
前面我们默认依赖了Tomcat内嵌容器的JAR所以下面的条件会成立进而就依赖上了Tomcat
```
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
```
有了Tomcat后当一个HTTP请求访问时会触发Tomcat底层提供的NIO通信来完成数据的接收这点我们可以从下面的代码org.apache.tomcat.util.net.NioEndpoint.Poller#run中看出来
```
@Override
public void run() {
while (true) {
//省略其他非关键代码
//轮询注册的兴趣事件
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
//省略其他非关键代码
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = (NioSocketWrapper)
//处理事件
processKey(sk, socketWrapper);
//省略其他非关键代码
}
//省略其他非关键代码
}
}
```
上述代码会完成请求事件的监听和处理最终在processKey中把请求事件丢入线程池去处理。请求事件的接收具体调用栈如下
![](https://static001.geekbang.org/resource/image/f4/e3/f4b3febfced888415038f4b7cccb2fe3.png)
线程池对这个请求的处理的调用栈如下:
![](https://static001.geekbang.org/resource/image/99/e0/99021847afb18bf522860cf2a42aa3e0.png)
在上述调用中最终会进入Spring Boot的处理核心即DispatcherServlet上述调用栈没有继续截取完整调用所以未显示。可以说DispatcherServlet是用来处理HTTP请求的中央调度入口程序为每一个 Web 请求映射一个请求的处理执行体API controller/method
我们可以看下它的核心是什么它本质上就是一种Servlet所以它是由下面的Servlet核心方法触发
> javax.servlet.http.HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
最终它执行到的是下面的doService(),这个方法完成了请求的分发和处理:
```
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
doDispatch(request, response);
}
```
我们可以看下它是如何分发和执行的:
```
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 省略其他非关键代码
// 1. 分发Determine handler for the current request.
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
// 省略其他非关键代码
//Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 省略其他非关键代码
// 2. 执行Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 省略其他非关键代码
}
```
在上述代码中,很明显有两个关键步骤:
**1\. 分发,即根据请求寻找对应的执行方法**
寻找方法参考DispatcherServlet#getHandler具体的查找远比开始给出的Map查找来得复杂但是无非还是一个根据请求寻找候选执行方法的过程这里我们可以通过一个调试视图感受下这种对应关系
![](https://static001.geekbang.org/resource/image/58/dc/58f9b4c2ac68e8648f441381f1ff88dc.png)
这里的关键映射Map其实就是上述调试视图中的RequestMappingHandlerMapping。
**2\. 执行,反射执行寻找到的执行方法**
这点可以参考下面的调试视图来验证这个结论参考代码org.springframework.web.method.support.InvocableHandlerMethod#doInvoke
![](https://static001.geekbang.org/resource/image/6d/94/6d83528c381441a11bfc111f0f645794.png)
最终我们是通过反射来调用执行方法的。
通过上面的梳理你应该基本了解了一个HTTP请求是如何执行的。但是你可能会产生这样一个疑惑Handler的映射是如何构建出来的呢
说白了核心关键就是RequestMappingHandlerMapping这个Bean的构建过程。
它的构建完成后会调用afterPropertiesSet来做一些额外的事这里我们可以先看下它的调用栈
![](https://static001.geekbang.org/resource/image/f1/16/f106c25aed5f62fce28d589390891b16.png)
其中关键的操作是AbstractHandlerMethodMapping#processCandidateBean方法
```
protected void processCandidateBean(String beanName) {
//省略非关键代码
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
```
isHandler(beanType)的实现参考以下关键代码:
```
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
```
这里你会发现判断的关键条件是是否标记了合适的注解Controller或者RequestMapping。只有标记了才能添加到Map信息。换言之Spring在构建RequestMappingHandlerMapping时会处理所有标记Controller和RequestMapping的注解然后解析它们构建出请求到处理的映射关系。
以上即为Spring Boot处理一个HTTP请求的核心过程无非就是绑定一个内嵌容器Tomcat/Jetty/其他来接收请求然后为请求寻找一个合适的方法最后反射执行它。当然这中间还会掺杂无数的细节不过这不重要抓住这个核心思想对你接下来理解Spring Web中各种类型的错误案例才是大有裨益的