Spring MVC学习之DispatcherServlet请求处理详析

时间:2022-11-11 12:38:45

前言

要深入理解spring mvc的工作流程,就需要先了解spring mvc的架构:

Spring MVC学习之DispatcherServlet请求处理详析

从上图可以看到 前端控制器dispatcherservlet在其中起着主导作用,理解了dispatcherservlet 就完全可以说弄清楚了spring mvc。

dispatcherservlet作为spring用于处理web请求注册的唯一一个servlet,所有的请求都是经由dispatcherservlet进行分发处理的。本文主要讲解dispatcherservlet是如何对请求进行分发,处理,并且生成相应的视图的。

1. 整体结构

在httpservlet中,其对不同方式的请求进行了分发,比如对于get请求,其提供了doget()方法,对于post请求,其提供了dopost()方法等等。通过这种方式,子类可以针对于当前请求的方式实现不同的方法即可。但是在dispatcherservlet中,由于需要使用同一的方式对不同的请求进行处理,因而其对各个请求方式进行了整合,如下就是dispatcherservlet针对get和post请求所编写的同一处理逻辑:

?
1
2
3
4
5
6
7
8
9
10
11
@override
protected final void doget(httpservletrequest request, httpservletresponse response)
 throws servletexception, ioexception {
 processrequest(request, response);
}
 
@override
protected final void dopost(httpservletrequest request, httpservletresponse response)
 throws servletexception, ioexception {
 processrequest(request, response);
}

