Tomcat剖析(五):Tomcat 容器

Tomcat剖析(五):Tomcat 容器

第一部分:概述

這一節基於《深度剖析Tomcat》第五章:容器 總結而成html

必定要先到個人github上下載本書相關的代碼,同時去上網下載這本書。java

看了不懂的歡迎加我QQ一塊兒探討或者在博客下評論發問。git

時隔兩個多月,中間包括了學校的期末考試月和一些瑣粹的事情。github

這一部分會先給你們先總結一下Tomcat鏈接器的相關內容,而後是 Tomcat 容器的整體介紹,最後在下一部分進入本章的主題: Tomcat 容器 的詳細講解。web

Tomcat 鏈接器總結

咱們都知道 Catalina 的核心組件包括 鏈接器和容器,那鏈接器是幹嗎的呢?前面講過,總的來講就是等待用戶的請求,接收用戶請求,解析請求,封裝成請求對象和響應對象,而後將請求和響應對象交給容器處理。apache

整個過程的細節包括了建立ServerSocket對象,建立處理器池,在每一個處理器內建立請求和響應對象,而後啓動全部處理器線程(阻塞,由於沒有請求到達,沒法處理),而後等待用戶請求(阻塞), 用戶請求到來時,獲取其中一個處理器線程,將socket交給處理器處理,以喚醒處理器線程(利用available變量),讓處理器線程解析請求,具體方法是解析端口,解析請求頭等信息,解析完後交給容器真正處理請求的內容(如servlet,就是調用咱們常說的invoke方法),處理完後就講處理器的全部變量恢復到未被使用前,最後回收回線程池。安全

connector.getContainer().invoke(request, response);

Tomcat 容器概述

前面幾節咱們都在學習 Tomcat鏈接器是如何實現的,也學到了它實現的巧妙之處,而這一節咱們要開始學習Tomcat的另外一個組件:容器。和鏈接器同樣,容器有着不可忽略的做用,咱們看到鏈接器從頭至尾都是在作一些基礎的工做,從等待請求到解析請求,卻沒有參與處處理用戶真正想要的東西(如servlet要進行某些操做),因此說容器是鏈接器基礎工做作完後,作真正的請求處理操做(加載servlet)。看到咱們上面的鏈接器總結沒,invoke方法就是本節咱們要講解的重中之重----獲取容器執行invoke方法。服務器

容器是一個處理用戶 servlet 請求並返回對象給 web 用戶的模塊。org.apache.catalina.Container 接口定義了容器的形式,有四種容器: Engine(引擎) , Host(主機) , Context(上下文) , 和 Wrapper(包裝器)。app

我也按照書上的邏輯,先介紹Context和Wrapper,其它兩個留之後講解。eclipse

對於 Catalina 的容器首先須要注意的是它一共有四種不一樣的容器:

  • Engine:表示整個 Catalina 的 servlet 引擎
  • Host:表示一個擁有數個上下文的虛擬主機
  • Context:表示一個 Web 應用,一個 context 包含一個或多個wrapper
  • Wrapper:表示一個獨立的 servlet

它們的標準實現是 StandardEngine,StandardHost, StandardContext, and StandardWrapper,它們都是org.apache.catalina.core 包的一部分,這些類都擴展了抽象類 ContainerBase。

要實現一個容器,首先必需要實現 org.apache.catalina.Container 接口。從上一節的鏈接器講解中咱們也看到了ex04.pyrmont.core.SimpleContainer 也實現了Container接口,實現了簡單的servlet的功能

public class SimpleContainer implements Container {
    public void invoke(Request request, Response response) throws IOException,
        ServletException {

        //中間省略
    }

}

同時,咱們須要知道的是並非每個功能都須要用到四種容器,下面的程序中會看到。

一個容器能夠有一個或多個低層次上的子容器,例如,一個 Context 有一個或多個 wrapper,而 wrapper 做爲容器層次中的最底層,不能包含子容器。可使用在 Container 接口中定義的 addChild()方法,wrapper調用會拋出異常。

容器的整個處理流程經過一個叫作Pipeline(流水線)的接口實現,這個接口的功能是放置一些Valve(閥門),這些閥門的功能是執行一些基礎功能操做,好比記錄日誌,記錄ip等,像流水線同樣,經過閥門上下文接口(ValveContext)調用下一個閥門,最後進入一個基礎閥門,實現用戶真正的請求。我估計若是以前沒學過容器的相關內容是看不懂我再說什麼的,第二部分再講解。

