servlet工作原理之tomcat篇
目錄
servlet容器
servlet容器的啟動過程
web應用的初始化
創建servlet實例
創建servlet對象
初始化servlet
servlet體系結構
ServletConfig
ServletRequest和ServletResponse
servlet如何工作
servlet中的listener
本文概要見另一篇文章:https://blog.csdn.net/java_2017_csdn/article/details/78248127
servlet容器
servlet容器作為一個獨立發展的標準化產品,種類很多。例如jetty,在定制化和移動領域有不錯的發展,我這里以tomcat為例介紹servlet容器如何管理servlet。tomcat容器等級中,context容器是直接管理servlet在容器中的包裝類wrapper,所以context容器如何運行將直接影響servlet的工作方式。
圖1.?tomcat容器模型
?
servlet容器的啟動過程
一個web應用對應一個context容器,添加一個應用時將會創建一個StandardContext容器,并且給這個context容器設置必要的參數,url和path分別代表這個應用在tomcat中的訪問路徑和這個應用實際的物理路徑。其中最重要的一個配置是ContextConfig,這個類將會負責整個web應用配置的解析工作,最后將這個context容器加到父容器host中。
接下來會調用tomcat的start方法啟動tomcat。tomcat的啟動邏輯是基于觀察者模式設計的,所有的容器都會繼承lifecycle接口,它管理著容器的整個生命周期,所有容器的修改和狀態改變都會由它去通知已經注冊的觀察者。
圖2 tomcat主要類的啟動時序圖:
當context容器初始化狀態為init時,添加在context容器的listener將會被調用。ContextConfig繼承了LifecycleListener接口,它是在調用清單3時被加入到StandardContext容器中的。ContextConfig類會負責整個web應用的配置文件解析工作。
ContextConfig的init方法主要完成以下工作:
ContextConfig的init方法完成后,context容器會執行startInternal方法,這個方法啟動邏輯比較復雜,主要包括如下幾個部分:
web應用的初始化
web應用的初始化是在ContextConfig的configureStart方法中實現的,應用的初始化主要是要解析web.xml文件,這個文件描述了一個web應用的關鍵信息,也是一個web應用的入口。
tomcat首先會找globalWebXml這個文件的搜索路徑,是在engine的工作目錄下尋找以下兩個文件中的任一個:org/apache/catalina/startup/NO_DEFAULT_XML或conf/web.xml。接著會找hostWebXml這個文件,可能會在System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,接著尋找應用的配置文件examples/WEB-INF/web.xml。web.xml文件中的各個配置項將會被解析成相應的屬性,保存在WebXml對象中。
如果當前應用支持servlet3.0,解析還將完成額外9項工作,這個額外的9項工作主要是為servlet3.0新增的特性,包括jar包中的META-INF/web-fragment.xml的解析以及對annotations的支持。
接下去會將webxml對象中的屬性設置到context容器中,這里包括創建servlet對象、filter、listener等。這段代碼在webxml的configureContext方法中。下面是解析servlet的代碼片段:
清單4.創建wrapper實例
for (ServletDef servlet : servlets.values()) { Wrapper wrapper = context.createWrapper(); String jspFile = servlet.getJspFile(); if (jspFile != null) { wrapper.setJspFile(jspFile); } if (servlet.getLoadOnStartup() != null) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } if (servlet.getEnabled() != null) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } wrapper.setName(servlet.getServletName()); Map<String,String> params = servlet.getParameterMap(); for (Entry<String, String> entry : params.entrySet()) { wrapper.addInitParameter(entry.getKey(), entry.getValue()); } wrapper.setRunAs(servlet.getRunAs()); Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); for (SecurityRoleRef roleRef : roleRefs) { wrapper.addSecurityReference( roleRef.getName(), roleRef.getLink()); } wrapper.setServletClass(servlet.getServletClass()); MultipartDef multipartdef = servlet.getMultipartDef(); if (multipartdef != null) { if (multipartdef.getMaxFileSize() != null && multipartdef.getMaxRequestSize()!= null && multipartdef.getFileSizeThreshold() != null) { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation(), Long.parseLong(multipartdef.getMaxFileSize()), Long.parseLong(multipartdef.getMaxRequestSize()), Integer.parseInt( multipartdef.getFileSizeThreshold()))); } else { wrapper.setMultipartConfigElement(new MultipartConfigElement( multipartdef.getLocation())); } } if (servlet.getAsyncSupported() != null) { wrapper.setAsyncSupported( servlet.getAsyncSupported().booleanValue()); } context.addChild(wrapper); }這段代碼清楚地描述了如何將servlet包裝成context容器中的StandardWrapper,這里有個疑問,為什么要將servlet包裝成StandardWrapper。這里StandardWrapper是tomcat容器中的一部分,它具有容器的特征;而servlet是一個獨立的web開發標準,不應該強耦合在tomcat中。
除了將servlet包裝成StandardWrapper并作為子容器添加到context中,其他的所有web.xml屬性都被解析到context中,所以說context容器才是真正運行servlet的容器。一個web應用對應一個context容器,容器的配置屬性由應用的web.xml指定。
?
創建servlet實例
前面已經完成了servlet的解析工作,并且被包裝成StandardWrapper添加在Context容器中,但是它仍然不能為我們工作,它還沒有被實例化。下面我們將介紹servlet對象是如何創建以及初始化的。
創建servlet對象
如果servlet的load-on-startup配置項大于0,那么在context容器啟動的時候就會被實例化,前面提到在解析配置文件時會讀取默認的globalWebXml,在conf的web.xml文件中定義了一些默認的配置項,其定義了兩個servlet,分別是:org.apache.catalina.servlets.DefaultServlet和org.apache.jasper.servlet.JspServlet。它們的load-on-startup分別是1和3,也就是當tomcat啟動時這兩個servlet就會被啟動。
創建servlet實例的方法是從Wrapper.loadServlet開始的。loadServlet方法要完成的就是獲取servletClass然后把它交給InstanceManager去創建一個基于servletClass.class的對象。如果這個servlet配置了jsp-file,那么這個servletClass就是conf/web.xml中定義的org.apache.jasper.servlet.JspServlet了。
圖3. 創建Servlet對象的相關類結構
初始化servlet
初始化servlet在StandardWrapper的initServlet方法中,這個方法很簡單,就是調用servlet的init方法,同時把包裝了StandardWrapper對象的StandardWrapperFacade作為ServletConfig傳給servlet。
如果該servlet關聯的是一個jsp文件,那么前面初始化的就是JspServlet,接下去會模擬一次簡單請求,請求調用這個jsp文件,以便編譯這個jsp文件為class,并初始化這個class。
這樣servlet對象就初始化完成了,事實上servlet從被web.xml中解析到完成初始化,這個過程非常復雜,中間有很多過程,包括各種容器狀態的轉化引起的監聽事件的觸發、各種訪問權限的控制和一些不可預料的錯誤發生的判斷行為等等。
圖4. 初始化servlet的時序圖
servlet體系結構
我們知道Java web 應用是基于servlet規范運轉的,那么sevlet本身又是如何運轉的呢?為何要設計這樣的體系結構。
圖5. servlet 頂層類關聯圖
從上圖可以看出servlet規范就是基于這幾個類運轉的,與servlet主動關聯的是三個類,分別是ServletConfig、ServletRequest和ServletResponse。這三個類都是通過容器傳給servlet的,其中ServletConfig是在servlet初始化時就傳給servlet了,而后兩個是在請求達到時調用servlet時傳遞過來的。
ServletConfig
仔細看servletConfig接口中聲明的方法,可以發現這些方法都是為了獲取這個servlet的一些配置屬性,而這些配置屬性可能在servlet運行時被用到。而ServletContext又是干什么的呢?
servlet的運行模式是一個典型的“握手型的交互式”運行模式。所謂“握手型的交互式”就是兩個模塊為了交換數據通常都會準備一個交易場景,這個場景一直跟隨這個交易過程直到交易完成為止。這個交易場景的初始化是根據這次交易對象指定的參數來定制的,這些指定參數通常就會是一個配置類。對號入座,交易場景就由ServletContext來描述,而定制的參數集合就由ServletConfig來描述。ServletConfig是在Servlet.init時由容器傳過來的。
圖6. ServletConfig在容器中的類關聯圖
上圖可以看出StandardWrapper和StandardWrapperFacade都實現了ServletConfig接口,而StandardWrapperFacade是StandardWrapper門面類。所以傳給servlet的是StandardWrapperFacade對象,這個類能夠保證從StandardWrapper中拿到ServletConfig所規定的數據,而又不把ServletConfig不關心的數據暴露給servlet。
同樣ServletContext也與ServletConfig有類似的結構,Servlet中能拿到的ServletContext的實際對象也是ApplicationContextFacade對象。ApplicationContextFacade同樣保證ServletContext只能從容器中拿到它該拿的數據,它們都起到對數據的封裝作用,它們使用的都是門面設計模式。通過ServletContext可以拿到Context容器中一些必要信息,比如應用的工作路徑,容器支持的Servlet最小版本等。
ServletRequest和ServletResponse
我們在創建自己的Servlet類時通常使用的都是HttpServletRequest和HttpServletResponse,它們繼承了ServletRequest和ServletResponse。
圖7. request相關類結構圖
上圖是tomcat創建的request和response的類結構圖。tomcat一接受到請求首先會創建org.apache.coyote.Request和org.apache.coyote.Response,這兩個類是Tomcat內部使用的描述一次請求和相應信息的類,它們是一個輕量級的類,作用就是在服務器接收到請求后,經過簡單解析將這個請求快速地分配給后續線程去處理。
接下去當交給一個用戶線程去處理這個請求時又創建org.apache.catalina.connector.Request和org.apache.catalina.connector.Response對象。這兩個對象一直穿越整個Servlet容器直到要傳給Servlet,傳給servlet的是request和response的門面類RequestFacade和ResponseFacade,這里使用門面模式與前面一樣都是基于同樣的目的——封裝容器中的數據。
圖8.一次請求中request和response的轉變過程
servlet如何工作
當用戶從瀏覽器向服務器發起一個請求,通常會包含如下信息:http://hostname:port/contextpath/servletpath,hostname和port是用來與服務器建立tcp連接,而后面的url才是用來選擇服務器中哪個子容器服務用戶的請求。
那服務器是如何根據這個url達到正確的servlet容器中的呢?這種映射工作有專門一個類來完成,這個就是org.apache.tomcat.util.http.mapper,這個類保存了Tomcat的Container容器中的所有子容器的信息。
當org.apache.catalina.connector.Request類在進入container容器之前,mapper會根據這次請求的hostname和contextPath,將host和context容器設置到request的mappingData屬性中。
圖9. request的mapper類關系圖
圖10. request在容器中的路由圖
上圖描述了一次request請求是如何達到最終的wrapper容器的,請求到達最終的servlet還要完成一些步驟,必須要執行filter鏈、以及要通知你在web.xml中定義的listener。接下去就要執行servlet的service方法。
當servlet從容器中移除時,也就表明servlet的生命周期結束了,這時servlet的destroy方法將被調用。
Session與cookie
servlet能夠給我們提供兩部分數據,一個是在servlet初始化時調用init方法時設置的ServletConfig,這個類基本上含有了servlet本身和servlet所運行的容器的基本信息。還有一部分是由ServiceRequest類提供,它的實際對象是RequestFacade,從提供的方法中發現主要是描述這次請求的http協議的信息。
session與cookie的作用都是為了保持訪問用戶與后端服務器的交互狀態。服務器通過session id創建HttpSession對象,第一次觸發是通過request.getSession方法,如果當前的session id還沒有對應的HttpSession對象那就創建一個新的,并將這個對象加到org.apache.catalina.Manager的session容器中保存,Manager類將管理所有Session的生命周期,Session過期將被回收,服務器關閉,Session將被序列化到磁盤。只要這個HttpSession對象存在,用戶就可以根據session id來獲取到這個對象,也就達到了狀態的保持。
圖11. Session相關類圖
從上圖可以看出request.getSession中獲取的HttpSession對象是StandardSession對象的門面對象,這與前面的Request和Servlet是一樣的原理。
圖12. Session工作的時序圖
servlet中的listener
listener的設計對開發servlet應用程序提供了一種快捷的手段,能夠方便地從另一個縱向維度控制程序和數據。目前servlet中提供了5種兩類事件的觀察者接口,它們分別是:4個EventListeners類型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener;和2個LifecycleListener類型的,ServlectContextListener、HttpSessionListener。
圖13. servlet中的listener
這些listener的實現類可以配置在web.xml中的<listener>標簽中,也可以在應用程序中動態添加。
總結
以上是生活随笔為你收集整理的servlet工作原理之tomcat篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 塔尖上的AI医疗,它要飞起来必须迈过这几
- 下一篇: TEM014 - 新版阿里云网站界面高保