浅析tomcat二: 容器默认实现

248 阅读39分钟

本章来自深入剖析Tomcat的阅读心得,供以后的面经和个人复习使用。

1. StandardWrapper

一、请求处理流程

关键组件

  1. Connector: 负责构造好Request和Response,然后连接到一个web容器,进行处理。
  2. Context: 经典的web容器,负责调用Wrapper,提供Mapper, Loader, Realm, Session, Logger等基础组件的能力
  3. Wrapper: Servlet的简单包装,对外提供了 Servlet的 加载(load()), 初始化(init()), 服务(service()), 卸载(unload()) 全流程。

image.png

逻辑执行的入口是 Pipeline 对象。容器一般都有一个配套的阀对象,在容器构造时 就会加入到Pipeline

容器负责高层的处理,组件和子容器的管理,而Valve对象则是真正负责处理请求。

二、Servlet和多线程

经常能听到 "无状态Servlet" 是安全的,Tomcat是如何把多线程技术和Servlet应用的呢?

实际上,这取决于标记接口SingleThreadModel, 由于其名字有误解,被弃用了。

这个接口的真正含义是:Servlet保证 不会有多个线程,访问同一个Servlet实例的service()方法

真正的实现有两种 1. 严格控制Servlet实例只能被一个线程访问 2. 初始化一个Servlet实例池,每个线程持有一个Servlet实例。


// 1. 严格同步的Servlet实现: 真正意义上的 SingleThreadModel

Servlet servlet = getContainer().allocate();

// 如果实现了标记接口 SingleThreadModel 则在执行service()时进行控制
if (servlet instanceof SingleThreadModel) {
    synchronized(servlet) {
        servlet.service();
    }
}

// 2. 高性能的实现: 只保证 一个Servlet实例严格被一个线程调用

// 这个模型里,如果每个Servlet实例都访问同一个静态区域,可能导致线程不安全。
ServletPoll pool = xxx; // 实际实现上,是一个Stack<Servlet>

if (servlet instanceof SingleThreadModel) {
    // 从对象池里取Servlet,如果取不到就等待
    synchronized(pool) {
        if (能取到对象) {
            return pool.pop();
        } else {
            // 阻塞等待
            pool.wait();
        }
    }
}

注意: 这里的线程不安全,指的是Servlet访问外部暴露的数据。如果Servlet本身是有状态的,该状态是线程安全的,因为 该模型下,每个线程都持有单独的Servlet实例,这个实例是单线程的。

总结下就是,SingleThreadModel只保证 Servlet自己的状态是线程安全的,如果Servlet访问外部的共享变量,是不安全的,除非这个共享变量本身支持线程安全的访问。


回顾: Connector会维护一个Processor实例池,每个Processor实例都在自己的线程中。

考虑 请求A和请求B都想获取 同一个Servlet,那么入口是 HttpProcessor#process(),这个方法会调用connector.getContainer().invoke(request, response);,将请求交给Context的pipeline处理。pipeline最终会执行到 StandardWrapperValve#invoke()

因此,肯定存在一个时刻,多个线程同时执行相同的Servlet的service方法。如果Servlet不实现SingleThreadModel接口,HttpServlet#service可能被多个线程访问,可想而知有线程安全问题。

所以,如果让Servlet是线程安全的,就得把Servlet设计为无状态的。

三、Wrapper是如何维护Servlet实例的

Wrapper负责 Servlet对象 从部署包里的字节码,到真正运行执行的全流程控制,那么具体是如何控制的呢?

  1. 如果Servlet没实现 SingleThreadModel 接口,直接从loadServlet()方法获取。
  2. 否则 从 ServletInstancePool里获取,这个Pool被实现为一个Stack。

那么,Servlet的载入过程具体是什么?

  1. 如果是非 SingleThreadModel,并且已经载入过了,直接返回实例。
  2. 否则开始Servlet的载入逻辑

2.1 获取Servlet的类加载器

image.png

2.2 根据 (2.1) 获取到的classloader,结合Servlet的全限定名,进行加载,得到class对象

tomcat的设计中,如果需要运行时加载一些组件,一般都是传入全限定名

2.3 通过 (2.2) 得到的class对象,进行实例化,fire一个BEFORE_INIT_EVENT, 并调用初始化方法。

实例化之前会安全检查,只有通过了才能实例化成功。

Tomcat支持 Servlet感知其所在的容器环境,即Servlet可以调用Wrapper的能力。如果我们声明自己的Servlet是这样的Servlet,实例化期间,Tomcat会将Wrapper注入到Servlet实例中。

2.4 初始化的过程

image.png

如果是STM的 Servlet,初始化后会把实例放到 ServletPool里。这个Pool可以视为 多个Servlet实例的副本。这种多线程下访问对象实例副本的 "类似原型模式"的设计,是经典多线程编程设计。

四、解密Servlet的初始化流程

Servlet的初始化需要传入一个 ServletConfig

实际上,StandardWrapper自己就实现了 ServletConfig接口。但是我们观察,StandardWrapper给Servlet的init()方法传入的是 new StandardWrapperFacade(this);

这里又用Facade,原因不必多言。

主要是避免老六把servletConfig实例强转为 StandardWrapper, 直接访问容器细节

但是,前面我们提到一种Servlet,可以知道容器细节,Wrapper在初始化阶段会把this传给Servlet。

这两个是不一样的,前者是一种规范的做法,我们看这个方法的实现

private boolean isContainerProvidedServlet(String classname) {

    if (classname.startsWith("org.apache.catalina.")) {
        return (true);
    }
    try {
        Class clazz =
            this.getClass().getClassLoader().loadClass(classname);
        return (ContainerServlet.class.isAssignableFrom(clazz));
    } catch (Throwable t) {
        return (false);
    }

}

不难发现,tomcat只给内部的Servlet开后门,内部的Servlet可以感知容器细节(得到的就是StandardWrapper),但是对于程序员而言,得到的只是StandardWrapperFacade。

这个StandardWrapperFacade 只暴露了 ServletConfig接口的方法实现,当然这个实现就是委托给config对象完成。

该接口提供了四个方法,不难理解是为了让容器感知Context对象的存在、

  1. getServletContext

会调用其父容器(context)的getServletContext()。熟悉tomcat的应该已经猜到了,context会返回一个Facade对象。

  1. getServletName

  2. getInitParameter

tomcat可以为容器注入一些初始化参数,这些参数会保存在wrapper实例的一个hashmap里。

