本系列文章皆在分析SpringMVC
的核心组件和工作原理,让你从springmvc
浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC
的工作原理SpringMVC
.
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
前言
在使用SpringMVC
开发Web
应用时,除了会在web.xml
中配置之前讨论的ContextLoaderListener
项以外(相关内容可参考:SpringMVC流程分析(一):从一行配置入手,搞懂web环境下Ioc容器的构建),还需要在配置一个Servlet
,而这个Servlet
正是本文所讨论的重点——DispatcherServlet
.
下图展示了本系列文章重点分析的组件信息,其中 DispatcherServlet
是本文分析的重点。
Servlet的前世今生
随着Web
开发工具的不断迭代, Servlet
可能已经成为一个相对陌生的话题。接下来,将以餐厅点菜为例,带你快速熟悉Servlet
.
当你走进一家餐厅,拿起菜单准备点菜时,总会有一个“服务员”会在你身旁,负责接收你的点菜信息,然后将你的餐单交给后厨去处理,等后厨烹调完毕后,“服务员”会将菜肴端到你的桌前。
类似的,当你在浏览器上访问一个网页或提交表单时,Servlet
就像是一个厨师和服务员的组合。它接收你的订单(请求),然后根据你的选择(数据),然后告诉厨师要做什么(执行业务逻辑),最后将做好的食物端到你的桌子上(返回动态生成的页面),让你享受美食.
简而言之,Servlet
的作用在于接收并处理客户端的HTTP请求,并生成响应结果.
Servlet的体系结构
Servlet
:Servlet
体系根接口GenericServlet
:Servlet
抽象实现类HttpServlet
: 对HTTP
协议封装的Servlet
实现类
Servlet
作为一个接口,并不能通过new
关键字来进行实例化,所以如果我们期待享受Servlet
所提供的服务,则必须依赖于Servlet
的实现类HttpServlet
,其扩展了GenericServlet
类,专门用于处理HTTP
请求.
此外,HttpServlet
提供了对HTTP
请求的封装和处理方法,使得Servlet
可以根据Http
的请求方法的不同信息,转发至对应的处理方法,如Get
请求转发至doGet()
处理,Post
请求转发至doPost()
处理.
Servlet的生命周期
事实上,在Servlet
对象的创建、初始化、接收请求、销毁等过程中,都会执行对应的方法,其分别为init()、service()、destory()
方法. 而这些方法也被称为是Servlet
的生命周期方法中. 这些方法的具体功能如下:
init()
:在Servlet
对象被创建后,容器会调用init()
方法进行初始化。该方法只会在Servlet的整个生命周期中被调用一次。可以在init()
方法中进行一些初始化操作,例如加载配置文件、建立数据库连接等。service()
:每当有HTTP请求到达Servlet
时,容器会调用service()
方法来处理请求,并据请求的类型(GET、POST等)来调用相应的doGet()
、doPost()
等方法。在这些方法中,可以定义请求处理逻辑、业务操作逻辑等,并生成相应的HTTP
响应.destroy()
:在Servlet
容器决定销毁Servlet
对象时,会调用destroy()
方法。这通常发生在Web应用程序关闭或Servlet
容器关闭的时候。在destroy()
方法中,你可以进行一些资源释放和清理的操作,例如关闭数据库连接、释放资源等.
总的来看,当Servlet
对象创建时,会调用init()
进行初始化;在接收HTTP
请求时,则会调用service()
方法处理请求;而在销毁时会调用destroy()
进行资源清理.
Servlet中的请求处理
- 首先,浏览器和服务器建立连接,生成请求数据包,将请求数据包发送给服务器;
- 接着,服务器解析请求数据包,创建
request
和response
对象,将请求数据存入request
对象中; - 随后,服务器调用
Servlet
的service()
方法,会将request
和response
作为参数传进来,并将处理结果写入到response
中; - 最后,浏览器解析
response
中的响应内容,在页面上生成响应内容.
揭秘DispatcherServlet
在Spring MVC
中,DispatcherServlet
作为一个前端控制器,其负责接收所有的HTTP
请求,同时,将请求分发给相应的处理器(Controller
)来处理,并最终返回响应结果。
DispatcherServlet体系结构
通过上图不难看出,DispatcherServlet
的体系结构可以分为两条脉络,一条以Servlet
体系结构为基础,另一条则以Spring
为基础,其中HttpServletBean
则是两者的一个媒介,进而使得Spring
中的某个Bean
具有Servlet
的相关功能.
但我们并不打算详细分析DispatcherServlet
的体系结构,因为这样容易让我们陷入到具体细节中,不利于全局的视野建立. 所以,我们将精力集中在DispatcherServlet
之上.
在接下来的分析中,请忘记掉上述DispatcherServlet
复杂的继承关系结构,只需要记住:DispatcherServlet
是一个Servlet
. 所以,我们关注的内容聚焦在Servlet
的初始化以及Http
请求的处理上.
DispatcherServlet初始化的秘密
因为DispatcherServlet
是一个Servlet
,所以在DispatcherServlet
对象被创建后,Tomcat
容器会调用其中的init()
方法来完成Servlet
的初始化工作,所以若要分析DispatcherServlet
初始化的秘密,就应该研究其DispatcherServlet
中init()
的方法.
DispatcherServlet
中的init()
方法的逻辑如上图所示,不难发现其中核心方法为initWebApplicationContext
,即初始化一个web容器. 其相关代码如下:
(ps:initWebApplicationContext()
的相关逻辑定义在DispatcherServlet
的父类FrameWorkServlet
之中.)
FrameWorkServlet
#initWebApplicationContext()
public abstract class FrameWorkServlet {
// 成员变量 webApplicationContext 用于保存web容器
private WebApplicationContext webApplicationContext;
// ....省略其他无关方法
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
//获取当前环境中的web容器信息,如果Spring和SpringMVC整合,则不需要重新创建
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
//设置父子容器将Spring容器设为SpringMVC的父容器
cwac.setParent(rootContext);
}
// 容器刷新,将SpringMVC配置文件中的属性加载到容器中
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// ....省略其他无关代码
if (wac == null) {
// 如果当前上下文中没有web容器,则重新创建一个
// 主要针对单独使用SpringMVC时的情况
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// 调用onRefresh方法,从而为容器中初始化一些组件信息
// DispathcerServlet的核心组件都通过此处完成初始化
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
return wac;
}
// ....省略其他无关方法
}
通过FrameworkServlet
的继承体系我们可以看到,其实现了ApplicationContextAware
接口,这使得其具有获取到Spring
中应用上下文(ApplicationContextContext
)的能力. 而FrameworkServlet
的成员变量webApplicationContext
可以将 Servlet
上下文与 Spring
容器上下文进行关联,其实际类型 ConfigurableWebApplicationContext
.
事实上,如果在web.xml
中配置的 ContextLoaderListener
监听器初始化的容器上下文容器信息. 那么Spring
和 SpringMVC
的容器之间便会存在一种父子关系,即 Spring
的容器是父容器,SpringMVC
的容器为子容器.
通过上述分析不难看出DispatcherServlet
在初始化阶段做的最核心工作就是构建容器,然后加载DispatcherServlet
的相关配置信息.
DispatcherServlet请求处理的秘密
在Servlet
的请求处理中,每当有Http
请求到达Servlet
时,都会调用其内部的service()
方法来进行处理,并返回相应的处理结果. 接下来,我们将分析DispatcherServlet
的service()
相关内容,从而分析清楚DispatcherServlet
内部是如何来完成一个请求处理的.
在开始分析之前,我们应该明白当Http
到达DispatcherServlet
时,其首先会调用DispatcherServlet
中的service()
方法,但该方法的具体实现是在FrameworkServlet
中,而FrameworkServlet
的service
方法则会通过super.service()
继续向上调用父类HttpServletBean
中service()
的方法,但HttpServletBean
中并未重写service()
方法,所以只能继续向上找到其父类HttpServlet
中的service()
方法,而此时HttpServlet
中的service()
方法,逻辑如下:
HttpServlet
#service()
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 获取请求方法
String method = req.getMethod();
// 当请求方式为Get时,调用doGet处理请求
if (method.equals(METHOD_GET)) {
doGet(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
}
// ....省略其他无关代码
}
不难发现HttpServlet
中service()
的主要逻辑为:首先,获取到请求方法的类型,然后,调用doXXX()方法(例如doGet、doPost等)来处理请求,而这些方法的处理逻辑在FrameworkServlet
中进行了重新定义,具体如下:
protected final void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 交给DispatcherServlet进行实现
processRequest(request, response);
}
protected final void processRequest(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// ....省略其他无关代码
// doService逻辑交给子类DispatcherServlet实现
doService(request, response);
// ....省略其他无关代码
}
因此,最终的所有处理逻辑都DispatcherServlet
中的doService
进行处理. 此时,请求处理的整个调用过程如下图所示. 所以如果要分析Http
请求在DispatcherServlet
的处理过程,只需要关注方法doService
即可.
Service()
的具体调用关系如下所示.
通过上图可以知道,当一个Http
请求到达DispatcherServlet
后,其会通过内部的service()
方法来完成请求的处理,并最终委托于DispatcherServlet
的doService
方法来进行处理.
(Ps:由于DispatcherServlet
复杂的继承关系,所以service()
的调用过程稍显复杂; 但通过梳理service()
的调用链我们可以知道,所有Http
请求的处理都会委托于DispatcherServlet
中的doService
.)
在doService
中,所有的处理逻辑又会委托于 doDispatch
进行处理,其核心代码如下:
protected void doDispatch(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 省略无关代码....
// 检测请求是否为上传请求,如果是则通过 multipartResolver 将其封装成 MultipartHttpServletRequest对象
processedRequest = checkMultipart(request);
// 获得请求对应的 HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
// 如果获取不到,则根据配置抛出异常或返回 404 错误
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 获得当前 handler 对应的 HandlerAdapter 对象
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 拦截器前置处理逻辑
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 调用 handler 方法,也就是执行对应的方法,并返回视图
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 后置处理 拦截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 处理正常和异常的请求调用结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
// 省略其他无关代码....
}
DispatcherServlet
中的doDispatch
方法是Spring MVC
的核心方法,用于处理HTTP
请求并进行请求的分发。它是根据请求的URL
和请求类型(GET、POST
等)来确定要调用哪个控制器(Controller
)的方法,并处理控制器方法的返回结果,最终生成HTTP
响应,其大致处理逻辑如下图所示:
doDispatch
方法会解析HTTP请求的URL、请求类型、请求参数等信息,以确定要调用哪个控制器的哪个方法来处理请求。doDispatch
会根据请求的URL查找合适的处理器(通常是Controller对象)和处理器方法。这个过程是通过Spring的HandlerMapping
组件来实现的,HandlerMapping
会根据URL和配置的映射规则,找到对应的Controller和方法。doDispatch
会调用该方法,并将HTTP请求的参数传递给方法。Controller方法会执行业务逻辑,并返回一个包含响应数据的ModelAndView
对象。doDispatch
会根据Controller方法返回的ModelAndView
对象,来决定如何处理响应结果。如果返回结果是一个视图(View),doDispatch
会通过ViewResolver
将逻辑视图名称解析为真实的视图对象,然后使用视图对象来渲染生成HTML等内容。doDispatch
将生成的响应内容发送给客户端(通常是浏览器),完成HTTP
响应的过程。DispatcherServlet
中的doDispatch
方法可以根据HTTP请求的信息,找到合适的控制器并调用处理器方法,然后处理返回结果,最终生成HTTP响应。所以一定程度上,可以将doDispatch
认为是Spring MVC
的核心,因此后续我们的讨论都将围绕上图doDispatch
的处理逻辑展开.
总结
本文首先介绍了Servlet
的相关内容,并以此为基础分析了Spring MVC
中的核心组件DispatcherServlet
,在整个过程中,我们所秉持的理念一直都是忽视掉DispatcherServlet
复杂的继承关系,而将其看做是一个Servlet
,进而从Servlet
的角度,并对DispatcherServlet
的初始化和请求处理进行了详细的分析.
不过在整个处理过程中涉及到SpringMVC
中处理请求的组件还没有进行分析,或许你对于许多细节存在疑惑,但不要慌,后续文章将对 SpringMVC
的其他核心组件进行分析. 而DispatcherServlet
中的doDispatch
方法中的处理逻辑将贯彻整个分析过程,这样有利于加深我们对 SpringMVC
的理解,同时能将DispatcherServlet
组件同其他组件串联起来,进而更好的理解SpringMVC
中对于一个请求的处理.