Tomcat系统架构概述
Tomcat 是一个 Web 应用服务器,它是对 HTTP 和 Servlet 规范的实现,简单来说它做了这几件事:处理 HTTP 协议、执行 Servlet 和处理网络 I/O。
Spring 框架就是对 Servlet 的封装,Spring 应用本身就是一个 Servlet,而 Servlet 容器是管理和运行 Servlet 的。
Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范。Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器。
Servlet 容器工作流程:
当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端。
下图是Tomcat作为实现Servlet规范的工作流程:
Servlet 规范提供了两种扩展机制:Filter和Listener。
- Filter 是干预过程的,它是过程的一部分,是基于过程行为的。
- Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致。
Tomcat系统架构
顶层架构:
Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,用于具体提供服务。
Service主要包含两个部分:Connector和Container。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下:
一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接。
多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。
- Tomcat中只有一个Server,一个Server可以有多个Service,一个Service可以有多个Connector和一个Container;
- Server掌管着整个Tomcat的生死大权;
- Service 是对外提供服务的;
- Connector用于接受请求并将请求封装成Request和Response来具体处理;
- Container用于封装和管理Servlet,以及具体处理request请求;
Tomcat 要实现 2 个核心功能:
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
- 加载和管理 Servlet,以及具体处理 Request 请求。
因此 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。连接器负责对外交流,容器负责内部处理。
1. 连接器
连接器需要完成 3 个高内聚的功能:
- 网络通信。
- 应用层协议解析。
- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。
Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 组件,连接器用 ProtocolHandler 来处理网络连接和应用层协议。
EndPoint 是一个接口,它的抽象实现类 AbstractEndpoint 里面定义了两个内部类:Acceptor 和 SocketProcessor。其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求。
EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
2. 容器
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。
Context 表示一个 Web 应用程序;Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
请求定位 Servlet 的过程:Tomcat 会创建一个 Service 组件和一个 Engine 容器组件,在 Engine 容器下创建两个 Host 子容器,在每个 Host 容器下创建两个 Context 子容器。由于一个 Web 应用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创建多个 Wrapper 子容器。
每一个容器都有一个 Pipeline 对象。
3. 一个请求在 Tomcat 中流转的过程:
4. startup.sh 启动 tomcat 的过程
服务器模型
服务器模型(或 I/O 模型),描述的是 TCP 连接的处理方式,以及 Socket 读写时线程的状态。Java 里常用的是 BIO 和 NIO,分别对应同步阻塞和同步非阻塞两种模型,Tomcat 中的 Connector 组件就是对这两种的封装实现。
BIO – 阻塞式
Tomcat 实现了一个一连接一线程的简单服务器模型,内部采用线程池做了优化,设计如下:
- 当 Acceptor 接收到一个 TCP 连接时,线程池分配一个线程进行处理;
- 线程调用 read() 方法读取 Socket 输入流中的字节,此时线程阻塞(Block),直到收到客户端发送的数据;
- 收到数据后,进行解码、业务处理、编码,最后把响应发送到客户端,关闭连接。
由此可以看出,合理的分配线程池大小可以一定程度上提高系统的并发能力。
NIO – 非阻塞
BIO 的缺点在于不管当前连接有没有数据传输,它始终阻塞占用线程池内的一个线程,而 NIO 的处理方式是若通道无数据可读取,此时线程不阻塞直接返回,可用于处理其他连接,提高了线程利用率。那怎么知道什么时候处理数据的读写呢?当通道可读或可写时,内核会通知用户程序进行处理。
NIO 的编程比较复杂,常用的是 Reactor 模式,它描述了一个利用多路复用 I/O,基于事件驱动的服务器处理模型, (这里) 基于 Doug Lea 的 Scalable IO in Java 对 Reactor 进行了实现。Tomcat 的设计略有不同,其设计如下:
- Acceptor 以阻塞模式接收 TCP 连接,然后将连接注册到 Poller 上;
- Poller 以非阻塞模式处理 SSL 握手和 HTTP 请求头的读取;
- BlockPoller 模拟阻塞处理 HTTP 请求体的读取和发送响应。
值得注意的是,两类 Poller 都只是负责事件的通知,I/O 操作都是由线程池中的线程完成。那么,ServerSocketChannel 为什么阻塞?为什么要模拟阻塞处理请求体和 Servlet 响应?相关的讨论可参考:
- Why Tomcat’s Non-Blocking Connector is using a blocking socket?
- Getting my head around NIO ‘simulated’ blocking (trying to)
Servlet API 的实现
Servlet 规范描述了容器如何加载和运行 Servlet,如和将请求映射到用户配置的 Servlet, 如何处理请求和响应等相关问题。Tomcat 主要实现了以下 API:
- ServletConfig :Servlet 名字和初始化参数;
- ServletContext :定义了 Web 应用程序,Servlet 运行的上下文;
- ServletRequest :封装客户端请求;
- ServletResponse :封装服务端响应;
- FilterChain :请求过滤调用链;
- FilterConfig :过滤配置对象;
- RequestDispatcher :转发请求,将请求转发给 JSP 或另一个 Servlet 处理。
其他如 Servlet、Filter、GenericServlet、HttpServlet 接口或类则由用户程序来实现,更多详细的介绍请参考规范内容。
Tomcat启动及应用启动流程
1. Tomcat自身的启动脚本
- tomcat的启动和关闭分别是通过执行bin目录下的startup.sh和shutdown.sh来实现的,而startup.sh和shutdown.sh里面会执行catalina.sh来完成实际的启动和关闭。在catalina.sh里面会获取或设置启动相关的环境变量,然后配置启动的各种参数,如下为启动脚本:可以看到是执行Bootstrap类。
- Bootstrap类在加载时,会初始化类加载器体系,主要是commonLoader,catalinaLoader,sharedLoader这三个类加载器(默认为同一个),并使用catalinaLoader来通过放射的方式加载和创建org.apache.catalina.startup.Catalina实现,最后在main方法中,根据脚本执行参数来执行启动或停止。其中启动start时,依次调用Catalina的load和start:
- load为加载解析conf/server.xml文件并创建StandardServer,StanderService,StandardHost对象实例,即只要server.xml中存在这些节点,则会创建对应的Container接口的实现类对象实例;
- start为从StanderServer开始整个Catalina容器的启动。
2. Tomcat容器启动流程
server.xml文件解析
tomcat中对xml配置文件的解析是通过Digester来完成的。xml文件的解析一般有Dom4j和SAX两种方式组成,其中Dom4j为在内存中构建一棵Dom树来解析的,而SAX则是读取输入流,然后在读到某个xml节点时,触发相应的节点事件并回调相应的处理方法来实现的。而Digester在SAX的基础上,进行了封装,使得更加方便使用,目前Digester已经是一个Apache commons子项目:Digester。具体的解析规则包括:
- 简单节点解析规则定义
- 复杂节点解析规则定义
- HostRuleSet
- LifecycleListenerRule
LifeCycleListener事件监听机制完成各子容器的配置解析和创建
- 在Host节点对应的StanderHost类的生命周期监听器LifeCycleListener列表,添加了一个org.apache.catalina.startup.HostConfig监听器实现。
- 在Catalina体系的核心组件的生命周期是通过LifeCycle来管理的,即初始化,启动,停止都对应相关的状态和listener事件产生,并交给相应的监听器Listener处理。对于核心组件Host,Context的启动,分别是交给org.apache.catalina.startup包的HostConfig和ContextConfig来完成的。
webapp目录下ServletContext应用加载
- HostConfig底层调用deployWAR方法来遍历webapp目录,并解压war包,解析META-INF/context.xml文件,创建StandardContext对象,然后加到StandardHost的children,即子容器列表。之后在StandardHost的startInternal中遍历已经填充好的children,调用StandardContext的start方法,从而对每个Context进行启动,初始化机制也是跟StandardHost一样,使用listener事件监听机制来完成实际创建,具体为交给ContextConfig。
- ContextConfig与HostConfig类似,不过是在StandardContext的startInternal方法,触发ContextConfig的lifecycleEvent方法
- ContextConfig底层调用configureStart来启动应用,核心方法为webConfig。
- webConfig是tomcat非常重要的一个方法,主要是根据servlet规范,解析web.xml,以及合并包含的jar包里面的web-fragment.xml文件到web.xml,获取注解信息等等工作,完成对应java servlet的ServletContext的创建,代表一个应用的配置和启动。
3. Tomcat在SringBoot 中启动
3.1 SpringBoot的Main方法
用过SpringBoot的人都知道,首先要写一个main方法来启动
@SpringBootApplication public class TomcatdebugApplication { public static void main(String[] args) { SpringApplication.run(TomcatdebugApplication.class, args); } }
我们直接点击run方法的源码,跟踪下来,发下最终 的run方法是调用ConfigurableApplicationContext方法,源码如下:
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); //设置系统属性『java.awt.headless』,为true则启用headless模式支持 configureHeadlessProperty(); //通过*SpringFactoriesLoader*检索*META-INF/spring.factories*, //找到声明的所有SpringApplicationRunListener的实现类并将其实例化, //之后逐个调用其started()方法,广播SpringBoot要开始执行了 SpringApplicationRunListeners listeners = getRunListeners(args); //发布应用开始启动事件 listeners.starting(); try { //初始化参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); //创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile), //并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); //打印banner Banner printedBanner = printBanner(environment); //创建应用上下文 context = createApplicationContext(); //通过*SpringFactoriesLoader*检索*META-INF/spring.factories*,获取并实例化异常分析器 exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext, //并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】, //之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成, //这里就包括通过**@EnableAutoConfiguration**导入的各种自动配置类。 prepareContext(context, environment, listeners, applicationArguments, printedBanner); //刷新上下文 refreshContext(context); //再一次刷新上下文,其实是空方法,可能是为了后续扩展。 afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } //发布应用已经启动的事件 listeners.started(context); //遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法。 //我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展。 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { //应用已经启动完成的监听事件 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
其实这个方法我们可以简单的总结下步骤为:
- 配置属性
- 获取监听器,发布应用开始启动事件
- 初始化输入参数
- 配置环境,输出banner
- 创建上下文
- 预处理上下文
- 刷新上下文
- 再刷新上下文
- 发布应用已经启动事件
- 发布应用启动完成事件
而启动Tomcat就是在第7步中“刷新上下文”,要分析tomcat内容的话,只需要关注两个内容即可,上下文是如何创建的,上下文是如何刷新的,分别对应的方法就是createApplicationContext() 和refreshContext(context),接下来我们来看看这两个方法做了什么。
protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch (this.webApplicationType) { case SERVLET: contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }
这里就是根据我们的webApplicationType 来判断创建哪种类型的Servlet,代码中分别对应着Web类型(SERVLET),响应式Web类型(REACTIVE),非Web类型(default),我们建立的是Web类型,所以肯定实例化
DEFAULT_SERVLET_WEB_CONTEXT_CLASS指定的类,也就是AnnotationConfigServletWebServerApplicationContext类,这个类继承的是ServletWebServerApplicationContext,这就是我们真正的主角,而这个类最终是继承了AbstractApplicationContext,了解完创建上下文的情况后,我们再来看看刷新上下文,相关代码如下:
//类:SpringApplication.java private void refreshContext(ConfigurableApplicationContext context) { //直接调用刷新方法 refresh(context); if (this.registerShutdownHook) { try { context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } } //类:SpringApplication.java protected void refresh(ApplicationContext applicationContext) { Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext); ((AbstractApplicationContext) applicationContext).refresh(); }
这里还是直接传递调用本类的refresh(context)方法,最后是强转成父类AbstractApplicationContext调用其refresh()方法,该代码如下:
// 类:AbstractApplicationContext public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses.这里的意思就是调用各个子类的onRefresh() onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } }
这里我们看到onRefresh()方法是调用其子类的实现,根据我们上文的分析,我们这里的子类是ServletWebServerApplicationContext。
//类:ServletWebServerApplicationContext protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); }
到这里,其实庐山真面目已经出来了,createWebServer()就是启动web服务,但是还没有真正启动Tomcat,既然webServer是通过ServletWebServerFactory来获取的,我们就来看看这个工厂的真面目。
3.2 走进Tomcat内部
工厂类是一个接口,各个具体服务的实现是由各个子类来实现的,所以我们就去看看TomcatServletWebServerFactory.getWebServer()的实现。
@Override public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); }
根据上面的代码,我们发现其主要做了两件事情,第一件事就是把Connnctor(我们称之为连接器)对象添加到Tomcat中,第二件事就是configureEngine。
我们再看看Tomcat类的源码:
//部分源码,其余部分省略。 public class Tomcat { //设置连接器 public void setConnector(Connector connector) { Service service = getService(); boolean found = false; for (Connector serviceConnector : service.findConnectors()) { if (connector == serviceConnector) { found = true; } } if (!found) { service.addConnector(connector); } } //获取service public Service getService() { return getServer().findServices()[0]; } //设置Host容器 public void setHost(Host host) { Engine engine = getEngine(); boolean found = false; for (Container engineHost : engine.findChildren()) { if (engineHost == host) { found = true; } } if (!found) { engine.addChild(host); } } //获取Engine容器 public Engine getEngine() { Service service = getServer().findServices()[0]; if (service.getContainer() != null) { return service.getContainer(); } Engine engine = new StandardEngine(); engine.setName( "Tomcat" ); engine.setDefaultHost(hostname); engine.setRealm(createDefaultRealm()); service.setContainer(engine); return engine; } //获取server public Server getServer() { if (server != null) { return server; } System.setProperty("catalina.useNaming", "false"); server = new StandardServer(); initBaseDir(); // Set configuration source ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null)); server.setPort( -1 ); Service service = new StandardService(); service.setName("Tomcat"); server.addService(service); return server; } //添加Context容器 public Context addContext(Host host, String contextPath, String contextName, String dir) { silence(host, contextName); Context ctx = createContext(host, contextPath); ctx.setName(contextName); ctx.setPath(contextPath); ctx.setDocBase(dir); ctx.addLifecycleListener(new FixContextListener()); if (host == null) { getHost().addChild(ctx); } else { host.addChild(ctx); } //添加Wrapper容器 public static Wrapper addServlet(Context ctx, String servletName, Servlet servlet) { // will do class for name and set init params Wrapper sw = new ExistingStandardWrapper(servlet); sw.setName(servletName); ctx.addChild(sw); return sw; } }
阅读Tomcat的getServer()我们可以知道,Tomcat的最顶层是Server,Server就是Tomcat的实例,一个Tomcat一个Server;通过getEngine()我们可以了解到Server下面是Service,而且是多个,一个Service代表我们部署的一个应用,而且我们还可以知道,Engine容器,一个service只有一个;根据父子关系,我们看setHost()源码可以知道,host容器有多个;同理,我们发现addContext()源码下,Context也是多个;addServlet()表明Wrapper容器也是多个,而且这段代码也暗示了,其实Wrapper和Servlet是一层意思。另外我们根据setConnector源码可以知道,连接器(Connector)是设置在service下的,而且是可以设置多个连接器(Connector)。
至此,Tomcat在SpringBoot启动就一目了然了。