從Servlet講到Tomcat

概念

Java Web,是基於Java語言實現web服務的技術總和。介於如今Java在web客戶端應用的比較少,我把學習重點放在了JavaWeb服務端應用。雖然用Springboot就能夠很快地搭建一個web項目了,可是若是想要深刻了解JavaWeb的實現原理,就不得不先學習Servlet和Servlet容器的相關知識。java

首先什麼是Servlet?web

Servlet從廣義上講是Sun公司提供的一門用於開發動態Web資源的技術,而狹義上指的是實現了javax.servlet.Servlet接口的類的統稱。Servlet接口很簡單,只有init、getServletConfig、service、getServletInfo和destroy這5個方法,它們構成了實現Servlet功能的規範。像Spring的DispatcherServlet,都是一種具體的Servlet。apache

固然了,光有Servlet這一個類可沒什麼用,它沒有main方法,不能獨立運行,就像光有子彈沒有槍,子彈的價值就發揮不出來。要想實現Servlet的功能,就必須有一個Servlet容器。設計模式

Servlet容器,也叫作Servlet引擎,是web服務器的一部分,用於接收網絡請求,把請求轉發給對應的Servlet,並把Servlet處理的結果返回給網絡。緩存

它是web服務器和Servlet之間的媒介。tomcat

它創建服務端socket、監聽端口、建立流。服務器

它管理着Servlet的生命週期,如加載部署Servlet,實例化初始化Servlet,調用Servlet方法(處理業務),以及銷燬Servlet。網絡

Tomcat就是一個獨立運行的Servlet容器。app

Tomcat組織結構

下面就以Tomcat8爲例,看看一個具體的Servlet容器是如何實現上述功能的。webapp

先來一張Tomcat的結構圖:

圖片描述

Server:Tomcat頂層容器,表明着整個服務器。包含一個或多個Service組件;

Service:存活在Server內部的中間組件,包含Connector和Container這兩個核心組件,負責將一個或多個Connector組件綁定到一個Container上;

Connector:監聽端口,處理與客戶端基於某種協議的通訊,提供Socket與request和response的轉換;

Container:封裝和管理Servlet,負責對請求進行處理,並生成響應。

(先介紹這幾個大的組件,小組件在後面會細講)

這樣展現可能比較抽象,咱們能夠打開咱們安裝的Tomcat目錄下的conf/server.xml,看下Tomcat是如何配置這些組件的:

<Server port="8005" shutdown="SHUTDOWN"> 

    <Service name="Catalina"> 

        <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> 

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

        <Engine name="Catalina" defaultHost="localhost"> 

            <Host name="localhost" appBase="webapps"> 

                ...

            </Host> 

    <Host...

        </Engine> 

    </Service> 

</Server>

先看Connector,能夠看到一個Service裏是能夠配置多個Connector的,這裏咱們主要關心HTTP協議的Connector。

Connector使用持有的ProtocolHandler類型對象來處理請求,它包含的三個部件:

Endpoint:綁定端口、監聽請求;

Processor:將Endpoint接收到的Socket封裝成Request;

Adapter:將Request交給Container進行具體的處理。

再看Container,它內部包含了4個子容器

Engine:Servlet引擎,Container最上層,每一個Service只能包含一個,表示一個特定的Service的請求處理流水線,從Connector接收處理全部的請求並返回響應;

Host:虛擬主機,一個引擎能夠包含多個Host;一個Host能夠包含多個Context;

Context:一個Context表示了一個Web應用程序(Web工程)。Context直接管理Servlet在容器中的包裝類;

Wrapper:每個Servlet在容器中的包裝類。

咱們能夠經過Tomcat文件夾裏的文件結構來幫助理解,上面提到的conf/server.xml裏配置Host時有個appBase的屬性是webapps,是否是很眼熟?咱們在Tomcat安裝目錄下老是有一個webapp文件夾,整個webapps就是一個Host站點,裏面放着的每一個文件夾目錄就對應一個Context,其中ROOT目錄中存放着主應用,其餘目錄存放着子應用。

Tomcat類加載

先看Tomcat是如何加載類的。

Tomcat啓動時建立的類加載器有

BootstrapClassLoader:加載JVM提供的基本運行類,和$JAVA_HOME/jre/lib/ext裏的jar包;

SystemClassLoader:加載tomcat啓動的類,即Catalina.bat中指定位置的類;

CommonClassLoader:加載tomcat以及應用通用的類,位於CATALINA_HOME/lib下。父加載器是AppClassLoader;

WebAppClassLoader:每一個應用在部署後都建立一個惟一的類加載器,加載位於WEB-INF/lib中的jar包和WEB-INF/classes下的class文件。父加載器是CommonClassLoader。

Tomcat默認類加載邏輯:

一、先在本地緩存中查找,若是已經加載即返回,不然繼續下一步

二、嘗試Bootstrap加載,若是加載到即返回,不然

