初探Tomcat的架构设计

Tomcat 做为 servlet 容器实现,它是基于 Java 语言开发的轻量级应用服务器。由于 Tomcat 做为应用服务器,它有着彻底开源,轻量,性能稳定,部署成本低等优势,因此它成为目前 Java 开发应用部署的首选,几乎每一个Java Web开发者都有使用过,可是,你对 Tomcat 的总体设计有进行过了解和思考吗?java

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

整体结构

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

如上图所描述的是: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,接口里面提供有以下图方法。tomcat

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

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

// 保存服务的服务集
private Service services[] = new Service[0];

final PropertyChangeSupport support = new PropertyChangeSupport(this);

@Override
public void addService(Service service) {
    // 相互关联
    service.setServer(this);
    
    // 利用同步锁,防止并发访问   来源:https://ytao.top
    synchronized (servicesLock) {
        Service results[] = new Service[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.coreStandardServiceapp

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

private Engine engine = null;

protected final MapperListener mapperListener = new MapperListener(this);

@Override
public void setContainer(Engine engine) {
    Engine oldEngine = this.engine;
    // 判断当前 Service 是否有关联 Engine
    if (oldEngine != null) {
        // 若是当前 Service 有关联 Engine,就去掉当前关联的 Engine
        oldEngine.setService(null);
    }
    // 若是当前新的 Engine 不为空,那么 Engine 关联当前 Service,这里是个双向关联
    this.engine = engine;
    if (this.engine != null) {
        this.engine.setService(this);
    }
    // 若是当前 Service 启动了,那么就开始启动当前新的 Engine
    if (getState().isAvailable()) {
        if (this.engine != null) {
            try {
                this.engine.start();
            } catch (LifecycleException e) {
                log.error(sm.getString("standardService.engine.startFailed"), e);
            }
        }
        // 重启 MapperListener ,获取一个新的 Engine ,必定是当前入参的 Engine
        try {
            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 关联,那么中止以前的 Engine
        if (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 实现双向关联
*/
@Override
public void addConnector(Connector connector) {

    synchronized (connectorsLock) {
        connector.setService(this);
        Connector results[] = new Connector[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 中配置信息的例子:

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

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

public Connector() {
    // 无参构造,下面 setProtocol 中默认使用HTTP/1.1
    this(null);
}

public Connector(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 中被移除,改成必配项
*/
public void setProtocol(String protocol) {

    boolean aprConnector = AprLifecycleListener.isAprAvailable() &amp;&amp;
            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");
        }
    } else if ("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是不容许操做设置的。

@Override
public void addChild(Container child) {
    // 添加的子容器必须是 Host 
    if (!(child instanceof Host))
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notHost"));
    super.addChild(child);
}

@Override
public void setParent(Container container) {

    throw new IllegalArgumentException
        (sm.getString("standardEngine.notParent"));

}

server.xml 能够配置咱们的数据:

<!-- 配置默认Host,及jvmRoute -->
<engine name="Catalina" defaulthost="localhost" jvmroute="jvm1">

Host

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

<!-- name 设置的时虚拟主机域名 -->
<host name="localhost" appbase="webapps" unpackwars="true" autodeploy="true">

Context

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

@Override
public void addChild(Container child) {

    // Global JspServlet
    Wrapper oldJspServlet = null;

    // 这里添加的子容器只能时 Wrapper
    if (!(child instanceof Wrapper)) {
        throw new IllegalArgumentException
            (sm.getString("standardContext.notWrapper"));
    }

    // 判断子容器 Wrapper 是否为 JspServlet
    boolean 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 &amp;&amp; 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 &amp;&amp; i<jspmappings.length; i++) { addservletmappingdecoded(jspmappings[i], child.getname()); } ``` 这里也就是每一个应用中的 servlet 管理中心。 # wrapper 是一个 的管理中心,它拥有 的整个生命周期,它是没有子容器的,由于它本身就是最底层的容器了。 这里主要对 加载的分析: ```java public synchronized loadservlet() throws servletexception 若是已经实例化或者用实例化池,就直接返回 if (!singlethreadmodel && (instance !="null)" return instance; printstream out="System.out;" (swallowoutput) systemloghandler.startcapture(); servlet; try long t1="System.currentTimeMillis();" 若是 servlet 类名为空,直接抛出 异常 (servletclass="=" null) unavailable(null); throw new (sm.getstring("standardwrapper.notclass", getname())); 从 context 中获取 instancemanager instancemanager="((StandardContext)getParent()).getInstanceManager();" instancemanager.newinstance(servletclass); catch (classcastexception e) restore the context classloader (sm.getstring("standardwrapper.notservlet", servletclass), e); (throwable e="ExceptionUtils.unwrapInvocationTargetException(e);" exceptionutils.handlethrowable(e); added extra log statement for bugzilla 36630: https: bz.apache.org bugzilla show_bug.cgi?id="36630" if(log.isdebugenabled()) log.debug(sm.getstring("standardwrapper.instantiate", (sm.getstring("standardwrapper.instantiate", 加载声明了 multipartconfig 注解的信息 (multipartconfigelement="=" annotation="servlet.getClass().getAnnotation(MultipartConfig.class);" (annotation multipartconfigelement="new" multipartconfigelement(annotation); 对 类型进行检查 (servlet instanceof containerservlet) ((containerservlet) servlet).setwrapper(this); classloadtime="(int)" (system.currenttimemillis() -t1); singlethreadmodel) (instancepool="=" instancepool="new" stack<>();
            }
            singleThreadModel = true;
        }

        // 初始化 servlet
        initServlet(servlet);

        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        if (swallowOutput) {
            String log = SystemLogHandler.stopCapture();
            if (log != null &amp;&amp; log.length() &gt; 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;

}

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

到目前为止,大体介绍了 Tomcat8 的主要组件,对 Tomcat 的总体架构也有个大体了解了,Tomcat 源码进行重构后,可读性确实要好不少,建议你们能够去尝试分析下,里面的使用的一些设计模式,咱们在实际编码过程当中,仍是有必定的借鉴意义。

我的博客: https://ytao.top

个人公众号 ytao

个人公众号

相关文章
相关标签/搜索