我连夜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() methodif (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 loaderif (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 completedBootstrap 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 reportingif (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() methodString 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 neededinitNaming();// Create and execute our DigesterDigester digester = createStartDigester();
我们可以看到这个有一个initDirs方法,看方法名表面的意思是初始化目录 。我们点进去看看一下,到底初始化的是什么目录
我们发现这里初始化的是当前系统用户的临时目录。好吧 下面的initNaming方法大家可以自己看看初始化的是什么内容。回到load方法中的那段代码里,我们来看看createStartDigester这个方法,这里先和大家说一下该方法是xml文件解析器。我们跟进去看看 就会发现该方法就是加载server.xml文件的。(部分代码如下)
// Configure the actions we will be usingdigester.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 servertry {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 namesonameStringCache = register(new StringCache(), "type=StringCache");// Register the MBeanFactoryMBeanFactory factory = new MBeanFactory();factory.setContainer(this);onameMBeanFactory = register(factory, "type=MBeanFactory");// Register the naming resourcesglobalNamingResources.init();// Populate the extension validator with JARs from common and shared// class loadersif (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 loaderswhile (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 Servicesfor (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 Executorsfor (Executor executor : findExecutors()) {if (executor instanceof JmxEnabled) {((JmxEnabled) executor).setDomain(getDomain());}executor.init();}// Initialize mapper listenermapperListener.init();// Initialize our defined Connectorssynchronized (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启动就可以体会整个启动的过程了。
