本文呢,主要从main启动入口了解Tomcat的启动大致步骤和如何终止,至于更深入的源码跟踪,下篇
本文阅读到的源代码分支
目录
Bootstrap为启动入口,负责创建Catalina,根据参数调用Catalina相关方方法完成对服务器的操作
Catalina用于解析service.xml创建各个组件。同时负责启动、停止服务器Server
Bootstrap静态块
有静态块先看静态块,内部设置了CATALINA_HOME路径
public final class Bootstrap {
public static final String CATALINA_HOME_PROP = "catalina.home";
public static final String CATALINA_BASE_PROP = "catalina.base";
static {
// Will always be non-null
// 当前工作路径
String userDir = System.getProperty("user.dir");
// 开始找Catalina.home,先看是否启动设置了
String home = System.getProperty(Globals.CATALINA_HOME_PROP);
File homeFile = null;
if (home != null) {
File f = new File(home);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
// 启动没设置,再看当前目录下是否有bootstrap.jar
// 如果有,表示当前是在/tomcat安装目录/bin下,上层目录就是catalina.home
// 可以看看平时部署的tomcat目录结构,bootstrap.jar在/安装目录/bin下,startup.bat中也是类似逻辑
if (homeFile == null) {
// First fall-back. See if current directory is a bin directory
// in a normal Tomcat install
File bootstrapJar = new File(userDir, "bootstrap.jar");
if (bootstrapJar.exists()) {
File f = new File(userDir, "..");
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
}
// 如果还没有找到,设置当前工作目录为catalina.home
if (homeFile == null) {
// Second fall-back. Use current directory
File f = new File(userDir);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
catalinaHomeFile = homeFile;
System.setProperty(
Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
// catalina.base默认和catalina.home一样
String base = System.getProperty(Globals.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
System.setProperty(
Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
}
Bootstrap.main启动流程
Main方法中,初始化BootStrap实例,根据参数命令来判断是启动还是终止Tomcat。
启动容器三个分支方法
- Bootstrap.init() 初始化三种类加载器,实例化Catalina
- Bootstrap.load() 加载server.xml配置,并初始化组件
- Bootstrap.start() 启动server及各组件,监听shutdown事件
private static volatile Bootstrap daemon = null;
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
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 {
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);
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.init
org.apache.catalina.startup.Bootstrap#init()
实例化三种类型类加载器,实例化 Catalina 实例
private Object catalinaDaemon = null;// catalina实例
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
/**
* Initialize daemon.
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {
// 实例化上边三种加载器
initClassLoaders();
// 设置当前启动线程的类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 如果启动了安全管理SecurityManager,加载一些类,忽略这部分
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 反射用catalinaLoader来实例化Catalina实例
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
//System.out.println("输出bootstrap");
//System.out.println(this.getClass().getName());
//System.out.println(this.getClass().getClassLoader().toString());
//Class<?> aClass = startupInstance.getClass();
//System.out.println("输出catalina");
//System.out.println(aClass.getName());
//System.out.println(aClass.getClassLoader().toString());
// 反射将sharedLoader设置为catalinaLoader的父类加载器
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;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
// 将catalina实例引用赋值
catalinaDaemon = startupInstance;
}
初始化加载器 initClassLoaders
org.apache.catalina.startup.Bootstrap#initClassLoaders
private void initClassLoaders() {
try {
// 创建三个类型的类加载器,默认配置下,三个是一个对象
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
// 删除了好多代码,看个大概意思
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 取配置文件的资源路径
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
不再往下贴代码了,大概一说,创建三个类型的加载器
CatalinaProperties.getProperty(name + ".loader");查询配置文件中配置的加载器要加载的资源路径(配置文件key为common.loader/server.loader/shared.loader)
配置文件路径优先级是:
- 先查系统参数配置的catalina.config路径,没有走2
- CATALINA_HOME/conf/catalina.properties,没有走3
- 包内的/org/apache/catalina/startup/catalina.properties
如果没有配置,使用参数中的父类加载器,默认配置下server.loader/shared.loader都为空,所以三个加载器是一个实例
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
为什么需要三种类加载器?
tomcat的加载器不止三种,还有加载/webapp目录下的WebAppClassLoader、加载jsp的JsperClassLoader,类加载器架构如下图:
因为一个完善的Wen服务器要解决如下问题:
- 部署在同一服务器上的web应用使用的类库需要相互隔离,可能都会依赖同一第三方类库的不同版本
- 部署在同一服务器上的web应用使用的类库也可能需要相互共享,同一版本的Spring被多个应用共享
- 服务器不受应用程序的影响,服务器使用的类库应该和应用程序的类库独立
- Jsp的热替换,对于jsp的运行时修改频率远远大于class文件,为了支持热替换
因此,在tomcat中提供多种classpath路径来存放类库,对应四种加载器,如下
- /common/* 被tomcat和所有web应用共享
- /server/* 被tomcat使用,web应用不可见
- /shared/* tomcat不可见,web应用共享
- /WEB-INF/* 仅被当前web应用使用
查看tomcat目录发现并没有前三个目录,因为默认配置下,server.loader和share.loader都为空,只创建一个加载器实例,所以自然将三个目录合并到/lib目录,tomcat团队做了简化,如果不能满足需要,可以修改配置并将目录拆分,启用上述加载器架构
Bootstrap.load
直接反射调用catalina实例的load方法
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);
}
method.invoke(catalinaDaemon, param);
}
继续来看Catalina.load做了什么
创建server.xml解析器,解析,初始化各组件,解析器Digester 这部分源码拆到下次详细看
/**
* Start a new server instance.
...省略一些代码,留着主要的
*/
public void load() {
if (loaded) {
return;
}
loaded = true;
// 检查java.io.tmpdir路径是否有效
// 我的默认是C:\Users\lxy\AppData\Local\Temp\,路径检查是否是有效路径
initDirs();
// 创建Digester前设置一些使用的参数
initNaming();
// 解析server.xml的类
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
// 获取conf/server.xml
File file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
// 开始解析
inputSource.setByteStream(inputStream);
digester.push(this);// 关联catalina和解析器
digester.parse(inputSource);
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
initStreams();
// Start the new server
getServer().init();
}
为什么Bootstrap类要反射调用Catalina
Bootstrap类中实例化Catalina或者调用Catalina的load或即将看到的start方法都是反射调用
我认为是因为为了使catalina类和启动类bootstrap不使用同一个加载器,当前启动类Bootstrap的类加载器是AppClassLoader,使真正的启动类Catalina都使用初始化的catalinaClassLoader加载
我在Bootstrap#init()方法中加了几行输出(上边贴的代码有),输出bootstrap和catalina实例的加载器
源码中因为两个都在一个包下,因此执行结果加载器是一个实例,如图
但是我们找一个我们平时发包使用的tomcat,会发现,tomcat把启动类bootstrap.java和catalina.java拆了,不在一个路径下
两个类这时候跑的时候就不是一个加载器了,测试一下,把源码bootstrap.java编译并替换到bootstrap.jar中,startup.bat运行截图如下
可以看到不是一个加载器,所以是为了使catalina类实例启动时涉及的类都由初始化的加载器catalinaClassLoader加载
Bootstrap.start
即反射调用Catalina.start()
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}
Catalina.start
- 启动Server,这个本文暂不跟代码
- 注册shutdown钩子,即注册一个线程任务,在jvm结束时调用(main结束时),如果server未停止调用stop方法停止
- 监听等待shutdown端口,监听到shutdown命令后调用Server.stop(),销毁容器
/**
* Start a new server instance.
*/
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
try {
// 1、启动Server
getServer().start();
} catch (LifecycleException e) {
getServer().destroy();
return;
}
// Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
// 2、注册shutdown钩子,main结束时调用,如果server未停止调用stop方法停止
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);
}
}
if (await) {
// main线程等待,等待接收shutdown命令,接受到则跳出阻塞
await();
// 执行Server.stop();
stop();
}
}
注册钩子CatalinaShutdownHook
即一个线程,判断当前server是否已被停止,如果还没则调用stop(),在Main结束时调用
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();
}
}
}
}
await监听shutdown端口
在server.xml刚开头
8605端口就是shutdown端口,后边的属性就是终止命令,服务启动后服务器会监听8605端口,如果这个端口接收到了"SHUTDOWN"这个字符串,那么就会终止Server
//org.apache.catalina.core.StandardServer#await
// 为方便阅读,删除了部分无关代码,日志等
@Override
public void await() {
if (port == -2) {
// shutdown端口配置为-2,启动完Server直接再终止Server
return;
}
if (port==-1) {
// 配置为-1,则不再监听shutdown端口
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端口
try {
awaitSocket = new ServerSocket(port, 1,
InetAddress.getByName(address));
} catch (IOException e) {
return;
}
try {
awaitThread = Thread.currentThread();
// Loop waiting for a connection and a valid command
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}
Socket socket = null;
StringBuilder command = new StringBuilder();
InputStream stream;
long acceptStartTime = System.currentTimeMillis();
try {
// accept阻塞监听端口
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
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;
}
}
}
服务器端口监听到了shutdown命令后,开始执行Catalina.stop()
public void stop() {
try {
if (useShutdownHook) {
// 移除shutdown钩子,这个stop方法会停止server,不需要钩子再次执行
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
// Shut down the server
try {
Server s = getServer();
LifecycleState state = s.getState();
if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
&& LifecycleState.DESTROYED.compareTo(state) >= 0) {
} else {
s.stop();
s.destroy();
}
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
Bootstrap的stop流程
在Bootstrap.main中默认没有参数走的是start,stop走stop流程
org.apache.catalina.startup.Catalina#stopServer(java.lang.String[])
public void stopServer(String[] arguments) {
if (arguments != null) {
arguments(arguments);
}
Server s = getServer();
if (s == null) {
// 没有server,需要创建个Digester解析一下server.xml,解析出shutdown端口等server信息
Digester digester = createStopDigester();
File file = configFile();
try (FileInputStream fis = new FileInputStream(file)) {
InputSource is =
new InputSource(file.toURI().toURL().toString());
is.setByteStream(fis);
digester.push(this);
digester.parse(is);
} catch (Exception e) {
log.error("Catalina.stop: ", e);
System.exit(1);
}
} else {
// server已经存在是什么情况,启动后的代码里直接再次调用了main方法吗?
try {
s.stop();
s.destroy();
} catch (LifecycleException e) {
log.error("Catalina.stop: ", e);
}
return;
}
// 连接server的shutdown端口,发送一个SHUTDOWN字符串,那边接受到就关闭Server
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) {
System.exit(1);
} catch (IOException e) {
System.exit(1);
}
} else {
System.exit(1);
}
}