说到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 {
//
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
//
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);
} //
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进行处理

Tomcat源码分析 (六)----- Tomcat 启动过程(一)的更多相关文章

  1. Tomcat源码分析之—具体启动流程分析

    从Tomcat启动调用栈可知,Bootstrap类的main方法为整个Tomcat的入口,在init初始化Bootstrap类的时候为设置Catalina的工作路径也就是Catalina_HOME信息 ...

  2. Tomcat源码分析(从启动流程到请求处理)

    Tomcat 8.5下载地址 https://tomcat.apache.org/download-80.cgi Tomcat启动流程 Tomcat源码目录 catalina目录 catalina包含 ...

  3. Tomcat源码分析之—组件启动实现分析

    Tomcat由多个组件组成,那么Tomcat是怎么对他们的生命周期进行管理的么,这里将从Tomcat源码去分析其生命周期的实现: Bootstrape类为Tomcat的入口,所有的组件够通过实现Lif ...

  4. tomcat8 源码分析 | 组件及启动过程

    tomcat 8 源码分析 ,本文主要讲解tomcat拥有哪些组件,容器,又是如何启动的 推荐访问我的个人网站,排版更好看呦: https://chenmingyu.top/tomcat-source ...

  5. tomcat源码分析(一)- tomcat源码导入IDEA并正常启动

    项目导入 代码下载 打开GitHub网站:https://github.com/apache/tomcat 下载对应的zip包 解压对应的压缩包(当然你也可以用工具对其进行解压) unzip tomc ...

  6. Netty源码分析之客户端启动过程

    一.先来看一下客户端示例代码. public class NettyClientTest { public void connect(int port, String host) throws Exc ...

  7. wxWidgets源码分析(1) - App启动过程

    目录 APP启动过程 wxApp入口定义 wxApp实例化准备 wxApp的实例化 wxApp运行 总结 APP启动过程 本文主要介绍wxWidgets应用程序的启动过程,从app.cpp入手. wx ...

  8. Hive源码分析(1)——HiveServer2启动过程

    1.想了解HiveServer2的启动过程,则需要找到启动HiveServer2的入口,hive服务的启动命令为hive --service HiveServer2,通过分析$HIVE_HOME/bi ...

  9. Tomcat 源码分析(转)

    本文转自:http://blog.csdn.net/haitao111313/article/category/1179996 Tomcat源码分析(一)--服务启动 1. Tomcat主要有两个组件 ...

  10. Tomcat源码分析三:Tomcat启动加载过程(一)的源码解析

    Tomcat启动加载过程(一)的源码解析 今天,我将分享用源码的方式讲解Tomcat启动的加载过程,关于Tomcat的架构请参阅<Tomcat源码分析二:先看看Tomcat的整体架构>一文 ...

随机推荐

  1. 对scanner.close方法的误解以及无法补救的错误

    scanner错误关闭导致的异常 public class test2 { public static void main(String[] args) { Scanner scanner1 = ne ...

  2. Flutter学习笔记(6)--Dart异常处理

    如需转载,请注明出处:Flutter学习笔记(6)--Dart异常处理 异常是表示发生了意外的错误,如果没有捕获异常,引发异常的隔离程序将被挂起,并且程序将被终止: Dart代码可以抛出并捕获异常,但 ...

  3. Linux 文件编程、时间编程基本函数

    文件编程 文件描述符 fd --->>>数字(文件的身份证,代表文件身份),通过 fd 可找到正在操作或需要打开的文件. 基本函数操作: 1)打开/创建文件 int open (co ...

  4. 个人永久性免费-Excel催化剂功能第83波-遍历文件夹内文件信息特别是图像、音视频等特有信息

    在过往的功能中,有体现出在Excel上管理文件的极大优势,在文件的信息元数据中,有图片和音视频这两类特有的属性数据,此篇对过往功能的一个补充,特别增加了图片和音视频信息的遍历功能. 使用场景 在文件管 ...

  5. 2019年7月22日 - LeetCode0004

    https://leetcode-cn.com/problems/median-of-two-sorted-arrays/submissions/ 我的解法: 我看到了那个log的要求,也第一时间想到 ...

  6. python基础之元祖、嵌套,for循环、 enumerate、range的试用案例

    元祖又叫做只读列表,可循环查询.可切片,元祖里的直接元素不能更改,但是若里面有嵌套的列表,则可以修改列表里的元素 tu = (1,2,3,'sun',[3,4,5,'cat']) tu[4][3] = ...

  7. Golang高效实践之interface、reflection、json实践

    前言 反射是程序校验自己数据结构和类型的一种机制.文章尝试解释Golang的反射机制工作原理,每种编程语言的反射模型都是不同的,有很多语言甚至都不支持反射. Interface 在将反射之前需要先介绍 ...

  8. java用最少循环求两个数组的交集、差集、并集

    import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List ...

  9. 1. 两数之和 Java解法

    这题属于Leetcode的签到题,基本上每个人一进来就是这题. 用哈希思想来做就是最好的解答. 如果一个target - num[i] 存在那么就返回那个数字对应的下标和当前元素的下标. public ...

  10. Redis(六)--- Redis过期策略与内存淘汰机制

    1.简述 关于Redis键的过期策略,首先要了解两种时间的区别,生存时间和过期时间: 生存时间:一段时长,如30秒.6000毫秒,设置键的生存时间就是设置这个键可以存在多长时间,命令有两个 expir ...