tomcat&jetty笔记1

Tomcat的总体架构

两个核心功能:

  1. 处理socket连接,负责网络字节流和Request和Response对象的转化
  2. 加载和管理Servlet,以及具体处理Request请求

设计了两个核心组件连接器connector和容器container分别做着两件事,container内部处理,connector对外交流。

连接器的设计

Tomcat支持的IO模型:

  1. NIO非阻塞IO,采用java NIO类库实现
  2. NIO2 异步IO,采用jdk7的NIO2类库实现
  3. APR 采用Apache可移植运行库实现,C/C++编写的本地库

Tomcat支持的应用层协议:

  1. HTTP/1 大部分web应用采用的访问协议
  2. AJP 用于和web服务器集成
  3. HTTP/2 2.0大幅度提升了web性能

 

一个容器可能对接多个连接器,但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

Server指的就是一个Tomcat实例。一个Server中有一个或者多个Service,一个Service中有多个连接器和一个容器。连接器与容器之间通过标准的ServletRequest和ServletResponse通信。

连接器

连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,在容器中获取到的都是一个标准的ServletRequest对象。

连接器的功能需求进一步细化:监听网络端口;接受网络连接请求;读取请求网络字节流;根据具体应用层协议解析字节流,生成统一的tomcat request对象;将tomcat request对象转成标准的ServletRequest;调用Servlet容器,得到ServletResponse;将ServletResponse转成tomcat response对象;将tomcat response转成网络字节流;将响应字节流写回给浏览器。

3个高内聚的功能:网络通信;应用层协议解析;Tomcat Request/Response与ServletRequest/ServletResponse的转化。

设计了三个组件实现3个功能,endpoint,processor,adaptor。组件之间通过抽象接口交互,封装变化。

由于I/O模型和应用层协议可以自由组合,比如NIO + HTTP或者NIO2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫ProtocolHandler的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol和AjpNioProtocol。

设计了一系列抽象基类来封装这些稳定的部分,抽象基类AbstractProtocol实现了ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。

EndPoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此EndPoint是用来实现TCP/IP协议的。EndPoint是一个接口,它的抽象实现类AbstractEndpoint里面定义了两个内部类:Acceptor和SocketProcessor。其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)。

Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。Processor是一个接口,定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AJPProcessor、HTTP11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。

EndPoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池去处理,SocketProcessor的Run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会调用Adoptor的Service方法。

Adaptor组件

 

Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的Service方法。

容器的层次结构

Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。父子关系。Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。

Context表示一个Web应用程序;Wrapper表示一个Servlet,一个Web应用程序中可能会有多个Servlet;

Host代表的是一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可以部署多个Web应用程序;Engine表示引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine。

Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。

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);
}

在该接口中有getParent、SetParent、addChild和removeChild等方法。Container接口扩展了LifeCycle接口,LifeCycle接口用来统一管理各组件的生命周期。

请求定位Servlet的过程

Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢?用Mapper组件来完成这个任务的。

Mapper组件的功能就是将用户请求的URL定位到一个Servlet,它的工作原理是:Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径,你可以想象这些配置信息就是一个多层次的Map。

 

Tomcat如何将这个URL定位到一个Servlet呢?

首先,根据协议和端口号选定Service和Engine。

Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。上面例子中的URL访问的是8080端口,因此这个请求会被HTTP连接器接收,而一个连接器是属于一个Service组件的,这样Service组件就确定了。我们还知道一个Service组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine容器,因此Service确定了也就意味着Engine也确定了。

然后,根据域名选定Host。

Service和Engine确定后,Mapper组件通过URL中的域名去查找相应的Host容器,比如例子中的URL访问的域名是user.shopping.com,因此Mapper会找到Host2这个容器。

之后,根据URL路径找到Context组件。

Host确定以后,Mapper根据URL的路径来匹配相应的Web应用的路径,比如例子中访问的是/order,因此找到了Context4这个Context容器。

最后,根据URL路径找到Wrapper(Servlet)。