要注意,一个Wrapper里的servlet可能在多线程内访问,所以对这个hashmap的访问需要上锁。

  1. getInitParameterName

被保存在枚举中

五、Wrapper父子容器关系

Wrapper作为容器的最底层,不能再有子容器,所以其addChild方法会直接扔出一个异常

Wrapper的上层容器只能是Context,所以其setParent方法会校验传入的参数是不是Context的实例。

六、Wrapper的业务逻辑在请求中执行。

说了这么多,Wrapper给我们的印象仍然是一个容器,那么Wrapper是怎么在请求期间被访问的呢?

回顾: Wrapper在初始化时(构造函数), 会把StandardWrapperValve加入到其父容器的pipeline里。所以这个Valve的功能就代表了wrapper处理请求的逻辑。

学习tomcat时,我们知道JavaEE的三大组件是 Servlet, Filter和Listner。Servlet和Filter在 StandardWrapperValve实现。tomcat既然作为web容器,务必要对这些组件提供支持。

image.png

七、Tomcat对Filter的支持。

  1. Filter可以进行配置,配置后的Filter会被收集为FilterDef对象。

这个对象就是Filter的配置对象,包括Filter的名字,配置的参数信息等。

  1. ApplicationFilterConfig

负责管理Filter的生命周期

Filter加载流程

2.1: 当前filter是不是org.apache.cataline包下的实例,如果是则调用加载 ApplicationFilterConfig的类的加载器来加载。

2.2: 如果是用户自定义的Filter,则直接用Wrapper容器的Loader加载。

2.3: 反射实例化出filter实例

2.4 调用filter的初始化方法进行初始化。

  1. ApplicationFilterChain

会根据createFilterChain传进来的所有过滤器,依次执行doFilter(),如果没有了,就执行servlet.service()

八、总结

StandardWrapper, StandardWrapperValve两个类,实现了Servlet容器的基本功能。

前者负责管理Servlet对象的全生命周期,后者则提供了 javaEE三大件中,Servlet和Filter的支持。

2. StandardContext类

Wrapper只负责其对应的Servlet的加载, 实例化, 初始化, 引用计数维护, 析构, 卸载维护。但是Context要考虑的就很多了。

这一章,我们从StandardContext的生命周期分析

一、构造StandardContext(instantiation)

我们简单区分下实例化和初始化,这里通过new或者反射获取一个对象,并调用对象的构造函数,这个过程我们叫做实例化。

尽管构造函数底层是"初始化器",我们仍然认为构造过程属于实例化阶段。而初始化则是通过额外配置的init-param属性等、

同样,在构造期间,StandardContext会把配套的阀对象(StandardContextValve)设置为pipeline的基础阀(BasicValve)

二、StandardContext: start

这个过程非常庞大,需要初始化所有的子容器和组件。

  1. 配置(交给其他组件完成,通过事件回调。)

这里留几个疑惑点

  1. 谁监听了BEFORE_START_EVENT事件,进行相关配置,配置了什么?

image.png

最后,我们也能看到事件回调的好处,配置工作和context的start工作同时进行。

三、invoke()

context#invoke() 就是真正做事的方法了。

如果有HostContainer,则由Host调用,否则由Connector调用。

调用链

  1. StandardContext#invoke()
  2. ContainerBase#invoke()
  3. pipeline.invoke()
  4. basicValve.invoke()
  5. StandardContextValve#invoke()

该方法检查 是否处于类重载阶段,如果在重载,则等一会。 是否重载由变量paused 决定。

invoke的真正实现位于 StandardContextValve

四、Tomcat的Mapper体系

  1. ContainerBase#addDefaultClass(className)

该方法负责把Mapper类加载进来, 并进行基础的配置,然后实例化之。

tomcat的很多方法,在加载类的时候,都是通过全类名。

好处在于,tomcat实际上覆盖了大多数加载点,这些类在加载时,可以用tomcat构造好的类加载器。这些加载器一般都是定制化的。

  1. StandardContext#start()在启动阶段,会调用 ContainerBase#addDefaultClass(className),此时Mapper被加载进来。

不难理解,这里className是一个常量,名字就是org.apache.catalina.core.StandardContextMapper

所以,在容器启动时,StandardContext把加载委托给父类方法,父方法根据传入的默认Mapper类进行实例化。

  1. ContainerBase#addMapper

注意,这里会把mapper关联到this上,如果是StandardContextMapper对象,会检查传入进来的容器是不是Context,如果不是则报错。

image.png

  1. StandardContextMapper#map()

根据请求对象,找到对应的Servlet(Wrapper)

其实现就是解析请求行,然后找ServletName对不对上

public Container map(Request request, boolean update) {

    // 1. 找到项目的根路径, 请求url和相对url
    // 比如根路径是 /demo, 请求url是 /demo/hello, 那么relativeURI就是/hello
    String contextPath =
            ((HttpServletRequest) request.getRequest()).getContextPath();
    String requestURI = ((HttpRequest) request).getDecodedRequestURI();
    String relativeURI = requestURI.substring(contextPath.length());

    // Apply the standard request URI mapping rules from the specification
    Wrapper wrapper = null;
    String servletPath = relativeURI;
    String pathInfo = null;
    String name = null;

    // 2. 开始匹配
    // 2.1 精准匹配
    if (wrapper == null) {
        if (!(relativeURI.equals("/")))
            // 根据uri模式 https://blog.csdn.net/chenwiehuang/article/details/52275396
            // 这个模式就是 web.xml里配置的 <url-pattern> 标签
            // 这个标签会关联到一个servlet-name,那么url-pattern和servlet-name的映射就存在这个ServletMapping里
            name = context.findServletMapping(relativeURI);
        if (name != null)
            wrapper = (Wrapper) context.findChild(name);
        if (wrapper != null) {
            servletPath = relativeURI;
            pathInfo = null;
        }
    }

    // 2.2 前缀匹配, 这里如果路径信息在后面没找到,比如请求/demo/aaa, 这个aaa没有配置,就会路由到/*对应的servlet
    if (wrapper == null) {
        servletPath = relativeURI;
        while (true) {
            name = context.findServletMapping(servletPath + "/*");
            if (name != null)
                wrapper = (Wrapper) context.findChild(name);
            if (wrapper != null) {
                // 找到请求路径的具体路径
                pathInfo = relativeURI.substring(servletPath.length());
                if (pathInfo.length() == 0)
                    pathInfo = null;
                break;
            }
            int slash = servletPath.lastIndexOf('/');
            if (slash < 0)
                break;
            servletPath = servletPath.substring(0, slash);
        }
    }
    // 2.3 精准匹配
    if (wrapper == null) {
        int slash = relativeURI.lastIndexOf('/');
        if (slash >= 0) {
            String last = relativeURI.substring(slash);
            // 古早的tomcat风格的url里,存在 xxx.yyy这样的格式
            int period = last.lastIndexOf('.');
            if (period >= 0) {
                String pattern = "*" + last.substring(period);
                name = context.findServletMapping(pattern);
                if (name != null)
                    wrapper = (Wrapper) context.findChild(name);
                if (wrapper != null) {
                    servletPath = relativeURI;
                    pathInfo = null;
                }
            }
        }
    }

    // 2.4 默认匹配
    if (wrapper == null) {
        name = context.findServletMapping("/");
        if (name != null)
            wrapper = (Wrapper) context.findChild(name);
        if (wrapper != null) {
            servletPath = relativeURI;
            pathInfo = null;
        }
    }

    if (update) {
        request.setWrapper(wrapper);
        ((HttpRequest) request).setServletPath(servletPath);
        ((HttpRequest) request).setPathInfo(pathInfo);
    }
    return (wrapper);

}

