tomcat源码分析02:启动流程

1,221 阅读7分钟

注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….

1. 示例demo

本文是tomcat源码分析的第二篇,在idea下搭建 tomcat9 源码调试环境一文中,我们搭建好了tomcat的源码调试环境,接下来我们就在里面添加示例代码,然后进行源码分析了。

我们将代码都放在test目录下,包名为org.apache.tomcat.demo

1.1 准备servlet

我们先准备一个servet,内容如下:

public class MyHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.getWriter().println("<h1>hello world!!!</h1>");
        System.out.println("hello world");
    }
}

这个servlet比较简单,就只是向页面与控制台打印了一句:hello world

1.2 实现ServletContainerInitializer

这个是servlet 3.0规范,本文的demo中主要用来代替 web.xml进行servlet注册的,都到2021年了,就不整web.xml的方式了,代码如下:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> clsSet, ServletContext servletContext)
            throws ServletException {
        MyHttpServlet servlet = new MyHttpServlet();
        ServletRegistration.Dynamic registration
                = servletContext.addServlet("servlet", servlet);
        // loadOnStartup 设置成 -1 时,只有在第一次请求时,才会调用 init 方法
        registration.setLoadOnStartup(-1);
        registration.addMapping("/*");
    }
}

关于servlet 3.0规范及ServletContainerInitializer的作用,本文就不展开了,想了解的 小伙伴可自行百度。

1.3 创建spi文件

为了MyServletContainerInitializer能被tomcat加载到,我们还需要创建一个spi文件:

如上图,我们需要在test的同级目录下创建test资源包,我这里将其命名为testRes,需要标记为Test Resources Root,然后在其下创建两个目录META-INF/service,再创建一个文本文件,将其命名为javax.servlet.ServletContainerInitializer(就是ServletContainerInitializer的全限定名了),文件内容如下:

org.apache.tomcat.demo.MyServletContainerInitializer

里面的内容就是我们自己实现的ServletContainerInitializer了。tomcat在启动时,会找到META-INF/service/javax.servlet.ServletContainerInitializer文件,读取内容后,就能加载MyServletContainerInitializer了,我们后面会从源码的角度来分析这个过程。

1.4 主类

接下来是主类:

public class Demo01 {

    @Test
    public void test() throws Exception {
        Tomcat tomcat = new Tomcat();

        // 创建连接器
        Connector connector = new Connector();
        connector.setPort(8080);
        connector.setURIEncoding("UTF-8");
        tomcat.getService().addConnector(connector);

        // 创建 context
        String docBase = System.getProperty("java.io.tmpdir");
        Context context = tomcat.addContext("", docBase);

        // 得到 lifecycleListener 实际类型为 ContextConfig
        LifecycleListener lifecycleListener = (LifecycleListener)
                Class.forName(tomcat.getHost().getConfigClass())
                        .getDeclaredConstructor().newInstance();
        context.addLifecycleListener(lifecycleListener);

        tomcat.start();
        tomcat.getServer().await();
    }
}

这个类还是比较简单的,先创建了一个Connector,然后向tomcat中添加了一个Context,最后就是启动tomcat了。

运行,然后在浏览器访问http://localhost:8080,结果如下:

可以看到,MyHttpServlet可以正常对外访问了。

2. tomcat架构体系

在正式分析tomcat源码前,我们首先要对tomcat有个整体的认识,tomcat的整个架构体系如下:

  • Server:整个Tomcat服务器,一个Tomcat只有一个Server标准实现为StandardServer
  • ServiceServer中的一个逻辑功能层, 一个Server可以包含多个Service(他们是彼此完全独立,只共享基本的JVM和系统路径上的类),一个Service负责维护一个或多个Connector和一个Container标准实现为StandardService
  • Connector:称作连接器,是Service的核心组件之一,一个Service可以有多个Connector,用于接受请求并将请求封装成RequestResponse,然后交给Container进行处理,Container处理完之后再交给Connector返回给客户端;
  • ContainerService的另一个核心组件,它由四个子容器组件构成,分别是:EngineHostContextWrapper,这四个组件不是平行的,而是父子关系,Engine包含HostHost包含ContextContext 包含 Wrapper
  • JasperJSP引擎;
  • Session:会话管理.

关于Container的进一步说明:

Container由四个子容器组件构成:EngineHostContextWrapper,关系如下:

  • Engine:一个Service中有 多个Connector一个EngineEngine表示整个Servlet引擎,一个Engine下面可以包含一个或者多个Host标准实现为StandardEngine
  • Host:代表一个站点,也可以叫虚拟主机,一个Host可以配置多个Context标准实现为StandardHost
  • Context:代表ServletContext,它具备了Servlet运行的基本环境,理论上只要有Context就能运行Servlet了,简单的 Tomcat可以没有EngineHost标准实现为StandardContext
  • WrapperWrapper代表一个Servlet,它负责管理一个Servlet,包括的Servlet的装载、初始化、执行以及资源回收,Wrapper是最底层的容器,它没有子容器了,标准实现为StandardWrapper.

了解了tomcat这一层层的结构后,接下来我们就正式从源码上来分析tomat的启动流程了。

3. Tomcat#start()方法

tomcat 的启动方法为Tomcat#start(),我们跟进去:

public void start() throws LifecycleException {
    getServer();
    server.start();
}

我们先看看getServer()

    public Server getServer() {
        if (server != null) {
            return server;
        }
        System.setProperty("catalina.useNaming", "false");
        // 创建标准的 server
        server = new StandardServer();
        ...
        // 添加 Service
        Service service = new StandardService();
        service.setName("Tomcat");
        server.addService(service);
        return server;
    }

这个方法干了两件事:

  1. 创建StandardServer
  2. 创建StandardService

让我们再回到Tomcat#start()方法,server对象有了,接下来就是server.start()方法了,StandardServerLifecycleBase的子类,让我们进入LifecycleBase#start方法:

    public final synchronized void start() throws LifecycleException {
        // 省略了部分代码
        ...
        if (state.equals(LifecycleState.NEW)) {
            // 初始化
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            // 启动
            startInternal();
            // 省略了部分代码
            ...
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.startFail", toString());
        }
    }

这里我们省略了部分代码,LifecycleBase#start 就只干了两件事:init()(初始化)与startInternal()(启动),我们先看初始化方法LifecycleBase#init

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

        try {
            // 设置状态,触发"正在初始化"事件
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            // 初始化
            initInternal();
            // 设置状态,触发"初始化完成"事件
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.initFail", toString());
        }
    }

这个方法干的事不多,我们重点关注initInternal()方法,这是个抽象方法,StandardService重写了它,我们进入StandardService#initInternal方法:

    protected void initInternal() throws LifecycleException {

        super.initInternal();

        // 1. 初始化 engine
        if (engine != null) {
            // 调用初始化方法
            engine.init();
        }

        // 2. 初始化 Executor
        for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }

        // 3. 初始化 mapperListener
        mapperListener.init();

        // 4. 初始化 Connectors
        synchronized (connectorsLock) {
            for (Connector connector : connectors) {
                connector.init();
            }
        }
    }

这个方法里会初始化engineExecutormapperListenerConnectors,我们先来看看engine.init(),点进去,发现它到了LifecycleBase#init方法!我们来看看LifecycleBase的实现类:

可以看到,前面介绍的tomcat各组件都是LifecycleBase的子类,上图未列出的:

Connector

/** 继承了 LifecycleMBeanBase */
public class Connector extends LifecycleMBeanBase  {

}

/** 而 LifecycleMBeanBase 又继承了 LifecycleBase  */
public abstract class LifecycleMBeanBase extends LifecycleBase
        implements JmxEnabled {

}

从源码上可以看到,Tomcat#start()方法里执行了Server#start()方法,Server#start()又会执行Engine.start()Host#start()方法的,一直到Wrapper#start()tomcat正是以这种套娃般的方式进行启动的,同样的方法还有LifecycleBase#init,也是这样层层执行下去的,整个流程如图:

StandardServerStandardServiceConnector等组件都有重写initInternal()startInternal()方法,想要了解某个组件初始化或启动时所做的工作,直接进入该类查看这两个方法就可以了。

需要注意的是几个Container类,StandardEngineStandardHostStandardContextStandardWrapper都是ContainerBase的子类:

我们重点来看看ContainerBasestartInternal(...)方法:

    protected synchronized void startInternal() throws LifecycleException {
        ...

        // 启动子容器
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<>();
        for (Container child : children) {
            // 在线程池中处理 启动操作
            results.add(startStopExecutor.submit(new StartChild(child)));
        }

        MultiThrowable multiThrowable = null;

        for (Future<Void> result : results) {
            try {
                result.get();
            } catch (Throwable e) {
                ...
            }

        }
        ...
    }

    /**
     * 启动子容器的类,实现了 Callable 接口
     */
    private static class StartChild implements Callable<Void> {

        private Container child;

        public StartChild(Container child) {
            this.child = child;
        }

        @Override
        public Void call() throws LifecycleException {
            // 启动操作,调用的是 LifecycleBase#start 方法
            child.start();
            return null;
        }
    }

ContainerBasestartInternal(...)方法会先获取到当前Container子Container,然后调用LifecycleBase#start(),这个启动操作会被包装成StartChild对象,放在线程池中执行。注意,LifecycleBase#start(...)操作时,会先调用init()方法再调用start(),因此Container类的启动与初始化是在这里同一个方法里进行的。

举例来说,

  1. StandardEngine调用startInternal(...)方法,找到的子ContainerStandardHost,再就调用StandardHost#init(...)StandardHost#start(...)方法;
  2. 调用StandardHost#start(...)方法时,又会调用ContainerBase#startInternal(...)方法,再又找到StandardHost子ContainerStandardContext,然后又执行它的init(...)start(...)方法;
  3. 直到运行StandardWrapper#start(...),由于StandardWrapper没有子Container了,这种套娃式的运行就结束了。

实际上,前面的组件都没做什么实质性工作,只是为了初始化或启动下一个组件,真正干活的是最后的组件,如

  • Context:会在这里处理servlet的加载
  • Connector:处理http连接,开启端口监听

这些内容在分析具体功能时再展开分析吧。

4. 总结

本文作为tomcat源码正式分析的第一篇,主要是准备了一个源码分析demo,然后介绍了tomcat的各大组件,最后阐述了tomcat的整个启动流程,旨在让大家对tomcat有一个全局上的认识。


限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!