初探Tomcat的架构设计

Tomcat 作为 servlet 容器实现,它是基于 Java 语言开发的轻量级应用服务器。因为 Tomcat 作为应用服务器,它有着完全开源,轻量,性能稳定,部署成本低等优点,所以它成为目前 Java 开发应用部署的首选,几乎每个Java Web开发者都有使用过,但是,你对 Tomcat 的整体设计有进行过了解和思考吗?

本文将基于 Tomcat8 进行分析,具体版本为 Tomcat8 当前官网最新修改(2019-11-21 09:28)的版本 v8.5.49

总体结构

Tomcat 的总体结构中有很多模块,下图列出我们将要进行分析结构中的主要模块。其中主要分析的是Service,Connector,Engine,Host,Context,Wrapper。为避免图层看着太乱,下图中 n代表该组件可允许存在多个。

如上图所描述的是:Server 是 tomcat 服务器,在 Server 中可以存在多个服务 Service 。每个服务中可有多个连接器和一个 Servlet 引擎 Engine,一个 Service 中多个连接器对应一个 Engine。每个 Engine 中,可存在多个域名,这里可用虚拟主机的概念来表示 Host。每个 Host 中可以存在多个应用 Context。Server,Service,Connector,Engine,Host,Context,Wrapper 它们之间的关系,除了Connector和Engine,它们是平行关系,其它的都是存在包含关系。同时,它们也都继承了 Lifecycle 接口,该接口提供的是生命周期的管理,里面包括:初始化(init),启动(start),停止(stop),销毁(destroy)。当它的父容器启动时,会调用它子容器的启动,停止也是一样的。

上图中,还可以看到,Engine,Host,Context,Wrapper 都继承自 Container。它有个 backgroundProcess()方法,后台异步处理,所以继承它后可以方便的创建异步线程。在 Tomcat7 中,有看到 Service 持有的是 Container,而不是 Engine。估计这也是为什么在当前版本中添加 Engine 方法名叫 setContainer

Server

Tomcat 源码中有提供 org.apache.catalina.Server接口,对应的默认实现类为 org.apache.catalina.core.StandardServer,接口里面提供有如下图方法。

上图中可以知道 Server 做的工作:对 Service,Address,Port,Catalina 以及全局命名资源的管理操作。Server 在进行初始化的时候,会加载我们 server.xml 中配置的数据。

这里对其中的 Service 操作的 addService向定义的服务集添加新服务进行分析:

// 保存服务的服务集privateService services[] = newService[0];
finalPropertyChangeSupport support = newPropertyChangeSupport(this);
@Overridepublicvoid addService(Service service) {// 相互关联    service.setServer(this);
// 利用同步锁,防止并发访问   来源:https://ytao.topsynchronized(servicesLock) {Service results[] = newService[services.length + 1];// copy 旧的服务到新的数组中System.arraycopy(services, 0, results, 0, services.length);// 添加新的 service        results[services.length] = service;        services = results;
// 如果当前 server 已经启动,那么当前添加的 service 就开始启动if(getState().isAvailable()) {try{                service.start();} catch(LifecycleException e) {// Ignore}}
// 使用观察者模式,当被监听对象属性值发生变化时通知监听器,remove 是也会调用。        support.firePropertyChange("service", null, service);}
}

源码中可以看到,向服务器中添加服务后,随机会启动服务,实则也服务启动入口。

Service

Service 的主要职责就是将 Connector 和 Engine 的组装在一起。两者分开的目的也就是使请求监听和请求处理进行解耦,能拥有更好的扩展性。每个 Service 都是相互独立的,但是共享一个JVM和系统类库。这里提供了 org.apache.catalina.Service接口和默认实现类 org.apache.catalina.coreStandardService

在实现类 StandardService 中,主要分析 setContaineraddConnector两个方法。