五、Tomcat的重载机制

StandardContext 通过 loadable 变量,和WebAppLoader类进行双向绑定。如果loadable从true->false,则关闭reload线程,否则开启。

简单来说,WebAppClassLoader#modified() 根据时间戳和一个默认的 "认为更新时间" 判断该类有没有被修改。

WebAppLoader类是一个线程

public void run() {

    // Loop until the termination semaphore is set
    while (!threadDone) {
        threadSleep();
        if (!started)
            break;
        try {
            if (!classLoader.modified())
                continue;
        } catch (Exception e) {
            continue;
        }
        notifyContext();
        break;
    }

如果检查到类发生了修改,则通知给Context对象。

protected class WebappContextNotifier implements Runnable {

    /**
     * Perform the requested notification.
     */
    public void run() {
        ((Context) container).reload();
    }

}

当然,最终就是单独启一个线程,调用StandardContext#reload

这个方法的实现,就是把能停的组件都停了,然后再启动一遍。启动过程中自然会触发类的加载。

六、tomcat5对线程工作的优化

由于组件有创建线程的需求,tomcat5将这个过程封装成一个方法。

七、总结

由于tomcat组件化和父子容器的设计,通篇大量使用组合技术。因此一个上层容器的初始化,会导致大量的组件初始化。此时生命周期就起到了直观作用。

另外,tomcat的初始化和配置交给不同的类来做,配置对象完成配置后,异步设置context实例的configured变量,异步解耦。

最后,业务逻辑实现在配套的valve对象,整个入口是invoke()方法,我们也见识到了相比Wrapper在web处理上最重要的组件: mapper的简单实现。

当然,tomcat的多数组件都需要一个线程来干自己的工作,tomcat也对其进行了封装。


3. Host和Engine

首先带着疑问去读这章

  1. Context这个容器已经具备了请求分发,session管理,以及大多数web请求处理的能力,为什么需要Host
  2. 目前容器父子层级(理解tomcat用组合实现这样的层级关系)为: Engine->Host->Context->Wrapper,Engine,Host,Context的层级关系具体是什么,相比Context增加了哪些功能?

和分析Context一样,我们按照

  1. Host的继承体系
  2. Host的实例化
  3. Host的start()方法
  4. Host在invoke()的时候干了啥,来分析。

一、Tomcat的Host体系

ctrl + alt + u 展示类的diagram

换汤不换药的组件还是那几个

  1. Pipeline, 充当执行链的组件
  2. Lifecycle,提供统一的生命周期管理和事件传播机制
  3. ContainerBase, 其invoke()会调用pipeline.invoke(),这个在context也讨论过。同时集成了父子容器管理的功能。

image.png

Host接口:

主要是 别名(alias)的管理,部署(deploy)相关的管理,绑定默认Context,还有最重要的map()方法。根据请求对象找到一个处理器。

我们回顾下,之前不是有一个StandardContextMapper实例吗,为什么这里有需要一个新的Mapper,新Mapper做了什么?

alt + 7 查看类的所有方法

image.png

StandardHost类:

作为默认的Host实现

  1. 实例化

不用多想,肯定是把某个valve加到pipeline里了。

image.png

  1. start()方法

image.png

  1. invoke()方法

Host会直接用ContainerBase#invoke()执行逻辑,而其父类的invoke()只是调用pipeline.invoke()

  1. map()方法。

容器start()阶段,会根据指定的mapperClass 的类全限定名,注册一个Mapper实例。

private String mapperClass =
    "org.apache.catalina.core.StandardHostMapper";

可以看到这个类是 StandardHostMapper,我们猜测这个Mapper只能绑定到Host容器上

image.png

那么,这个map的作用就是: 由于同一个Host 可能有多个Context, 每个context都有自己的StandardContextMapper组件,我们说这个组件就是根据请求行和配置文件进行Servlet的分发。所以StandardHostMapper实际上就是多加了一层分发到具体的context的逻辑。

image.png

但是map方法的实际入口是一个携带两个参数的map,这个map会把map方法委托给StandardHostMapper, 然后StandardHostMapper简单调用StandardHost的map方法。

这个设计在之前也比较常见,也就是组件作为调用的入口,真正的实现在容器对象上。可以理解,组件可能有很多实例,可以通过get/set进行插拔,但是一些通用逻辑放在哪里呢? tomcat的设计者把这些通用的逻辑放在了组件所在的容器实例上。

比如map的实现放在Host上,其Mapper组件会调用该方法。

回顾下StandardHost#start(), 最后一步调用了super.start(), 而ContainerBase#start()里调用了addDefaultMapper, 负责把mapperClass反射加载进来。

但是StandardContainer#start()却不是这个逻辑,它完全的重写了父类的start()。

所以,尽管父类提供了start的很多模板流程,但如果子类需要更大的定制空间,还是需要把start完全重写的。相反如果子类需要父类提供的能力就行,那么可以把重复的操作委托给父类实现。

image.png


二、StandardHostValve对象

  1. 通过map方法找到真正要访问的context 容器
  2. 进行前置处理,比如调用session.access()刷新session对象的lastAccessTime
  3. 调用context容器的invoke()方法处理。

三、Host的意义

实际上,一个Host就是一个虚拟主机。

这里不引入额外的复杂概念,简单理解就是。

虚拟主机,在tomcat的语义下,就是一台物理机,配置多个域名,不同域名会在http请求体里携带Host请求头,而tomcat正是根据该请求头分发到指定的Host对象

好处就是一台主机可以配置多个域名,但现在其实也不太缺计算资源,而且现在推崇云原生,也别管是不是一个物理机了,反正只要是物理机的一部分,都可以视为整个物理机。

比如我们用8个1c2g的机器弄一个集群,可以得到最多8c16g的基础设施池。我们可以管这个池子要一个1c1g的资源,作为虚拟主机HostA的部署,再要一个2c2g的部署HostB。

云原生环境提供的资源隔离能力,肯定比tomcat原生提供的虚拟主机能力要强。简单概括,tomcat就是单纯的把请求分发给指定的context,通过请求体的Host头

Tomcat部署及虚拟主机配置与优化_tomcat安装及配置教程虚拟机-CSDN博客

所以,见上文。我们可以配置多个Host,可以让tomcat在同一个物理机上提供针对不同host的服务。

这里又有一个八股文: 我们知道tomcat把多个context都加载进来,如果contextA依赖某个第三方库的A版本,contextB依赖B版本,我们知道如果两个context容器都采取双亲委派机制,肯定是不行的,所以每个context都有自己的Loader。


四、Engine体系

对于Engine, StandardEngine, StandardEngineValve 这几个类型,换汤不换药的在Host上又包装了一层。

Engine的职责自然就是分发找到具体的Host来执行处理。不多分析。

可以简单看下Engine#start()更是大道至简,全交给ContainerBase#start()来做了。

/**
 * Start this Engine component.
 *
 * @exception LifecycleException if a startup error occurs
 */
public void start() throws LifecycleException {

    // Log our server identification information
    System.out.println(ServerInfo.getServerInfo());

    // Standard container startup
    super.start();

}

Engine#invoke()更是没实现,直接交给ContainerBase#invoke()来做了。

StandardEngineValve#invoke()

image.png

可以看到,Engine就是加了一个map层,根据请求找Host, 我们看下具体的实现。

这个Mapper对象,也不难理解,肯定就是通过mapperClass反射加进来的。其类名肯定就是StandardEngineMapper

StandardEngineMapper#map

image.png

可以看到,Engine是根据 request.getServerName()分发具体的Host的。

我们之前说这个其实是请求头Host的值,怎么验证究竟是不是呢?

image.png

这个值其实是 HttpRequestBase的一个成员,只提供了set方法。没找到何时被调用的。

我们回顾下,这个字段肯定是在解析请求报文的时候被设置,那么我们之前提到,Connector有一个Processor组件负责处理请求报文。因此我们去Processor看下。

破案了,在HttpProcessor处理请求头的过程中,不难看到,的确是根据Host这个key的值,设置了serverName,这也证实了我们之前的猜想,

Engine可以根据 请求头的host值,分发到具体的Host对象,这里每个Host对象都是一个虚拟主机。

image.png

五、总结

简而言之,Host对应一个虚拟主机,可以认为Host是 域名 到 具体的Context对象的映射。

而Engine则是 物理机到虚拟主机的映射,可以认为Engine是 ip地址 到所有域名的映射。

但是在目前云原生的环境下,感觉也没人会用tomcat提供的虚拟主机能力。

六、扩展:

目前为止,我们都是根据http协议实现了一个主机多个域名,如果协议主体是https呢?

这个的实现叫做 SNI,可以先看下其定义,再看下其用法

一个用途是,

服务器名称指示 - 维基百科,自由的百科全书 (wikipedia.org)

说说SNI Proxy | 天天の記事簿 (ttionya.com)

利用域前置技术绕过GFW - Gulut's Blog


4. 服务器组件

组件可以通过拼拼图的方式,组合成一个high-level的更大组件,而Server就是这样的组件。

  1. Server接口

image.png

  1. StandardServer默认实现

2.1 组件生命周期

实例化 -> 初始化(initialized?) -> 开始(start?) -> 结束(stop, !start)

组件的生命周期由 两个变量和三个方法控制。如果是可配置的组件,比如Context,其生命周期还有configured状态。

在开始和结束阶段,会通过Lifecycle的事件机制,对外发送事件。

image.png

2.2 关机机制

await()是一个阻塞调用,负责两件事

  1. 开始端口监听请求(默认在8005)端口
  2. 阻塞等待输入,如果输入为shutdown指定的字符串,则退出。

image.png

退出后,会进入组件生命周期的stop()阶段

image.png

  1. Service接口
  1. Service本身依托一个Container, 即一个Service关联一个Container
  2. Service本身依托一个Server, 即一个Service关联一个Server,但一个Server有多个Services
  3. Service下可以管理多个Connectors

image.png

  1. StandardService实现

4.1 学习组合代替继承的实现

Tomcat把所有对监听器(Listener)的管理 和 事件发布 都抽取成一个 lifecycleSupport 来做。

组件通过继承Lifecycle接口,通过new LifecycleSupport(this) 完成双向绑定,相关方法都委托给LifecycleSupport来做。

这个逻辑其实仔细想一下,tomcat的lifecycle其实是两部分

  1. 真正的生命周期,也就是下图红框的部分
  2. 事件监听回调相关,也就是其他部分。

tomcat设计的时候,把他俩混在一个接口里,其实硬要说的话,"生命周期阶段的状态转移产生的事件"也能算成是生命周期管理的一部分,但是感觉拆开更符合单一职责。

而tomcat确实是这么设计的,生命周期流转的方法,start(), stop() 确实通过继承实现,而事件的相关管理,则通过组合实现,事件通知的逻辑写在另一个类LifecycleSupport里

这样,接口的部分方法继承实现,符合单一职责,而另一部分通过组合实现,也符合单一职责。尽管定义上看上去不是单一职责的,但是实现却是单一职责的,所以设计模式不是一成不变的东西。

image.png

image.png

image.png

Service本身生命周期流转的方法还是start, stop, initialize()

同样有两个变量started, initialized 表示当前的生命周期。

image.png

4.2 Service的绑定。

  1. 和Container进行绑定

这里就能看出生命周期接口的好处了

可以让一些操作和生命周期关联,比如 绑定容器 = 容器初始化,解绑容器 = 容器销毁。同时让整个业务逻辑变得很清晰。

image.png

  1. 和Server绑定

这里倒没啥说法,单纯把自己设置为server的一个属性,当server触发生命周期流转时,就会带着自己一起流转。

image.png

  1. 和Connector绑定

这里还是几点

  1. 由于tomcat每个线程处理请求,Service可能是并发访问的,需要进行加锁。
  2. 添加/移除 即 组件启动/销毁,这一点用生命周期接口可以清晰可见。

image.png

5. digester库

这一章主要讲了下xml是咋被解析的。

javaSE基本用不到的地方就是

  1. ClassLoader 加载类和资源文件
  2. 解析资源文件,一般资源文件就是一些配置,将配置转换为对象。

无论是tomcat,还是spring这些框架,大量用到了类加载,资源加载,反射,注解这些东西,建议花点功夫学习下

(1) ClassLoader的原理,简单实现一个能加载任意磁盘路径的,如果遇到自己的类就优先加载的ClassLoader

(2) 能加载任意路径下资源文件的 getResource, findResource, getResources, findResources, 四个方法


tomcat使用Digester库对xml进行解析,这个是apache维护的一个xml解析库。我们知道xml其实是一个DOM文档,而且是一个很强的描述语言,xml能描述的信息非常多,但是冗余性也比较高。

那么,xml如何将DOM文档解析为对象呢,主要有几个设计

  1. 元素定位: 如果是外层元素,就是该元素的标签名,如果存在标签嵌套,那么就是parentTag/childTag,即分隔符是/
  2. 规则: 规则指的是,对于某个元素标识,当解析到该元素时,会触发哪些回调,比较常用的如

image.png

image.png

  1. 内部栈

由于xml存在嵌套关系,digester维护了一个内部栈,用于辅助对象的创建,好比我们用hashmap辅助建树。在创建一些复杂结构的时候,当然需要额外的数据结构辅助创建。

有了这三把利器,基于简单的XMLReader解析,可以把xml文件解析为一个个的java类。


  1. Tomcat的内部配置类ContextConfig

该类实现了 LifecycleListener 接口,既然如此,他肯定负责监听一类事件的回调,我们从两点分析

  1. 这个类何时把自己加入到EventContext, 也就是其所在容器的lifecycleSupport类里?
  2. 这个类对什么事件感兴趣,具体的行为是什么?

对于2, 我们可以猜测其行为,就是根据web.xml进行配置的初始化。

对于第一点,我们可以认为,在Context对象创建之初,就为其绑定一个用于处理配置的监听器对象

该方法位于 org.apache.catalina.startup.Embedded.java

image.png

然后是该组件的触发逻辑, 前面提到过,组件&容器的生命周期统一用Lifecycle接口控制。那么流程是

Server#start -> Service#start -> ContainerBase#start

回顾Context#start, 这个容器的start有一套自己的流程,在容器启动时,会发出一个START_EVENT,如果contextConfig 收到该事件,就会完成配置解析操作,并把容器的configured设置为true,container进一步检查该值是否为true,判断是否成功完成配置工作。

这里根据事件类型进行分发。由此可见,该监听器只对START_EVENTSTOP_EVENT感兴趣。

image.png

ContextConfig#start()中,完成了大量配置,我们只关心defaultConfig()applicationConfig以及对阀的基本配置。配置后会将其所在容器的configured设置为true。

image.png

defaultConfig()查找的配置路径是conf/web.xml, 这里就涉及到java的资源定位解析机制。我们需要自己定义好哪里是存放资源的路径,以方便项目在配置阶段进行查找。

得到配置的InputStream对象后,就可以调用webDigester 进行解析,整个过程是加锁的。

至于解析了那些配置,就按照tomcat官方提供的内容,能配置的内容就能被解析。

image.png

applicationConfig 会在WEB_INF/web.xml 寻找,这里我们看到了经典的getResourceAsStream,这个方法基本上就是在类路径下进行资源查找。

也能看出两种区别,java的资源定位无外乎两种: 类路径下的,以及任意路径下的(一般都是 xxxHome下的,比如TOMCAT_HOME)。 所以,很多时候要我们配置环境变量,就是为了项目启动时可以找到相关资源(主要是配置)的路径

image.png

注意,这里说的是,针对Context容器进行的配置类ContextConfig,也就是web.xml 主要用于配置Context容器。

如果留意过tomcat的应用程序目录,可能会注意到还有conf/server.xml的类似配置


解析web请求中的"上下文"

6. 关闭钩子

  1. 触发时机

可以用于在

  1. 系统正常退出: 最后一个非守护进程退出
  2. 用户中止(收到SIGINT信号),或者前台进程退出(可能收到SIGHUP信号)。

总之,要么正常退出,要么收到某个信号。要是断电或者系统崩溃,就不会触发。这里理解关闭钩子本质上就是一个jvm层面的信号处理器比较关键。

  1. 用途

自然是清理一些资源,我们可能会疑惑,jvm进程都销毁了,进程空间都释放了,栈也清空了,还释放啥资源?

这里指的是进程和OS交互的资源,比如文件锁,我们希望程序退出后,要销毁创建的文件锁。就可以用shutdownHook

  1. Tomcat的shutdownHook

只是确保Server.stop一定会被调用。Server是一个大对象,里面的一些操作,比如SessionManager可能涉及到一些OS资源的分配,所以这里也是保证能安全释放。

image.png

当然,这里也是做了幂等处理,比如刚好stop结束,此时jvm进程还没退出,然后收到了用户的sigint信号,stop重复调用收到状态started的控制

image.png

7. 启动tomcat

这一章,将分析tomcat的入口,为什么可以通过某些脚本启动tomcat。这里我们不止要学习tomcat是如何启动的,也要分析其他的类似的启动脚本的实现,以后我们自己的服务也可以用脚本实现启动。

另外,很多流水线也可以构造 构建脚本,这些脚本一般都是shell脚本。

和启动相关的代码大多数位于 org.apache.catalina.startup 包下。


回顾

  1. tomcat所有组件的最高层组织结构就是 Server对象。

因此,Catalina.java 负责该配置的解析,以及Server对象的构建。

一、Catalina.java

Catalina负责启动的主要逻辑,好比我们的操作系统。然而,我们不直接通过main()启动,而是通过引导类Bootstrap 进行启动。好比在进入OS前的 BIOS做的工作。

好处在于,不同的Bootstrap可以提供不同的启动方式。

image.png

这里catalina.homecatalina.base 如果不设置,就是user.dir的别名。可以理解为用户会在哪里调用Java命令。

catalina.java一般通过脚本启动,因此会解析命令行参数

image.png

execute() 会根据指定的参数是-start 还是-stop 调用同名方法。

简单而言,start()分三步

  1. 解析配置,构造Server对象
  2. 初始化并启动Server对象,启动成功则阻塞等待关机命令
  3. 如果收到关机命令,就执行销毁。

image.png

image.png

回收上文,我们说tomcat的目录下存在conf/server.xml,那么这个是啥时候被解析的呢?

可以看到,digester正是对这个文件进行了解析。

简单而言,就是把文件的一堆东西塞到Server里。

image.png

这个配置也很繁杂,理清容器和组件之间的属性嵌套关系会好很多

比如,Service下存在多个Connector, 一个Container,一个Server。

每个Connector下又存在一个ConnectorFactory, 每个Host下存在HostConfig, Valve等,我们需要根据嵌套的规则进行对象创建,属性注入,组件关系设置等一系列行为。只要配置文件可以配置的,解析类都应该解析到。

二、Bootstrap.java

简单分两步

  1. 创建三个类加载器
  2. 执行Catalina#main

这里就涉及到 tomcat的类加载机制。

Tomcat 的类加载机制_tomcat类加载机制-CSDN博客

我们说,ClassLoader无非关心两点, 类在哪里(字节码文件的仓库),资源在哪里(资源和类本质上都是二进制字节流,只不过具体职能不同,我们有意区分开)。

所以,classLoader主要在意: 在哪里找字节码,在哪里找配置。同时还关心: 找不到的话是直接ClassNotFound还是委托给父亲找? 什么样的类是我先自己找,然后再让父亲找?

tomcat的类加载器类型为 StandardClassLoader。对于类加载器,我们主要关注

  1. findClass, findResource,先不管别人,这两个方法定义了我们自己如何找类找资源
  2. loadClass, getResource, 这两个类决定了在整个类加载器体系下怎么找。

这里,双亲委派体现在 loadClassgetResource层,tomcat的设计中,传入了delegate变量,如果这个变量传的是true,则先让父亲来,否则自己来

注意,java里的Class和Resource是同一个东西(字节流)的两个抽象,所以二者有类似的性质,都可以具备双亲委派。

对于找资源

简单来说,就是

  1. 如果开启了委派,则先让父亲找,父亲找不到就自己找
  2. 如果没开启委派,就先自己找,自己找不到才父亲找,所以,tomcat打破双亲委派,就是这么打破的。
  3. 如果找不到,返回null。不会扔异常
public URL getResource(String name) {
    URL url = null;
    
    // 1. 如果开启了委托,先让父亲查一下
    if (delegate) {
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;  // null,此时交给BootstrapClassLoader
        url = loader.getResource(name);
        if (url != null) {
            return (url);
        }
    }

    // 2. 如果父亲没找到,或者就不想让父亲参与资源查找(delegate=false), 按照自己的逻辑来查
    url = findResource(name);
    if (url != null) {
        return (url);
    }

    // 3. 如果之前就没让父亲找(自己逞能),然后自己还没找到,还是得叫爹找
    if( !delegate ) {
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        url = loader.getResource(name);
        if (url != null) {
            return (url);
        }
    }

    // (4) Resource was not found
    if (debug >= 2)
        log("  --> Resource not found, returning null");
    return (null);

}

我们再来分析下loadClass,还是那句话,findClass 表示不管别人,只看自己怎么找,loadClass表示纵观整个类加载体系,要怎么找。

这里我们先了解下前置知识: java原生的classloader规范

  1. 无论如何先让父亲找,如果父亲找不到就自己找。
  2. 解析顺序是

2.1 loadClass: 类加载的入口,每个loadClass会先获取自己的parent,然后调用parent的loadClass

2.2 findClass: (当前类加载器) 根据自己的查找逻辑进行查找

2.3 defineClass: 如果findClass表示能在自己的查找范围内找到类,就可以把类的二进制流载入方法区,此方法返回堆上的class<?>对象

2.4 resolveClass: 对类进行符号解析,解析后resolved为true

2.5 findLoadedClass 在执行上面的真正加载操作前,会判断类是不是加载过了,是的话就直接返回。

2.6 如果加载器路径上的所有classloader都没找到,扔ClassNotFound异常

public Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    
    Class clazz = null;

    // 1. 这一步和Java的classloader规范一样: 如果已经加载过了,就不执行加载逻辑
    clazz = findLoadedClass(name);
    if (clazz != null) {
        // 如果类还没进行符号解析,则解析后返回
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // 2. 尽管说tomcat打破了双亲委派,但tomcat仍然不允许直接加载一些黑名单的类
    // 比方说我们用tomcat的类加载器,加载指定路径下的java.lang.Object, 会直接交给BootstrapClassLoader
    // 这里也没进行双亲委派,而是直接交给最顶级的ClassLoader,是因为java包下一般都是交给BootstrapClassLoader处理的,相当于我们提前知道了这些类该谁来加载。
    if( name.startsWith("java.") ) {
        ClassLoader loader = system;
        clazz = loader.loadClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
        throw new ClassNotFoundException(name);
    }

    // (.5) Permission to access this class when using a SecurityManager
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                securityManager.checkPackageAccess(name.substring(0,i));
            } catch (SecurityException se) {
                String error = "Security Violation, attempt to use " +
                    "Restricted Class: " + name;
                System.out.println(error);
                se.printStackTrace();
                log(error);
                throw new ClassNotFoundException(error);
            }
        }
    }

    // 下面就是经典的三段式
    // 1. 如果允许委派,先让父亲查一下
    if (delegate) {
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = loader.loadClass(name);
            if (clazz != null) {
                if (debug >= 3)
                    log("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // 父亲没查到不能直接摆烂,我们自己还得查一下
            ;
        }
    }

    // 2. 如果开启委托,但是父亲没查到。或者不开启委托。
    try {
        clazz = findClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
        // 同样还没完,不能直接摆烂
        ;
    }

    // 3. 能走到这里,需要 1. 没开启委派 2. 自己没查找
    if (!delegate) {
        if (debug >= 3)
            log("  Delegating to parent classloader");
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = loader.loadClass(name);
            if (clazz != null) {
                if (debug >= 3)
                    log("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            ;
        }
    }

    // This class was not found
    throw new ClassNotFoundException(name);

}

整个流程和getResource一模一样,我们可以仿照类似的设计。

if (delegate) { (1) 双亲委派代码; }
(2) 自己查一遍;
if (!delegate) { (3) 双亲委派代码; }
(4) 没找到时的逻辑

实现类似的逻辑。

而Tomcat打破双亲委派,也是不严谨的,应该是

  1. 针对非java包下的类,设置delegate为false时,才是不按java类加载器规范来的"打破双亲委派"。

回归正题,tomcat的类加载体系,就是在 BootStrap里初始化的。

org.apache.catalina.startup.Bootstrap

  1. commonClassLoader

image.png

我们可能疑惑,jar包在java里是什么样的抽象。实际上,jar包本质就是一组class文件,Java本身就可以处理jar包,同样,jar包也被抽象为一个URL,通过URL就可以找到jar包

image.png

  1. catalinaClassLoader

image.png

  1. sharedClassLoader

image.png

同时我们发现,catalinaClassLoadersharedClassLoader 的父加载器就是 commonClassLoader

另外,通过ClassLoader工厂创建出的类加载器,默认开启双亲委派

image.png


启动脚本

这一章主要是windows下bat脚本的编写,当然如果我们后面也有类似的需求,可以写对应的bat脚本或者shell脚本来实现。

首先,要理解为什么需要先配置一些系统变量,才能正确运行脚本。原因很简单,脚本内使用了这些变量。必须配置的有

  1. CATALINA_HOME: tomcat的安装路径
  2. CATALINA_BASE: tomcat的一些动态内容存放的目录,如果不配就是 CATALINA_HOME

上面分析了,整个入口是Bootstrap.java。 而Bootstrap负责调用Catalina#main()。。

那么Catalina的相关库是在何时被加载的呢? 注意到bat脚本的目录下有一个bootstrap.jar 还有一些tomcat的工具类,这些类会在catalina.bat里被加入classpath。

然后就是一般的脚本都需要做的: 解析命令行,如果不认识命令则显示usage信息。

主要看下 当运行catalina.bat start 时,主要执行下面的操作

另外,我们提到了Bootstrap的设计,以及为什么不直接用Catalina#main启动。实际上tomcat支持不同的启动方式,如果启动脚本传入参数embedded ,则会调用Embbed的方式进行启动。

image.png

那么,Bootstrap的入口何时被指定?

整个执行流程,设置了

  1. _RUNJAVA

该变量通过call setclasspath.bat 设置,具体为_RUNJAVA="%JRE_HOME%\bin\java.exe"

  1. MAINCLASS 两个变量。

2.1 如果不采取 embedded启动,则MAINCLASS为 org.apache.catalina.startup.Bootstrap

2.2 如果采取embedded启动,则MAINCLASS为 org.apache.catalina.stratup.Embedded

总而言之,最终执行的东西就是


start Tomcat

%_EXECJAVA%  java.exe 以什么命令启动

%CATALINA_LOGGING_CONFIG%  tomcat日志配置

%LOGGING_MANAGER% 

%JAVA_OPTS% java变量信息

%CATALINA_OPTS%  catalina 变量信息

%DEBUG_OPTS% 

-D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" 指定endorsed目录,该目录下的内容由commonClassLoader加载

-classpath "%CLASSPATH%" 这里在setclasspath中,指定为 catalina核心库和tomcat的工具类

-Dcatalina.base="%CATALINA_BASE%" 
-Dcatalina.home="%CATALINA_HOME%" 
-Djava.io.tmpdir="%CATALINA_TMPDIR%" 

%MAINCLASS%  Bootstrap.java(MAINCLASS启动的话,就是调用该类的main方法)

%CMD_LINE_ARGS% %ACTION%

所以总结就是,tomcat设置了一些环境变量,jvm运行参数,指定了一些类的路径后,调用Bootstrap#main进行启动。


对于linux平台的启动,也差不多。

但多了一个对各OS版本的专门处理,以进行平台上的统一入口。同样会调用setenv.shsetclasspath.sh

要注意,在linxu中,如果一个脚本调用另一个脚本,而且另外的脚本可能会设置一些环境变量,那么要用source 或者 .的方式。 比如 . setenv.sh

同样,会将tomcat的启动源码和工具类加入classpath。

8. 部署器

简单理解,部署器以 Context为单位,可以将一个ContextPath和具体的Context实现(目录, war包,资源)进行绑定,同时部署并启动对应的Context。

看完职能,应该也能理解部署器是 Host层面的。

image.png

回顾一下Lifecycle的设计。

其中,关于生命周期流转的方法,由Lifecycle实现类自己实现。而关于事件发布管理的方法,由内部对象LifecycleSupport组合实现。

部署器也采取类似的设计,Host实现Deployer接口,同时内部有一个 真正的实现类作为Host的成员。Host使用Deployer的方法,全部交给内部成员完成。

为什么Host不自己完成呢?原因有两点

  1. Deployer 可能会有拓展,用户也可以实现自己的deployer,Host组合deployer的实现,并将真正的接口实现委托给该实现,可以方便deployer进行更新和替换。这就是组件化
  2. 相比Lifecycle接口,Host也需要实现start()方法(或者直接用ContainerBase的方法),这是因为和生命周期流转的业务逻辑确实应该由容器完成,而"部署器"更多的应该是一种增强的逻辑,而不是容器本身应该具备的逻辑。简单来说,部署器是 只有Host才具备的一种增强性质能力。这种能力是不在 "容器体系"范畴内的。

1、 实现接口

image.png

2、内部组合具体的支持对象,通过双向绑定 (传入this)

插个题外话,这里可以注意下,我们发现 如果这么写,有把this暴露出去的风险。

如果外部系统用this访问了内部的私有方法,然后私有方法又能访问本类的属性,如果this传出的时候,本类还没构造好,可能存在风险。

因此,我们观察一下StandardHost的构造函数,没有进行额外的赋值操作,这是因为,StandardHostDeployer这个成员的初始化顺序,优先于构造函数。也就是deployer可以通过this访问到StandardHost还没构造好的成员,好在StandardHost在构造函数里没有进行构造,那么就不存在上述问题。

image.png

3、 接口具体实现委托给support类。

image.png


  1. 部署的触发时机。

我们知道,Host不关心其子组件的部署,所以通过事件机制发布相关事件。

回顾: tomcat的关键组件 都是通过解析配置完成的。

下图就一目了然了,解析某个配置(server.xml), 应用某个规则(ruleSet),得到一些对象。

经过如此处理,配置就可以转换为一个个复杂对象。

image.png

其中,只要遇到Host就会创建一个HostConfig对象。这里回收上文,为什么说必须要有Host的配置,明明只有Context也能完成基本的web功能。tomcat把部署工作放在这里了。

image.png

既然是通过监听实现,那么不难发现HostConfig实现了LifecycleListener接口,其处理方法就是下面的

image.png

那么这个监听器是什么时候加入到EventContext的呢?

begin()方法是digester的一个方法,规则触发前会调用。在解析过程中,栈顶元素是Host的实例,那么这里就是调用了host的addLifecycleListener()方法,所以就能监听来自host发布的事件。

这个方法在host里委托给host的成员support: LifecycleSupport 完成

image.png


  1. 部署器做了啥?

一句话: 先部署,再执行

同时开启一个守护线程,定期检查各个deployed的context里的web.xml是否有修改,有则重新部署。

这个守护线程可以支持动态部署,因此无需重启tomcat,可以对已有的部署进行修改,也可以运行时加入新的Context容器(运行时触发部署)

image.png

用程序描述一个部署可能需要耦合一些代码, tomcat同样提供了 一些 配置实现,以及无需配置的 "约定大于配置"

所谓约定大于配置,就是默认去CATALINA_HOME/webapps 下找需要部署的内容,有约定大于配置,就一定有自行配置,所以docBase 属性,可以指定deployer去哪里寻找要部署的内容。

那么,deployer知道如何寻找要部署的内容后,可以部署什么内容呢?

image.png

3.1 部署一个文件描述符

appBase 目录下,找.xml 结尾的文件。 换句话说,该方法找的是, 部署的元数据信息,简单理解就是约定大于配置中的web.xml。

传统的部署就是 在webapp下找到web.xml 然后作为配置context容器的元数据。既然现在可以在任何地方部署,那deployDescriptors就充当这样的作用。

  1. 寻找是否有满足要求的.xml文件
  2. 将其转换为输入流。
  3. 调用host.install()方法

image.png

3.2 部署一个war包

和上面的类似

  1. 寻找是不是有满足要求的war包
  2. 将其转换为输入流 3.将war包 去掉.war的部分,作为ContextPath
  3. 调用host.install(contextPath, url)方法
((Deployer) host).install(contextPath, url);

3.3 部署一个目录

  1. 在appBase下找到所有符合要求的目录(目录下面需要有可读的WEB-INF目录)
  2. 将其转换为URL对象
  3. 将目录名作为contextPath
  4. 调用host.install(contextPath, url)方法

  1. Context管理

部署成功后,Host会通过addChild加入部署成功的Context对象,所以,实际上所有context被管理在 Host的父类型 ContainerBase的 children 成员变量里

这个东西就是一个 hashmap,配置了相关的CRUD方法。

而如何寻找Context呢?每个Context的key就是ContextPath,就是本章概览图里的/demo1, /demo2

如果没有ContextPath,也就是ContextPath="", Tomcat会将其处理为根/路径。同时其在hashmap的key为""

  1. Deployer接口

我们其实也发现了,无论是部署什么,最终都要调用host的install方法,而这个方法代理给了StandardHostDeployer。

所有我们看下Deployer接口和 StandardHostDeployer的实现。

简单来说,Deployer就

5.1 CRUD 可以添加(install),删除(remove),查找部署(findDeployedApp)。

不难发现,部署的单元是Context,所以findDeployedApp返回一个Context实现类。

而无论是install还是remove,都是根据ContextPath这个key进行新Context的部署或者旧Context的卸载。

image.png

5.1.1 install系列

安装描述符,这个名字太土了,简单理解,他要求我们第一个参数是URL(配置对象的引用),那么他只负责处理配置信息。

核心代码为

image.png

我们主要关注 接收String作为第一个参数的,也就是接受一个ContextPath作为首个参数。

其核心主要就是,将ContextPath和Context绑定,然后加入到Host

image.png

此时相当于注册一个Context到Host,只不过整体还没完全启动。

5.2 生命周期 Deployer作为一个组件,需要实现自己的生命周期管理方法

那么,start和stop就不多说了,install负责从配置中解析出context对象,然后加入到host的children里。

start就负责启动他们

image.png

9. Manager

这个东西和Deployer配合用,其实上面我们 StandardHostDeployer有一个传入ContextPath的方法,这个方法没有在启动阶段进行调用,那我们部署的context何时被调用呢?实际上Manager就是一个入口。

image.png

image.png

作为一个 组件,manager同样可以通过xml描述,可以通过appBase 人工指定manage.xml的路径,也可以约定大于配置,在默认的server/webapps/manage.xml 被读取。

这个Manager同样是Context,同样可以部署,同样需要配置Servlet,但和我们自己的Context有一些区别

  1. 其配置的Servlet类型为

image.png

我们可以给他一个 ContextPath为 /manage,那么该命名空间下的所有api都和内部操作相关。

同时,我们留意到这个Servlet实现了ContainerServlet,回顾一下如果Servlet想获取Wrapper,或者更高层次的容器,获取到的永远是Facade对象,除了tomcat给开后门的Servlet。tomcat会检查Servlet是不是org.apache.catalina 或者继承自这样的全限定名对应的类,如果是的话,那么Servlet可以直接拿到豪华版的Wrapper或者更高层次的容器。

对于用户而言,拿到Facade对象就够了,但是对于内部vip的Servlet,显然希望拿到更多的功能。

同时,回顾一下我们几乎不用的tomcat提供的realm组件,manage也需要额外的身份认证,类似网站的admin页。

通过manage配置的servlet,我们可以访问到上一张 Deployer的相关方法,通过http请求的方式对 Context进行运行时的管理


小结

这一章主要是

  1. tomcat对管理接口的设计
  2. tomcat是如何给 内部Servlet开后门的。

10. JMX

简单了解即可。

简单总结

  1. JMX 是J2EE的一个接口,用于对象的管理。
  2. 基于对象的自省(控制粒度是 字段,方法)提供的权限(可读,可写)机制。
  3. JMX的实现本身也是JAvaBean,只不过是 管理javabean的javabean,也叫MBean
  4. tomcat内置了一些MBean,用于管理常见的组件和容器Bean。

其他的 J2EE这一套东西其实也不太了解,感觉以后也用不到。。