死磕Tomcat系列(5)——容器

死磕Tomcat系列(5)——容器

Posted by 不学无数 on July 8, 2019

死磕Tomcat系列(5)——容器

回顾

死磕Tomcat系列(1)——整体架构中我们简单介绍了容器的概念,并且说了在容器中所有子容器的父接口是Container。在死磕Tomcat系列(2)——EndPoint源码解析中,我们知道了连接器将请求过来的数据解析成Tomcat需要的ServletRequest对象给容器。那么容器又是如何将这个对象准确的分到到对应的请求上去的呢?

容器的整体设计

Container是容器的父接口,所有子容器都需要实现此接口,我们首先看一下Container接口的设计。

public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

Tomcat是如何管理这些容器的呢?我们可以通过接口的设计可以了解到是通过设置父子关系,形成一个树形的结构(一父多子)、链式结构(一父一子)来管理的。一想到树形的结构我们应该就立马能够联想到设计模式中的组合模式,而链式结构我们应该能够想到设计模式中的责任链设计模式。无论这两种的哪一种我们都知道这种关系是上下层级的关系。用图来表示就是如下。

既然是父子的结构,那么连接器是如何将转换好的ServletRequest给到容器的呢?我们可以看CoyoteAdapter中的service方法。因为在连接器中最后一环是将解析过的Request给到Adapter运用适配器设计模式解析为ServletRequest对象。在service方法中我们看到有这么一句。

connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);

而其中的getContainer方法,返回的是Engine对象

public Engine getContainer();

这里看到了PipelinePipeline应该大家有所熟悉,是管道的概念,那么管道里面装的是什么呢?我们看其定义的方法

public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

可以看到Pipeline 管道里面装的是Valve,那么Valve是如何组织起来的呢?我们也可以看它的代码定义

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

可以知道每个Valve都是一个处理点,它的invoke就是相对应的处理逻辑。可以看到有setNext的方法,因此我们大概能够猜到是通过链表将Valve组织起来的。然后将此Valve装入Pipeline中。因此每个容器都有一个Pipeline,里面装入系统定义或者自定义的一些拦截节点来做一些相应的处理。因此只要获得了容器中Pipeline管道中的第一个Valve对象,那么后面一系列链条都会执行到。

但是不同容器之间Pipeline之间是如何进行触发的呢?即例如EnginePipeline处理完了最后一个Valve,那么如何调用HostPipeLine管道中的Valve呢?我们可以看到每个Pipeline中还有一个方法。setBasic 这个方法设置的就是Valve链条的末端节点是什么,它负责调用底层容器的Pipeline第一个Valve节点。用图表示就是这样的。

Engine容器

Engine容器比较简单,只是定义了一些基本的关联关系。它的实现类是StandardEngine

    @Override
    public void addChild(Container child) {
        if (!(child instanceof Host))
            throw new IllegalArgumentException
                (sm.getString("standardEngine.notHost"));
        super.addChild(child);
    }
    @Override
    public void setParent(Container container) {
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notParent"));

    }

需要注意Engine容器是没有父容器的。如果添加是会报错。添加子容器也只是能添加Host容器。

Host 容器

Host容器是Engine的子容器,一个Host在Engine中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这个应用,并且标识这个应用以便能够区分它们。它的子容器通常是Context容器。我们可以看配置文件中也能够看出Host文件的作用。

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

那么Host容器在启动时具体干了什么呢?我们看它的startInternal方法看不出来什么,只是启动了相应的Valve,是因为在Tomcat的设计中引入了生命周期的概念,即每个模块都有自己相应的生命周期,模块的生命周期定义有NEW、INITIALIZING、INITIALIZED、SSTARTING_PREP、STARTING、STARTED,每个模块状态的变化都会引发一系列的动作,那么这些动作的执行是直接写在startInternal 中吗?这样会违反开闭原则,那么如何解决这个问题呢?开闭原则说的是为了扩展性系统的功能,你不能修改系统中现有的类,但是你可以定义新的类。

于是每个模块状态的变化相当于一个事件的发生,而事件是有相应的监听器的。在监听器中实现具体的逻辑,监听器也可以方便的增加和删除。这就是典型的观察者模式。

那么Host容器在启动的时候需要扫描webapps目录下面的所有Web应用,创建相应的Context容器。那么Host的监听器就是HostConfig,它实现了LifecycleListener接口

public interface LifecycleListener {

    public void lifecycleEvent(LifecycleEvent event);
}

接口中只定义了一个方法,即监听到相应事件的处理逻辑。可以看到在setState方法中调用了监听器的触发。

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

所以容器中各组件的具体处理逻辑是在监听器中实现的。

Context 容器

一个Context对应一个Web应用

Context代表的是Servlet的Context,它具备了Servlet的运行的基本环境。Context最重要的功能就是管理它里面的Servlet实例,Servlet实例在Context中是以Wrapper出现的。Context准备运行环境是在ContextConfiglifecycleEvent方法准备的。

@Override
public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }
}

Wrapper容器

Wrapper容器代表一个Servlet,包括Servlet的装载、初始化、执行以及资源的回收。Wrapper是最底层的容器,它没有子容器。

Wrapper的实现类是StandardWrapper,主要任务是载入Servlet类,并进行实例化。但是StandardWrapper类并不会调用Servletservice方法。而是StandardWrapperValue 类通过调用StandardWrpper allocate 方法获得相应的servlet,然后通过拦截器的过滤之后才会调用相应的Servlet的service方法

总结

Tomcat的容器中有许多值得我们学习的设计思想,例如将不变的抽取出来,然后变化的子类来实现的模板设计模式、维护一堆父子关系的组合设计模式、事件的发生伴随监听者的相应动作执行的观察者设计模式等等。

在学习框架的时候,有时没必要深究里面一行一行的代码,而要学习它的思想。知道它是如何运行,随后如果查找问题,或者是对框架进行相应扩展。这时候再深入学习里面的代码将会事半功倍。

参考文章

往期文章

如何断点调试Tomcat源码

死磕Tomcat系列(1)——整体架构

死磕Tomcat系列(2)——EndPoint源码解析

死磕Tomcat系列(3)——Tomcat如何做到一键式启停的

死磕Tomcat系列(4)——Tomcat中的类加载器

一次奇怪的StackOverflowError问题查找之旅

徒手撸一个简单的RPC框架

徒手撸一个简单的RPC框架(2)——项目改造