Context确定后,Mapper再根据web.xml中配置的Servlet映射路径来找到具体的Wrapper和Servlet。

责任链模式

这个查找路径上的父子容器都会对请求做一些处理。连接器中的Adapter会调用容器的Service方法来执行Servlet,最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,依次类推,最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理。那么这个调用过程具体是怎么实现的呢?答案是使用Pipeline-Valve管道。

Pipeline-Valve是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve表示一个处理点,比如权限认证和记录日志。如果你还不太理解的话,可以来看看Valve和Pipeline接口中的关键方法。

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

由于Valve是一个处理点,因此invoke方法就是来处理请求的。注意到Valve中有getNext和setNext方法,因此我们大概可以猜到有一个链表将Valve链起来了。请你继续看Pipeline接口:

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

Pipeline中有addValve方法。Pipeline中维护了Valve链表,Valve可以插入到Pipeline中,对请求做某

些处理。我们还发现Pipeline中没有invoke方法,因为整个调用链的触发是Valve来完成的,Valve完成自己的处理后,调用getNext.invoke()来触发下一个Valve调用。

每一个容器都有一个Pipeline对象,只要触发这个Pipeline的第一个Valve,这个容器里Pipeline中的Valve就都会被调用到。但是,不同容器的Pipeline是怎么链式触发的呢,比如Engine中Pipeline需要调用下层容器Host中的Pipeline。

这是因为Pipeline中还有个getBasic方法。这个BasicValve处于Valve链表的末端,它是Pipeline中必不可少的一个Valve,负责调用下层容器的Pipeline里的第一个Valve。

整个调用过程由连接器中的Adapter触发的,它会调用Engine的第一个Valve:

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter()方法,最终会调到Servlet的service方法。

ServletContext、Context、ApplicationContext的关系

Servlet规范中ServletContext表示web应用的上下文环境,而web应用对应tomcat的概念是Context,

所以从设计上,ServletContext自然会成为tomcat的Context具体实现的一个成员变量。

tomcat内部实现也是这样完成的,ServletContext对应tomcat实现是org.apache.catalina.core.Applica

tionContext,Context容器对应tomcat实现是org.apache.catalina.core.StandardContext。ApplicationCo

ntext是StandardContext的一个成员变量。

Spring的ApplicationContext之前已经介绍过,tomcat启动过程中ContextLoaderListener会监听到容

器初始化事件,它的contextInitialized方法中,Spring会初始化全局的Spring根容器ApplicationContext,

初始化完毕后,Spring将其存储到ServletContext中。

总而言之,Servlet规范中ServletContext是tomcat的Context实现的一个成员变量,而Spring的Applicatio

nContext是Servlet规范中ServletContext的一个属性。

 

如果想让一个系统能够对外提供服务,我们需要创建、组装并启动这些组件;在服务停止的时候,我们还需要释放资源,销毁这些组件,因此这是一个动态的过程。也就是说,Tomcat需要动态地管理这些组件的生命周期。

如果你需要设计一个比较大的系统或者框架时,你同样也需要考虑这几个问题:如何统一管理组件的创建、初始化、启动、停止和销毁?如何做到代码逻辑清晰?如何方便地添加或者删除组件?如何做到组件启动和停止不遗漏、不重复?

组件具有两层关系:第一层关系是组件有大有小,大组件管理小组件,比如Server管理Service,Service又管理连接器和容器;第二层关系是组件有外有内,外层组件控制内层组件,比如连接器是外层组件,负责对外交流,外层组件调用内层组件完成业务功能。也就是说,请求的处理过程是由外层组件来驱动的。

这两层关系决定了系统在创建组件时应该遵循一定的顺序:第一个原则是先创建子组件,再创建父组件,子组件需要被“注入”到父组件中;第二个原则是先创建内层组件,再创建外层组件,内层组建需要被“注入”到外层组件。

最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。但这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。希望找到一种通用的、统一的方法来管理组件的生命周期,就像汽车“一键启动”那样的效果。