Tomcat 容器核心接口:

Pipeline: 一個 pipeline 包含了改容器要喚醒的全部任務。每個閥門表示了一個特定的任務。一個容器的流水線有一個基本的閥門,可是你能夠添加任意你想要添加的閥門。閥門的數目定義爲添加的閥門的個數(不包括基本閥門)。

Valve:閥門接口表示一個閥門,該組件負責處理請求。

ValveContext: 用於切換到下一個閥門

Contained:一個閥門能夠選擇性的實現 org.apache.catalina.Contained 接口。該接口定義了其實現類跟一個容器相關聯。包含 getContainer和setContainer 方法

Wrapper:org.apache.catalina.Wrapper 接口表示了一個包裝器。一個包裝器是表示一個獨立 servlet 定義的容器。包裝器繼承了 Container 接口,而且添加了幾個方法。包裝器的實現類負責管理其下層 servlet 的生命週期,包括 servlet 的 init,service,和 destroy 方法。

Context:一個 context 在容器中表示一個 web 應用。一個 context 一般含有一個或多個包裝器做爲其子容器。

但願你們看到不懂的不要沮喪,經過下面的講解,你們應該都能明白吧。 這裏省略了全部鏈接器的內容,從invoke方法開始講起。

第二部分:詳細講解

爲了模擬容器的實現,容器,流水線等都是用ex05.pyrmont包下相關的類,如SimpleWrapper等,而不是去查看StandardWrapper源代碼

啓動類Bootstrap1.java

一樣的咱們先接觸啓動類ex05.pyrmont.startup.Bootstrap1.java,這個類是對單個servlet的處理,因此能夠看到只用了wrapper,沒有用到context。若是咱們要訪問,能夠用localhost:8080/ModernServlet

public final class Bootstrap1 {