privateEngine engine = null;
protectedfinalMapperListener mapperListener = newMapperListener(this);
@Overridepublicvoid setContainer(Engine engine) {Engine oldEngine = this.engine;// 判断当前 Service 是否有关联 Engineif(oldEngine != null) {// 如果当前 Service 有关联 Engine,就去掉当前关联的 Engine        oldEngine.setService(null);}// 如果当前新的 Engine 不为空,那么 Engine 关联当前 Service,这里是个双向关联this.engine = engine;if(this.engine != null) {this.engine.setService(this);}// 如果当前 Service 启动了,那么就开始启动当前新的 Engineif(getState().isAvailable()) {if(this.engine != null) {try{this.engine.start();} catch(LifecycleException e) {                log.error(sm.getString("standardService.engine.startFailed"), e);}}// 重启 MapperListener ,获取一个新的 Engine ,一定是当前入参的 Enginetry{            mapperListener.stop();} catch(LifecycleException e) {            log.error(sm.getString("standardService.mapperListener.stopFailed"), e);}try{            mapperListener.start();} catch(LifecycleException e) {            log.error(sm.getString("standardService.mapperListener.startFailed"), e);}
// 如果当前 Service 之前有 Engine 关联,那么停止之前的 Engineif(oldEngine != null) {try{                oldEngine.stop();} catch(LifecycleException e) {                log.error(sm.getString("standardService.engine.stopFailed"), e);}}}
// Report this property change to interested listeners    support.firePropertyChange("container", oldEngine, this.engine);}
/*** 实现方式和 StandardServer#addService 类似,不在细述* 注意,Connector 这里没有像 Engine 一样与 Service 实现双向关联*/@Overridepublicvoid addConnector(Connector connector) {
synchronized(connectorsLock) {        connector.setService(this);Connector results[] = newConnector[connectors.length + 1];System.arraycopy(connectors, 0, results, 0, connectors.length);        results[connectors.length] = connector;        connectors = results;
if(getState().isAvailable()) {try{                connector.start();} catch(LifecycleException e) {                log.error(sm.getString("standardService.connector.startFailed",                        connector), e);}}
// Report this property change to interested listeners        support.firePropertyChange("connector", null, connector);}
}

Connector

Connector 主要用于接收请求,然后交给 Engine 处理请求,处理完后再给 Connector 去返回给客户端。当前使用版本支持的协议有:HTTP,HHTP/2,AJP,NIO,NIO2,APR 主要的功能包括:

  • 监听服务器端口来读取客户端的请求。
  • 解析协议并交给对应的容器处理请求。
  • 返回处理后的信息给客户端

Connector 对应服务器 server.xml 中配置信息的例子:

<Connectorport="8080"protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443"/>

这里通过配置监听的端口号 port,指定处理协议 protocol,以及重定向地址 redirectPort。协议处理类型通过实例化连接器时设置:

publicConnector() {// 无参构造,下面 setProtocol 中默认使用HTTP/1.1this(null);}
publicConnector(String protocol) {// 设置当前连接器协议处理类型    setProtocol(protocol);// 实例化协议处理器,并保存到当前 Connector 中ProtocolHandler p = null;try{Class<?> clazz = Class.forName(protocolHandlerClassName);        p = (ProtocolHandler) clazz.getConstructor().newInstance();} catch(Exception e) {        log.error(sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"), e);} finally{this.protocolHandler = p;}
if(Globals.STRICT_SERVLET_COMPLIANCE) {        uriCharset = StandardCharsets.ISO_8859_1;} else{        uriCharset = StandardCharsets.UTF_8;}}
/*** 这个设置再 tomcat9 中被移除,改为必配项*/publicvoid setProtocol(String protocol) {
boolean aprConnector = AprLifecycleListener.isAprAvailable() &&AprLifecycleListener.getUseAprConnector();
// 这里指定了默认协议和 HTTP/1.1 一样if("HTTP/1.1".equals(protocol) || protocol == null) {if(aprConnector) {            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");} else{            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");}} elseif("AJP/1.3".equals(protocol)) {if(aprConnector) {            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");} else{            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");}} else{// 最后如果不是通过指定 HTTP/1.1,AJP/1.3 类型的协议,就通过类名实例化一个协议处理器        setProtocolHandlerClassName(protocol);}}

ProtocolHandler 是一个协议处理器,针对不同的请求,提供不同实现。实现类 AbstractProtocol 在初始化时,会在最后调用一个抽象类 AbstractEndpoint 初始化来启动线程来监听服务器端口,当接收到请求后,调用 Processor 读取请求,然后交给 Engine 处理请求。

Engine

Engine 对应的是, org.apache.catalina.Engine接口和 org.apache.catalina.core.StandardEngine默认实现类。Engine 的功能也比较简单,处理容器关系的关联。

但是实现类中的 addChild()不是指的子 Engine,而是只能是 Host。同时没有父容器, setParent是不允许操作设置的。

@Overridepublicvoid addChild(Container child) {// 添加的子容器必须是 Hostif(!(child instanceofHost))thrownewIllegalArgumentException(sm.getString("standardEngine.notHost"));super.addChild(child);}
@Overridepublicvoid setParent(Container container) {
thrownewIllegalArgumentException(sm.getString("standardEngine.notParent"));
}

server.xml 可以配置我们的数据:

<!-- 配置默认Host,及jvmRoute --><Enginename="Catalina"defaultHost="localhost"jvmRoute="jvm1">

Host

Host 表示一个虚拟主机。应为我们的服务器可设置多个域名,比如 demo.ytao.top,dev.ytao.top。那么我们就要设置两个不同 Host 来处理不同域名的请求。当过来的请求域名为 demo.ytao.top 时,那么它就会去找该域名 Host 下的 Context。 所以我们的 server.xml 配置文件也提供该配置:

<!-- name 设置的时虚拟主机域名 --><Hostname="localhost"appBase="webapps"unpackWARs="true"autoDeploy="true">

Context

到 Context 这里来,就拥有 Servlet 的运行环境,Engine,Host都是主要维护容器关系,不具备运行环境。我们暂且可将 Context 理解为一个应用,例如我们在根目录下有 ytao-demo-1 和 ytao-demo-2 两个应用,那么这里就是有两个 Context。这里主要介绍的 addChild方法,该添加的子容器是 Wrapper:

@Overridepublicvoid addChild(Container child) {
// Global JspServletWrapper oldJspServlet = null;
// 这里添加的子容器只能时 Wrapperif(!(child instanceofWrapper)) {thrownewIllegalArgumentException(sm.getString("standardContext.notWrapper"));}
// 判断子容器 Wrapper 是否为 JspServletboolean isJspServlet = "jsp".equals(child.getName());
// Allow webapp to override JspServlet inherited from global web.xml.if(isJspServlet) {        oldJspServlet = (Wrapper) findChild("jsp");if(oldJspServlet != null) {            removeChild(oldJspServlet);}}
super.addChild(child);
// 将servlet映射添加到Context组件if(isJspServlet && oldJspServlet != null) {/*         * The webapp-specific JspServlet inherits all the mappings         * specified in the global web.xml, and may add additional ones.         */String[] jspMappings = oldJspServlet.findMappings();for(int i=0; jspMappings!=null&& i<jspMappings.length; i++) {            addServletMappingDecoded(jspMappings[i], child.getName());}}}

这里也就是每个应用中的 Servlet 管理中心。

Wrapper

Wrapper 是一个 Servlet 的管理中心,它拥有 Servlet 的整个生命周期,它是没有子容器的,因为它自己就是最底层的容器了。这里主要对 Servlet 加载的分析:

publicsynchronizedServlet loadServlet() throwsServletException{
// 如果已经实例化或者用实例化池,就直接返回if(!singleThreadModel && (instance != null))return instance;
PrintStream out = System.out;if(swallowOutput) {SystemLogHandler.startCapture();}
Servlet servlet;try{long t1=System.currentTimeMillis();// 如果 servlet 类名为空,直接抛出 Servlet 异常if(servletClass == null) {            unavailable(null);thrownewServletException(sm.getString("standardWrapper.notClass", getName()));}
// 从 Context 中获取 ServletInstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();try{            servlet = (Servlet) instanceManager.newInstance(servletClass);} catch(ClassCastException e) {            unavailable(null);// Restore the context ClassLoaderthrownewServletException(sm.getString("standardWrapper.notServlet", servletClass), e);} catch(Throwable e) {            e = ExceptionUtils.unwrapInvocationTargetException(e);ExceptionUtils.handleThrowable(e);            unavailable(null);
// Added extra log statement for Bugzilla 36630:// https://bz.apache.org/bugzilla/show_bug.cgi?id=36630if(log.isDebugEnabled()) {                log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);}
// Restore the context ClassLoaderthrownewServletException(sm.getString("standardWrapper.instantiate", servletClass), e);}
// 加载声明了 MultipartConfig 注解的信息if(multipartConfigElement == null) {MultipartConfig annotation =                    servlet.getClass().getAnnotation(MultipartConfig.class);if(annotation != null) {                multipartConfigElement =newMultipartConfigElement(annotation);}}
// 对 servlet 类型进行检查if(servlet instanceofContainerServlet) {((ContainerServlet) servlet).setWrapper(this);}
        classLoadTime=(int) (System.currentTimeMillis() -t1);
if(servlet instanceofSingleThreadModel) {if(instancePool == null) {                instancePool = newStack<>();}            singleThreadModel = true;}
// 初始化 servlet        initServlet(servlet);
        fireContainerEvent("load", this);
        loadTime=System.currentTimeMillis() -t1;} finally{if(swallowOutput) {String log = SystemLogHandler.stopCapture();if(log != null&& log.length() > 0) {if(getServletContext() != null) {                    getServletContext().log(log);} else{                    out.println(log);}}}}return servlet;
}

这里加载 Servlet,如果该 Servlet 没有被实例化过,那么一定要加载一个。

到目前为止,大致介绍了 Tomcat8 的主要组件,对 Tomcat 的整体架构也有个大致了解了,Tomcat 源码进行重构后,可读性确实要好很多,建议大家可以去尝试分析下,里面的使用的一些设计模式,我们在实际编码过程中,还是有一定的借鉴意义。

本文分享自微信公众号 - Java架构沉思录(code-thinker)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏未读代码的专栏

设计模式 -创建型模式之单例模式的五种实现

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对...

6130
来自专栏全栈前端精选

我的前端成长之路

注:这是在阿里内部前端大学的一个分享,整理了一份对外的版本,希望分享内容能对你有所帮助。

8110
来自专栏老司机的技术博客

5种单例模式的实现方式,哪种最优?

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

8630
来自专栏笔记1

docker的一些命令

docker create --name myrunoob nginx:latest

6410
来自专栏用户1052078的专栏

Jenkins安装 基于宝塔面板

点开Java项目管理器,在版本管理中安装tomcat8,这个版本安装的jdk是1.8版本的。

12130
来自专栏Don的成长史

大整数相乘

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

6620
来自专栏腾讯开源的专栏

微服务云原生等场景,腾讯 Kona JDK 正式开源

? Tencent Kona 是基于 OpenJDK8,由腾讯专业技术团队提供技术维护、优化及安全保障的 JDK 产品。腾讯的 Java 应用场景丰富,结合微...

18640
来自专栏刷题笔记

【java入门】01配置环境变量

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

9630
来自专栏Don的成长史

【PAT乙级】科学计数法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

9220
来自专栏刷题笔记

7-6 列车调度 (25 分)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

11410

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励