一键式启停:LifeCycle接口

设计就是要找到系统的变化点和不变点。这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。而变化点是每个具体组件的初始化方法,也就是启动方法是不一样的。把不变点抽象出来成为一个接口,这个接口跟生命周期有关,叫作LifeCycle。LifeCycle接口里应该定义这么几个方法:init()、start()、stop()和destroy(),每个具体的组件去实现这些方法。在父组件的init()方法里需要创建子组件并调用子组件的init()方法。同样,在父组件的start()方法里也需要调用子组件的start()方法,因此调用者可以无差别的调用各组件的init()方法和start()方法,这就是组合模式的使用,并且只要调用最顶层组件,也就是Server组件的init()和start()方法,整个Tomcat就被启动起来了。

可扩展性:LifeCycle事件

各个组件init()和start()方法的具体实现是复杂多变的,比如在Host容器的启动方法里需要扫描webapps目录下的Web应用,创建相应的Context容器,如果将来需要增加新的逻辑,直接修改start()方法?这样会违反开闭原则,那如何解决这个问题呢?开闭原则说的是为了扩展系统的功能,你不能直接修改系统中已有的类,但是你可以定义新的类。

组件的init()和start()调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动,因此我们把组件的生命周期定义成一个个状态,把状态的

转变看作是一个事件。而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式。

具体来说就是在LifeCycle接口里加入两个方法:添加监听器和删除监听器。除此之外,我们还需要定义一个Enum来表示组件有哪些状态,以及处在什么状态会触发什么样的事件。因此LifeCycle接口和LifeCycleState就定义成了下面这样。

组件的生命周期有NEWINITIALIZINGINITIALIZEDSTARTING_PREPSTARTINGSTARTED等,而一旦组件到达相应的状态就触发相应的事件,比如NEW状态表示组件刚刚被实例化;而当init()方法被调用时,状态就变成INITIALIZING状态,这个时候,就会触发BEFORE_INIT_EVENT事件,如果有监听器在监听这个事件,它的方法就会被调用。

重用性:LifeCycleBase抽象基类

基类中往往会定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。

Tomcat定义一个基类LifeCycleBase来实现LifeCycle接口,把一些公共的逻辑放到基类

中去,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。

LifecycleBase的init()方法实现:

