Tomcat源码分析 (六)----- Tomcat 启动过程(一)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

说到Tomcat的启动,我们都知道,我们每次需要运行tomcat/bin/startup.sh这个脚本,而这个脚本的内容到底是什么呢?我们来看看。

启动脚本

startup.sh 脚本

    #!/bin/sh
    os400=false
    case "`uname`" in
    OS400*) os400=true;;
    esac
    
    # resolve links - $0 may be a softlink
    PRG="$0"
    
    while [ -h "$PRG" ] ; do
      ls=`ls -ld "$PRG"`
      link=`expr "$ls" : '.*-> \(.*\)$'`
      if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
      else
        PRG=`dirname "$PRG"`/"$link"
      fi
    done
    
     PRGDIR  =`dirname "$PRG"`
    EXECUTABLE=  catalina.sh 
    
    # Check that target executable exists
    if $os400; then
      # -x will Only work on the os400 if the files are:
      # 1. owned by the user
      # 2. owned by the PRIMARY group of the user
      # this will not work if the user belongs in secondary groups
      eval
    else
      if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
        echo "Cannot find $PRGDIR/$EXECUTABLE"
        echo "The file is absent or does not have execute permission"
        echo "This file is needed to run this program"
        exit 1
      fi
    fi
    
     exec   "$PRGDIR"/"$EXECUTABLE" start "$@" 

我们来看看这脚本。该脚本中有2个重要的变量:

  1. PRGDIR:表示当前脚本所在的路径
  2. EXECUTABLE:catalina.sh 脚本名称
    其中最关键的一行代码就是 exec "$PRGDIR"/"$EXECUTABLE" start "$@",表示执行了脚本catalina.sh,参数是start。

catalina.sh 脚本

然后我们看看catalina.sh 脚本中的实现:

    elif [ "$1" = "start" ] ; then
    
      if [ ! -z "$CATALINA_PID" ]; then
        if [ -f "$CATALINA_PID" ]; then
          if [ -s "$CATALINA_PID" ]; then
            echo "Existing PID file found during start."
            if [ -r "$CATALINA_PID" ]; then
              PID=`cat "$CATALINA_PID"`
              ps -p $PID >/dev/null 2>&1
              if [ $? -eq 0 ] ; then
                echo "Tomcat appears to still be running with PID $PID. Start aborted."
                echo "If the following process is not a Tomcat process, remove the PID file and try again:"
                ps -f -p $PID
                exit 1
              else
                echo "Removing/clearing stale PID file."
                rm -f "$CATALINA_PID" >/dev/null 2>&1
                if [ $? != 0 ]; then
                  if [ -w "$CATALINA_PID" ]; then
                    cat /dev/null > "$CATALINA_PID"
                  else
                    echo "Unable to remove or clear stale PID file. Start aborted."
                    exit 1
                  fi
                fi
              fi
            else
              echo "Unable to read PID file. Start aborted."
              exit 1
            fi
          else
            rm -f "$CATALINA_PID" >/dev/null 2>&1
            if [ $? != 0 ]; then
              if [ ! -w "$CATALINA_PID" ]; then
                echo "Unable to remove or write to empty PID file. Start aborted."
                exit 1
              fi
            fi
          fi
        fi
      fi
    
      shift
      touch "$CATALINA_OUT"
      if [ "$1" = "-security" ] ; then
        if [ $have_tty -eq 1 ]; then
          echo "Using Security Manager"
        fi
        shift
        eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
          -classpath "\"$CLASSPATH\"" \
          -Djava.security.manager \
          -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
          -Dcatalina.base="\"$CATALINA_BASE\"" \
          -Dcatalina.home="\"$CATALINA_HOME\"" \
          -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
           org.apache.catalina.startup.Bootstrap   "$@"   start  \
          >> "$CATALINA_OUT" 2>&1 "&"
    
      else
        eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
          -classpath "\"$CLASSPATH\"" \
          -Dcatalina.base="\"$CATALINA_BASE\"" \
          -Dcatalina.home="\"$CATALINA_HOME\"" \
          -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
          org.apache.catalina.startup.Bootstrap "$@" start \
          >> "$CATALINA_OUT" 2>&1 "&"
    
      fi
    
      if [ ! -z "$CATALINA_PID" ]; then
        echo $! > "$CATALINA_PID"
      fi
    
      echo "Tomcat started."

