初探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代表該組件可允許存在多個。
附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經拿到了大廠offer,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「Java面試」即可獲取
如上圖所描述的是: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,接口里面提供有如下圖方法。
上圖中可以知道 Server 做的工作:對 Service,Address,Port,Catalina 以及全局命名資源的管理操作。Server 在進行初始化的時候,會加載我們 server.xml 中配置的數據。
這里對其中的 Service 操作的 addService向定義的服務集添加新服務進行分析:
// 保存服務的服務集 privateService services[] = newService[0]; finalPropertyChangeSupport support = newPropertyChangeSupport(this); @Override publicvoid addService(Service service) { // 相互關聯service.setServer(this); // 利用同步鎖,防止并發訪問 來源:https://ytao.top synchronized(servicesLock) { Service results[] = newService[services.length + 1]; // copy 舊的服務到新的數組中 System.arraycopy(services, 0, results, 0, services.length); // 添加新的 serviceresults[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。
在實現類 StandardService 中,主要分析 setContainer和 addConnector兩個方法。
privateEngine engine = null; protectedfinalMapperListener mapperListener = newMapperListener(this); @Override publicvoid setContainer(Engine engine) { Engine oldEngine = this.engine; // 判斷當前 Service 是否有關聯 Engine if(oldEngine != null) { // 如果當前 Service 有關聯 Engine,就去掉當前關聯的 EngineoldEngine.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 listenerssupport.firePropertyChange("container", oldEngine, this.engine); } /** * 實現方式和 StandardServer#addService 類似,不在細述 * 注意,Connector 這里沒有像 Engine 一樣與 Service 實現雙向關聯 */ @Override publicvoid addConnector(Connector connector) { synchronized(connectorsLock) {connector.setService(this); Connector results[] = newConnector[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 listenerssupport.firePropertyChange("connector", null, connector); } }Connector
Connector 主要用于接收請求,然后交給 Engine 處理請求,處理完后再給 Connector 去返回給客戶端。當前使用版本支持的協議有:HTTP,HHTP/2,AJP,NIO,NIO2,APR 主要的功能包括:
監聽服務器端口來讀取客戶端的請求。
解析協議并交給對應的容器處理請求。
返回處理后的信息給客戶端
Connector 對應服務器 server.xml 中配置信息的例子:
<Connectorport="8080"protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>這里通過配置監聽的端口號 port,指定處理協議 protocol,以及重定向地址 redirectPort。協議處理類型通過實例化連接器時設置:
publicConnector() { // 無參構造,下面 setProtocol 中默認使用HTTP/1.1 this(null); } publicConnector(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 中被移除,改為必配項 */ publicvoid 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"); } } elseif("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 publicvoid addChild(Container child) { // 添加的子容器必須是 Host if(!(child instanceofHost)) thrownewIllegalArgumentException (sm.getString("standardEngine.notHost")); super.addChild(child); } @Override publicvoid setParent(Container container) { thrownewIllegalArgumentException (sm.getString("standardEngine.notParent")); }server.xml 可以配置我們的數據:
<!-- 配置默認Host,及jvmRoute --> <Enginename="Catalina"defaultHost="localhost"jvmRoute="jvm1">Host
Host 表示一個虛擬主機。應為我們的服務器可設置多個域名,比如 demo.ytao.top,dev.ytao.top。那么我們就要設置兩個不同 Host 來處理不同域名的請求。當過來的請求域名為 demo.ytao.top 時,那么它就會去找該域名 Host 下的 Context。 所以我們的 server.xml 配置文件也提供該配置:
<!-- name 設置的時虛擬主機域名 --> <Hostname="localhost"appBase="webapps" unpackWARs="true"autoDeploy="true">Context
到 Context 這里來,就擁有 Servlet 的運行環境,Engine,Host都是主要維護容器關系,不具備運行環境。我們暫且可將 Context 理解為一個應用,例如我們在根目錄下有 ytao-demo-1 和 ytao-demo-2 兩個應用,那么這里就是有兩個 Context。這里主要介紹的 addChild方法,該添加的子容器是 Wrapper:
@Override publicvoid addChild(Container child) { // Global JspServlet Wrapper oldJspServlet = null; // 這里添加的子容器只能時 Wrapper if(!(child instanceofWrapper)) { thrownewIllegalArgumentException (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 加載的分析:
publicsynchronizedServlet loadServlet() throwsServletException{ // 如果已經實例化或者用實例化池,就直接返回 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); thrownewServletException (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 thrownewServletException (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 thrownewServletException (sm.getString("standardWrapper.instantiate", servletClass), e); } // 加載聲明了 MultipartConfig 注解的信息 if(multipartConfigElement == null) { MultipartConfig annotation =servlet.getClass().getAnnotation(MultipartConfig.class); if(annotation != null) {multipartConfigElement = newMultipartConfigElement(annotation); } } // 對 servlet 類型進行檢查 if(servlet instanceofContainerServlet) { ((ContainerServlet) servlet).setWrapper(this); }classLoadTime=(int) (System.currentTimeMillis() -t1); if(servlet instanceofSingleThreadModel) { if(instancePool == null) {instancePool = newStack<>(); }singleThreadModel = true; } // 初始化 servletinitServlet(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 源碼進行重構后,可讀性確實要好很多,建議大家可以去嘗試分析下,里面的使用的一些設計模式,我們在實際編碼過程中,還是有一定的借鑒意義。
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經拿到了大廠offer,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「Java面試」即可獲取
總結
以上是生活随笔為你收集整理的初探Tomcat的架构设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 再见,Eclipse...
- 下一篇: 数据库密码配置项都不加密?心也太大了!