说到Tomcat的启动,咱们都知道,咱们每次须要运行tomcat/bin/startup.sh这个脚本,而这个脚本的内容究竟是什么呢?咱们来看看。java
#!/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个重要的变量:web
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
,表示执行了脚本catalina.sh,参数是start。而后咱们看看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方法是如何实现的。
首先咱们启动 main 方法:apache
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();的部分代码bootstrap
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);tomcat
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); }
咱们能够看到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) { // ...... } // ...... }
LifecycleBase.init()app
@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方法。
@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组件.webapp
StandardService和StandardServer都是继承至LifecycleMBeanBase,所以公共的初始化逻辑都是同样的,这里不作过多介绍,咱们直接看下initInternalide
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 } } } }
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线程池有什么用呢?
在前面的文章中咱们介绍了Container组件,StandardEngine做为顶层容器,它的直接子容器是StardandHost,可是对StandardEngine的代码分析,咱们并无发现它会对子容器StardandHost进行初始化操做,StandardEngine不按照套路出牌,而是把初始化过程放在start阶段。我的认为Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,所以在start阶段用多线程完成初始化以及start生命周期,不然,像顶层的Server、Service等组件须要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具备传递性的
Connector初始化会在后面有专门的Connector文章讲解
至此,整个初始化过程便告一段落。整个初始化过程,由parent组件控制child组件的初始化,一层层往下传递,直到最后所有初始化OK。下图描述了总体的传递流程
默认状况下,Server只有一个Service组件,Service组件前后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector链接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理