  public static void main(String[] args) {

    HttpConnector connector = new HttpConnector();
    //定義一個包裝器類(容器),實際上connector.getContainer().invoke(request, response);
  //就是調用SimpleWrapper對象的invoke方法 Wrapper wrapper = new SimpleWrapper(); wrapper.setServletClass("ModernServlet"); //定義一個類加載器 Loader loader = new SimpleLoader(); //下面就是兩個閥門 Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); //容器綁定類加載器 wrapper.setLoader(loader); //容器添加閥門 ((Pipeline) wrapper).addValve(valve1); ((Pipeline) wrapper).addValve(valve2); //鏈接器綁定容器,就是connector.getContainer().invoke時要將請求交給容器處理 connector.setContainer(wrapper); try { //下面兩個方法是鏈接器相關內容,再也不重複 connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }

固然,直接看註釋是沒用的,由於咱們不清楚咱們爲何要這樣作。不急,下面慢慢講解。記得不斷調回這個類整理邏輯哦

SimpleWrapper.java

這個類算是本節比較重要的類了,大部分的接口均可以在這裏看到。 SimpleWrapper是invoke出現的地方。

補充說明在代碼後面

下面貼上比較重要的代碼和相應註釋

public class SimpleWrapper implements Wrapper, Pipeline {

  // the servlet instance
  private Servlet instance = null;//加載到的servlet實例
  private String servletClass;
  private Loader loader;//類加載器
  private String name;
    //從一開始就綁定建立的流水線是該容器
  private SimplePipeline pipeline = new SimplePipeline(this);
  protected Container parent = null;

  public SimpleWrapper() {
    //其實咱們在建立SimpleWrapper對象時就設置流水線的基礎閥門,經過setBasic
    pipeline.setBasic(new SimpleWrapperValve());
  }

  public synchronized void addValve(Valve valve) {
    //爲流水線添加其餘Valve
    pipeline.addValve(valve);
  }
    //這個方法的做用是加載Servlet,獲取到Servlet實例
    public Servlet allocate() throws ServletException {
    // Load and initialize our instance if necessary
    if (instance==null) {
      try {
        instance = loadServlet();
      }
      catch (ServletException e) {
        throw e;
      }
      catch (Throwable e) {
        throw new ServletException("Cannot allocate a servlet instance", e);
      }
    }
    return instance;
  }
 //具體加載servlet的方法
  private Servlet loadServlet() throws ServletException {
    if (instance!=null)
      return instance;

    Servlet servlet = null;
    String actualClass = servletClass;
    if (actualClass == null) {
      throw new ServletException("servlet class has not been specified");
    }

    Loader loader = getLoader();//獲取到咱們定義的類加載器類SimpleLoader,定義和咱們須要的加載方式
    // Acquire an instance of the class loader to be used
    if (loader==null) {
      throw new ServletException("No loader.");
    }
    ClassLoader classLoader = loader.getClassLoader();

    // Load the specified servlet class from the appropriate class loader
    Class classClass = null;
    try {
      if (classLoader!=null) {
        classClass = classLoader.loadClass(actualClass);//加載servlet
      }
    }
    catch (ClassNotFoundException e) {
      throw new ServletException("Servlet class not found");
    }
    // Instantiate and initialize an instance of the servlet class itself
    try {
      servlet = (Servlet) classClass.newInstance();//建立servlet實例
    }
    catch (Throwable e) {
      throw new ServletException("Failed to instantiate servlet");
    }

    // Call the initialization method of this servlet
    try {
//這裏調用了servlet的初始化方法,全部跟咱們之前學過的知識同樣,init方法只調用一次
      servlet.init(null);
    }
    catch (Throwable f) {
      throw new ServletException("Failed initialize servlet.");
    }
    return servlet;
  }
//就是調用這個invoke方法
 public void invoke(Request request, Response response)
    throws IOException, ServletException {
    pipeline.invoke(request, response);
  }

這個類裏面有幾個比較關心的方法:

  • invoke方法:咱們講解的入口
  • addValve方法:添加閥門,其中基礎閥門在建立SimpleWrapper實例時就設置了
  • allocate方法:獲取類加載器,獲取servlet實例

invoke方面裏面的是pipeline.invoke(request, response),前面咱們說到,Pineline的做用是負責處理多個任務,

有的人就認爲爲何不直接調用咱們的Valve,甚至說不直接去掉Pineline,這裏我要說一下,咱們不能簡簡單單的認爲容器就只是處理咱們要作的任務,還有不少任務要作的,好比記錄是什麼IP操做過的,這些就像咱們程序中用的攔截器同樣,須要先通過不少的中間步驟。因此回到Bootstrap1.java中的addValve操做,和setLoader方法,能夠理解了吧。固然,addValve要真正理解須要繼續看下去

那流水線的invoke是如何實現多個任務按順序處理執行的呢?

SimplePipeline.java

SimpleWrapper.java中一開始就建立流水線對象,並綁定容器爲SimpleWrapper,由於invoke中要用到

pipeline.invoke(request, response);

下面查看部分關鍵代碼

public class SimplePipeline implements Pipeline {

 public void invoke(Request request, Response response)
    throws IOException, ServletException {
    // Invoke the first Valve in this pipeline for this request
    (new SimplePipelineValveContext()).invokeNext(request, response);
  }
//添加閥門
 public void addValve(Valve valve) {
        if (valve instanceof Contained)
          ((Contained) valve).setContainer(this.container);

        synchronized (valves) {
          Valve results[] = new Valve[valves.length +1];
          System.arraycopy(valves, 0, results, 0, valves.length);
          results[valves.length] = valve;
          valves = results;
        }
}
public void setBasic(Valve valve) {
    this.basic = valve;
    ((Contained) valve).setContainer(container);
}
//SimplePipeline的內部類,實現ValveContext接口,負責切換到下一個閥門
protected class SimplePipelineValveContext implements ValveContext {

    protected int stage = 0;

    public String getInfo() {
      return null;
    }

    public void invokeNext(Request request, Response response)
      throws IOException, ServletException {
      int subscript = stage;
      stage = stage + 1;
      // Invoke the requested Valve for the current request thread
      if (subscript < valves.length) {//先調用設置的閥門
        valves[subscript].invoke(request, response, this);
      }
      else if ((subscript == valves.length) && (basic != null)) {
        basic.invoke(request, response, this);//最後纔是basic閥門
      }
      else {
        throw new ServletException("No valve");
      }
    }
  } // end of inner class

}

須要注意的是:我專門加setBasic方法做爲關鍵代碼貼出來,是爲了提醒你們SimplePineline上在SimpleWrapper中已經設置了基本Valve,

咱們看到SimplePipeline的invoke方法裏面是建立一個內部類SimplePipelineValveContext對象:這個對象負責流水線上閥門Valve的切換,這個內部類實現了ValveContext接口,ValveContext接口中其中一個方法是invokeNext方法,用來調取下一個Valve,具體的實現也不難

  • invokeNext 方法使用下標(subscript)和級別( stage)記住哪一個閥門被喚醒。當第一次喚醒的時候,下標的值是 0,級的值是 1。第一次,第一個閥門被喚醒,流水線的閥門得到 ValveContext 實例調用它的 invokeNext 方法。這時下標的值是 1 因此下一個閥門被喚醒,而後一步步的進行。

下一小節,咱們先選擇其中一個閥門講解

HeaderLoggerValve.java

這個類實現了Valve接口和Contained接口,咱們知道Valve的接口功能是負責處理請求,因此接口裏面有invoke方法

public void invoke(Request request, Response response,
                       ValveContext context)
        throws IOException, ServletException;

HeaderLoggerValve.java是用來輸出請求頭的內容。

public class HeaderLoggerValve implements Valve, Contained {

  protected Container container;


  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    // Pass this request on to the next valve in our pipeline
    valveContext.invokeNext(request, response);//執行下一個閥門

    System.out.println("Header Logger Valve");
    ServletRequest sreq = request.getRequest();
    if (sreq instanceof HttpServletRequest) {
      HttpServletRequest hreq = (HttpServletRequest) sreq;
      Enumeration headerNames = hreq.getHeaderNames();
      while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement().toString();
        String headerValue = hreq.getHeader(headerName);
        System.out.println(headerName + ":" + headerValue);
      }

    }
    else
      System.out.println("Not an HTTP Request");

    System.out.println("------------------------------------");
  }
}

invoke(Request request, Response response, ValveContext context)方法中須要注意的是第三個參數ValveContext context, 咱們會看到SimplePipeline裏面傳遞了this引用,在某個閥門調用context的invokeNext方法時

valveContext.invokeNext(request, response);

以保證下標(subscript)和級別(stage)的值是正確的(由於是同一個引用),下一次執行的閥門就是valve[1], 也就是ClientIPLoggerValve。

最後調用的是基礎閥門

basic.invoke(request, response, this);//最後纔是basic閥門

SimpleWrapperValve.java

SimpleWrapperValve.java也是一個閥門,因此也實現了Valve和Contained接口

public class SimpleWrapperValve implements Valve, Contained {

