-
【Tomcat】第五篇:Tomcat启动流程源码分析(上)main入口
2020-09-18 21:44:06Tomcat运行入口类BootStrap的入口方法,即启动方法;将Tomcat的启动分为了两部分 init 和 start init:初始化阶段,即后面要依次调用的是各组件的初始化方法 InitInternal:Standard~,Connector;因为他们继承了...Tomcat运行入口类BootStrap的入口方法,即启动方法;将Tomcat的启动分为了两部分 init 和 start
- init:初始化阶段,即后面要依次调用的是各组件的初始化方法
- InitInternal:Standard~,Connector;因为他们继承了LifeCycleBase,里面用模板模式实现了init方法
- init:ProtocolHandler是enum,Endpoint是抽象类;他们都自己定义了init,而不是LifeCycle的init
- start:运行阶段,即后面要依次调用各组件的start方法
- startInternal:Standard~,Connector;因为他们继承了LifeCycleBase,里面用模板模式实现了start方法
- start:ProtocolHandler是enum,Endpoint是抽象类;他们都自己定义了start,而不是LifeCycle的start
public static void main(String args[]) { //1.初始化阶段 init() if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { // 调用init()初始化BootStrap bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } // 将守护线程bootstop赋给守护线程daemon daemon = bootstrap; } else { Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } //2.运行阶段 start() try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; // deamon.load(args),实际上会去调用Catalina#load(args)方法, // 会去初始化一些资源,优先加载conf/server.xml,找不到再去加载server-embed.xml; // 此外,load方法还会初始化Server daemon.load(args); // daemon.start(),实例上是调用Catalina.start() daemon.start(); // 输入stop命令就停止 } 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(); } 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); } }
- init:初始化阶段,即后面要依次调用的是各组件的初始化方法
-
【Tomcat】第七篇:Tomcat启动流程源码分析(下)start启动组件
2020-09-19 00:51:373.start 组件启动 BootStrap 启动过程和初始化一样,由Bootstrap反射调用Catalina的start方法 public void start() throws Exception { if( catalinaDaemon==null ) init(); // 调用CatAlina的Start方法 Method...启动过程和初始化一样,由Bootstrap反射调用Catalina的start方法
public void start() throws Exception { if( catalinaDaemon==null ) init(); // 调用CatAlina的Start方法 Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); method.invoke(catalinaDaemon, (Object [])null); }
Catalina
主要分为以下三个步骤,其核心逻辑在于Server组件:
- 调用Server的start方法,启动Server组件
- 注册jvm关闭的勾子程序,用于安全地关闭Server组件,以及其它组件
- 开启shutdown端口的监听并阻塞,用于监听关闭指令
public void start() { // 省略若干代码...... // 1.Start the new server try { getServer().start(); } catch (LifecycleException e) { // 省略...... return; } // 2.注册勾子,用于安全关闭tomcat if (useShutdownHook) { if (shutdownHook == null) { shutdownHook = new CatalinaShutdownHook(); } Runtime.getRuntime().addShutdownHook(shutdownHook); } // 3.Bootstrap中会设置await为true,其目的在于让tomcat在shutdown端口阻塞监听关闭命令 if (await) { await(); stop(); } }
1.启动Server
StandardSever
- 先是由LifecycleBase统一发出STARTING_PREP事件,StandardServer额外还会发出CONFIGURE_START_EVENT、STARTING事件,用于通知LifecycleListener在启动前做一些准备工作,比如NamingContextListener会处理CONFIGURE_START_EVENT事件,实例化tomcat相关的上下文,以及ContextResource资源.
- 然后,启动内部的NamingResourcesImpl实例,这个类封装了各种各样的数据,比如ContextEnvironment、ContextResource、Container等等,它用于Resource资源的初始化,以及为webapp应用提供相关的数据资源,比如 JNDI 数据源(对应ContextResource).
- 接着,启动Service组件,这一块的逻辑将在下面进行详细分析,最后由LifecycleBase发出STARTED事件,完成start.
protected void startInternal() throws LifecycleException { fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); globalNamingResources.start(); // Start our defined Services synchronized (servicesLock) { for (int i = 0; i < services.length; i++) { services[i].start(); } } }
2.启动Service
tandardService
- 启动Engine,Engine的child容器都会被启动,webapp的部署会在这个步骤完成;
- 启动Executor,这是tomcat用Lifecycle封装的线程池,继承至java.util.concurrent.Executor以及tomcat的Lifecycle接口
- 启动MapperListener
- 启动Connector组件,由Connector完成Endpoint的启动,这个时候意味着tomcat可以对外提供请求服务了。
protected void startInternal() throws LifecycleException { setState(LifecycleState.STARTING); // 1.启动Engine if (engine != null) { synchronized (engine) { engine.start(); } } // 2.启动Executor线程池 synchronized (executors) { for (Executor executor: executors) { executor.start(); } } // 3.启动MapperListener mapperListener.start(); // 4.启动Connector synchronized (connectorsLock) { for (Connector connector: connectors) { try { // If it has already failed, don't try and start it if (connector.getState() != LifecycleState.FAILED) { connector.start(); } } catch (Exception e) { // logger...... } } } }
3.启动Engine
StandardEngine
Engine主要是调用调用父类ContainerBase的startInternal
protected synchronized void startInternal() throws LifecycleException { // Log our server identification information if(log.isInfoEnabled()) log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo()); // 调用父类ContainerBase的startInternal方法 super.startInternal(); }
ContainerBase
-
启动子容器,通过线程池(默认只有一个线程,可以在配置文件中设置(startStopThread))
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" startStopThreads="4"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Host>
-
启动Pipeline,并且发出STARTING事件
-
如果backgroundProcessorDelay参数 >= 0,则开启ContainerBackgroundProcessor线程,用于调用子容器的backgroundProcess。
protected synchronized void startInternal() throws LifecycleException { // 省略若干代码...... // 把子容器的启动步骤放在线程中处理,默认情况下线程池只有一个线程处理任务队列 Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } // 1.阻塞当前线程,直到子容器start完成 boolean fail = false; for (Future<Void> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); fail = true; } } // 2.启用Pipeline if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); setState(LifecycleState.STARTING); // 3.开启ContainerBackgroundProcessor线程用于调用子容器的backgroundProcess方法 // 默认情况下backgroundProcessorDelay=-1,不会启用该线程 threadStart(); }
4.启动Pipeline
**StandPipeline **
我们来看 Pipeline 启动过程,默认使用 StandardPipeline 实现类,它也是一个Lifecycle。在容器启动的时候,StandardPipeline 会遍历 Valve 链表,如果 Valve 是 Lifecycle 的子类,则会调用其 start 方法启动 Valve 组件,代码如下\
public class StandardPipeline extends LifecycleBase implements Pipeline, Contained { // 省略若干代码...... protected synchronized void startInternal() throws LifecycleException { Valve current = first; if (current == null) { current = basic; } while (current != null) { if (current instanceof Lifecycle) ((Lifecycle) current).start(); current = current.getNext(); } setState(LifecycleState.STARTING); } }
5.启动Host
StandardHost
protected synchronized void startInternal() throws LifecycleException { // errorValve默认使用org.apache.catalina.valves.ErrorReportValve String errorValve = getErrorReportValveClass(); if ((errorValve != null) && (!errorValve.equals(""))) { try { boolean found = false; // 如果所有的阀门中已经存在这个实例,则不进行处理,否则添加到 Pipeline 中 Valve[] valves = getPipeline().getValves(); for (Valve valve : valves) { if (errorValve.equals(valve.getClass().getName())) { found = true; break; } } // 如果未找到则添加到 Pipeline 中,注意是添加到 basic valve 的前面 // 默认情况下,first valve 是 AccessLogValve,basic 是 StandardHostValve if(!found) { Valve valve = (Valve) Class.forName(errorValve).getConstructor().newInstance(); getPipeline().addValve(valve); } } catch (Throwable t) { // 处理异常,省略...... } } // 调用父类 ContainerBase,完成统一的启动动作 super.startInternal(); }
StandardHost Pipeline 包含的Valve 组件:
- basic:org.apache.catalina.core.StandardHostValve
- first:org.apache.catalina.valves.AccessLogValve
需要注意的是,在往Pipeline 中添加 Valve 阀门时,是添加到first 后面,basic 前面
由上面的代码可知,在start 的时候,StandardHost 并没有做太多的处理,那么StandardHost 又是怎么知道它有哪些 child 容器需要启动呢?
HostConfig
tomcat 在这块的逻辑处理有点特殊,使用 HostConfig 加载子容器,而这个 HostConfig 是一个LifecycleListener,它会处理 start、stop事件通知,并且会在线程池中启动、停止 Context 容器,接下来看下HostConfig 是如何工作的
- 以下是HostConfig 处理事件通知的代码,
- 我们着重关注下 start 方法,这个方法主要是做一些应用部署的准备工作,比如过滤无效的webapp、解压war包等
- 而主要的逻辑在于deployDirectories 中,它会往线程池中提交一个 DeployDirectory 任务,并且调用Future#get() 阻塞当前线程,直到 deploy 工作完成
// org.apache.catalina.startup.HostConfig public void lifecycleEvent(LifecycleEvent event) { // (省略若干代码) 判断事件是否由 Host 发出,并且为 HostConfig 设置属性 if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { check(); } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) { beforeStart(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { stop(); } }
-start()
public void start() { // (省略若干代码) if (host.getDeployOnStartup()) deployApps(); }
–deployApps()
protected void deployApps() { File appBase = host.getAppBaseFile(); File configBase = host.getConfigBaseFile(); // 过滤出 webapp 要部署应用的目录 String[] filteredAppPaths = filterAppPaths(appBase.list()); // 部署 xml 描述文件 deployDescriptors(configBase, configBase.list()); // 解压 war 包,但是这里还不会去启动应用 deployWARs(appBase, filteredAppPaths); // 处理已经存在的目录,前面解压的 war 包不会再行处理 deployDirectories(appBase, filteredAppPaths); }
—DeployDirectory
这个DeployDirectory 任务很简单,只是调用HostConfig#deployDirectory(cn, dir)
private static class DeployDirectory implements Runnable { // (省略若干代码) @Override public void run() { config.deployDirectory(cn, dir); } }
----deployDirectory()
protected void deployDirectory(ContextName cn, File dir) { Context context = null; File xml = new File(dir, Constants.ApplicationContextXml); File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); // 实例化 StandardContext if (deployThisXML && xml.exists()) { synchronized (digesterLock) { // 省略若干异常处理的代码 context = (Context) digester.parse(xml); } // (省略)为 Context 设置 configFile } else if (!deployThisXML && xml.exists()) { // 异常处理 context = new FailedContext(); } else { context = (Context) Class.forName(contextClass).getConstructor().newInstance(); } // 实例化 ContextConfig,作为 LifecycleListener 添加到 Context 容器中, // S这和 StandardHost 的套路一样,都是使用 XXXConfig Class<?> clazz = Class.forName(host.getConfigClass()); LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); context.addLifecycleListener(listener); context.setName(cn.getName()); context.setPath(cn.getPath()); context.setWebappVersion(cn.getVersion()); context.setDocBase(cn.getBaseName()); // 实例化 Context 之后,为 Host 添加子容器 host.addChild(context); }
现在有两个疑问:
- 为什么要使用HostConfig 组件启动 Context 容器呢,不可以直接在Host 容器中直接启动吗?
- HostConfig 不仅仅是启动、停止Context 容器,还封装了很多应用部署的逻辑
- 此外,还会对 web.xml、context.xml文件的改动进行监听,默认情况会重新启动 Context 容器。
- 而这个Host 只是负责管理 Context 的生命周期,基于单一职责的原则,tomcat 利用事件通知的方式,很好地解决了藕合问题,Context 容器也是如此,它会对应一个 ContextConfig
- Context 容器又是如何启动的?
- 前面我们也提到了,HostConfig将当前 Context 实例作为子容器添加到Host 容器中(调用 ContainerBase.addChild 方法 ),而Context 的启动就是在添加的时候调用的
- ContainerBase 的关键代码如下所示,Context启动的时候会解析web.xml,以及启动Servlet、Listener,Servlet3.0还支持注解配置,等等这一系列逻辑将在下一篇文章进行分析
6.启动Context
首先我们思考两个问题:
-
tomcat 如何支持servlet3.0 的注解编程,比如对javax.servlet.annotation.WebListener 注解的支持?
- 如果 tomcat 利用ClassLoader 加载 webapp 下面所有的class,从而分析 Class 对象的注解,这样子肯定会导致很多问题,比如 MetaSpace 出现内存溢出,而且加载了很多不想干的类我们知道 jvm 卸载 class 的条件非常苛刻,这显然是不可取的。
- 因此,tomcat 开发了字节码解析的工具类,位于 org.apache.tomcat.util.bcel,bcel即:Byte Code Engineering Library,专门用于解析class 字节码,而不是像我们前面猜测的那样,把类加载到 jvm 中。
-
假如 webapp 目录有多个应用,使用的开源框架的 jar 版本不尽一致,tomcat 是怎样避免出现类冲突?
不同的 webapp 使用不同的ClassLoader 实例加载 class,因此webapp 内部加载的 class 是不同的,自然不会出现类冲突,当然这里要排除 ClassLoader 的parent 能够加载的 class。
StandardContext的属性主要有:
// 即ServletContext上下文 protected ApplicationContext context // 根据 class 实例化对象,比如 Listener、Filter、Servlet 实例对象 private InstanceManager instanceManager // SessionListener、ContextListner 等集合 private List<Object> applicationEventListenersList // filer 名字与 FilterConfig 的映射关系 private HashMap<String, ApplicationFilterConfig> filterConfigs // 用于加载class等资源 private Loader loader // 用于对loader的读写操作 private final ReadWriteLock loaderLock // Session管理器 protected Manager manager // 用于对manager的读写操作 private final ReadWriteLock managerLock // url与Servlet名字的映射关系 private HashMap<String, String> servletMappings // 错误码与错误页的映射 private HashMap<Integer, ErrorPage> statusPages // 用于扫描jar包资源 private JarScanner jarScanner // cookies处理器,默认使用Rfc6265CookieProcessor private CookieProcessor cookieProcessor
StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用Loader 加载 class、使用JarScanner 扫描 jar 包,等等。因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:
- 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
- 实例化WebResourceRoot,默认实现类是 StandardRoot,用于读取webapp 的文件资源
- 实例化Loader 对象,Loader 是tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
- 发出CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的Listener、Servlet、Filter等
- 实例化Sesssion 管理器,默认使用 StandardManager
- 调用listenerStart,实例化 servlet 相关的各种Listener,并且调用 ServletContextListener
- 处理Filter
- 加载Servlet
下面,将分析下几个重要的步骤
6.1 ContextConfig读取web.xml
ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener等,其核心逻辑在 ContextConfig#webConfig() 方法中实现。下面,我们对 ContextConfig 进行详细分析
首先,是通过 WebXmlParser 对web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的Servlet、Filter、Listener注册到 WebXml 实例中
protected void webConfig() { WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(), context.getXmlValidation(), context.getXmlBlockExternal()); Set<WebXml> defaults = new HashSet<>(); defaults.add(getDefaultWebXmlFragment(webXmlParser)); // 创建 WebXml实例,并解析 web.xml 文件 WebXml webXml = createWebXml(); InputSource contextWebXml = getContextWebXmlSource(); if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) { ok = false; } }
如果没有 web.xml 文件,tomcat 会先扫描 WEB-INF/classes 目录下面的 class 文件,然后扫描 WEB-INF/lib 目录下面的 jar 包,解析字节码读取 servlet 相关的注解配置类,这里不得不吐槽下 serlvet3.0 注解,对 servlet 注解的处理相当重量级。tomcat不会预先把该 class 加载到 jvm 中,而是通过解析字节码文件,获取对应类的一些信息,比如注解、实现的接口等,核心代码如下所示
protected void processAnnotationsStream(InputStream is, WebXml fragment, boolean handlesTypesOnly, Map<String,JavaClassCacheEntry> javaClassCache) throws ClassFormatException, IOException { // is 即 class 字节码文件的 IO 流 ClassParser parser = new ClassParser(is); // 使用 JavaClass 封装 class 相关的信息 JavaClass clazz = parser.parse(); checkHandlesTypes(clazz, javaClassCache); if (handlesTypesOnly) { return; } AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries(); if (annotationsEntries != null) { String className = clazz.getClassName(); for (AnnotationEntry ae : annotationsEntries) { String type = ae.getAnnotationType(); if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) { processAnnotationWebServlet(className, ae, fragment); }else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) { processAnnotationWebFilter(className, ae, fragment); }else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) { fragment.addListener(className); } else { // Unknown annotation - ignore } } } }
tomcat 还会加载 WEB-INF/classes/META-INF/resources/、WEB-INF/lib/xxx.jar/META-INF/resources/ 的静态资源,这一块的作用暂时不清楚,关键代码如下所示:
protected void processResourceJARs(Set<WebXml> fragments) { for (WebXml fragment : fragments) { URL url = fragment.getURL(); if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) { try (Jar jar = JarFactory.newInstance(url)) { jar.nextEntry(); String entryName = jar.getEntryName(); while (entryName != null) { if (entryName.startsWith("META-INF/resources/")) { context.getResources().createWebResourceSet( WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, "/META-INF/resources"); break; } jar.nextEntry(); entryName = jar.getEntryName(); } } } else if ("file".equals(url.getProtocol())) { File file = new File(url.toURI()); File resources = new File(file, "META-INF/resources/"); if (resources.isDirectory()) { context.getResources().createWebResourceSet( WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", resources.getAbsolutePath(), null, "/"); } } } }
6.2 启动Wrapper容器
ContextConfig 把 Wrapper 子容器添加到 StandardContext 容器中之后,便会挨个启动 Wrapper 子容器。但是实际上,由于StandardContext 至 ContainerBase,在添加子容器的时候,便会调用 start 方法启动 Wrapper。
for (Container child : findChildren()) { if (!child.getState().isAvailable()) { child.start(); } }
6.3 初始化处理
在初始化 Servlet、Listener 之前,便会先调用 ServletContainerInitializer,进行额外的初始化处理。注意:ServletContainerInitializer 需要的是 Class 对象,而不是具体的实例对象,这个时候 servlet 相关的Listener 并没有被实例化,因此不会产生矛盾
// 指定 ServletContext 的相关参数 mergeParameters(); // 调用 ServletContainerInitializer#onStartup() for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail"), e); ok = false; break; } }
6.4 启动Servlet 相关的 Listener
WebConfig 加载Listener 时,只是保存了 className,实例化动作由StandardContext 触发。前面在介绍StandardContext 的时候提到了InstanceManager,创建实例的逻辑由 InstanceManager 完成。
Listener 监听器分为Event、Lifecycle 监听器,WebConfig在加载 Listener 的时候是不会区分的,实例化之后才会分开存储。在完成 Listener 实例化之后,tomcat容器便启动 OK 了。此时,tomcat 需要通知应用程序定义的ServletContextListener,方便应用程序完成自己的初始化逻辑,它会遍历 ServletContextListener 实例,并调用其contextInitialized 方法,比如 spring 的ContextLoaderListener
- 有以下 Event 监听器,主要是针对事件通知:
- ServletContextAttributeListener
- ServletRequestAttributeListener
- ServletRequestListener
- HttpSessionIdListener
- HttpSessionAttributeListener
- 有以下两种 Lifecycle 监听器,主要是针对ServletContext、HttpSession 的生命周期管理,比如创建、销毁等
- ServletContextListener
- HttpSessionListener
6.5 初始化Filter
ContextConfig 在处理CONFIGURE_START_EVENT 事件的时候,会使用 FilterDef 保存Filter 信息。而 StandardContext 会把FilterDef 转化成 ApplicationFilterConfig,在ApplicationFilterConfig 构造方法中完成 Filter 的实例化,并且调用Filter 接口的 init 方法,完成Filter 的初始化。ApplicationFilterConfig 是javax.servlet.FilterConfig 接口的实现类。
public boolean filterStart() { boolean ok = true; synchronized (filterConfigs) { filterConfigs.clear(); for (Entry<String,FilterDef> entry : filterDefs.entrySet()) { String name = entry.getKey(); try { // 在构造方法中完成 Filter 的实例化, // 并且调用 Filter 接口的 init 方法,完成 Filter 的初始化 ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue()); filterConfigs.put(name, filterConfig); } catch (Throwable t) { // 省略 logger 处理 ok = false; } } } return ok; }
6.6 处理Wrapper 容器
Servlet对应 tomcat 的Wrapper 容器,完成 Filter 初始化之后便会对Wrapper 容器进行处理,如果 Servlet 的loadOnStartup >= 0,便会在这一阶段完成 Servlet 的加载,并且值越小越先被加载,否则在接受到请求的时候才会加载 Servlet。加载过程,主要是完成 Servlet 的实例化,并且调用Servlet 接口的 init 方法
// StandardWrapper 实例化并且启动 Servlet,由于 Servlet 存在 loadOnStartup 属性 // 因此使用了 TreeMap,根据 loadOnStartup 值 对 Wrapper 容器进行排序,然后依次启动 Servlet if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail")); ok = false; } }
loadOnStartup 方法使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同的 Wrapper 放在同一个 List 中,代码如下所示:
public boolean loadOnStartup(Container children[]) { // 使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同 Wrapper 放在同一个 List 中 TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>(); for (int i = 0; i < children.length; i++) { Wrapper wrapper = (Wrapper) children[i]; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup < 0) continue; Integer key = Integer.valueOf(loadOnStartup); ArrayList<Wrapper> list = map.get(key); if (list == null) { list = new ArrayList<>(); map.put(key, list); } list.add(wrapper); } // 根据 loadOnStartup 值有序加载 Wrapper 容器 for (ArrayList<Wrapper> list : map.values()) { for (Wrapper wrapper : list) { try { wrapper.load(); } catch (ServletException e) { if(getComputedFailCtxIfServletStartFails()) { return false; } } } } return true; }
7.启动Wrapper
Wrapper 容器是 tomcat 所有容器中最底层子容器,它没有子容器,并且父容器是 Context。默认实现StandardWrapper,我们先来看看类定义,它继承至 ContainBase,实现了 servlet 的 ServletConfig 接口,以及 tomcat 的 Wrapper 接口,说明 StandardWrapper 不仅仅是一个 Wrapper 容器实现,还是 ServletConfig 实现,部分代码如下所示:
public class StandardWrapper extends ContainerBase implements ServletConfig, Wrapper, NotificationEmitter { // Wrapper 的门面模式,调用 Servlet 的 init 方法传入的是该对象 protected final StandardWrapperFacade facade = new StandardWrapperFacade(this); protected volatile Servlet instance = null; // Servlet 实例对象 protected int loadOnStartup = -1; // 默认值为 -1,不立即启动 Servlet protected String servletClass = null; public StandardWrapper() { super(); swValve=new StandardWrapperValve(); pipeline.setBasic(swValve); broadcaster = new NotificationBroadcasterSupport(); } }
由前面对 Context 的分析可知,StandardContext 在启动的时候会发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,通过解析 web.xml 或者读取注解信息获取Wrapper 子容器,并且会添加到 Context 容器中。由于 StandardContext 继承至 ContainerBase,在调用 addChild 的时候默认会启动 child 容器(即 Wrapper),我们来看看 StandardWrapper 的启动逻辑
7.1 启动Wrapper容器
StandardWrapper 没有子容器,启动逻辑相对比较简单清晰,它重写了 startInternal 方法,主要是完成了 jmx 的事件通知,先后向 jmx 发出 starting、running事件,代码如下所示:
protected synchronized void startInternal() throws LifecycleException { // 发出 j2ee.state.starting 事件通知 if (this.getObjectName() != null) { Notification notification = new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++); broadcaster.sendNotification(notification); } // ConainerBase 的启动逻辑 super.startInternal(); setAvailable(0L); // 发出 j2ee.state.running 事件通知 if (this.getObjectName() != null) { Notification notification = new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++); broadcaster.sendNotification(notification); } }
7.2 加载 Wrapper
由前面对 Context 容器的分析可知,Context 完成 Filter 初始化之后,如果 loadOnStartup >= 0 便会调用 load 方法加载
Wrapper 容器。StandardWrapper 使用 InstanceManager 实例化 Servlet,并且调用 Servlet 的 init 方法进行初始化,传入的 ServletConfig 是 StandardWrapperFacade 对象。public synchronized void load() throws ServletException { // 实例化 Servlet,并且调用 init 方法完成初始化 instance = loadServlet(); if (!instanceInitialized) { initServlet(instance); } if (isJspServlet) { // 处理 jsp Servlet } }
总结
tomcat 实现了javax.servlet.ServletContext 接口,在 Context 启动的时候会实例化该对象。由 Context 容器通过 web.xml 或者 扫描class 字节码读取 servlet3.0 的注解配置,从而加载webapp 定义的 Listener、Servlet、Filter等 servlet 组件,但是并不会立即实例化对象。全部加载完毕之后,依次对 Listener、Filter、Servlet进行实例化、并且调用其初始化方法,比如ServletContextListener#contextInitialized()、Flter#init() 等。
初始化和启动的的时序图
-
【Tomcat】第六篇:Tomcat启动流程源码分析(中)init初始化组件
2020-09-18 23:46:29初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。...先放上一张初始化时的时序图,后面将逐一讲解
1.初始化环境BootStrap
首先从上篇main函数调用bootstrap.init()初始化BootStrap看起:
public void init() throws Exception { // 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再单独分析 initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // 反射方法实例化Catalina,后面初始化Catalina也用了很多反射,不知道意图是什么 Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // 反射调用setParentClassLoader方法,设置其parentClassLoader为sharedLoader 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()
Tomcat自定义了类加载器,打破了jvm的双亲委派机制
private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. 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); } }
2.初始化加载组件的Catalina
由前面的分析,可知Bootstrap中的load逻辑实际上是交给Catalina去处理的,下面我们对Catalina的初始化过程进行分析
- 首先初始化jmx的环境变量
- 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类,如果我们要改变server.xml的某个属性值(比如优化tomcat线程池),直接查看对应实现类的setXXX方法即可
- 解析conf/server.xml或者server-embed.xml,并且实例化对应的组件并且赋值操作,比如Server、Container、Connector等等
- 为Server设置catalina信息,指定Catalina实例,设置catalina的home、base路径
- 调用StarndServer#init()方法,完成各个组件的初始化,并且由parent组件初始化child组件,一层套一层,这个设计真心牛逼
public void load() { initDirs(); // 1.初始化jmx的环境变量 initNaming(); // 2.定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类 // Digester是利用jdk提供的sax解析功能,将server.xml的配置解析成对应的Bean,并完成注入 // 比如往Server中注入Service Digester digester = createStartDigester(); InputSource inputSource = null; InputStream inputStream = null; File file = null; try { // 3.1首先尝试加载conf/server.xml,省略部分代码...... // 3.2如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中) // 省略部分代码...... // 3.3如果还是加载不到xml,则直接return,省略部分代码...... try { inputSource.setByteStream(inputStream); // 把Catalina作为一个顶级实例 digester.push(this); // 3.4解析过程会实例化各个组件,比如Server、Container、Connector等 digester.parse(inputSource); } catch (SAXParseException spe) { // 处理异常...... } } finally { // 关闭IO流...... } // 4.给Server设置catalina信息 getServer().setCatalina(this); getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile()); getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); // Stream redirection initStreams(); // 5.调用server的init()方法 try { getServer().init(); } catch (LifecycleException e) { // ...... } // ...... }
注:这里需要明白两点
- 所有调用 init 方法的地方,比如 server.init 或者后面的 service.init 实际都是调用StandardServer、StandardService的 init 方法,因为 Server 和 Service 都是接口,Standard~~是实现类
- Standard~~中的 init 方法实际调用的是父类的 initInternal 方法
3.初始化Server
- 先是调用super.initInternal(),把自己注册到jmx
- 然后注册StringCache和MBeanFactory
- 初始化NamingResources,就是server.xml中指定的GlobalNamingResources
- 调用Service子容器的init方法,让Service组件完成初始化,注意:在同一个Server下面,可能存在多个Service组件
protected void initInternal() throws LifecycleException { super.initInternal(); // 1.往jmx中注册全局的String cache,尽管这个cache是全局听,但是如果在同一个jvm中存在多个Server, // 那么则会注册多个不同名字的StringCache,这种情况在内嵌的tomcat中可能会出现 onameStringCache = register(new StringCache(), "type=StringCache"); // 2.注册MBeanFactory,用来管理Server MBeanFactory factory = new MBeanFactory(); factory.setContainer(this); onameMBeanFactory = register(factory, "type=MBeanFactory"); // 3.往jmx中注册全局的NamingResources globalNamingResources.init(); // Populate the extension validator with JARs from common and shared class loaders if (getCatalina() != null) { // 忽略ClassLoader操作 } // 4.初始化内部的Service for (int i = 0; i < services.length; i++) { services[i].init(); } }
- JMX:Java Management Extensions,是通过MBean来监控,管理和远程连接Tomcat的一个框架
4.初始化Service
StandardService和StandardServer都是继承至LifecycleMBeanBase,因此公共的初始化逻辑都是一样的,这里不做过多介绍,我们直接看下initInternal。
- 首先,往jmx中注册StandardService
- 初始化Engine,而Engine初始化过程中会去初始化Realm(权限相关的组件)
- 如果存在Executor线程池,还会进行init操作,这个**Excecutor是tomcat的接口,继承至java.util.concurrent.Executor、**org.apache.catalina.Lifecycle
- 待初始化完Engine后,再初始化Connector连接器,默认有http1.1、ajp连接器,而这个Connector初始化过程,又会对ProtocolHandler进行初始化,开启应用端口的监听,后面会详细分析
protected void initInternal() throws LifecycleException { // 1.往jmx中注册自己 super.initInternal(); // 2.初始化Engine if (engine != null) { engine.init(); } // 3.存在Executor线程池,则进行初始化,默认是没有的 for (Executor executor : findExecutors()) { if (executor instanceof JmxEnabled) { ((JmxEnabled) executor).setDomain(getDomain()); } executor.init(); } // 暂时不知道这个MapperListener的作用 mapperListener.init(); // 4.初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听 synchronized (connectorsLock) { for (Connector connector : connectors) { try { connector.init(); } catch (Exception e) { // 省略部分代码,logger and throw exception } } } }
5.初始化Engine
protected void initInternal() throws LifecycleException { getRealm(); super.initInternal(); }
- StandardEngine在init阶段,需要获取Realm,这个Realm是干嘛用的?
- Realm(域)是用于对单个用户进行身份验证的底层安全领域的只读外观,并标识与这些用户相关联的安全角色。
- 域可以在任何容器级别上附加,但是通常只附加到Context,或者更高级别的容器。
public Realm getRealm() { Realm configured = super.getRealm(); if (configured == null) { configured = new NullRealm(); this.setRealm(configured); } return configured; }
- StandardEngine继承至ContainerBase,而ContainerBase重写了initInternal()方法;用于初始化start、stop线程池
- 在start的时候,如果发现有子容器,则会把子容器的start操作放在线程池中进行处理
- 在stop的时候,也会把stop操作放在线程池中处理
// 默认是一个线程 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; }
在前面的文章中我们介绍了Container组件,StandardEngine作为顶层容器,它的直接子容器是StardandHost,但是对StandardEngine的代码分析,我们并没有发现它会对子容器StardandHost进行初始化操作,StandardEngine不按照套路出牌,将子容器的初始化过程放在start阶段。可能Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。
6.初始化Connector
Connector也是继承至LifecycleMBeanBase,公共的初始化逻辑都是一样的。
Connector容器的主要作用:初始化ProtocolHandler,选择合适的协议- 我们先来看下Connector的默认配置,大部分属性配置都可以在Connector类中找到,tomcat默认开启了HTTP/1.1、AJP/1.3,其实AJP的用处不大,可以去掉。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
- Connector定义了很多属性,比如port、redirectPort、maxCookieCount、maxPostSize等等,比较有意思的是竟然找不到connectionTimeout的定义,全文搜索后发现使用了属性名映射,估计是为了兼容以前的版本
initInternal
- 注册jmx
- 实例化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的,后续的博客会进行深入分析
- 为ProtocolHander指定CoyoteAdapter用于处理请求
- 初始化ProtocolHander
protected void initInternal() throws LifecycleException { // 1.注册jmx super.initInternal(); // 2.初始化Coyote适配器 // 这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的 adapter = new CoyoteAdapter(this); // 3.protocolHandler需要指定Adapter用于处理请求 protocolHandler.setAdapter(adapter); // Make sure parseBodyMethodsSet has a default if (null == parseBodyMethodsSet) { setParseBodyMethods(getParseBodyMethods()); } // apr支持,忽略部分代码...... // 4.初始化ProtocolHandler,这个init不是Lifecycle定义的init,而是ProtocolHandler接口的init try { protocolHandler.init(); } catch (Exception e) { throw new LifecycleException( sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); } }
7.初始化protocolHandler
首先,我们来认识下ProtocolHandler,它是一个抽象的协议实现,它不同于JNI这样的Jk协议,它是单线程、基于流的协议。ProtocolHandler是一个Cycote连接器实现的主要接口,而Adapter适配器是由一个CoyoteServlet容器实现的主要接口,定义了处理请求的抽象接口。
ProtocolHandler的子类如下所示,AbstractProtocol(org.apache.coyote)是基本的实现,而NIO默认使用的是Http11NioProtocol
public abstract class AbstractProtocol<S> implements ProtocolHandler, MBeanRegistration { public void init() throws Exception { // 1.完成jmx注册 if (oname == null) { oname = createObjectName(); if (oname != null) { Registry.getRegistry(null, null).registerComponent(this, oname, null); } } if (this.domain != null) { rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName()); Registry.getRegistry(null, null).registerComponent( getHandler().getGlobal(), rgOname, null); } String endpointName = getName(); endpoint.setName(endpointName.substring(1, endpointName.length()-1)); endpoint.setDomain(domain); // 2.初始化endpoint endpoint.init(); } }
8.初始化EndPoint
NioEndpoint初始化过程,最重要的是完成端口和地址的绑定监听工作,即进行最基本的Socket操作,封装Request,Response;即实现TCP/IP协议
// org.apache.tomcat.util.net.NioEndpoint public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> { public void bind() throws Exception { // 实例化ServerSocketChannel,并且绑定端口和地址 serverSock = ServerSocketChannel.open(); socketProperties.setProperties(serverSock.socket()); InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort())); // 设置最大连接数,原来是在这里设置的 serverSock.socket().bind(addr,getAcceptCount()); serverSock.configureBlocking(true); //mimic APR behavior // 初始化acceptor、poller线程的数量 // Initialize thread count defaults for acceptor, poller if (acceptorThreadCount == 0) { // FIXME: Doesn't seem to work that well with multiple accept threads acceptorThreadCount = 1; } if (pollerThreadCount <= 0) { pollerThreadCount = 1; } setStopLatch(new CountDownLatch(pollerThreadCount)); // 如果有必要的话初始化ssl initialiseSsl(); // 初始化selector selectorPool.open(); } }
总结
至此,初始化流程结束!
默认情况下,Server只有一个Service组件,Service组件先后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector连接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理。