@Override
public final synchronized void init() throws LifecycleException {
    // 1. 检查状态
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }

    try {
        // 2. 触发INITIALIZING事件的监听器
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        // 3. 调用具体子类的初始化方法
        initInternal();
        // 4. 触发INITIALIZED事件的监听器
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

逻辑为:

第一步,检查状态的合法性,比如当前状态必须是NEW然后才能进行初始化。

第二步,触发INITIALIZING事件的监听器 在这个setStateInternal方法里,会调用监听器的业务方法。

第三步,调用具体子类实现的抽象方法initInternal()方法。为了实现一键式启动,具体组件在实现initInternal()方法时,又会调用它的子组件的init()方法。 

第四步,子组件初始化后,触发INITIALIZED事件的监听器,相应监听器的业务方法就会被调用。

 

LifeCycleBase负责触发事件,并调用监听器的方法,那是什么时候、谁把监听器注册进来的呢?

  1. Tomcat自定义了一些监听器,这些监听器是父组件在创建子组件的过程中注册到子组件的。比如MemoryLeakTrackingListener监听器,用来检测Context容器中的内存泄漏,这个监听器是Host容器在创建Context容器时注册到Context中的。
  2. 还可以在server.xml中定义自己的监听器,Tomcat在启动时会解析server.xml,创建监听器并注册到容器组件。

 生命周期管理总体类图

图中出错之处:Container接口应该也继承Lifecycle接口

StandardEngineStandardHostStandardContextStandardWrapper是相应容器组件的具体实现类,因
为它们都是容器,所以继承了ContainerBase抽象基类,而ContainerBase实现了Container接口,也继承了LifeCycleBase类,它们的生命周期管理接口和功能接口是分开的,这也符合设计中接口分离的原则

ContainerBase也是一个骨架抽象类,通用逻辑包括:容器的创建/初始化/销毁;容器添加/删除子容器;如果还要监听容器状态变化的话还需要有添加/移除事件的方法。

Tomcat的高层都负责什么?

通过startup.sh启动tomcat,之后发生什么?

  1. Tomcat本质是个java程序,startup.sh脚本会启动一个jvm运行tomcat的启动类Bootstrap。
  2. Bootstrap的主要任务是初始化tomcat的类加载器,并且创建Catalina。
  3. Catalina是个启动类,通过解析server.xml、创建相应的组件,并调用Server的start方法。
  4. Server组件的职责是管理Service组件,会负责调用Service的start方法
  5. Service组件的职责就是管理连接器和顶层容器Engine,调用连接器和Engine的start方法

这样就完成启动了,Bootstrap初始化类加载器,也就是创造万物的工具。如果tomcat比作公司,Catalina就是创始人,负责组件团队,创建Server和它的子组件。Server是CEO,负责多个事业群,每个事业群就是一个Service。Service是事业群总经理,管理两个职能部门,一个是对外的市场部,也就是连接器组件;另一个是对内的研发部,也就是容器组件。Engine是研发部经理,是最顶层的容器组件。

这些启动类或组件不处理具体请求,任务主要是“管理”,管理下层组件的生命周期,并给下层组件分配任务,也就是把请求路由到负责干活的组件,因此称为高层。

Catalina

Catalina的主要任务就是创建Server,需要解析server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用Server组件的init方法和start方法,这样整个Tomcat就启动起来了。作为管理者Catalina还需要处理各种异常情况,比如当我们通过“Ctrl +C”关闭Tomcat时,Tomcat将如何优雅的停止并且清理资源呢?因此CatalinaJVM中注册一个关闭钩子

public void start() {
    // 1. 如果持有的server实例为空,就解析server.xml创建出来
    if (getServer() == null) {
        load();
    }
    // 2. 如果创建失败,报错退出
    if (getServer() == null) {
        log.fatal("Cannot start server. Server instance is not configured.");
        return;
    }
    // 3. 启动server
    try {
        getServer().start();
    } catch (LifecycleException e) {
        return;
    }
    // 创建并注册关闭钩子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
    // 用await方法监听停止请求
    if (await) {
        await();
        stop();
    }
}

什么是关闭钩子?如果我们需要在JVM关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向JVM注册一个关闭钩子关闭钩子其实就是一个线程,JVM在停止之前会尝试执行这个线程的run方法。下面我们来看看Tomcat关闭钩子”CatalinaShutdownHook做了些什么。

protected class CatalinaShutdownHook extends Thread {
    @Override
    public void run() {
        try {
            if (getServer() != null) {
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
          ...
        }
    }
}

Tomcat的关闭钩子实际上就执行了Server的stop方法,Server的stop方法会释放和清理所有的资源。

Server

Server组件的具体实现类是StandardServerServer继承了LifeCycleBase,它的生命周期被统一管理,并且它的子组件是Service,因此它还需要管理Service的生命周期,也就是说在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,那Server是如何添加一个Service到数组中的呢?

@Override
public void addService(Service service) {
    service.setServer(this);
    synchronized (servicesLock) {
        // 创建一个长度+1的新数组
        Service[] results = new Service[services.length + 1];
        // 将老的数据复制过去
        System.arraycopy(services, 0, results, 0, services.length);
        results[services.length] = service;
        services = results;
        // 启动Service组件
        if (getState().isAvailable()) {
            try {
                service.start();
            } catch (LifecycleException e) {
                // Ignore
            }
        }
        // 触发监听事件
        support.firePropertyChange("service", null, service);
    }
}

并未在一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的Service实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。

此外,Server组件还有一个重要的任务是启动一个Socket来监听停止端口,这就是为什么你能通过shutdown命令来关闭TomcatCatalina的启动方法的最后一行代码就是调用了Serverawait方法。
await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程。

@Override
public void await() {
    ...
    // Set up a server socket to wait on, port=8005
    try {
        awaitSocket = new ServerSocket(port, 1,
                InetAddress.getByName(address));
    } catch (IOException e) {
        ...
    }
    try {
        awaitThread = Thread.currentThread();
        // Loop waiting for a connection and a valid command
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }
            // Wait for the next connection
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (Exception e) {
                    ...
                }
                // Read a set of characters from the socket 
                int expected = 1024; // Cut off to avoid DoS attack
                while (expected < shutdown.length()) {
                    // ...
                    expected += (random.nextInt() % 1024);
                }
                while (expected > 0) {
                    // ...
                }
            } finally {
                // Close the socket now that we are done with it
            }
            // Match against our command string
            boolean match = command.toString().equals(shutdown);
            if (match) {
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else {
                ...
            }
        }
    } finally {
        // Close the server socket and return
    }
}

