MiniSpring框架学习-分解 Dispatcher
- [09. 分解 Dispatcher:如何把专门的事情交给专门的部件去做?](#09. 分解 Dispatcher:如何把专门的事情交给专门的部件去做?)
教程: https://github.com/YaleGuo/minis
极客时间: 手把手带你写一个 MiniSpring
前言:真的有大佬,可以看懂教程,然后跟着教程手搓代码运行吗?我去,我顺序读完还是不通畅,看的头大。只有从入口函数开始DeBug代码,然后回头看教程,有不懂的问GPT,这才能勉强能捋清楚逻辑。不过本节对大杂烩功能进行拆解还是有点意思,对单一职责有一点点感觉了。
09. 分解 Dispatcher:如何把专门的事情交给专门的部件去做?
这一节侧重设计思想:怎么让代码结构更清晰、功能更解耦。核心思路很简单:把专门的事情交给专门的部件去做。
目前 AnnotationConfigWebApplicationContext 和 DispatcherServlet 承担的事情有点多,后面功能一多,就容易变成一个"大杂烩"。所以这一节主要做两类拆分:
- 拆容器:把 Web 层的 Controller 容器和业务层的 Service 容器分开。
- 拆分发:把 URL 查找和方法执行从
DispatcherServlet里拆出去。
整体关系可以先记成这样:
text
ServletContext
|
| 保存 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
v
Root WebApplicationContext(父容器)
- Service
- Dao
- 数据源、事务等公共 Bean
^
| parent
DispatcherServlet 创建的 WebApplicationContext(子容器)
- Controller
- HandlerMapping
- HandlerAdapter
父容器由 ContextLoaderListener 创建,主要放公共业务 Bean。子容器由 DispatcherServlet 创建,主要放 Web 层 Bean。子容器可以通过 parent 找父容器里的 Bean,反过来不行。
一、父容器的创建
根据 web.xml 的配置,Tomcat 启动 Web 应用时,会先回调 ContextLoaderListener。这个监听器负责创建根 IoC 容器,并把它放进 ServletContext,方便后面的 DispatcherServlet 取出来当父容器。
这里要注意一个小修正:不是"一个 Tomcat 只有一个 ServletContext",而是一个 Web 应用对应一个 ServletContext 。一个 Tomcat 可以部署多个 Web 应用,所以也可能有多个 ServletContext。
java
private void initWebApplicationContext(ServletContext servletContext) {
// 一个 Web 应用只能初始化一个根容器,重复初始化通常说明 web.xml 配置有问题。
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException("Root WebApplicationContext already initialized");
}
if (this.context == null) {
// context-param 中配置的是根容器 XML,例如 applicationContext.xml。
String contextLocation = servletContext.getInitParameter(CONFIG_LOCATION_PARAM);
if (contextLocation == null || contextLocation.trim().isEmpty()) {
contextLocation = DEFAULT_CONFIG_LOCATION;
}
try {
// 创建根容器,里面主要放 Service、Dao 这类公共 Bean。
this.context = new XmlWebApplicationContext(contextLocation.trim());
} catch (Exception e) {
throw new IllegalStateException("Create WebApplicationContext failed: " + contextLocation, e);
}
}
// 建立双向关联:
// 1. IoC 容器知道自己属于哪个 Web 应用。
// 2. Web 应用也能通过 ServletContext 找到这个根容器。
this.context.setServletContext(servletContext);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
}
这段代码主要做了三件事:
- 从
ServletContext读取contextConfigLocation。 - 根据 XML 创建根容器
XmlWebApplicationContext。 - 把根容器存到
ServletContext里。
后面 DispatcherServlet 启动时,就是通过同一个 key 把根容器取出来:
java
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
这样子容器里的 Controller 才能访问父容器里的 Service。
二、子容器的创建
接下来 Tomcat 会启动 DispatcherServlet。它负责创建 MVC 子容器,这个容器主要放 Controller、HandlerMapping、HandlerAdapter 等 Web 层对象。
java
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// 第一步:从 ServletContext 里取出 Listener 创建的根容器。
this.parentApplicationContext = (WebApplicationContext) this.getServletContext()
.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if (this.parentApplicationContext == null) {
throw new ServletException("No root WebApplicationContext found in ServletContext");
}
// 第二步:读取当前 DispatcherServlet 自己的 MVC 配置文件。
// 注意:这里的 init-param 和根容器的 context-param 名字可以一样,但来源不同。
this.contextConfigLocation = config.getInitParameter("contextConfigLocation");
if (this.contextConfigLocation == null || this.contextConfigLocation.trim().isEmpty()) {
throw new ServletException("Missing servlet init-param: contextConfigLocation");
}
this.contextConfigLocation = this.contextConfigLocation.trim();
try {
// 第三步:创建 MVC 子容器,并把根容器作为 parent 传进去。
this.webApplicationContext = new AnnotationConfigWebApplicationContext(
this.contextConfigLocation, this.parentApplicationContext);
refresh();
} catch (Exception e) {
throw new ServletException("Create DispatcherServlet WebApplicationContext failed", e);
}
}
protected void refresh() {
// 这里的 refresh 是 DispatcherServlet 初始化 MVC 组件,
// 不要和 IoC 容器内部的 refresh 混成一件事。
initHandlerMappings(this.webApplicationContext);
initHandlerAdapters(this.webApplicationContext);
}
子容器创建时,仍然会完成 Bean 的加载和依赖注入。关键变化是:它内部多了一个 parent 引用。
查找 Bean 时,可以理解成这个顺序:
text
先在子容器查找 Controller / HandlerMapping / HandlerAdapter
-> 找不到,再去父容器查找 Service / Dao 等公共 Bean
所以 Controller 里如果依赖 UserService,子容器自己找不到,就会继续到父容器里找。
三、请求分发流程的拆分
上一节里,我们主要是重写 doGet,在里面处理 URL 和 Controller 方法的映射关系。
更准确地说,Tomcat 入口调用的是 service。HttpServlet 默认的 service 会根据 HTTP 方法再分发到 doGet、doPost 等方法。现在我们直接重写 service,就相当于接管了所有 HTTP 方法的统一入口。
java
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 把当前 MVC 子容器放到 request 里,后续处理流程如果需要,可以从这里取。
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webApplicationContext);
try {
doDispatch(request, response);
} catch (Exception e) {
throw new ServletException("Dispatch request failed: " + request.getServletPath(), e);
}
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 第一步:根据 URL 找到 HandlerMethod。
HandlerMethod handlerMethod = this.handlerMapping.getHandler(request);
if (handlerMethod == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 第二步:交给 HandlerAdapter 执行 HandlerMethod。
// 现在主要是反射调用 Controller 方法,以后可以继续扩展参数绑定、返回值处理等能力。
this.handlerAdapter.handle(request, response, handlerMethod);
}
这里的关键不只是"换成重写 service",而是把原来堆在 DispatcherServlet 里的事情拆开:
| 部件 | 负责什么 |
|---|---|
DispatcherServlet |
统一入口,编排请求处理流程 |
HandlerMapping |
根据请求 URL 找到 HandlerMethod |
HandlerAdapter |
执行 HandlerMethod,也就是调用 Controller 方法 |
也就是说,DispatcherServlet 后面只做调度:
text
请求进入 DispatcherServlet
-> HandlerMapping 找处理方法
-> HandlerAdapter 执行处理方法
-> 写回响应
原来的 urlMappingNames、mappingObjs、mappingMethods 这些映射信息,不应该继续散落在 DispatcherServlet 里。它们更适合被封装进 HandlerMapping,反射执行逻辑则交给 HandlerAdapter。
四、为什么要用父子容器?
父子容器最重要的原因是共享和隔离。
text
RootContext
(Service / Dao / 公共 Bean)
^
|
+-----------------+-----------------+
| | |
Admin WebContext App WebContext OpenApi WebContext
(Controller) (Controller) (Controller)
Service、Dao、数据源、事务这些对象通常是公共能力,多个 Web 模块都可能用到,所以适合放在父容器里。
Controller 属于具体 Web 入口,通常跟某个模块或某个 DispatcherServlet 绑定,所以适合放在子容器里。
这样设计之后,依赖方向也更清楚:
text
Controller 可以依赖 Service。
Service 不应该反过来依赖 Controller。
所以子容器可以访问父容器,父容器不用也不应该知道子容器里有哪些 Controller。
五、这里体现了哪些设计思想?
这一节不用强行套设计模式,但几个思想确实很明显。
单一职责原则 :DispatcherServlet 不再负责所有细节。查找交给 HandlerMapping,执行交给 HandlerAdapter,容器创建也分成 Listener 和 Servlet 两条线。
前端控制器模式 :DispatcherServlet 仍然是统一请求入口,所有 Web 请求先进入它,再由它分派给具体 Controller。
策略和适配的味道 :HandlerMapping 可以有不同实现,负责不同的查找规则;HandlerAdapter 可以适配不同类型的 Handler。现在 MiniSpring 里实现还比较简单,但这个拆法已经为后续扩展留好了位置。
六、总结
本节重点就是进一步拆职责:
- 父容器管公共业务对象,比如 Service、Dao。
- 子容器管 Web 层对象,比如 Controller、
HandlerMapping、HandlerAdapter。 DispatcherServlet只做统一入口和流程编排。- URL 查找交给
HandlerMapping。 - Controller 方法执行交给
HandlerAdapter。
这样拆完以后,代码结构会更接近 Spring MVC 的真实思路:入口统一,但每个部件只做自己擅长的事。