• 概述
  • Tomcat的启停
  • 各组件与实现类的加载与启动
  • 总结

概述

  • Tomcat作为一款经典的Web容器,一直经久不衰,接下来我们从源码角度来看一下Tomcat的启动流程,了解其工作的原理,并看看其在设计编码上有哪些思想值得我们借鉴。
  • 当然在开始之前还是要先强调一下源码的阅读方式:①熟练使用这个工具;②把握整体架构,设计模式等等;③不要深入研究具体的代码实现(除非做二次开发),以全局思路与功能实现为主;④一定要将源代码跑起来调试;⑤接口与抽象类是要重点关注的,在接口中往往能看到很多全局的设计思路;而抽象类是具体公共点的抽取,变化部分交给子类实现。

Tomcat的启停

  • 由于Tomcat也是由Java实现的程序,因此Tomcat启动必然会依赖main函数的启动。
  • 我们Tomcat一般都是通过脚本来启动(Win:catalina.bat;Linux:catalina.sh),在启动Tomcat的时候,会从控制台传入一个“start”的参数,main函数如果接收到“start”参数,就进入Tomcat各组件的初始化和启动阶段。
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\startup\Bootstrap.java
//Tomcat的main函数源码
public static void main(String args[]) {
    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            
            try {
                //在init()中的操作:
                //1.初始化Tomcat自身组件的一些类加载器(common、shared、server)
                //2.反射创建“org.apache.catalina.startup.Catalina”对象,并赋值给Bootstrap的catalinaDaemon成员属性
                //3.反射调用Catalina的setParentClassLoader方法,传递sharedLoader参数;
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }
    
    try {
        //参数默认为start
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }
        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            //是否阻塞当前唯一的非守护线程(持续监听Tomcat配置的SHUTDOWN端口)
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}
  • 在Bootstrap启动时反射调用的Catalina.java的start方法中,当其他组件都启动加载完成后,就会调用await方法,如果不是其他框架内嵌Tomcat的话,就会新建一个socket持续监听配置文件中配置的SHUTDOWN端口,这也起到阻塞主线程的作用(其他线程都是守护线程,这样的好处是当主线程退出时,Tomcat可以有效地立即退出),我们可以通过连接上配置的SHUTDOWN端口去关闭Tomcat(telnet)。
  • 当然如果我们直接通过catalina脚本文件并使用“stop”参数来关闭Tomcat,其流程依旧为运行Tomcat的main函数,加载配置文件,拿到SHUTDOWN的端口与命令,然后通过socket连接去关闭(与手动连接SHUTDOWN端口关闭一致)。
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\core\StandardServer.java的await方法
//通过socket连接SHUTDOWN端口关闭Tomcat的实现
   s = getServer();
   if (s.getPort()>0) {
       try (Socket socket = new Socket(s.getAddress(), s.getPort());
               OutputStream stream = socket.getOutputStream()) {
           String shutdown = s.getShutdown();
           for (int i = 0; i < shutdown.length(); i++) {
               stream.write(shutdown.charAt(i));
           }
           stream.flush();
       } catch (ConnectException ce) {
       ......
  • 当然在很多时候,可能并不是我们主动发出的停止Tomcat的命令,其可能是操作系统(收到中断请求)也可能是程序中某个线程出现致命错误需要停止服务;这个时候就会使用到JDK提供的“SHOUDOWN HOOK”,Java的关闭钩子;我们的Tomcat在启动时(Catalina的start()方法中)也会注册上“SHOUDOWN HOOK”,在程序停止前就会运行“SHOUDOWN HOOK”中的方法,当然“SHOUDOWN HOOK”中的方法肯定是不太耗时的,否则可能中断时间过长超时后还是可能被强行停止程序。
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\startup\Catalina.java的start方法
//Catalina在启动时注册“SHOUDOWN HOOK”的源码
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
        
        // If JULI is being used, disable JULI's shutdown hook since
        // shutdown hooks run in parallel and log messages may be lost
        // if JULI's hook completes before the CatalinaShutdownHook()
        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }

//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\startup\Catalina.java
//“SHOUDOWN HOOK”具体实现
// XXX Should be moved to embedded !
/**
 * Shutdown hook which will perform a clean shutdown of Catalina if needed.
 */
protected class CatalinaShutdownHook extends Thread {

    @Override
    public void run() {
        try {
            if (getServer() != null) {
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
            ExceptionUtils.handleThrowable(ex);
            log.error(sm.getString("catalina.shutdownHookFail"), ex);
        } finally {
            // If JULI is used, shut JULI down *after* the server shuts down
            // so log messages aren't lost
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).shutdown();
            }
        }
    }
}
  • 由于Tomcat的类加载机制,Bootstrap.java与Catalina.java并不是同一个类加载器加载的,因此Bootstrap.java调用Catalina.java中的方法全都使用的反射调用。