Service组件

具体实现类为StandardService

public class StandardService extends LifecycleBase implements Service {
    //名字
    private String name = null;
    //Server实例
    private Server server = null;
    //连接器数组
    protected Connector connectors[] = new Connector[0];
    private final Object connectorsLock = new Object();
    //对应的Engine容器
    private Engine engine = null;
    //映射器及其监听器
    protected final Mapper mapper = new Mapper();
    protected final MapperListener mapperListener = new MapperListener(this);

StandardService继承了LifecycleBase抽象类,此外,StandardService还有熟悉的组件,Server、Connector、Engine、Mapper。

还有个MapperListener,tomcat支持热部署,Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,它监听容器的变化,并把信息更新到Mapper中,这是典型的观察者模式。

作为管理角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。我们来看看Service启动方法:

protected void startInternal() throws LifecycleException {
    //1. 触发启动监听器
    setState(LifecycleState.STARTING);
    //2. 先启动Engine, Engine会启动它⼦容器
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }
    //3. 再启动Mapper监听器
    mapperListener.start();
    //4.最后启动连接器, 连接器会启动它⼦组件, ⽐如Endpoint
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

从中可以看出,Service先启动了Engine组件,再启动Mapper监听器,最后启动连接器。内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而Mapper也依赖容器组件,容器组件启动好了才能监听它们的变化,因此MapperMapperListener在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。

Engine组件

Engine本质是一个容器,因此它继承了ContainerBase基类,并且实现了Engine接口。

public class StandardEngine extends ContainerBase implements Engine {
}

Engine的子容器是Host,所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase中,ContainerBase中有这样一个数据结构:

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBaseHashMap保存了它的子容器,并且ContainerBase还实现了子容器的增删改查,甚至连子组件的启动和停止都提供了默认实现,比如ContainerBase会用专门的线程池来启动子容器。

for (int i = 0; i < children.length; i++) {
    results.add(startStopExecutor.submit(new StartChild(children[i])));
}

Engine在启动Host子容器时就直接重用了这个方法。

Engine自己做了什么呢?容器组件最重要的功能是处理请求,而Engine容器对请求的处理,其实就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的。

每一个容器组件都有一个Pipeline,而Pipeline中有一个基础阀(Basic Valve),而Engine容器的基础阀定义如下:

final class StandardEngineValve extends ValveBase {
    public final void invoke(Request request, Response response) throws IOException, ServletException {
        //拿到请求中的Host容器
        Host host = request.getHost();
        if (host == null) {
            return;
        } 
        // 调用Host容器中的Pipeline中的第一个Valve
        host.getPipeline().getFirst().invoke(request, response);
    }
}

这个基础阀实现非常简单,就是把请求转发到Host容器。从代码中可以看到,处理请求的Host容器对象是从请求中拿到的,请求对象中怎么会有Host容器呢?这是因为请求到达Engine容器中之前,Mapper组件已经对请求进行了路由处理,Mapper组件通过请求的URL定位了相应的容器,并且把容器对象保存到了请求对象中。

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/102598296
今日推荐