初探Tomcat的架構設計

ytao發表於2019-11-25

初探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代表該元件可允許存在多個。

初探Tomcat的架構設計

如上圖所描述的是: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)。當它的父容器啟動時,會呼叫它子容器的啟動,停止也是一樣的。

初探Tomcat的架構設計

上圖中,還可以看到,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 中配置的資料。

初探Tomcat的架構設計

這裡對其中的 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.coreStandardService

初探Tomcat的架構設計

在實現類 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() &&
            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 的功能也比較簡單,處理容器關係的關聯。

初探Tomcat的架構設計

但是實現類中的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 && 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 載入的分析:

public synchronized Servlet loadServlet() throws ServletException {

    // 如果已經例項化或者用例項化池,就直接返回
    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);
            throw new ServletException
                (sm.getString("standardWrapper.notClass", getName()));
        }

        // 從 Context 中獲取 Servlet
        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
            unavailable(null);
            // Restore the context ClassLoader
            throw new ServletException
                (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=36630
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
            }

            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.instantiate", servletClass), e);
        }

        // 載入宣告瞭 MultipartConfig 註解的資訊
        if (multipartConfigElement == null) {
            MultipartConfig annotation =
                    servlet.getClass().getAnnotation(MultipartConfig.class);
            if (annotation != null) {
                multipartConfigElement =
                        new MultipartConfigElement(annotation);
            }
        }

        // 對 servlet 型別進行檢查
        if (servlet instanceof ContainerServlet) {
            ((ContainerServlet) servlet).setWrapper(this);
        }

        classLoadTime=(int) (System.currentTimeMillis() -t1);

        if (servlet instanceof SingleThreadModel) {
            if (instancePool == null) {
                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 && log.length() > 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;

}
複製程式碼

這裡載入 Servlet,如果該 Servlet 沒有被例項化過,那麼一定要載入一個。

到目前為止,大致介紹了 Tomcat8 的主要元件,對 Tomcat 的整體架構也有個大致瞭解了,Tomcat 原始碼進行重構後,可讀性確實要好很多,建議大家可以去嘗試分析下,裡面的使用的一些設計模式,我們在實際編碼過程中,還是有一定的借鑑意義。


個人部落格: ytao.top

我的公眾號 ytao

我的公眾號

相關文章