vlambda博客
学习文章列表

我连夜Debug了一遍Tomcat源码


      上次构建了一把Tomcat的源码,还简单的分析了一下Tomcat的项目架构,

(这篇文章)。写完之后总感觉还是少了一点什么,好吧! 今天我们就一起来看看Tomcat是怎么启动的。我们通过在源码上打断点 来一步一步的跟踪Tomcat的启动流程。

      首先打开上次构建的源码,我们找到启动类:


我连夜Debug了一遍Tomcat源码

  

     毫无疑问,在init()这一行代码这里我们必须加上一断点,emmm............

直接这样似乎有点草率了,好吧 下来看看书上怎么说的吧,首先我在书上找到了一张架构图:


我连夜Debug了一遍Tomcat源码


     上面这张图主要是描述了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方法验证一下我们的推断,如下图所示:


我连夜Debug了一遍Tomcat源码


      我们在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方法,看方法名表面的意思是初始化目录 。我们点进去看看一下,到底初始化的是什么目录


我连夜Debug了一遍Tomcat源码


      我们发现这里初始化的是当前系统用户的临时目录。好吧 下面的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文件看看可能就懂了


我连夜Debug了一遍Tomcat源码

       

      我们发现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这个接口中去了,同时我们还发现这个接口中定义了好几个生命周期方法


我连夜Debug了一遍Tomcat源码


     我们发现Lifecycle接口有两个实现类,我们关注LifecycleBase这个实现类,我们发现它是一个抽象类。


我连夜Debug了一遍Tomcat源码


我们跟进去看看 是怎么实现init方法的,


我连夜Debug了一遍Tomcat源码


      其中上图中标出来的一行代码 调用了 initInternal 这个方法 ,我们继续按着crtl键鼠标点进去 发现这个方法实际调用的是 protected abstract void initInternal() 这个抽象方法。好吧 ,我们来查看这个抽象方法的实现


我连夜Debug了一遍Tomcat源码


      我们发现一共有很多,那么我们怎么知道哪个才是这个地方的实现呢?我也不知道,不过可以先回到下面这张图中:


我连夜Debug了一遍Tomcat源码


      我们来看整个调用的流程Bootstrap -> Catalina -> Server -> Service .........

我们大概知道从Catalina中调用到了一个Server中了,同时仔细观protected abstract void initInternal() 这个抽象方法的实现,我们发现还真有一个StandardServer的实现,同样的我们还发现有一个StandardService的实现,如下图所示:


我连夜Debug了一遍Tomcat源码


好吧 我们点进去看看 StandardServer具体的实现,代码如下:

 /** * Invoke a pre-startup initialization. This is used to allow connectors * to bind to restricted ports under Unix operating environments. */ @Override 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方法


我连夜Debug了一遍Tomcat源码


    好了,毫无疑问 接下来又是LifecycleBase中的protected abstract void initInternal方法,这里我们应该知道了 这个时候的具体实现应该在StandardService这个类中,好吧 我们继续选择StandardService这个类跟进去看看


我连夜Debug了一遍Tomcat源码


       好吧 ,其实我们这个时候就应该想到了一个套路 ,似乎所有的生命周期方法都被提取到了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. */ @Override    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中,我们会发现这是一个类。


我连夜Debug了一遍Tomcat源码


我们继续看看具体的实现


我连夜Debug了一遍Tomcat源码


       我们发现前面的流程图中 最后一个组件就是在这个地方初始化的。好吧 我们跟进去会发现这个init有两个实现


我连夜Debug了一遍Tomcat源码


    大家可以回忆一下这个变文章中描述的tomcat默认使用的是HTTP1.1的协议那这个地方我们肯定就选择AbstractHttp11Protocol这个实现了


@Override    public void init() throws Exception { for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { configureUpgradeProtocol(upgradeProtocol); } super.init(); }


我们发现设置参数后  最终还是调用到了父类中,如下图所示


我连夜Debug了一遍Tomcat源码


      我们重点重点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这个方法,这个方法我们发现是有三个实现。如下所示:


我连夜Debug了一遍Tomcat源码


       在上次这篇文章中给大家说过,tomcat8默认使用的nio。所以这里我们应该进入NioEndpoint中


我连夜Debug了一遍Tomcat源码


      这里我们就可以看到最终绑定了端口号,初始化的过程也就完成了。我们再回到最开始的main方法中


我连夜Debug了一遍Tomcat源码

     

      load方法走完之后还会有一个start方法  同样的这个start方法的执行过和init方法是类似的,我们跟进去发现依然还是通过反射调用Catalina类中的start方法。


我连夜Debug了一遍Tomcat源码


好吧 我们继续看Catalina中的start方法


我连夜Debug了一遍Tomcat源码


      同样的也是调用server中的start方法 也是通过模板方法设计模式进行一层一层的start。这个过程大家可以自己看看,由于是重复性的流程这里就不做过多的介绍了。我们直接看 AbstractEndpoint中的实现


我连夜Debug了一遍Tomcat源码


跟进之后我们依然选择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启动就可以体会整个启动的过程了。