可以看到,无论是get请求还是post请求,dispatcherservlet都是委托给了processrequest()方法处理,对于其他的请求方式,其处理方式也是类似的。通过这种方式,dispatcherservlet将各个请求整合在了一起,虽然整合在了一起,但是request中也还是保存有当前请求的请求方式的,因而保存了后续对请求进行分发的能力。这里我们直接看processrequest()方法是如何处理各个请求的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
protected final void processrequest(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {
 long starttime = system.currenttimemillis();
 throwable failurecause = null;
 // 获取先前请求的localecontext
 localecontext previouslocalecontext = localecontextholder.getlocalecontext();
 // 获取当前请求的localecontext,其中保存了当前请求的locale信息
 localecontext localecontext = buildlocalecontext(request);
 
 // 获取先前请求的attributes信息
 requestattributes previousattributes = requestcontextholder.getrequestattributes();
 // 获取当前请求的attributes信息,其中保存了当前请求的各个属性数据
 servletrequestattributes requestattributes =
 buildrequestattributes(request, response, previousattributes);
 
 // 获取当前请求的webasyncmanager,这只有在当前请求是请求的异步任务时才会真正用到
 webasyncmanager asyncmanager = webasyncutils.getasyncmanager(request);
 // 注册异步任务的拦截器,如果请求的是异步任务,这个拦截器可以拦截异步任务的前置,后置和异常等情况
 asyncmanager.registercallableinterceptor(frameworkservlet.class.getname(),
 new requestbindinginterceptor());
 
 // 将当前请求的locale,attributes信息初始化到对应的threadlocal对象中,用于后续使用
 initcontextholders(request, localecontext, requestattributes);
 
 try {
 // 对当前请求进行分发
 doservice(request, response);
 } catch (servletexception | ioexception ex) {
 failurecause = ex;
 throw ex;
 } catch (throwable ex) {
 failurecause = ex;
 throw new nestedservletexception("request processing failed", ex);
 } finally {
 // 在请求完成之后,判断当前请求的locale和attributes信息是否需要继承,如果需要继承,
 // 则会将locale信息设置到inheritablelocalecontextholder中,而将attributes
 // 信息设置到inheritablerequestattributesholder中;否则就会移除对应的信息,
 // 而只为当前请求的contextholder设置相应的属性
 resetcontextholders(request, previouslocalecontext, previousattributes);
 if (requestattributes != null) {
 // 调用已注册的在当前请求被销毁时的回调函数,并且更新session中当前请求所更新的属性
 requestattributes.requestcompleted();
 }
 
 if (logger.isdebugenabled()) {
 if (failurecause != null) {
 this.logger.debug("could not complete request", failurecause);
 } else {
 if (asyncmanager.isconcurrenthandlingstarted()) {
 logger.debug("leaving response open for concurrent processing");
 } else {
 this.logger.debug("successfully completed request");
 }
 }
 }
 
 // 发布请求已经完成的事件,以便对该事件进行监听的程序进行相应的处理
 publishrequesthandledevent(request, response, starttime, failurecause);
 }
}

可以看到,processrequest()方法主要是对locale和attributes信息进行了处理,然后就通过doservice()方法对请求再次进行了分发。我们这里继续阅读doservice()方法的源码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@override
protected void doservice(httpservletrequest request, httpservletresponse response) throws exception {
 if (logger.isdebugenabled()) {
 string resumed = webasyncutils.getasyncmanager(request).hasconcurrentresult()
 ? " resumed" : "";
 logger.debug("dispatcherservlet with name '" + getservletname() + "'" + resumed
 + " processing " + request.getmethod() + " request for ["
 + getrequesturi(request) + "]");
 }
 
 // 这里主要是判断当前请求是否为include请求,如果是include请求,那么就会将当前请求中的
 // 数据都放入一个快照中,在当前请求完成之后,会从该块中中取出数据,然后将其重新加载到
 // 当前request中,以便request进行后续的处理。这里默认情况下是会对所有的属性进行处理的,
 // 因为cleanupafterinclude默认值为true,如果将其设置为false,那么就只会对spring框架
 // 相关的属性进行处理
 map<string, object> attributessnapshot = null;
 if (webutils.isincluderequest(request)) {
 attributessnapshot = new hashmap<>();
 enumeration<?> attrnames = request.getattributenames();
 while (attrnames.hasmoreelements()) {
 string attrname = (string) attrnames.nextelement();
 if (this.cleanupafterinclude
 || attrname.startswith(default_strategies_prefix)) {
 attributessnapshot.put(attrname, request.getattribute(attrname));
 }
 }
 }
 
 // 这里分别将applicationcontext,loacleresolver,themeresolver和themesource等
 // bean添加到当前request中
 request.setattribute(web_application_context_attribute, getwebapplicationcontext());
 request.setattribute(locale_resolver_attribute, this.localeresolver);
 request.setattribute(theme_resolver_attribute, this.themeresolver);
 request.setattribute(theme_source_attribute, getthemesource());
 
 // 这里flashmapmanager主要的作用在于当请求如果是重定向的请求,那么可以将一些属性保存在flashmap
 // 中,然后通过flashmapmanager进行管理,从而在重定向之后能够获取到重定向之前所保存的请求
 if (this.flashmapmanager != null) {
 // 在当前请求中获取flashmap数据,如果不是重定向之后的请求,那么这里获取到的就是空值
 flashmap inputflashmap =
 this.flashmapmanager.retrieveandupdate(request, response);
 if (inputflashmap != null) {
 // 将获取到的flashmap数据保存在request中
 request.setattribute(input_flash_map_attribute,
 collections.unmodifiablemap(inputflashmap));
 }
 // 设置默认的flashmap和flashmapmanager
 request.setattribute(output_flash_map_attribute, new flashmap());
 request.setattribute(flash_map_manager_attribute, this.flashmapmanager);
 }
 
 try {
 // 这里才是真正的对请求进行分发处理的位置
 dodispatch(request, response);
 } finally {
 // 判断当前请求不是一个异步任务的请求,但是是一个include请求,那么就会重新加载
 // 请求之前保存的快照数据
 if (!webasyncutils.getasyncmanager(request).isconcurrenthandlingstarted()) {
 if (attributessnapshot != null) {
 restoreattributesafterinclude(request, attributessnapshot);
 }
 }
 }
}

这里的doservice()方法也还没有对请求进行真正的处理,其首先判断了当前请求是不是一个include请求,如果是include请求,那么就将请求的属性都保存在一个快照中,以便请求完成之后能够重新进行加载;然后会判断当前是否是一个重定向之后的请求,如果是重定向之后的请求,那么其flashmapmanager就不是空的,此时会将重定向之前保存的属性重新加载到当前请求中;最后doservice()方法才会调用dodispatch()方法进行请求的分发和处理。如下是dodispatch()方法的源码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
protected void dodispatch(httpservletrequest request, httpservletresponse response)
 throws exception {
 httpservletrequest processedrequest = request;
 handlerexecutionchain mappedhandler = null;
 boolean multipartrequestparsed = false;
 
 // 获取当前的异步任务管理器
 webasyncmanager asyncmanager = webasyncutils.getasyncmanager(request);
 
 try {
 modelandview mv = null;
 exception dispatchexception = null;
 
 try {
 // 这里判断当前请求是否为一个文件请求,这里的判断方式就是要求当前请求满足两点:①请求
 // 方式是post;②判断contenttype是否以multipart/开头。如果满足这两点,那么就认为当前
 // 请求是一个文件请求,此时会将当前请求的request对象封装为一个
 // multiparthttpservletrequest对象,这也是我们在定义文件请求的controller时
 // 能够将request参数写为multiparthttpservletrequest的原因。这里如果不是文件请求,
 // 那么会将request直接返回。
 processedrequest = checkmultipart(request);
 // 这里判断原始request与转换后的request是否为同一个request,如果不是同一个,则说明
 // 其是一个文件请求
 multipartrequestparsed = (processedrequest != request);
 // 这里gethandler()方法就是通过遍历当前spring容器中所有定义的handlermapping对象,
 // 通过调用它们的gethandler()方法,看当前的handlermapping能否将当前request映射
 // 到某个handler,也就是某个controller方法上,如果能够映射到,则说明该handler能够
 // 处理当前请求
 mappedhandler = gethandler(processedrequest);
 if (mappedhandler == null) {
 // 如果每个handlermapping都无法找到与当前request匹配的handler,那么就认为
 // 无法处理当前请求,此时一般会返回给页面404状态码
 nohandlerfound(processedrequest, response);
 return;
 }
 
 // 通过找到的handler,然后在当前spring容器中找到能够支持将当前request请求适配到
 // 找到的handler上的handleradapter。这里需要找到这样的适配器的原因是,我们的handler
 // 一般都是controller的某个方法,其是一个java方法,而当前request则是一种符合http
 // 协议的请求,这里是无法直接将request直接应用到handler上的,因而需要使用一个适配器,
 // 也就是这里的handleradapter。由于前面获取handler的时候,不同的handlermapping
 // 所产生的handler是不一样的,比如reqeustmappinghandlermappin*生的handler是一个
 // handlermethod对象,因而这里在判断某个handleradapter是否能够用于适配当前handler的
 // 时候是通过其supports()方法进行的,比如requestmappinghandleradapter就是判断
 // 当前的handler是否为handlermethod类型,从而判断其是否能够用于适配当前handler。
 handleradapter ha = gethandleradapter(mappedhandler.gethandler());
 string method = request.getmethod();
 boolean isget = "get".equals(method);
 // 这里判断请求方式是否为get或head请求,如果是这两种请求的一种,那么就会判断
 // 当前请求的资源是否超过了其lastmodified时间,如果没超过,则直接返回,
 // 并且告知浏览器可以直接使用缓存来处理当前请求
 if (isget || "head".equals(method)) {
 long lastmodified = ha.getlastmodified(request,
 mappedhandler.gethandler());
 if (logger.isdebugenabled()) {
 logger.debug("last-modified value for [" + getrequesturi(request)
 + "] is: " + lastmodified);
 }
 if (new servletwebrequest(request, response)
 .checknotmodified(lastmodified) && isget) {
 return;
 }
 }
 
 // 这里在真正处理请求之前会获取容器中所有的拦截器,也就是handlerinterceptor对象,
 // 然后依次调用其prehandle()方法,如果某个prehandle()方法返回了false,那么就说明
 // 当前请求无法通过拦截器的过滤,因而就会直接出发其aftercompletion()方法,只有在
 // 所有的prehandle()方法都返回true时才会认为当前请求是能够使用目标handler进行处理的
 if (!mappedhandler.applyprehandle(processedrequest, response)) {
 return;
 }
 
 // 在当前请求通过了所有拦截器的预处理之后,这里就直接调用handleradapter.handle()
 // 方法来处理当前请求,并且将处理结果封装为一个modelandview对象。该对象中主要有两个
 // 属性:view和model,这里的view存储了后续需要展示的逻辑视图名或视图对象,而model
 // 中则保存了用于渲染视图所需要的属性
 mv = ha.handle(processedrequest, response, mappedhandler.gethandler());
 
 // 如果当前是一个异步任务,那么就会释放当前线程,等待异步任务处理完成之后才将
 // 任务的处理结果返回到页面
 if (asyncmanager.isconcurrenthandlingstarted()) {
 return;
 }
 
 // 如果返回的modelandview对象中没有指定视图名或视图对象,那么就会根据当前请求的url
 // 来生成一个视图名
 applydefaultviewname(processedrequest, mv);
 // 在请求处理完成之后,依次调用拦截器的posthandle()方法,对请求进行后置处理
 mappedhandler.applyposthandle(processedrequest, response, mv);
 } catch (exception ex) {
 dispatchexception = ex;
 } catch (throwable err) {
 // 将处理请求过程中产生的异常封装到dispatchexception中
 dispatchexception = new nestedservletexception("handler dispatch failed",
 err);
 }
 
 // 这里主要是请求处理之后生成的视图进行渲染,也包括出现异常之后对异常的处理。
 // 渲染完之后会依次调用拦截器的aftercompletion()方法来对请求进行最终处理
 processdispatchresult(processedrequest, response, mappedhandler, mv,
 dispatchexception);
 } catch (exception ex) {
 // 如果在上述过程中任意位置抛出异常,包括渲染视图时抛出异常,那么都会触发拦截器的
 // aftercompletion()方法的调用
 triggeraftercompletion(processedrequest, response, mappedhandler, ex);
 } catch (throwable err) {
 triggeraftercompletion(processedrequest, response, mappedhandler,
 new nestedservletexception("handler processing failed", err));
 } finally {
 // 如果当前异步任务已经开始,则触发异步任务拦截器的afterconcurrenthandlingstarted()方法
 if (asyncmanager.isconcurrenthandlingstarted()) {
 if (mappedhandler != null) {
 mappedhandler.applyafterconcurrenthandlingstarted(processedrequest,
 response);
 }
 } else {
 // 如果当前是一个文件请求,则清理当前request中的文件数据
 if (multipartrequestparsed) {
 cleanupmultipart(processedrequest);
 }
 }
 }
}

这里dodispatch()方法是进行请求分发和处理的主*分,其主要分为如下几个步骤:

  • 判断当前是否为文件请求,如果是,则将request对象类型转换为multiparthttpservletrequest;
  • 在handlermapping中查找能够处理当前request的handlermapping,并且获取能够处理当前请求的handler;
  • 根据获取到的handler,查找当前容器中支持将当前request适配到该handler的handleradapter;
  • 应用容器中所有拦截器的prehandle()方法,只有在所有的prehandle()方法都通过之后才会将当前请求交由具体的handler进行处理;
  • 调用handleradapter.handle()方法将request适配给获取到的handler进行处理;
  • 应用容器中所有拦截器的posthandle()方法,以对当前请求进行后置处理;
  • 根据处理后得到的modelandview对象对视图进行渲染;
  • 应用容器中所有拦截器的aftercompletion()方法,以对当前请求进行完成处理。

2. handler获取

从前面的步骤可以看出,请求的具体处理过程主要是通过handlermapping根据当前request获取到对应的handler,然后交由handleradapter将request适配给该handler进行处理,并将处理结果封装为一个modelandview对象,最后将该modelandview对象渲染出来。这里我们首先看handlermapping根据request查找具体的handler的过程:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@nullable
protected handlerexecutionchain gethandler(httpservletrequest request) throws exception {
 if (this.handlermappings != null) {
 // 遍历当前容器中所有的handlermapping对象,调用其gethandler()方法,如果其能够根据
 // 当前request获取一个handler,那么就直接返回。
 for (handlermapping hm : this.handlermappings) {
 if (logger.istraceenabled()) {
 logger.trace(
  "testing handler map [" + hm + "] in dispatcherservlet with name '"
  + getservletname() + "'");
 }
 handlerexecutionchain handler = hm.gethandler(request);
 if (handler != null) {
 return handler;
 }
 }
 }
 return null;
}

这里的逻辑比较简单,就是遍历当前容器中所有的handlermapping对象,然后依次判断其是否能够根据当前request获取一个handler,如果能够获取就直接使用该handler。这里关于handlermapping将request映射为handler的过程可以阅读本人之前的文章:spring mvc之requestmappinghandlermapping匹配

3. handleradapter获取与请求处理

在获取到具体的handler之后,dispatcher就会根据获取到的handler查找能够将当前request适配到该handler的adapter,这里获取handleradapter的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected handleradapter gethandleradapter(object handler) throws servletexception {
 if (this.handleradapters != null) {
 // 遍历当前容器中所有的handleradapter,通过调用其supports()方法,判断当前handleradapter
 // 能否用于适配当前的handler,如果可以,则直接使用该handleradapter
 for (handleradapter ha : this.handleradapters) {
 if (logger.istraceenabled()) {
 logger.trace("testing handler adapter [" + ha + "]");
 }
 if (ha.supports(handler)) {
 return ha;
 }
 }
 }
 
 // 如果找不到任何一个handleradapter用于适配当前请求,则抛出异常
 throw new servletexception("no adapter for handler [" + handler
 + "]: the dispatcherservlet configuration needs to include a handleradapter"
 + " that supports this handler");
}

这里获取handleradapter的过程与handlermapping非常的相似,也是遍历当前容器中所有的handleradapter对象,然后调用其supports()方法,判断该适配器能否应用于当前handler的适配,如果可以则直接使用该handleradapter。关于handleradapter进行request与handler适配的过程,读者可阅读本人之前的文章:spring mvc之requestmappinghandleradapter详解

4. 视图渲染

在handleradapter进行了请求的适配,并且调用了目标handler之后,其会返回一个modelandview对象,该对象中保存有用于渲染视图的模型数据和需要渲染的视图名。具体的视图渲染工作是在processdispatchresult()方法中进行的,这里我们直接阅读器源码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private void processdispatchresult(httpservletrequest request,
 httpservletresponse response, @nullable handlerexecutionchain mappedhandler,
 @nullable modelandview mv, @nullable exception exception) throws exception {
 
 // 用于标记当前生成view是否是异常处理之后生成的view
 boolean errorview = false;
 if (exception != null) {
 // 如果当前的异常是modelandviewdefiningexception类型,则说明是modelandview的定义
 // 异常,那么就会调用其getmodelandview()方法生成一个新的view
 if (exception instanceof modelandviewdefiningexception) {
 logger.debug("modelandviewdefiningexception encountered", exception);
 mv = ((modelandviewdefiningexception) exception).getmodelandview();
 } else {
 // 如果生成的异常是其他类型的异常,就会在当前容器中查找能够处理当前异常的“拦截器”,
 // 找到之后调用这些拦截器,然后生成一个新的modelandview
 object handler = (mappedhandler != null ? mappedhandler.gethandler() : null);
 mv = processhandlerexception(request, response, handler, exception);
 errorview = (mv != null);
 }
 }
 
 // 如果得到的modelandview对象(无论是否为异常处理之后生成的modelandview)不为空,并且没有被清理,
 // 那么就会对其进行渲染,渲染的主要逻辑在render()方法中
 if (mv != null && !mv.wascleared()) {
 render(mv, request, response);
 if (errorview) {
 // 如果当前是异常处理之后生成的视图,那么就请求当前request中与异常相关的属性
 webutils.clearerrorrequestattributes(request);
 }
 } else {
 if (logger.isdebugenabled()) {
 logger.debug("null modelandview returned to dispatcherservlet with name '"
 + getservletname() + "': assuming handleradapter completed request "
 + "handling");
 }
 }
 
 // 如果当前正在进行异步请求任务的调用,则直接释放当前线程,等异步任务处理完之后再进行处理
 if (webasyncutils.getasyncmanager(request).isconcurrenthandlingstarted()) {
 return;
 }
 
 // 在视图渲染完成之后,依次调用当前容器中所有拦截器的aftercompletion()方法
 if (mappedhandler != null) {
 mappedhandler.triggeraftercompletion(request, response, null);
 }
}

从上面的逻辑可以看出,在进行视图渲染时,首先会判断请求处理过程中是否抛出了异常,如果抛出了异常,则会调用相应的异常处理器,获取异常处理之后的modelandview对象,然后通过modelandview对象渲染具体的视图,最后会依次触发当前容器中所有拦截器的aftercompletion()方法。这里对视图的具体渲染工作在render()方法中,我们继续阅读其源码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void render(modelandview mv, httpservletrequest request, httpservletresponse response) throws exception {
 // 获取当前请求的locale信息,该信息在进行视图的国际化展示时将会非常有用
 locale locale = (this.localeresolver != null
 ? this.localeresolver.resolvelocale(request) : request.getlocale());
 response.setlocale(locale);
 
 view view;
 string viewname = mv.getviewname();
 if (viewname != null) {
 // 如果视图名不为空,那么就会使用当前容器中配置的viewresolver根据视图名获取一个view对象
 view = resolveviewname(viewname, mv.getmodelinternal(), locale, request);
 if (view == null) {
 throw new servletexception("could not resolve view with name '"
 + mv.getviewname() + "' in servlet with name '" + getservletname() + "'");
 }
 } else {
 // 如果modelandview中没有视图名,而提供的view对象,则直接使用该view对象
 view = mv.getview();
 if (view == null) {
 throw new servletexception("modelandview [" + mv + "] neither contains a "
 + "view name nor a view object in servlet with name '"
 + getservletname() + "'");
 }
 }
 
 if (logger.isdebugenabled()) {
 logger.debug("rendering view [" + view + "] in dispatcherservlet with name '"
 + getservletname() + "'");
 }
 try {
 // 设置响应的status属性
 if (mv.getstatus() != null) {
 response.setstatus(mv.getstatus().value());
 }
 
 // 调用view对象的render()方法来渲染具体的视图
 view.render(mv.getmodelinternal(), request, response);
 } catch (exception ex) {
 if (logger.isdebugenabled()) {
 logger.debug("error rendering view [" + view + "] in dispatcherservlet"
 + " with name '" + getservletname() + "'", ex);
 }
 throw ex;
 }
}

这里的render()方法才是进行视图渲染的真正方法,首先该方法首先通过modelandview对象获取所要渲染的视图名,通过viewresolver生成一个用于视图渲染的view对象;如果modelandview中不是保存的视图名,而是保存的view对象,则直接使用该对象。在生成view对象之后,通过调用该对象的render()方法渲染得到具体的视图。这里关于viewresolver如何获取到view对象,并且如何进行视图渲染的过程,读者可以阅读本人的文章:spring mvc之视图解析

5. 小结

本文首先从整体上讲解了dispatcherservlet是如何对请求进行聚合并且处理的,然后分别从handler获取,handleradapter进行请求适配,以及视图的渲染三个方面对请求处理的整体流程进行了讲解。这里主要是对dispatcherservlet处理请求的整体流程进行讲解,其各个部分的细节读者可以阅读本人前面的文章以进行详细的了解。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://my.oschina.net/zhangxufeng/blog/2222590