该脚本很长,但我们只关心我们感兴趣的:如果参数是 start, 那么执行这里的逻辑,关键再最后一行执行了 org.apache.catalina.startup.Bootstrap "$@" start, 也就是说,执行了我们熟悉的main方法,并且携带了start 参数,那么我们就来看Bootstrap 的main方法是如何实现的。

Bootstrap.main

首先我们启动 main 方法:

    public static void main(String args[]) {
        System.err.println("Have fun and Enjoy! cxs");
    
        // daemon 就是 bootstrap
        if (daemon == null) {
            Bootstrap bootstrap = new Bootstrap();
            try {
                //类加载机制我们前面已经讲过,在这里就不在重复了
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
        try {
            // 命令
            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")) {
                daemon.setAwait(true);// bootstrap 和 Catalina 一脉相连, 这里设置, 方法内部设置 Catalina 实例setAwait方法
                daemon.load(args);// args 为 空,方法内部调用 Catalina 的 load 方法.
                daemon.start();// 相同, 反射调用 Catalina 的 start 方法 ,至此,启动结束
            } 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.init();的部分代码

    public void init() throws Exception {
    
        // 类加载机制我们前面已经讲过,在这里就不在重复了
        initClassLoaders();
    
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        SecurityClassLoad.securityClassLoad(catalinaLoader);
    
         // 反射方法实例化Catalina
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance =   startupClass.getConstructor().newInstance(); 
    
       
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
    
         // 引用Catalina实例
        catalinaDaemon =   startupInstance; 
    }

我们可以看到是通过反射实例化Catalina类,并将实例引用赋值给catalinaDaemon,接着我们看看 daemon.load(args);

    private void load(String[] arguments)
        throws Exception {
    
        // Call the load() method
        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);
        if (log.isDebugEnabled())
            log.debug("Calling startup class " + method);
        //通过反射调用Catalina的load()方法
         method.invoke(catalinaDaemon, param); 
    
    }

Catalina.load

我们可以看到daemon.load(args)实际上就是通过反射调用Catalina的load()方法.那么我们进入 Catalina 类的 load 方法看看:

    public void load() {
    
        initDirs();
    
        // 初始化jmx的环境变量
        initNaming();
    
        // Create and execute our Digester
        // 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类
        Digester digester = createStartDigester();
    
        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
    
          // 首先尝试加载conf/server.xml,省略部分代码......
          // 如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中),省略部分代码......
          // 如果还是加载不到xml,则直接return,省略部分代码......
    
          try {
              inputSource.setByteStream(inputStream);
    
              // 把Catalina作为一个顶级实例
              digester.push(this);
    
              // 解析过程会实例化各个组件,比如Server、Container、Connector等
              digester.parse(inputSource);
          } catch (SAXParseException spe) {
              // 处理异常......
          }
        } finally {
            // 关闭IO流......
        }
    
        // 给Server设置catalina信息
        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    
        // Stream redirection
        initStreams();
    
        // 调用Lifecycle的init阶段
        try {
             getServer().init(); 
        } catch (LifecycleException e) {
            // ......
        }
    
        // ......
    
    }

Server初始化

可以看到, 这里有一个我们今天感兴趣的方法, getServer.init(), 这个方法看名字是启动 Server 的初始化, 而 Server 是我们上面图中最外层的容器. 因此, 我们去看看该方法, 也就是LifecycleBase.init() 方法. 该方法是一个模板方法, 只是定义了一个算法的骨架, 将一些细节算法放在子类中去实现.我们看看该方法:

LifecycleBase.init()

    @Override
    public final synchronized void init() throws LifecycleException {
        // 1
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }
        // 2
        setStateInternal(LifecycleState.INITIALIZING, null, false);
    
        try {
            // 模板方法
            /**
             * 采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,
             * 每个需要生命周期管理的组件只需要继承这个基类,
             * 然后覆盖对应的钩子方法即可完成相应的声明周期阶段的管理工作
             */ 
            initInternal(); 
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(
                    sm.getString("lifecycleBase.initFail",toString()), t);
        }
    
        // 3
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    }

Server的实现类为StandardServer,我们分析一下 StandardServer.initInternal() 方法。该方法用于对Server进行初始化,关键的地方就是代码最后对services的循环操作,对每个service调用init方法。
【注】:这儿我们只粘贴出这部分代码。

