我连夜Debug了一遍Tomcat源码
上次构建了一把Tomcat的源码,还简单的分析了一下Tomcat的项目架构,
(这篇文章)。写完之后总感觉还是少了一点什么,好吧! 今天我们就一起来看看Tomcat是怎么启动的。我们通过在源码上打断点 来一步一步的跟踪Tomcat的启动流程。
首先打开上次构建的源码,我们找到启动类:
毫无疑问,在init()这一行代码这里我们必须加上一断点,emmm............
直接这样似乎有点草率了,好吧 下来看看书上怎么说的吧,首先我在书上找到了一张架构图:
上面这张图主要是描述了tomcat启动的流程,我们看可以看到上述流程主要分为了两个部分 一个是初始化init()另外一个是start()。
我们可以看你的出来入口是Bootstrap中的init方法,接着调用到了Catalina中的init方法,然后就是一系列的init初始化。好了我们回到代码上 来瞅瞅从Bootstrap是怎么调用到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");
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;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
上次和大家介绍了这个方法主要是初始化了Catalina这个类,从.Class<?> startupClass=catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");这行代码就可以看得出来,到这里我们必须知道一件事 也就是Bootstrap中的init方法初始化的其实是Catalina这个类(敲黑板!!!)。好吧,初始化之后做了什么呢 我们继续来看看main方法:
public static void main(String args[]) {
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);
}
}
我们可以看到上述代码中有一个 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);
method.invoke(catalinaDaemon, param);
}
我们可以看到methodName的值是load 从上面的init方法中我们可以知道catalinaDaemon肯定就是Catalina这个类了,好吧这里我们又可以知道一个信息,这里的load方法实际上调用的是Catalina类中的load方法。下面我们可以debug启动main方法验证一下我们的推断,如下图所示:
我们在load方法的最后一行发现的确和我们推断的一样,这里通过反射调用的就是Catalina类中的load方法。好了,我们接着去看看Catalina类中的load方法。(load方法片段,对应源码531行到564行)
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester();
我们可以看到这个有一个initDirs方法,看方法名表面的意思是初始化目录 。我们点进去看看一下,到底初始化的是什么目录
我们发现这里初始化的是当前系统用户的临时目录。好吧 下面的initNaming方法大家可以自己看看初始化的是什么内容。回到load方法中的那段代码里,我们来看看createStartDigester这个方法,这里先和大家说一下该方法是xml文件解析器。我们跟进去看看 就会发现该方法就是加载server.xml文件的。(部分代码如下)
// Configure the actions we will be using
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
digester.addObjectCreate("Server/GlobalNamingResources",
"org.apache.catalina.deploy.NamingResourcesImpl");
digester.addSetProperties("Server/GlobalNamingResources");
digester.addSetNext("Server/GlobalNamingResources",
"setGlobalNamingResources",
"org.apache.catalina.deploy.NamingResourcesImpl");
digester.addObjectCreate("Server/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties("Server/Listener");
digester.addSetNext("Server/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
我们可以看到在该方法这里会有很多上述代码片段中的代码,相信大家看到参数打开server.xml文件看看可能就懂了
我们发现server.xml文件中的顶层标签就是Server 然后在Server 标签中有Listener、GlobalNamingResources、Service这几个标签,我们再回到代码中 ,比如:
digester.addObjectCreate("Server/GlobalNamingResources"...
digester.addObjectCreate("Server/Listener",
digester.addObjectCreate("Server/Service",
从上面的结构中就可以发现这个是按照标签来解析server.xml文件的。
好了,我们继续回到load方法中,来看下面这段代码(638行)
// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
上述代码中有一行getServer().init(),我们可以先看看getServer()方法中都做了什么
public Server getServer() {
return server;
}
好吧 ,从这里我们又可以发现此处实际上是调用的Server对象的init方法。我们点进去发现进入到了Lifecycle这个接口中去了,同时我们还发现这个接口中定义了好几个生命周期方法
我们发现Lifecycle接口有两个实现类,我们关注LifecycleBase这个实现类,我们发现它是一个抽象类。
我们跟进去看看 是怎么实现init方法的,
其中上图中标出来的一行代码 调用了 initInternal 这个方法 ,我们继续按着crtl键鼠标点进去 发现这个方法实际调用的是 protected abstract void initInternal() 这个抽象方法。好吧 ,我们来查看这个抽象方法的实现
我们发现一共有很多,那么我们怎么知道哪个才是这个地方的实现呢?我也不知道,不过可以先回到下面这张图中:
我们来看整个调用的流程Bootstrap -> Catalina -> Server -> Service .........
我们大概知道从Catalina中调用到了一个Server中了,同时仔细观protected abstract void initInternal() 这个抽象方法的实现,我们发现还真有一个StandardServer的实现,同样的我们还发现有一个StandardService的实现,如下图所示:
好吧 我们点进去看看 StandardServer具体的实现,代码如下:
/**
* Invoke a pre-startup initialization. This is used to allow connectors
* to bind to restricted ports under Unix operating environments.
*/
protected void initInternal() throws LifecycleException {
super.initInternal();
// Register global String cache
// Note although the cache is global, if there are multiple Servers
// present in the JVM (may happen when embedding) then the same cache
// will be registered under multiple names
onameStringCache = register(new StringCache(), "type=StringCache");
// Register the MBeanFactory
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");
// Register the naming resources
globalNamingResources.init();
// Populate the extension validator with JARs from common and shared
// class loaders
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
// Walk the class loader hierarchy. Stop at the system class loader.
// This will add the shared (if present) and common class loaders
while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL url : urls) {
if (url.getProtocol().equals("file")) {
try {
File f = new File (url.toURI());
if (f.isFile() &&
f.getName().endsWith(".jar")) {
ExtensionValidator.addSystemResource(f);
}
} catch (URISyntaxException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
}
}
}
cl = cl.getParent();
}
}
// Initialize our defined Services
for (int i = 0; i < services.length; i++) {
services[i].init();
}
}
我们发现在方法的最后有一个循环 ,把所有的services中的Service对象迭代出来。然后调用Service对象的init方法进行初始化,我们继续点进去查看,发现又回到了Lifecycle接口中的init方法
好了,毫无疑问 接下来又是LifecycleBase中的protected abstract void initInternal方法,这里我们应该知道了 这个时候的具体实现应该在StandardService这个类中,好吧 我们继续选择StandardService这个类跟进去看看
好吧 ,其实我们这个时候就应该想到了一个套路 ,似乎所有的生命周期方法都被提取到了Lifecycle这个接口中,LifecycleBase这个抽象类实现类该接口。init方法的具体的实现由 initInternal 这个抽象方法去适配不同的实现类(模板)去实现,似乎这就是大名鼎鼎的模板方法设计模式。好吧 我们继续往下看,根据前面的框图我们发现下一个初始化的组件是Executor。下面我们来看看 这段源码
/**
* Invoke a pre-startup initialization. This is used to allow connectors
* to bind to restricted ports under Unix operating environments.
*/
protected void initInternal() throws LifecycleException {
super.initInternal();
if (engine != null) {
engine.init();
}
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize mapper listener
mapperListener.init();
// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
这里我们可以发现有好几个init方法,毫无疑问我们跟进去发现肯定也是回到了Lifecycle中,好吧 这里就不再阐述这个过程了,我们直接看connector的init方法,这个方法的实现是在Connector中,我们会发现这是一个类。
我们继续看看具体的实现
我们发现前面的流程图中 最后一个组件就是在这个地方初始化的。好吧 我们跟进去会发现这个init有两个实现
大家可以回忆一下这个变文章中描述的tomcat默认使用的是HTTP1.1的协议那这个地方我们肯定就选择AbstractHttp11Protocol这个实现了
public void init() throws Exception {
for (UpgradeProtocol upgradeProtocol : upgradeProtocols) {
configureUpgradeProtocol(upgradeProtocol);
}
super.init();
}
我们发现设置参数后 最终还是调用到了父类中,如下图所示
我们重点重点endpoint.init()这行代,这里的init方法我们跟进去看看具体的逻辑,代码如下:
public void init() throws Exception {
if (bindOnInit) {
bind();
bindState = BindState.BOUND_ON_INIT;
}
if (this.domain != null) {
// Register endpoint (as ThreadPool - historical name)
oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
Registry.getRegistry(null, null).registerComponent(this, oname, null);
ObjectName socketPropertiesOname = new ObjectName(domain +
":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");
socketProperties.setObjectName(socketPropertiesOname);
Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);
for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
registerJmx(sslHostConfig);
}
}
}
好吧 我们来看第3行代码,调用的bind这个方法,这个方法我们发现是有三个实现。如下所示:
在上次这篇文章中给大家说过,tomcat8默认使用的nio。所以这里我们应该进入NioEndpoint中
这里我们就可以看到最终绑定了端口号,初始化的过程也就完成了。我们再回到最开始的main方法中
load方法走完之后还会有一个start方法 同样的这个start方法的执行过和init方法是类似的,我们跟进去发现依然还是通过反射调用Catalina类中的start方法。
好吧 我们继续看Catalina中的start方法
同样的也是调用server中的start方法 也是通过模板方法设计模式进行一层一层的start。这个过程大家可以自己看看,由于是重复性的流程这里就不做过多的介绍了。我们直接看 AbstractEndpoint中的实现
跟进之后我们依然选择Nio的实现
我们再最后可以发现有一行startAcceptorThreads(),这个执行完成之后我们就可以启动容器里了。
最后大家可以自己看看这个方法的实现,代码如下:
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}
好了,到了这里整个流程就走完了,大家可以根据本文 标出的地方打上断点,debug启动就可以体会整个启动的过程了。