Tomcat源码解析(二): Bootstrap和Catalina

Tomcat源码系列文章

Tomcat源码解析(一): Tomcat整体架构

Tomcat源码解析(二): Bootstrap和Catalina



前言

  • 在tomcat的bin目录下有两个启动tomcat的文件
    • 一个是startup.bat,它用于windows环境下启动tomcat
    • 另一个是startup.sh,它用于linux环境下启动tomcat
  • 两个文件中的逻辑是一样的, 我们只分析其中的startup.bat
  • 而startup.bat文件实际上就做了一件事情:启动catalina.bat
  • catalina.bat中下面这段指定了tomcat的启动类为Bootstrap这个类,catalina.bat最终执行了Bootstrap类中的main方法来启动tomcat
set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start

一、启动类Bootstrap

  • 首先来看下整个启动过程,我们可以看到Bootstrap作为启动入口首先进行了初始化方法init然后load方法加载了Catalina

在这里插入图片描述

1、main

  • Bootstrap的main方法首先会创建一个Bootstrap对象,调用它的init方法初始化
  • 然后根据启动参数,调用Bootstrap对象的不同方法,默认模式为start,该模式下将会先后调用loadstart方法
private static final Object daemonLock = new Object();
private static volatile Bootstrap daemon = null;

// Bootstrap类的main方法
public static void main(String args[]) {
    // 创建一个 Bootstrap 对象
    synchronized (daemonLock) {
        if (daemon == null) {
            Bootstrap bootstrap = new Bootstrap();
            try {
                // 调用init方法初始化
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    // 根据启动参数,分别调用 Bootstrap 对象的不同方法
    try {
        // 默认参数为start
        String command = "start"; 
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            ...
        } else if (command.equals("stopd")) {
            ...
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
           ...
        } else if (command.equals("configtest")) {
            ...                    
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}

2、init

  • 本文对类加载器内容不做分析,后续看情况单独讲
  • 简单来说init就是反射实例化Catalina对象
public void init() throws Exception {
	// 初始化类加载器相关内容
    initClassLoaders();
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
        
    // 通过catalinaLoader加载Catalina,反射实例化Catalina对象
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
        
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
   // 反射将sharedLoader设置为catalinaLoader的父类加载器,本文不做分析
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);
	
	// 将catalina实例引用赋值
    catalinaDaemon = startupInstance;
}

3、load与start

  • load与start都是通过上一步获取到的catalinaDaemon对象反射调用catalina类的loadstart方法
  • 这两个过程我们会在下面的Catalina内容中介绍

load方法:

private void load(String[] arguments) throws Exception {
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes); 
    // 反射调用catalina的load方法,参数为null
    method.invoke(catalinaDaemon, param);
}

start方法:

 public void start()
     throws Exception {
     if( catalinaDaemon==null ) init();
	 // 反射调用catalina的start方法,参数为null
     Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
     method.invoke(catalinaDaemon, (Object [])null);
 }

二、加载Catalina

  • 上文中Bootstrap类的load与start方法实质上就是反射调用catalina类的load与start方法

1、load

  • 创建Digester对象,解析conf/server.xml文件
  • 核心内容调用Server实现类StandardServerinit方法来初始化组件(下篇文章单独讲)
public void load() {
	// 如果已经加载则退出,默认false,下面会置为true
    if (loaded) {
        return;
    }
    loaded = true;

    initDirs();
    initNaming();

    // 创建Digester对象,用来解析server.xml文件
    Digester digester = createStartDigester();

    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;

     try {
     	 // 加载conf目录下的server.xml文件
         file = configFile();
         inputStream = new FileInputStream(file);
         inputSource = new InputSource(file.toURI().toURL().toString());
     } catch (Exception e) {
		...
     }

     try {
         inputSource.setByteStream(inputStream);
         digester.push(this);
         // 开始解析conf/server.xml文件
         digester.parse(inputSource);
     } catch (SAXParseException spe) {
		...
     } 

	// server和catalina之间建立关联
	// Server接口实现类StandardServer是在解析server.xml文件时候创建
	// 当时StandardServer对象set到Catalina
	// 此时又将Catalinaset到StandardServer对象中
	// 形成:你中有我,我中有你
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

	...

    // 初始化server,后面另开一篇单独讲
    try {
        getServer().init();
    } catch (LifecycleException e) {
		...
    }
}

Digester对象解析server.xml文件

  • 解析server.xml文件,根据<Server>和<Service>标签内容,创建ServerService是实现类StandardServerStandardService

在这里插入图片描述

2、start

  • 再来看下整个启动过程
  • Catalina的load方法最后一步getServer().init(),就是Server、Service、Engine等一系列组件的初始化

在这里插入图片描述

  • 核心内容调用server实现类StandardServerstart方法来启动服务器(下篇文章单独讲)
public void start() {

    if (getServer() == null) {
        load();
    }

    if (getServer() == null) {
        // 无法启动服务器。未配置服务器实例
        log.fatal("Cannot start server. Server instance is not configured.");
        return;
    }

    long t1 = System.nanoTime();

    // 调用server的start方法来启动服务器
    try {
        getServer().start();
    } catch (LifecycleException e) {
        log.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }

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

    // 注册关闭钩子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        // 向addShutdownHook方法传入的Thread,其run方法即为自定义的shutdown时清理逻辑
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }
	// 进入等待状态
	// 启动类Bootstrap默认调start方法设置await=true
    if (await) {
    	// main线程等待,等待接收shutdown命令,接受到则跳出阻塞
        await();
        // 跳出阻塞,执行Server.stop();
        stop();
    }
}

2.1、注册shutdown钩子

  • 向addShutdownHook方法传入的Thread线程任务,其run方法即为自定义的shutdown时清理逻辑
  • JDK内部,是通过一个Map来保存所有添加的多个ShutdownHook,在被触发时执行
  • 那这个shutdownHook一般是在什么时候会被调用呢?
    • 程序正常退出,这发生在最后的非守护线程退出时,或者在调用exit(等同于 System.exit)方法时
    • 为响应用户中断而终止虚拟机,如键入^C或发生系统事件,比如用户注销或系统关闭
  • 在Java虚拟机退出的时候,这些设置的shutdownHook会被并行的调用
  • 相当于此线程等待着虚拟机正常退出,就会执行Catalina#stop方法
  • 整体逻辑下方Socket监听SHUTDOWN响应然后调用Catalina#stop方法一样

ps:对于非正常方式退出Java虚拟机,例如杀进程,系统断电等,这些情况下,shutdownHook不会被执行

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

2.2、阻塞tomcat主线程

server.xml开头内容

在这里插入图片描述

  • Catalina的await方法实际是调用Server实现类StandardServer的await方法
public void await() {
    getServer().await();
}
  • 阻塞tomcat主线程,只要stopAwait不为true, tomcat主线程在此无限循环
  • 监听到客户端发起SHUTDOWN命令后,退出阻塞,往下执行stop方法

在这里插入图片描述

// StandardServer类方法

private int port = 8005;

private String shutdown = "SHUTDOWN";

private volatile ServerSocket awaitSocket = null;

@Override
public void await() {
    // shutdown端口配置为-2,启动完Server直接再终止Server
    if( port == -2 ) {
        return;
    }
    // 配置为-1,则不再监听shutdown端口
    if( port==-1 ) {
        try {
            awaitThread = Thread.currentThread();
            while(!stopAwait) {
                try {
                    Thread.sleep( 10000 );
                } catch( InterruptedException ex ) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
        return;
    }

    // 开启socket监听server.xml中的shutdown端口
    // 创建socket服务端
    try {
        awaitSocket = new ServerSocket(port, 1,
                InetAddress.getByName(address));
    } catch (IOException e) {
        return;
    }
    
    // 默认false,进入while循环
    while (!stopAwait) {
        ServerSocket serverSocket = awaitSocket;
        if (serverSocket == null) {
            break;
        }

        // Wait for the next connection
        Socket socket = null;
        StringBuilder command = new StringBuilder();
        InputStream stream;
        try {
        	// accept阻塞监听端口
            socket = serverSocket.accept();
            // 设置阻塞超时时间10秒,如果超时抛异常,catch捕捉到重新进入while循环
            socket.setSoTimeout(10 * 1000);  
            stream = socket.getInputStream();
        } catch (SocketTimeoutException ste) {
            continue;
        }
		
		// 从流中读取字符串
		...
                
        // 如果读取到字符串命令是"SHUTDOWN"则,跳出循环,开始终止服务器
        // shutdown变量是取server.xml中Server的shutdown属性
        boolean match = command.toString().equals(shutdown);
        if (match) {
            log.info(sm.getString("standardServer.shutdownViaPort"));
            break;
        } else
            log.warn("StandardServer.await: Invalid command '"
                    + command.toString() + "' received");
    }
}

2.3、停止Tomcat

  • 最终调用Server的stop和destroy方法(下篇文章单独讲)
public void stop() {
    try {
        if (useShutdownHook) {
        	// 移除shutdown钩子,这个stop方法会停止server,不需要钩子再次执行
            Runtime.getRuntime().removeShutdownHook(shutdownHook);
            
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        true);
            }
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
    }

    // 调用Server的stop和destroy方法
    try {
        Server s = getServer();
        LifecycleState state = s.getState();
        if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
                && LifecycleState.DESTROYED.compareTo(state) >= 0) {
            // Nothing to do. stop() was already called
        } else {
            s.stop();
            s.destroy();
        }
    } catch (LifecycleException e) {
        log.error("Catalina.stop", e);
    }
}

三、总结

  • Bootstrap是一个启动引导类,本身没有太多启动关闭细节的实现
  • 而是通过加载Catalina,对Catalina发号施令,调用start、stop等方法

在这里插入图片描述

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冬天vs不冷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值