三、WebApp自行加載,先/WEB-INF/classes,再/WEB-INF/lib/*.jar,若是加載到即返回,不然

四、委託WebApp父類加載器(Common ClassLoader)去加載。。。

注意:第三、4兩步違反了雙親委託機制,但也只是Tomcat自定義的ClassLoader加載順序違反了,頂層仍是相同的。

Tomcat的這種加載邏輯保證了每一個應用程序的同名類庫是獨立的,同時能夠共享共有類庫。

每個JSP文件對應一個Jsp類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器,從新建立類加載器,從新加載jsp文件。

Tomcat啓動流程

下面開始分析Tomcat大體啓動流程,建議配合源碼食用。

Tomcat傳統的啓動入口經過startup.bat和catalina.bat腳本調用org.apache.catalina.startup.Bootstrap.main(),分爲兩部分:

1、init():初始化main線程的daemon(一個Bootstrap對象)。初始化Tomcat類加載器,經過反射來實例化Catalina對象;

2、daemon執行三個方法setAwait(true)、load(args)和start():

一、setAwait:經過反射調用catalina的setAwait方法設置await屬性,後面會用到;

二、load(args):經過反射調用catalina的load方法,建立xml解析器,解析conf/server.xml建立出了StandardServer對象並init,繼而調用內部包含的service的int,以此逐層初始化全部組件;

三、start():經過反射調用catalina的start()方法,和init方法同樣逐層start全部組件;最後利用前面設置的await屬性調用await方法,繼而調用server的await方法,保證主線程運行並持續監聽8005端口的SHUTDOWN指令,接收到後調用stop方法關閉Tomcat。

上面各個組件的init和start都是一筆帶過,那麼他們實際完成了什麼樣的工做呢?

Server.init():調用包含的Service的init;

Service.init():初始化Engine,初始化Executor(全部Connector共享的線程池),初始化mapperListener(用來保存容器映射),調用Connector.init;

Connector.init():初始化ProtocolHandler、Adapter,Endpoint建立ServerSocket並綁定監聽端口

Server.start():調用包含的Services的start;

Service.start():與初始化對應,調用Engine.start,啓動Executor,啓動mapperListener(做爲監聽者加到容器和它們的子容器中),調用Connector.start

Connector.start():Endpoint建立acceptor線程來接收客戶端的鏈接以及poller線程來處理鏈接中的讀寫請求

Engine.start():逐一啓動Host、Context、Wrapper

Context.start():步驟不少,這裏列舉幾個重要的:

*)建立讀取資源文件的對象

*)建立ClassLoader對象,就是上面提到過的每一個應用惟一的WebAppClassLoader

*)設置應用的工做目錄

*)啓動相關輔助對象,如Logger、realm、resources等

*)通知監聽者ContextConfig讀取和解析Web應用web.xml和註解

*)啓動web.xml解析到的子容器(解析時將Servlet包裝成StandardWrapper)

*)啓動Pipeline(一種責任鏈設計模式後面會講)

*)獲取或建立ServletContext,並設置必要的參數

*)建立Context中配置的Listener;

*)建立和初始化配置的Filter;

*)建立和初始化loadOnStartup大於等於0的Servlet

Tomcat處理請求過程

如今咱們知道Tomcat是如何啓動的,那麼啓動以後Tomcat如何處理一次請求的呢?

下面以一次Http請求爲例來講明,請求URL=http://hostname:port/contextpath/servletpath。

在Connector組件樹中:

前面啓動的過程當中提到過Connector的Endpoint的acceptor線程負責接收Socket鏈接,acceptor接收請求以後調用processSocket方法,把socket包裝成SocketWrapper,建立一個SocketProcessor任務,從線程池中獲取一個線程處理該任務。run方法中調用AbstractEndpoint.Handler.process方法,根據請求的協議類型(con/server.xml中connector元素的protocol屬性值)建立相應的類型處理類Processor,對SocketWrapper的輸入流和輸出流進行包裝,根據SocketWrapper建立輕量級的coyote.Request和coyote.Response,解析http請求的請求頭和請求行,最後Adapter.service(Request, Response),將coyote.Request和coyote.Response轉化成Connector.Request和Connector.Response,調用connector.getService().getMapper().map(),根據hostname、contextpath和servletpath找到對應的host、context和Wapper(前面利用mapperListener保存的容器完整關係),設置到Request中去;再調用connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)將請求傳遞給與Connector關聯的Container逐級傳遞下去(Engine->Host->Context->Wapper)。

在Container組件樹中:

Container容器按照責任鏈的設計模式,使用管道Pipeline和Value的方式來傳遞請求。

第一層是Engine,先經過conf/server.xml中配置的value,最後總會流到StandardEngineValue,調用host.getPipeline().getFirst().invoke(request, response)將請求傳遞給request中保存的Host;

第二層是Host,一樣先經過配置的value,最後流到StandardHostValue,再傳遞給request中保存的Context;

第三層是Context,流到StandardContextValue傳遞給request中保存的Wapper;

最後是Wapper,流到StandardWapperValue,獲取Servlet單例(雙檢查鎖機制),獲取FilterChain執行Filter鏈,也是一種責任鏈模式,執行完全部配置的Filter後執行Servlet.service,即咱們但願其完成的業務邏輯。

(在進入Filter的時候,傳入的是Connector.Request的門面類RequestFacade,和Request同樣都是HttpServletRequest和HttpServletResponse的實現類)

返回過程略。

第一次寫文章,條理排版不是很清晰,之後慢慢改進。

相關文章
相關標籤/搜索