StandardServer.initInternal()

    @Override
    protected void initInternal() throws LifecycleException {
        super.initInternal();
    
        // Initialize our defined Services
        for (int i = 0; i < services.length; i++) {
            services[i].init();
        }
    }

调用Service子容器的init方法,让Service组件完成初始化,注意:在同一个Server下面,可能存在多个Service组件.

Service初始化

StandardService和StandardServer都是继承至LifecycleMBeanBase,因此公共的初始化逻辑都是一样的,这里不做过多介绍,我们直接看下initInternal

StandardService. initInternal()

    protected void initInternal() throws LifecycleException {
    
        // 往jmx中注册自己
        super.initInternal();
    
        // 初始化Engine
        if (engine != null) {
             engine.init(); 
        }
    
        // 存在Executor线程池,则进行初始化,默认是没有的
        for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }
    
        mapperListener.init();
    
        // 初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听,
        synchronized (connectorsLock) {
            for (Connector connector : connectors) {
                try {
                     connector.init(); 
                } catch (Exception e) {
                    // 省略部分代码,logger and throw exception
                }
            }
        }
    }
  1. 首先,往jmx中注册StandardService
  2. 初始化Engine,而Engine初始化过程中会去初始化Realm(权限相关的组件)
  3. 如果存在Executor线程池,还会进行init操作,这个Excecutor是tomcat的接口,继承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
  4. 初始化Connector连接器,默认有http1.1、ajp连接器,而这个Connector初始化过程,又会对ProtocolHandler进行初始化,开启应用端口的监听,后面会详细分析

Engine初始化

StandardEngine初始化的代码如下:

    @Override
    protected void initInternal() throws LifecycleException {
        getRealm();
        super.initInternal();
    }
    
    public Realm getRealm() {
        Realm configured = super.getRealm();
        if (configured == null) {
            configured = new NullRealm();
            this.setRealm(configured);
        }
        return configured;
    }

StandardEngine继承至ContainerBase,而ContainerBase重写了initInternal()方法,用于初始化start、stop线程池,这个线程池有以下特点:

  1. core线程和max是相等的,默认为1
  2. 允许core线程在超时未获取到任务时退出线程
  3. 线程获取任务的超时时间是10s,也就是说所有的线程(包括core线程),超过10s未获取到任务,那么这个线程就会被销毁

这么做的初衷是什么呢?因为这个线程池只需要在容器启动和停止的时候发挥作用,没必要时时刻刻处理任务队列

ContainerBase的代码如下所示:

    // 默认是1个线程
    private int startStopThreads = 1;
    protected ThreadPoolExecutor startStopExecutor;
    
    @Override
    protected void initInternal() throws LifecycleException {
        BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
        startStopExecutor = new ThreadPoolExecutor(
                getStartStopThreadsInternal(),
                getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
                startStopQueue,
                new StartStopThreadFactory(getName() + "-startStop-"));
        // 允许core线程超时未获取任务时退出
        startStopExecutor.allowCoreThreadTimeOut(true);
        super.initInternal();
    }
    
    private int getStartStopThreadsInternal() {
        int result = getStartStopThreads();
    
        if (result > 0) {
            return result;
        }
        result = Runtime.getRuntime().availableProcessors() + result;
        if (result < 1) {
            result = 1;
        }
        return result;
    }

这个startStopExecutor线程池有什么用呢?

  1. 在start的时候,如果发现有子容器,则会把子容器的start操作放在线程池中进行处理
  2. 在stop的时候,也会把stop操作放在线程池中处理

在前面的文章中我们介绍了Container组件,StandardEngine作为顶层容器,它的直接子容器是StardandHost,但是对StandardEngine的代码分析,我们并没有发现它会对子容器StardandHost进行初始化操作,StandardEngine不按照套路出牌,而是把初始化过程放在start阶段。个人认为Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的

Connector初始化

Connector初始化会在后面有专门的Connector文章讲解

总结

至此,整个初始化过程便告一段落。整个初始化过程,由parent组件控制child组件的初始化,一层层往下传递,直到最后全部初始化OK。下图描述了整体的传递流程

默认情况下,Server只有一个Service组件,Service组件先后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector连接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值