  protected Container container;

  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    SimpleWrapper wrapper = (SimpleWrapper) getContainer();
    ServletRequest sreq = request.getRequest();
    ServletResponse sres = response.getResponse();
    Servlet servlet = null;
    HttpServletRequest hreq = null;
    //看到沒,這裏強制轉換爲HttpServletRequest和HttpServletResponse
    if (sreq instanceof HttpServletRequest)
      hreq = (HttpServletRequest) sreq;
    HttpServletResponse hres = null;
    if (sres instanceof HttpServletResponse)
      hres = (HttpServletResponse) sres;

    // Allocate a servlet instance to process this request
    try {
      //加載咱們想訪問的真正的servlet,這個方法上面講了
      servlet = wrapper.allocate();
      if (hres!=null && hreq!=null) {
        servlet.service(hreq, hres);//這裏調用了service
      }
      else {
        servlet.service(sreq, sres);
      }
    }
    catch (ServletException e) {
    }
  }

  public String getInfo() {
    return null;
  }

  public Container getContainer() {
    return container;
  }

  public void setContainer(Container container) {
    this.container = container;
  }
}

SimpleWrapperValve做爲基礎閥門,終於找到咱們夢寐已久的service方法

其中SimpleWrapper中咱們看到了這個容器是如何加載servlet的,加載的時候調用了servlet的init方法,

也就是基礎閥門的invoke方法中調用加載初始化(wrapper.allocate())

接下來調用了servlet的service方法,

咱們看到servlet是加載一次的而已,因此咱們能夠獲得結論:Servlet不是線程安全的(此次再也不是隻看結論了哦,咱們是推出來的,實在不信,能夠看看Standard*的源代碼)

第三部分:小結

講了這麼多,將全部的重要接口都混合在代碼裏面講解了,不知道你們認識到多少

我感受 Tomcat容器 理解代碼仍是次要的 最重要的是明白 Tomcat 爲何要這樣進行模塊設計,咱們能夠好好學習這種思想,爲何要分這些接口, 無論讀什麼,讀源代碼就是這樣,在理解是如何自己是如何實現的基礎上,理解設計思想。

Point: 讀同一份源代碼,一萬我的有一萬中感悟

相應代碼能夠在個人github上找到下載,拷貝到eclipse,而後打開對應包的代碼便可。

如發現編譯錯誤,多是因爲jdk不一樣版本對編譯的要求不一樣致使的,能夠無論,供學習研究使用。

相關文章
相關標籤/搜索