MiniSpring框架学习-分解 Dispatcher

MiniSpring框架学习-分解 Dispatcher

教程: https://github.com/YaleGuo/minis

极客时间: 手把手带你写一个 MiniSpring

前言:真的有大佬,可以看懂教程,然后跟着教程手搓代码运行吗?我去,我顺序读完还是不通畅,看的头大。只有从入口函数开始DeBug代码,然后回头看教程,有不懂的问GPT,这才能勉强能捋清楚逻辑。不过本节对大杂烩功能进行拆解还是有点意思,对单一职责有一点点感觉了。

09. 分解 Dispatcher:如何把专门的事情交给专门的部件去做?

这一节侧重设计思想:怎么让代码结构更清晰、功能更解耦。核心思路很简单:把专门的事情交给专门的部件去做。

目前 AnnotationConfigWebApplicationContextDispatcherServlet 承担的事情有点多,后面功能一多,就容易变成一个"大杂烩"。所以这一节主要做两类拆分:

  1. 拆容器:把 Web 层的 Controller 容器和业务层的 Service 容器分开。
  2. 拆分发:把 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);
}

这段代码主要做了三件事:

  1. ServletContext 读取 contextConfigLocation
  2. 根据 XML 创建根容器 XmlWebApplicationContext
  3. 把根容器存到 ServletContext 里。

后面 DispatcherServlet 启动时,就是通过同一个 key 把根容器取出来:

java 复制代码
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

这样子容器里的 Controller 才能访问父容器里的 Service。

二、子容器的创建

接下来 Tomcat 会启动 DispatcherServlet。它负责创建 MVC 子容器,这个容器主要放 Controller、HandlerMappingHandlerAdapter 等 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 入口调用的是 serviceHttpServlet 默认的 service 会根据 HTTP 方法再分发到 doGetdoPost 等方法。现在我们直接重写 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 执行处理方法
    -> 写回响应

原来的 urlMappingNamesmappingObjsmappingMethods 这些映射信息,不应该继续散落在 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 里实现还比较简单,但这个拆法已经为后续扩展留好了位置。

六、总结

本节重点就是进一步拆职责:

  1. 父容器管公共业务对象,比如 Service、Dao。
  2. 子容器管 Web 层对象,比如 Controller、HandlerMappingHandlerAdapter
  3. DispatcherServlet 只做统一入口和流程编排。
  4. URL 查找交给 HandlerMapping
  5. Controller 方法执行交给 HandlerAdapter

这样拆完以后,代码结构会更接近 Spring MVC 的真实思路:入口统一,但每个部件只做自己擅长的事。

相关推荐
初夏睡觉1 小时前
数据结构学习之~二叉堆 (P3378 【模版】堆)
数据结构·c++·学习
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第84题】【Mysql篇】第14题:为什么用 InnoDB 存储引擎的表建议用整型的自增主键?
java·开发语言·数据库·mysql·面试
小江的记录本1 小时前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
金銀銅鐵2 小时前
[Java] 用图形化界面演示 iadd, isub, iconst_<i> 指令的效果
java·后端·python
J2虾虾2 小时前
Spring AI Alibaba文档
java·人工智能·spring
z200509302 小时前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
YikNjy2 小时前
break和continue
java·开发语言·算法
SomeOtherTime2 小时前
Geojson相关(AI回答)
java·前端·python