各组件与实现类的加载与启动

  • Tomcat的各组件的加载与启动使用到了责任链设计模式,即每一个组件都会向下负责一个或者多个组件的加载与启动及其生命周期。
  • Tomcat的各组件的生命周期管理与组件的初始化还用到了模板设计模式,其生命周期管理都来自其父类LifecycleBase中,并且由父类中调用initInternal()方法进行组件初始化,此方法在子类中均有相应实现。
  • Tomcat的初始化各个组件与启动各个组件是两条链路,即在完成各个组件的初始化之后,才会进行各个组件的启动流程。
  • 下面简单列举几个组件的加载过程,其余组件的加载过程也是非常类似的,均可以触类旁通。

Catalina的加载与初始化

  • 在Catalina的load()方法中,主要做了三件事情:(1)创建xml的解析器(Digester)解析server.xml;(2)创建StandardServer实例 (server)以及其他server.xml配置涉及的诸多实例;(3)初始化server对象(StandardServer实例)。
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\startup\Catalina.java
/**
 * Start a new server instance.
 */
public void load() {

    if (loaded) {
        return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    initDirs();

    // Before digester - it may be needed
    initNaming();

    //创建xml解析器解析server.xml,方法内部会创建大量相关的对象
    Digester digester = createStartDigester();

    //此处省略各种文件流的校验
    ......

    //将当前Catalina对象赋值给StandardServer对象
    getServer().setCatalina(this);
    //加载我们配置的应用程序路径
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    // Stream redirection
    initStreams();

    // Start the new server
    try {
        //初始化StandardServer对象
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }
    }

    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
    }
}

Server(StandardServer)的加载与初始化

  • 还是责任链模式的启动方式,StandardServer的初始化是由Catalina的load()方法中的getServer().init()触发的;从StandardServer开始,其组件的生命周期被统一管理(LifeCycleBase),这是典型的模板方法设计模式;在LifeCycleBase抽象类的init()方法中,会调用initInternal()方法,该方法也是一个抽象方法,在相应的组件中会有实现。
  • 在StandardSever的initInternal()方法中,主要做了这几件事:①调用Mbean父类(LifeCycleMbeanBase)的initInternal();②注册StringCache、注册MBeanFactory用于管理Server初始化全局资源;③通过Catalina中的成员变量parentClassLoader指向的SharedClassLoader加载Jar包;④进行StandardServer管理的多个services的初始化操作。
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\core\StandardServer.java
protected void initInternal() throws LifecycleException {
    //Mbean父类(LifeCycleMbeanBase)的初始化
    super.initInternal();

    // Register global String cache
    // Note although the cache is global, if there are multiple Servers
    // present in the JVM (may happen when embedding) then the same cache
    // will be registered under multiple names
    onameStringCache = register(new StringCache(), "type=StringCache");

    // Register the MBeanFactory
    MBeanFactory factory = new MBeanFactory();
    factory.setContainer(this);
    onameMBeanFactory = register(factory, "type=MBeanFactory");

    // Register the naming resources
    globalNamingResources.init();

    // Populate the extension validator with JARs from common and shared
    // class loaders
    if (getCatalina() != null) {
        ClassLoader cl = getCatalina().getParentClassLoader();
        
        //省略Jar包加载流程
        ......
    }
    
    //进行StandardServer管理的多个services的初始化操作
    for (Service service : services) {
        service.init();
    }
}

Service(StandardService)的加载与初始化

  • Service的初始化还是与之前一样,由LifeCycleBase抽象类调用initInternal(),StandardService会对其进行实现;
  • StandardService中管理的组件就会多一些,我们先来查看一下其成员变量:
//源码位于:.\apache-tomcat-8.5.57-src\java\org\apache\catalina\core\StandardService.java
public class StandardService extends LifecycleMBeanBase implements Service {

    /**
     * The <code>Server</code> that owns this Service, if any.
     */
    private Server server = null;


    /**
     * 多个连接器
     */
    protected Connector connectors[] = new Connector[0];
  
    /**
     * 线程池
     */
    protected final ArrayList<Executor> executors = new ArrayList<>();

    private Engine engine = null;

    private ClassLoader parentClassLoader = null;

    /**
     * Mapper.
     */
    protected final Mapper mapper = new Mapper();


    /**
     * Mapper listener.
     */
    protected final MapperListener mapperListener = new MapperListener(this);
  • 上面成员变量中的 Connector 、Engine 等组件分别都有其初始化与启动流程,并且也都是通过LifecycleBase进行生命周期管理,这种思想可以说贯穿了Tomcat的整个启动过程。

总结

  • 上面通过两个组件说明了Tomcat的启动流程,如果你继续阅读源码就会发现,Tomcat的各个组件的启动过程从宏观上来说,都是统一的生命周期管理(模板方法模式)。后续还有其他组件包括 StandardEngine、StandardHost、StandardContext等等。在这些组件初始化完成后(initInternal()),会继续进行所有组件的 启动(start())操作;这些设计方式在自己后续的编码中都可以进行非常广泛的应用。

注意:本文归作者所有,未经作者允许,不得转载

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