tomcat(18)部署器
生活随笔
收集整理的這篇文章主要介紹了
tomcat(18)部署器
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
【0】README
-1)先上干貨:本文重點分析了tomcat 如何部署WAR文件的項目形式 以及 普通文件夾的項目形式;不管是WAR文件 還是 普通文件夾的項目形式,在tomcat中,它們都是Context容器;(Bingo)
0)本文部分文字描述轉自“how tomcat works”,旨在學習“tomcat(18)部署器”的相關知識;
1)intro:要使用一個web 應用程序,必須要將表示該應用程序的 Context實例部署到一個Host 實例中;(干貨——要使用一個web 應用程序,必須要將表示該應用程序的 Context實例部署到一個Host 實例中)
2)在tomcat中的部署方式:Context實例可以用WAR 文件的形式來部署,也可以將整個web 應用程序copy 到 tomcat安裝目錄下的 webapp 下;
Supplement) 在tomcat4 和 tomcat5中是,使用了兩個應用程序來管理tomcat 和 部署tomcat中的 web應用程序,分別是 manager 和 admin 應用程序;
s1)這兩個應用程序所使用到的類文件都位于 %CATALINA_HOME%/server/webapps 目錄下,并且分別使用了兩個描述符文件:manager.xml and admin.xml;(干貨——描述符文件:manager.xml and admin.xml) s2)在tomcat4中, 在%CATALINA_HOME%/server/webapps 目錄下,有3個描述符文件; s3)在tomcat5中,分別位于?%CATALINA_HOME%/server/webapps/admin 目錄 和?%CATALINA_HOME%/server/webapps/manager 目錄下;
3)intro to 部署器:部署器是 org.apache.catalina.Deployer 接口的實例,部署器是與一個Host容器相關聯, 用來安裝 Context實例;
3.1)安裝Context實例的意思是:創建一個 StandardContext實例,將該實例添加到Host實例中;創建的Context實例會隨其父容器——Host實例一起啟動。 3.2)部署器也可以單獨地啟動和關閉Context實例; public final class Bootstrap { public static void main(String[] args) {System.setProperty("catalina.base", System.getProperty("user.dir"));Connector connector = new HttpConnector();Context context = new StandardContext();// StandardContext's start method adds a default mappercontext.setPath("/app1");context.setDocBase("app1");LifecycleListener listener = new ContextConfig();((Lifecycle) context).addLifecycleListener(listener);Host host = new StandardHost();host.addChild(context); //highlight line.將StandardContext實例添加到 Host實例中.host.setName("localhost");host.setAppBase("webapps");Loader loader = new WebappLoader();context.setLoader(loader);connector.setContainer(host);try {connector.initialize();((Lifecycle) connector).start();((Lifecycle) host).start();Container[] c = context.findChildren();int length = c.length;for (int i=0; i<length; i++) {Container child = c[i];System.out.println(child.getName());}// make the application wait until we press a key.System.in.read();((Lifecycle) host).stop();}catch (Exception e) {e.printStackTrace();}}
}
【1】 部署一個web 應用程序
1)在實際環境中,如何將Context實例添加到 Host容器呢?答案在于:StandardHost實例中使用的org.apache.catalina.startup.HostConfig 類的 生命周期監聽器;(干貨——HostConfig?類似于StandardContext的ContextConfig監聽器)
2)org.apache.catalina.startrup.Catalina類是啟動類,使用Digester對象來解析server.xml文件,將其中的XML 元素 轉換為 java對象:Catalina類定義了 createStartDigester()方法來為 Digester對象添加規則。createStartDigester()方法中的其中一行如下所示:
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
對以上代碼的分析(Analysis):HostRuleSet extends RuleSetBase類,HostRuleSet必須提供addRuleInstance()方法的實現,需要在該方法中定義 RuleSet的規則;下面是 HostRuleSet.addRuleInstance()方法:(因為父類?RuleSetBase 實現了RuleSet接口,提供了getNamespaceURI的具體實現,而addRuleInstances()方法聲明為了抽象方法,具體參見 tomcat(15)Digester庫?中章節“【1.5】”)
public void addRuleInstances(Digester digester) { //org.apache.catalina.startup.HosRuleSet.addRuleInstances().digester.addObjectCreate(prefix + "Host","org.apache.catalina.core.StandardHost","className");digester.addSetProperties(prefix + "Host");digester.addRule(prefix + "Host",new CopyParentClassLoaderRule(digester));digester.addRule(prefix + "Host",new LifecycleListenerRule(digester,"org.apache.catalina.startup.HostConfig", // highlight line."hostConfigClass"));//......}
對以上代碼的分析(Analysis):
A1)當 在 server.xml 文件中遇到符合 "Server/Service/Engine/Host" 模式的標簽時:會創建 org.apache.catalina.startup.HostConfig 類的一個實例,并將其添加到 Host實例中,作為生命周期監聽器;
A2)that's to say, HostConfig 類會處理 StandardHost.start()方法 和 stop()方法的觸發事件; <?xml version='1.0' encoding='utf-8'?> <!--conf/server.xml源碼如下 --> <!--Licensed to the Apache Software Foundation (ASF) under one or morecontributor license agreements. See the NOTICE file distributed withthis work for additional information regarding copyright ownership.The ASF licenses this file to You under the Apache License, Version 2.0(the "License"); you may not use this file except in compliance withthe License. You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License. --> <!-- Note: A "Server" is not itself a "Container", so you may notdefine subcomponents such as "Valves" at this level.Documentation at /docs/config/server.html--> <Server port="8005" shutdown="SHUTDOWN"><!-- Security listener. Documentation at /docs/config/listeners.html<Listener className="org.apache.catalina.security.SecurityListener" />--><!--APR library loader. Documentation at /docs/apr.html --><Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /><!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html --><Listener className="org.apache.catalina.core.JasperListener" /><!-- Prevent memory leaks due to use of particular java/javax APIs--><Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /><Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /><Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /><!-- Global JNDI resourcesDocumentation at /docs/jndi-resources-howto.html--><GlobalNamingResources><!-- Editable user database that can also be used byUserDatabaseRealm to authenticate users--><Resource name="UserDatabase" auth="Container"type="org.apache.catalina.UserDatabase"description="User database that can be updated and saved"factory="org.apache.catalina.users.MemoryUserDatabaseFactory"pathname="conf/tomcat-users.xml" /></GlobalNamingResources><!-- A "Service" is a collection of one or more "Connectors" that sharea single "Container" Note: A "Service" is not itself a "Container",so you may not define subcomponents such as "Valves" at this level.Documentation at /docs/config/service.html--><Service name="Catalina"><!--The connectors can use a shared executor, you can define one or more named thread pools--><!--<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"maxThreads="150" minSpareThreads="4"/>--><!-- A "Connector" represents an endpoint by which requests are receivedand responses are returned. Documentation at :Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)Java AJP Connector: /docs/config/ajp.htmlAPR (HTTP/AJP) Connector: /docs/apr.htmlDefine a non-SSL HTTP/1.1 Connector on port 8080--><Connector port="8888" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" /><!-- A "Connector" using the shared thread pool--><!--<Connector executor="tomcatThreadPool"port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />--><!-- Define a SSL HTTP/1.1 Connector on port 8443This connector uses the JSSE configuration, when using APR, theconnector should be using the OpenSSL style configurationdescribed in the APR documentation --><!--<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"maxThreads="150" scheme="https" secure="true"clientAuth="false" sslProtocol="TLS" />--><!-- Define an AJP 1.3 Connector on port 8009 --><Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /><!-- An Engine represents the entry point (within Catalina) that processesevery request. The Engine implementation for Tomcat stand aloneanalyzes the HTTP headers included with the request, and passes themon to the appropriate Host (virtual host).Documentation at /docs/config/engine.html --><!-- You should set jvmRoute to support load-balancing via AJP ie :<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">--><Engine name="Catalina" defaultHost="localhost"><!--For clustering, please take a look at documentation at:/docs/cluster-howto.html (simple how to)/docs/config/cluster.html (reference documentation) --><!--<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>--><!-- Use the LockOutRealm to prevent attempts to guess user passwordsvia a brute-force attack --><Realm className="org.apache.catalina.realm.LockOutRealm"><!-- This Realm uses the UserDatabase configured in the global JNDIresources under the key "UserDatabase". Any editsthat are performed against this UserDatabase are immediatelyavailable for use by the Realm. --><Realm className="org.apache.catalina.realm.UserDatabaseRealm"resourceName="UserDatabase"/></Realm><Host name="localhost" appBase="webapps"unpackWARs="true" autoDeploy="true"><!-- SingleSignOn valve, share authentication between web applicationsDocumentation at: /docs/config/valve.html --><!--<Valve className="org.apache.catalina.authenticator.SingleSignOn" />--><!-- Access log processes all example.Documentation at: /docs/config/valve.htmlNote: The pattern used is equivalent to using pattern="common" --><Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"prefix="localhost_access_log." suffix=".txt"pattern="%h %l %u %t "%r" %s %b" /></Host></Engine></Service> </Server>
Attention)不要問我,為什么StandardHost的生命周期監聽器是 HostConfig?詳情參見?tomcat(17)啟動tomcat,結合org.apache.catalina.startup.Catalina.createStartDigester() 所設定的規則 和 conf/server.xml 的代碼,你就懂的。
3)HostConfig.lifecycleEvent()方法的源代碼如下:因為HostConifg的實例是 StandardHost實例的監聽器,每當StandardHost實例啟動或關閉時,都會調用 lifecycleEvent()方法; public void lifecycleEvent(LifecycleEvent event) { //org.apache.catalina.startup.HostConfig.lifecycleEvent().// Identify the host we are associated withtry {host = (Host) event.getLifecycle();if (host instanceof StandardHost) {int hostDebug = ((StandardHost) host).getDebug();if (hostDebug > this.debug) {this.debug = hostDebug;}setDeployXML(((StandardHost) host).isDeployXML());setLiveDeploy(((StandardHost) host).getLiveDeploy());setUnpackWARs(((StandardHost) host).isUnpackWARs());}} catch (ClassCastException e) {log(sm.getString("hostConfig.cce", event.getLifecycle()), e);return;}// Process the event that has occurredif (event.getType().equals(Lifecycle.START_EVENT))start(); // highlight line.else if (event.getType().equals(Lifecycle.STOP_EVENT))stop(); // highlight line.} 對以上代碼的分析(Analysis):如果變量host是 StandardHost的實例,則調用 setDeployXML方法,setLiveDeploy方法,setUnpackWARs方法; A1)setDeployXML方法:指明了Host實例是否需要部署一個 Context實例的描述符文件; public void setDeployXML(boolean deployXML) {this.deployXML= deployXML;} A2)setLiveDeploy方法:指明了Host實例 是否需要周期性檢查一個新的 部署; public void setLiveDeploy(boolean liveDeploy) {this.liveDeploy = liveDeploy;} A3)setUnpackWARs方法:指定是要將WAR 文件形式的web 應用程序解壓縮; public void setUnpackWARs(boolean unpackWARs) {this.unpackWARs = unpackWARs;} 4)lifecycleEvent方法?調用start()方法; protected void start() { //org.apache.catalina.startup.HostConfig.start().if (debug >= 1)log(sm.getString("hostConfig.start"));if (host.getAutoDeploy()) {deployApps();}if (isLiveDeploy()) {threadStart();}} protected void stop() {if (debug >= 1)log(sm.getString("hostConfig.stop"));threadStop();undeployApps();} 對以上代碼的分析(Analysis): A1)若autoDeploy為true時: start()方法調用deployApps()方法; protected void deployApps() {if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase(); // highlight line.if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files);deployWARs(appBase, files);deployDirectories(appBase, files);} protected File appBase() {File file = new File(host.getAppBase());if (!file.isAbsolute())file = new File(System.getProperty("catalina.base"),host.getAppBase());return (file);} A2)若liveDeploy為true時:start()方法調用threadStart()方法;
5)deployApps方法源代碼如下: protected void deployApps() { //org.apache.catalina.startup.HostConfig.deployApps().if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase();if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files); //highlight line.deployWARs(appBase, files); //highlight line.deployDirectories(appBase, files); //highlight line.} 對以上代碼的分析(Analysis): A1)該方法會獲取host的實例的appBase屬性的值,默認為 webapps 的值(參見 server.xml);部署進程會將 %CATALINA_HOME%/webapps 目錄下的所有目錄都看做是 web 應用程序的目錄來執行部署工作。此外,該目錄中所有的WAR 文件和描述符文件也都會進行部署;(干貨——想想以前總是要把項目打個war 包,放到webapps 目錄下,我在這里找到了答案。) A2)deployApps()方法會調用其他3個方法:deployDescriptors方法,deployWARs方法,deployDirectories方法; A2.1)deployDescriptors方法: protected void deployDescriptors(File appBase, String[] files) {if (!deployXML)return;for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".xml")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString file = files[i].substring(0, files[i].length() - 4);String contextPath = "/" + file;if (file.equals("ROOT")) {contextPath = "";}if (host.findChild(contextPath) != null) {continue;}// Assume this is a configuration descriptor and deploy itlog(sm.getString("hostConfig.deployDescriptor", files[i]));try {URL config =new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(config, null);} catch (Throwable t) {log(sm.getString("hostConfig.deployDescriptor.error",files[i]), t);}}}} A2.2)deployWARs方法: protected void deployWARs(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".war")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];int period = contextPath.lastIndexOf(".");if (period >= 0)contextPath = contextPath.substring(0, period);if (contextPath.equals("/ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;if (isUnpackWARs()) {// Expand and deploy this application as a directorylog(sm.getString("hostConfig.expand", files[i]));try {URL url = new URL("jar:file:" +dir.getCanonicalPath() + "!/");String path = ExpandWar.expand(host,url);url = new URL("file:" + path);((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.expand.error", files[i]),t);}} else {// Deploy the application in this WAR filelog(sm.getString("hostConfig.deployJar", files[i]));try {URL url = new URL("file", null,dir.getCanonicalPath());url = new URL("jar:" + url.toString() + "!/");((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployJar.error",files[i]), t);}}}}} A2.3)deployDirectories方法: protected void deployDirectories(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (dir.isDirectory()) {deployed.add(files[i]); File webInf = new File(dir, "/WEB-INF");if (!webInf.exists() || !webInf.isDirectory() ||!webInf.canRead())continue;// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];if (files[i].equals("ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;// Deploy the application in this directorylog(sm.getString("hostConfig.deployDir", files[i]));try {URL url = new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployDir.error", files[i]),t);}}}}
【1.1】 部署一個描述符 1)可以編寫一個 ?XML 文件來描述 Context對象;如,在tomcat4和tomcat5中的admin 和 manager 應用中就分別使用了如下兩個XML 文件;(tomcat distribution list?http://archive.apache.org/dist/tomcat/) <!-- tomcat4下的admin.xml 文件(\container\webapps\admin)--> <Context path="/admin" docBase="../server/webapps/admin"debug="0" privileged="true"><!-- Uncomment this Valve to limit access to the Admin app to localhostfor obvious security reasons. Allow may be a comma-separated list ofhosts (or even regular expressions).<Valve className="org.apache.catalina.valves.RemoteAddrValve"allow="127.0.0.1"/>--><Logger className="org.apache.catalina.logger.FileLogger"prefix="localhost_admin_log." suffix=".txt"timestamp="true"/> </Context> <!-- tomcat5下的 manager.xml 文件(\container\webapps\manager --> <Context docBase="${catalina.home}/server/webapps/manager"privileged="true" antiResourceLocking="false" antiJARLocking="false" useHttpOnly="true"><!-- Link to the user database we will get roles from --><ResourceLink name="users" global="UserDatabase"type="org.apache.catalina.UserDatabase"/> </Context> Attention)這兩個描述符都有一個Context 元素。Context元素中的 docBase屬性的值分別為 %CATALINA_HOME%/server/webapps/admin and %CATALINA_HOME%/server/webapps/manager,這表明,admin 應用程序和manager應用程序并沒有部署到默認的地方;
2)HostConfig類使用了 deployDescriptor()方法來部署XML 文件。在tomcat4中, 這些文件位于 %CATALINA_HOME%/webapps 目錄下;在tomcat5中, 位于 %CATALINA_HOME%/server/webapps 子目錄下;
【1.2】部署一個WAR文件 1)intro:可以將web 應用程序以一個 WAR形式的文件來部署。HostConfig.deployWARs()方法 將位于 %CATALINA_HOME%/webapps 目錄下的任何WAR文件進行部署; protected void deployWARs(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".war")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];int period = contextPath.lastIndexOf(".");if (period >= 0)contextPath = contextPath.substring(0, period);if (contextPath.equals("/ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;if (isUnpackWARs()) {// Expand and deploy this application as a directorylog(sm.getString("hostConfig.expand", files[i]));try {URL url = new URL("jar:file:" +dir.getCanonicalPath() + "!/");String path = ExpandWar.expand(host,url);url = new URL("file:" + path);((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.expand.error", files[i]),t);}} else {// Deploy the application in this WAR filelog(sm.getString("hostConfig.deployJar", files[i]));try {URL url = new URL("file", null,dir.getCanonicalPath());url = new URL("jar:" + url.toString() + "!/");((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployJar.error",files[i]), t);}}}}}
【1.3】部署一個目錄 1)intro:可以直接將web 應用程序的整個目錄copy 到 %CATALINA_HOME%/webapps 目錄下來完成web 應用程序的部署。HostConfig.deployDirectories()方法完成對這些web 應用程序的部署; protected void deployDirectories(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (dir.isDirectory()) {deployed.add(files[i]);// Make sure there is an application configuration directory// This is needed if the Context appBase is the same as the// web server document root to make sure only web applications// are deployed and not directories for web space.File webInf = new File(dir, "/WEB-INF");if (!webInf.exists() || !webInf.isDirectory() ||!webInf.canRead())continue;// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];if (files[i].equals("ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;// Deploy the application in this directorylog(sm.getString("hostConfig.deployDir", files[i]));try {URL url = new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployDir.error", files[i]),t);}}}}
Attention)上面兩種圖,我們分析了 HostConfig 如何部署WAR 文件和 普通文件夾,其實它們都是項目的打包形式,現在回想起以前部署 java web 項目到 tomcat的時候,我們為什么要那樣操作(將項目文件夾編譯后copy 到webapps dir下,也可以將項目打個WAR包進行部署),這兩張圖是不是給出了很好的詮釋。(Bingo)
【1.4】動態部署 1)intro:StandardHost實例使用HostConfig對象作為生命周期監聽器,當StandardHost對象啟動時,它的start()方法會觸發一個START事件; 2)為了響應START 事件:HostConfig.lifecycleEvent()方法和 HostConfig中的事件處理事件調用 start()方法; 3)在tomcat4中,在start()方法的最后一行,當isliveDeploy==true時(default case下,該屬性為true),start()方法會調用 threadStart()方法:? protected void start() { // org.apache.catalina.startup.HostConfig.start().if (debug >= 1)log(sm.getString("hostConfig.start"));if (host.getAutoDeploy()) {deployApps();}if (isLiveDeploy()) { //highlight line.threadStart();}} 4)threadStart()方法會派生一個新線程并調用run()方法。 protected void threadStart() { // org.apache.catalina.startup.HostConfig.threadStart().// Has the background thread already been started?if (thread != null)return;// Start the background threadif (debug >= 1)log(" Starting background thread");threadDone = false;threadName = "HostConfig[" + host.getName() + "]";thread = new Thread(this, threadName);thread.setDaemon(true);thread.start();} public void run() { //org.apache.catalina.startup.HostConfig.run().if (debug >= 1)log("BACKGROUND THREAD Starting");// Loop until the termination semaphore is setwhile (!threadDone) {// Wait for our check intervalthreadSleep();// Deploy apps if the Host allows auto deployingdeployApps();// Check for web.xml modificationcheckWebXmlLastModified();}if (debug >= 1)log("BACKGROUND THREAD Stopping");} 對以上代碼的分析(Analysis):threadSleep()方法:會使該線程休眠一段時間; protected void threadSleep() {try {Thread.sleep(checkInterval * 1000L);} // ......}5)在tomcat5中,HostConfig 類沒有再使用專用線程來執行檢查工作,而是由StandardHost.backgroundProcess()方法周期性地觸發一個 "check"事件; Attention)backgroundProcess()方法會由一個專門的線程來周期性地調用,用來執行容器中所有的后臺處理工作;
public void backgroundProcess() {lifecycle.fireLifecycleEvent("check", null); }public void lifecycleEvent(LifecycleEvent event) { //org.apache.catalina.startup.HostConfig.lifecycleEvent().if (event.getType().equals(Lifecycle.PERIODIC_EVENT))check(); // highlight line.// Identify the host we are associated withtry {host = (Host) event.getLifecycle();if (host instanceof StandardHost) {setDeployXML(((StandardHost) host).isDeployXML());setUnpackWARs(((StandardHost) host).isUnpackWARs());setXmlNamespaceAware(((StandardHost) host).getXmlNamespaceAware());setXmlValidation(((StandardHost) host).getXmlValidation());}} catch (ClassCastException e) {log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);return;}// Process the event that has occurredif (event.getType().equals(Lifecycle.START_EVENT))start();else if (event.getType().equals(Lifecycle.STOP_EVENT))stop();} protected void check() { //org.apache.catalina.startup.HostConfig.check().if (host.getAutoDeploy()) {// Check for resources modification to trigger redeploymentDeployedApplication[] apps =(DeployedApplication[]) deployed.values().toArray(new DeployedApplication[0]);for (int i = 0; i < apps.length; i++) {if (!isServiced(apps[i].name))checkResources(apps[i]);}// Hotdeploy applicationsdeployApps();// ......checkContextLastModified();}} 對以上代碼的分析(Analysis):
A1)check方法會調用 deployApps()方法,該方法都會完成web 應用程序的部署工作,該方法會調用?deployDescriptors方法,deployWARs方法,deployDirectories方法; protected void deployApps() { //org.apache.catalina.startup.HostConfig.deployApps().if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase();if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files);deployWARs(appBase, files);deployDirectories(appBase, files);} A2)check()方法還會調用checkContextLastModified方法,后者遍歷所有已經部署的Context,檢查web.xml 文件的 時間戳,以及每個Context中 WEB-INF 目錄下的內容;如果某個檢查的資源被修改了,會重新啟動相應的Context實例。此外,該方法還會檢查所有已經部署的WAR文件的時間戳,如果某個應用程序的WAR 文件被修改了,會重新對該應用程序進行部署;
【2】Deployer接口 1)intro:部署器是 org.apache.catalina.Deployer 接口的實例; 2)StandardHost類實現了 Deployer 接口 :所以,StandardHost 實例也是一個部署器,它也是一個容器,web 應用可以部署到其中,或從其中取消部署; public class StandardHost extends ContainerBase implements Deployer, Host { //...... 3)Deployer接口的源代碼如下: public interface Deployer { //org.apache.catalina.Deployer.public static final String PRE_INSTALL_EVENT = "pre-install";public static final String INSTALL_EVENT = "install"; public static final String REMOVE_EVENT = "remove"; public String getName(); public void install(String contextPath, URL war) throws IOException; public void install(URL config, URL war) throws IOException; public Context findDeployedApp(String contextPath); public String[] findDeployedApps(); public void remove(String contextPath) throws IOException;public void remove(String contextPath, boolean undeploy) throws IOException; public void start(String contextPath) throws IOException; public void stop(String contextPath) throws IOException; } // the follwing code is defined in org.apache.catalina.core.StandardHostpublic void install(String contextPath, URL war) throws IOException {deployer.install(contextPath, war);}public synchronized void install(URL config, URL war) throws IOException {deployer.install(config, war);}public Context findDeployedApp(String contextPath) {return (deployer.findDeployedApp(contextPath));}public String[] findDeployedApps() {return (deployer.findDeployedApps());}public void remove(String contextPath) throws IOException {deployer.remove(contextPath);}public void remove(String contextPath, boolean undeploy) throws IOException {deployer.remove(contextPath,undeploy);}public void start(String contextPath) throws IOException { deployer.start(contextPath);}public void stop(String contextPath) throws IOException { deployer.stop(contextPath);}protected void addDefaultMapper(String mapperClass) {super.addDefaultMapper(this.mapperClass);} 【3】StandardHostDeployer類(org.apache.catalina.core.StandardHostDeployer) 1)intro to org.apache.catalina.core.StandardHostDeployer:該類是一個輔助類,幫助完成將web 應用程序部署到StandardHost 實例的工作。StandardHostDeployer實例由?StandardHost 對象來調用,在其構造函數中,會傳入?StandardHost 類的一個實例: public StandardHostDeployer(StandardHost host) {super();this.host = host;} 【3.1】安裝一個描述符 1)intro to install()方法:當 HostConfig.deployDescriptors()方法調用了 StandardHost.install()方法后,StandardHost實例調用該 install()方法;然后再調用?StandardHostDeployer.install()方法; protected void deployDescriptors(File appBase, String[] files) { // org.apache.catalina.startup.HostConfig.deployDescriptors()if (!deployXML)return;for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".xml")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString file = files[i].substring(0, files[i].length() - 4);String contextPath = "/" + file;if (file.equals("ROOT")) {contextPath = "";}if (host.findChild(contextPath) != null) {continue;}// Assume this is a configuration descriptor and deploy itlog(sm.getString("hostConfig.deployDescriptor", files[i]));try {URL config =new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(config, null); // highlight line.} catch (Throwable t) {log(sm.getString("hostConfig.deployDescriptor.error",files[i]), t);}}}}public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHost.install().deployer.install(config, war); // highlight line.} public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHostDeployer.install().// Validate the format and state of our argumentsif (config == null)throw new IllegalArgumentException(sm.getString("standardHost.configRequired"));if (!host.isDeployXML())throw new IllegalArgumentException(sm.getString("standardHost.configNotAllowed"));// Calculate the document base for the new web application (if needed)String docBase = null; // Optional override for value in config fileif (war != null) {String url = war.toString();host.log(sm.getString("standardHost.installingWAR", url));// Calculate the WAR file absolute pathnameif (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));}// Install the new web applicationthis.context = null;this.overrideDocBase = docBase;InputStream stream = null;try {stream = config.openStream();Digester digester = createDigester();digester.setDebug(host.getDebug());digester.clear();digester.push(this);digester.parse(stream);stream.close();stream = null;} catch (Exception e) {host.log(sm.getString("standardHost.installError", docBase), e);throw new IOException(e.toString());} finally {if (stream != null) {try {stream.close();} catch (Throwable t) {;}}}}
【3.2】安裝一個WAR 文件或目錄 1)org.apache.catalina.core.StandardHostDeployer.install(String contextPath, URL war)方法: 接收一個表示上下文路徑的字符串和一個表示WAR 文件的URL; public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHost.install().deployer.install(config, war); // highlight line.}public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHostDeployer.install().// Validate the format and state of our argumentsif (config == null)throw new IllegalArgumentException(sm.getString("standardHost.configRequired"));if (!host.isDeployXML())throw new IllegalArgumentException(sm.getString("standardHost.configNotAllowed"));// Calculate the document base for the new web application (if needed)String docBase = null; // Optional override for value in config fileif (war != null) {String url = war.toString();host.log(sm.getString("standardHost.installingWAR", url));// Calculate the WAR file absolute pathnameif (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));}// Install the new web applicationthis.context = null;this.overrideDocBase = docBase;InputStream stream = null;try {stream = config.openStream();Digester digester = createDigester();digester.setDebug(host.getDebug());digester.clear();digester.push(this);digester.parse(stream);stream.close();stream = null;} catch (Exception e) {host.log(sm.getString("standardHost.installError", docBase), e);throw new IOException(e.toString());} finally {if (stream != null) {try {stream.close();} catch (Throwable t) {;}}}}public synchronized void install(String contextPath, URL war) throws IOException { // Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));if (findDeployedApp(contextPath) != null)throw new IllegalStateException(sm.getString("standardHost.pathUsed", contextPath));if (war == null)throw new IllegalArgumentException(sm.getString("standardHost.warRequired"));// Calculate the document base for the new web applicationhost.log(sm.getString("standardHost.installing",contextPath, war.toString()));String url = war.toString();String docBase = null;boolean isWAR = false;if (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);if (!url.toLowerCase().endsWith(".war")) {throw new IllegalArgumentException(sm.getString("standardHost.warURL", url));}isWAR = true;}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));// Determine if directory/war to install is in the host appBaseboolean isAppBase = false;File appBase = new File(host.getAppBase());if (!appBase.isAbsolute())appBase = new File(System.getProperty("catalina.base"),host.getAppBase());File contextFile = new File(docBase);File baseDir = contextFile.getParentFile();if (appBase.getCanonicalPath().equals(baseDir.getCanonicalPath())) {isAppBase = true;}// For security, if deployXML is false only allow directories// and war files from the hosts appBaseif (!host.isDeployXML() && !isAppBase) {throw new IllegalArgumentException(sm.getString("standardHost.installBase", url));}// Make sure contextPath and directory/war names match when// installing from the host appBaseif (isAppBase && (host.getAutoDeploy() || host.getLiveDeploy())) {String filename = contextFile.getName();if (isWAR) {filename = filename.substring(0,filename.length()-4);}if (contextPath.length() == 0) {if (!filename.equals("ROOT")) {throw new IllegalArgumentException(sm.getString("standardHost.pathMatch", "/", "ROOT"));}} else if (!filename.equals(contextPath.substring(1))) {throw new IllegalArgumentException(sm.getString("standardHost.pathMatch", contextPath, filename));}}// Expand war file if host wants wars unpackedif (isWAR && host.isUnpackWARs()) {if (contextPath.equals("")) {docBase = ExpandWar.expand(host,war,"/ROOT");} else {docBase = ExpandWar.expand(host,war,contextPath);}}// Install the new web applicationtry {Class clazz = Class.forName(host.getContextClass());Context context = (Context) clazz.newInstance();context.setPath(contextPath);context.setDocBase(docBase);if (context instanceof Lifecycle) {clazz = Class.forName(host.getConfigClass());LifecycleListener listener =(LifecycleListener) clazz.newInstance();((Lifecycle) context).addLifecycleListener(listener);}host.fireContainerEvent(PRE_INSTALL_EVENT, context);host.addChild(context);host.fireContainerEvent(INSTALL_EVENT, context);} catch (Exception e) {host.log(sm.getString("standardHost.installError", contextPath),e);throw new IOException(e.toString());}} Attention)當安裝一個Context后,就會將其添加到 StandardHost實例中;
【3.3】啟動Context實例 1)org.apache.catalina.core.StandardHostDeployer.start()方法:用于啟動Context實例,以下代碼給出了start()方法的實現; public void start(String contextPath) throws IOException {// Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));Context context = findDeployedApp(contextPath);if (context == null)throw new IllegalArgumentException(sm.getString("standardHost.pathMissing", contextPath));host.log("standardHost.start " + contextPath);try {((Lifecycle) context).start();} catch (LifecycleException e) {host.log("standardHost.start " + contextPath + ": ", e);throw new IllegalStateException("standardHost.start " + contextPath + ": " + e);}} 【3.4】停止一個Context實例 1)org.apache.catalina.core.StandardHostDeployer.stop()方法用于停止 Context實例,以下代碼給出了stop()方法的實現; public void stop(String contextPath) throws IOException {// Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));Context context = findDeployedApp(contextPath);if (context == null)throw new IllegalArgumentException(sm.getString("standardHost.pathMissing", contextPath));host.log("standardHost.stop " + contextPath);try {((Lifecycle) context).stop();} catch (LifecycleException e) {host.log("standardHost.stop " + contextPath + ": ", e);throw new IllegalStateException("standardHost.stop " + contextPath + ": " + e);}} Conclusion) C1)部署器:是用來部署和安裝web 應用程序的組件,是org.apache.catalina.Deployer接口的實例; C2)StandardHost:是Deployer接口的一個實例,使其成為一個 可以向其中部署web 應用程序的特殊容器; C3)StandardHost類會將部署和安裝web 應用程序的任務委托給其輔助類 org.apache.catalina.core.StandardHostDeployer類完成。 C4)StandardHostDeployer類:提供了部署和安裝web 應用程序以及啟動/ 關閉 Context實例的代碼;
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎
A2)that's to say, HostConfig 類會處理 StandardHost.start()方法 和 stop()方法的觸發事件; <?xml version='1.0' encoding='utf-8'?> <!--conf/server.xml源碼如下 --> <!--Licensed to the Apache Software Foundation (ASF) under one or morecontributor license agreements. See the NOTICE file distributed withthis work for additional information regarding copyright ownership.The ASF licenses this file to You under the Apache License, Version 2.0(the "License"); you may not use this file except in compliance withthe License. You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License. --> <!-- Note: A "Server" is not itself a "Container", so you may notdefine subcomponents such as "Valves" at this level.Documentation at /docs/config/server.html--> <Server port="8005" shutdown="SHUTDOWN"><!-- Security listener. Documentation at /docs/config/listeners.html<Listener className="org.apache.catalina.security.SecurityListener" />--><!--APR library loader. Documentation at /docs/apr.html --><Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /><!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html --><Listener className="org.apache.catalina.core.JasperListener" /><!-- Prevent memory leaks due to use of particular java/javax APIs--><Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /><Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /><Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /><!-- Global JNDI resourcesDocumentation at /docs/jndi-resources-howto.html--><GlobalNamingResources><!-- Editable user database that can also be used byUserDatabaseRealm to authenticate users--><Resource name="UserDatabase" auth="Container"type="org.apache.catalina.UserDatabase"description="User database that can be updated and saved"factory="org.apache.catalina.users.MemoryUserDatabaseFactory"pathname="conf/tomcat-users.xml" /></GlobalNamingResources><!-- A "Service" is a collection of one or more "Connectors" that sharea single "Container" Note: A "Service" is not itself a "Container",so you may not define subcomponents such as "Valves" at this level.Documentation at /docs/config/service.html--><Service name="Catalina"><!--The connectors can use a shared executor, you can define one or more named thread pools--><!--<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"maxThreads="150" minSpareThreads="4"/>--><!-- A "Connector" represents an endpoint by which requests are receivedand responses are returned. Documentation at :Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)Java AJP Connector: /docs/config/ajp.htmlAPR (HTTP/AJP) Connector: /docs/apr.htmlDefine a non-SSL HTTP/1.1 Connector on port 8080--><Connector port="8888" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" /><!-- A "Connector" using the shared thread pool--><!--<Connector executor="tomcatThreadPool"port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />--><!-- Define a SSL HTTP/1.1 Connector on port 8443This connector uses the JSSE configuration, when using APR, theconnector should be using the OpenSSL style configurationdescribed in the APR documentation --><!--<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"maxThreads="150" scheme="https" secure="true"clientAuth="false" sslProtocol="TLS" />--><!-- Define an AJP 1.3 Connector on port 8009 --><Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /><!-- An Engine represents the entry point (within Catalina) that processesevery request. The Engine implementation for Tomcat stand aloneanalyzes the HTTP headers included with the request, and passes themon to the appropriate Host (virtual host).Documentation at /docs/config/engine.html --><!-- You should set jvmRoute to support load-balancing via AJP ie :<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">--><Engine name="Catalina" defaultHost="localhost"><!--For clustering, please take a look at documentation at:/docs/cluster-howto.html (simple how to)/docs/config/cluster.html (reference documentation) --><!--<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>--><!-- Use the LockOutRealm to prevent attempts to guess user passwordsvia a brute-force attack --><Realm className="org.apache.catalina.realm.LockOutRealm"><!-- This Realm uses the UserDatabase configured in the global JNDIresources under the key "UserDatabase". Any editsthat are performed against this UserDatabase are immediatelyavailable for use by the Realm. --><Realm className="org.apache.catalina.realm.UserDatabaseRealm"resourceName="UserDatabase"/></Realm><Host name="localhost" appBase="webapps"unpackWARs="true" autoDeploy="true"><!-- SingleSignOn valve, share authentication between web applicationsDocumentation at: /docs/config/valve.html --><!--<Valve className="org.apache.catalina.authenticator.SingleSignOn" />--><!-- Access log processes all example.Documentation at: /docs/config/valve.htmlNote: The pattern used is equivalent to using pattern="common" --><Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"prefix="localhost_access_log." suffix=".txt"pattern="%h %l %u %t "%r" %s %b" /></Host></Engine></Service> </Server>
Attention)不要問我,為什么StandardHost的生命周期監聽器是 HostConfig?詳情參見?tomcat(17)啟動tomcat,結合org.apache.catalina.startup.Catalina.createStartDigester() 所設定的規則 和 conf/server.xml 的代碼,你就懂的。
3)HostConfig.lifecycleEvent()方法的源代碼如下:因為HostConifg的實例是 StandardHost實例的監聽器,每當StandardHost實例啟動或關閉時,都會調用 lifecycleEvent()方法; public void lifecycleEvent(LifecycleEvent event) { //org.apache.catalina.startup.HostConfig.lifecycleEvent().// Identify the host we are associated withtry {host = (Host) event.getLifecycle();if (host instanceof StandardHost) {int hostDebug = ((StandardHost) host).getDebug();if (hostDebug > this.debug) {this.debug = hostDebug;}setDeployXML(((StandardHost) host).isDeployXML());setLiveDeploy(((StandardHost) host).getLiveDeploy());setUnpackWARs(((StandardHost) host).isUnpackWARs());}} catch (ClassCastException e) {log(sm.getString("hostConfig.cce", event.getLifecycle()), e);return;}// Process the event that has occurredif (event.getType().equals(Lifecycle.START_EVENT))start(); // highlight line.else if (event.getType().equals(Lifecycle.STOP_EVENT))stop(); // highlight line.} 對以上代碼的分析(Analysis):如果變量host是 StandardHost的實例,則調用 setDeployXML方法,setLiveDeploy方法,setUnpackWARs方法; A1)setDeployXML方法:指明了Host實例是否需要部署一個 Context實例的描述符文件; public void setDeployXML(boolean deployXML) {this.deployXML= deployXML;} A2)setLiveDeploy方法:指明了Host實例 是否需要周期性檢查一個新的 部署; public void setLiveDeploy(boolean liveDeploy) {this.liveDeploy = liveDeploy;} A3)setUnpackWARs方法:指定是要將WAR 文件形式的web 應用程序解壓縮; public void setUnpackWARs(boolean unpackWARs) {this.unpackWARs = unpackWARs;} 4)lifecycleEvent方法?調用start()方法; protected void start() { //org.apache.catalina.startup.HostConfig.start().if (debug >= 1)log(sm.getString("hostConfig.start"));if (host.getAutoDeploy()) {deployApps();}if (isLiveDeploy()) {threadStart();}} protected void stop() {if (debug >= 1)log(sm.getString("hostConfig.stop"));threadStop();undeployApps();} 對以上代碼的分析(Analysis): A1)若autoDeploy為true時: start()方法調用deployApps()方法; protected void deployApps() {if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase(); // highlight line.if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files);deployWARs(appBase, files);deployDirectories(appBase, files);} protected File appBase() {File file = new File(host.getAppBase());if (!file.isAbsolute())file = new File(System.getProperty("catalina.base"),host.getAppBase());return (file);} A2)若liveDeploy為true時:start()方法調用threadStart()方法;
5)deployApps方法源代碼如下: protected void deployApps() { //org.apache.catalina.startup.HostConfig.deployApps().if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase();if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files); //highlight line.deployWARs(appBase, files); //highlight line.deployDirectories(appBase, files); //highlight line.} 對以上代碼的分析(Analysis): A1)該方法會獲取host的實例的appBase屬性的值,默認為 webapps 的值(參見 server.xml);部署進程會將 %CATALINA_HOME%/webapps 目錄下的所有目錄都看做是 web 應用程序的目錄來執行部署工作。此外,該目錄中所有的WAR 文件和描述符文件也都會進行部署;(干貨——想想以前總是要把項目打個war 包,放到webapps 目錄下,我在這里找到了答案。) A2)deployApps()方法會調用其他3個方法:deployDescriptors方法,deployWARs方法,deployDirectories方法; A2.1)deployDescriptors方法: protected void deployDescriptors(File appBase, String[] files) {if (!deployXML)return;for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".xml")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString file = files[i].substring(0, files[i].length() - 4);String contextPath = "/" + file;if (file.equals("ROOT")) {contextPath = "";}if (host.findChild(contextPath) != null) {continue;}// Assume this is a configuration descriptor and deploy itlog(sm.getString("hostConfig.deployDescriptor", files[i]));try {URL config =new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(config, null);} catch (Throwable t) {log(sm.getString("hostConfig.deployDescriptor.error",files[i]), t);}}}} A2.2)deployWARs方法: protected void deployWARs(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".war")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];int period = contextPath.lastIndexOf(".");if (period >= 0)contextPath = contextPath.substring(0, period);if (contextPath.equals("/ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;if (isUnpackWARs()) {// Expand and deploy this application as a directorylog(sm.getString("hostConfig.expand", files[i]));try {URL url = new URL("jar:file:" +dir.getCanonicalPath() + "!/");String path = ExpandWar.expand(host,url);url = new URL("file:" + path);((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.expand.error", files[i]),t);}} else {// Deploy the application in this WAR filelog(sm.getString("hostConfig.deployJar", files[i]));try {URL url = new URL("file", null,dir.getCanonicalPath());url = new URL("jar:" + url.toString() + "!/");((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployJar.error",files[i]), t);}}}}} A2.3)deployDirectories方法: protected void deployDirectories(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (dir.isDirectory()) {deployed.add(files[i]); File webInf = new File(dir, "/WEB-INF");if (!webInf.exists() || !webInf.isDirectory() ||!webInf.canRead())continue;// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];if (files[i].equals("ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;// Deploy the application in this directorylog(sm.getString("hostConfig.deployDir", files[i]));try {URL url = new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployDir.error", files[i]),t);}}}}
【1.1】 部署一個描述符 1)可以編寫一個 ?XML 文件來描述 Context對象;如,在tomcat4和tomcat5中的admin 和 manager 應用中就分別使用了如下兩個XML 文件;(tomcat distribution list?http://archive.apache.org/dist/tomcat/) <!-- tomcat4下的admin.xml 文件(\container\webapps\admin)--> <Context path="/admin" docBase="../server/webapps/admin"debug="0" privileged="true"><!-- Uncomment this Valve to limit access to the Admin app to localhostfor obvious security reasons. Allow may be a comma-separated list ofhosts (or even regular expressions).<Valve className="org.apache.catalina.valves.RemoteAddrValve"allow="127.0.0.1"/>--><Logger className="org.apache.catalina.logger.FileLogger"prefix="localhost_admin_log." suffix=".txt"timestamp="true"/> </Context> <!-- tomcat5下的 manager.xml 文件(\container\webapps\manager --> <Context docBase="${catalina.home}/server/webapps/manager"privileged="true" antiResourceLocking="false" antiJARLocking="false" useHttpOnly="true"><!-- Link to the user database we will get roles from --><ResourceLink name="users" global="UserDatabase"type="org.apache.catalina.UserDatabase"/> </Context> Attention)這兩個描述符都有一個Context 元素。Context元素中的 docBase屬性的值分別為 %CATALINA_HOME%/server/webapps/admin and %CATALINA_HOME%/server/webapps/manager,這表明,admin 應用程序和manager應用程序并沒有部署到默認的地方;
2)HostConfig類使用了 deployDescriptor()方法來部署XML 文件。在tomcat4中, 這些文件位于 %CATALINA_HOME%/webapps 目錄下;在tomcat5中, 位于 %CATALINA_HOME%/server/webapps 子目錄下;
【1.2】部署一個WAR文件 1)intro:可以將web 應用程序以一個 WAR形式的文件來部署。HostConfig.deployWARs()方法 將位于 %CATALINA_HOME%/webapps 目錄下的任何WAR文件進行部署; protected void deployWARs(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".war")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];int period = contextPath.lastIndexOf(".");if (period >= 0)contextPath = contextPath.substring(0, period);if (contextPath.equals("/ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;if (isUnpackWARs()) {// Expand and deploy this application as a directorylog(sm.getString("hostConfig.expand", files[i]));try {URL url = new URL("jar:file:" +dir.getCanonicalPath() + "!/");String path = ExpandWar.expand(host,url);url = new URL("file:" + path);((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.expand.error", files[i]),t);}} else {// Deploy the application in this WAR filelog(sm.getString("hostConfig.deployJar", files[i]));try {URL url = new URL("file", null,dir.getCanonicalPath());url = new URL("jar:" + url.toString() + "!/");((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployJar.error",files[i]), t);}}}}}
【1.3】部署一個目錄 1)intro:可以直接將web 應用程序的整個目錄copy 到 %CATALINA_HOME%/webapps 目錄下來完成web 應用程序的部署。HostConfig.deployDirectories()方法完成對這些web 應用程序的部署; protected void deployDirectories(File appBase, String[] files) {for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (dir.isDirectory()) {deployed.add(files[i]);// Make sure there is an application configuration directory// This is needed if the Context appBase is the same as the// web server document root to make sure only web applications// are deployed and not directories for web space.File webInf = new File(dir, "/WEB-INF");if (!webInf.exists() || !webInf.isDirectory() ||!webInf.canRead())continue;// Calculate the context path and make sure it is uniqueString contextPath = "/" + files[i];if (files[i].equals("ROOT"))contextPath = "";if (host.findChild(contextPath) != null)continue;// Deploy the application in this directorylog(sm.getString("hostConfig.deployDir", files[i]));try {URL url = new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(contextPath, url);} catch (Throwable t) {log(sm.getString("hostConfig.deployDir.error", files[i]),t);}}}}
Attention)上面兩種圖,我們分析了 HostConfig 如何部署WAR 文件和 普通文件夾,其實它們都是項目的打包形式,現在回想起以前部署 java web 項目到 tomcat的時候,我們為什么要那樣操作(將項目文件夾編譯后copy 到webapps dir下,也可以將項目打個WAR包進行部署),這兩張圖是不是給出了很好的詮釋。(Bingo)
【1.4】動態部署 1)intro:StandardHost實例使用HostConfig對象作為生命周期監聽器,當StandardHost對象啟動時,它的start()方法會觸發一個START事件; 2)為了響應START 事件:HostConfig.lifecycleEvent()方法和 HostConfig中的事件處理事件調用 start()方法; 3)在tomcat4中,在start()方法的最后一行,當isliveDeploy==true時(default case下,該屬性為true),start()方法會調用 threadStart()方法:? protected void start() { // org.apache.catalina.startup.HostConfig.start().if (debug >= 1)log(sm.getString("hostConfig.start"));if (host.getAutoDeploy()) {deployApps();}if (isLiveDeploy()) { //highlight line.threadStart();}} 4)threadStart()方法會派生一個新線程并調用run()方法。 protected void threadStart() { // org.apache.catalina.startup.HostConfig.threadStart().// Has the background thread already been started?if (thread != null)return;// Start the background threadif (debug >= 1)log(" Starting background thread");threadDone = false;threadName = "HostConfig[" + host.getName() + "]";thread = new Thread(this, threadName);thread.setDaemon(true);thread.start();} public void run() { //org.apache.catalina.startup.HostConfig.run().if (debug >= 1)log("BACKGROUND THREAD Starting");// Loop until the termination semaphore is setwhile (!threadDone) {// Wait for our check intervalthreadSleep();// Deploy apps if the Host allows auto deployingdeployApps();// Check for web.xml modificationcheckWebXmlLastModified();}if (debug >= 1)log("BACKGROUND THREAD Stopping");} 對以上代碼的分析(Analysis):threadSleep()方法:會使該線程休眠一段時間; protected void threadSleep() {try {Thread.sleep(checkInterval * 1000L);} // ......}5)在tomcat5中,HostConfig 類沒有再使用專用線程來執行檢查工作,而是由StandardHost.backgroundProcess()方法周期性地觸發一個 "check"事件; Attention)backgroundProcess()方法會由一個專門的線程來周期性地調用,用來執行容器中所有的后臺處理工作;
public void backgroundProcess() {lifecycle.fireLifecycleEvent("check", null); }public void lifecycleEvent(LifecycleEvent event) { //org.apache.catalina.startup.HostConfig.lifecycleEvent().if (event.getType().equals(Lifecycle.PERIODIC_EVENT))check(); // highlight line.// Identify the host we are associated withtry {host = (Host) event.getLifecycle();if (host instanceof StandardHost) {setDeployXML(((StandardHost) host).isDeployXML());setUnpackWARs(((StandardHost) host).isUnpackWARs());setXmlNamespaceAware(((StandardHost) host).getXmlNamespaceAware());setXmlValidation(((StandardHost) host).getXmlValidation());}} catch (ClassCastException e) {log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);return;}// Process the event that has occurredif (event.getType().equals(Lifecycle.START_EVENT))start();else if (event.getType().equals(Lifecycle.STOP_EVENT))stop();} protected void check() { //org.apache.catalina.startup.HostConfig.check().if (host.getAutoDeploy()) {// Check for resources modification to trigger redeploymentDeployedApplication[] apps =(DeployedApplication[]) deployed.values().toArray(new DeployedApplication[0]);for (int i = 0; i < apps.length; i++) {if (!isServiced(apps[i].name))checkResources(apps[i]);}// Hotdeploy applicationsdeployApps();// ......checkContextLastModified();}} 對以上代碼的分析(Analysis):
A1)check方法會調用 deployApps()方法,該方法都會完成web 應用程序的部署工作,該方法會調用?deployDescriptors方法,deployWARs方法,deployDirectories方法; protected void deployApps() { //org.apache.catalina.startup.HostConfig.deployApps().if (!(host instanceof Deployer))return;if (debug >= 1)log(sm.getString("hostConfig.deploying"));File appBase = appBase();if (!appBase.exists() || !appBase.isDirectory())return;String files[] = appBase.list();deployDescriptors(appBase, files);deployWARs(appBase, files);deployDirectories(appBase, files);} A2)check()方法還會調用checkContextLastModified方法,后者遍歷所有已經部署的Context,檢查web.xml 文件的 時間戳,以及每個Context中 WEB-INF 目錄下的內容;如果某個檢查的資源被修改了,會重新啟動相應的Context實例。此外,該方法還會檢查所有已經部署的WAR文件的時間戳,如果某個應用程序的WAR 文件被修改了,會重新對該應用程序進行部署;
【2】Deployer接口 1)intro:部署器是 org.apache.catalina.Deployer 接口的實例; 2)StandardHost類實現了 Deployer 接口 :所以,StandardHost 實例也是一個部署器,它也是一個容器,web 應用可以部署到其中,或從其中取消部署; public class StandardHost extends ContainerBase implements Deployer, Host { //...... 3)Deployer接口的源代碼如下: public interface Deployer { //org.apache.catalina.Deployer.public static final String PRE_INSTALL_EVENT = "pre-install";public static final String INSTALL_EVENT = "install"; public static final String REMOVE_EVENT = "remove"; public String getName(); public void install(String contextPath, URL war) throws IOException; public void install(URL config, URL war) throws IOException; public Context findDeployedApp(String contextPath); public String[] findDeployedApps(); public void remove(String contextPath) throws IOException;public void remove(String contextPath, boolean undeploy) throws IOException; public void start(String contextPath) throws IOException; public void stop(String contextPath) throws IOException; } // the follwing code is defined in org.apache.catalina.core.StandardHostpublic void install(String contextPath, URL war) throws IOException {deployer.install(contextPath, war);}public synchronized void install(URL config, URL war) throws IOException {deployer.install(config, war);}public Context findDeployedApp(String contextPath) {return (deployer.findDeployedApp(contextPath));}public String[] findDeployedApps() {return (deployer.findDeployedApps());}public void remove(String contextPath) throws IOException {deployer.remove(contextPath);}public void remove(String contextPath, boolean undeploy) throws IOException {deployer.remove(contextPath,undeploy);}public void start(String contextPath) throws IOException { deployer.start(contextPath);}public void stop(String contextPath) throws IOException { deployer.stop(contextPath);}protected void addDefaultMapper(String mapperClass) {super.addDefaultMapper(this.mapperClass);} 【3】StandardHostDeployer類(org.apache.catalina.core.StandardHostDeployer) 1)intro to org.apache.catalina.core.StandardHostDeployer:該類是一個輔助類,幫助完成將web 應用程序部署到StandardHost 實例的工作。StandardHostDeployer實例由?StandardHost 對象來調用,在其構造函數中,會傳入?StandardHost 類的一個實例: public StandardHostDeployer(StandardHost host) {super();this.host = host;} 【3.1】安裝一個描述符 1)intro to install()方法:當 HostConfig.deployDescriptors()方法調用了 StandardHost.install()方法后,StandardHost實例調用該 install()方法;然后再調用?StandardHostDeployer.install()方法; protected void deployDescriptors(File appBase, String[] files) { // org.apache.catalina.startup.HostConfig.deployDescriptors()if (!deployXML)return;for (int i = 0; i < files.length; i++) {if (files[i].equalsIgnoreCase("META-INF"))continue;if (files[i].equalsIgnoreCase("WEB-INF"))continue;if (deployed.contains(files[i]))continue;File dir = new File(appBase, files[i]);if (files[i].toLowerCase().endsWith(".xml")) {deployed.add(files[i]);// Calculate the context path and make sure it is uniqueString file = files[i].substring(0, files[i].length() - 4);String contextPath = "/" + file;if (file.equals("ROOT")) {contextPath = "";}if (host.findChild(contextPath) != null) {continue;}// Assume this is a configuration descriptor and deploy itlog(sm.getString("hostConfig.deployDescriptor", files[i]));try {URL config =new URL("file", null, dir.getCanonicalPath());((Deployer) host).install(config, null); // highlight line.} catch (Throwable t) {log(sm.getString("hostConfig.deployDescriptor.error",files[i]), t);}}}}public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHost.install().deployer.install(config, war); // highlight line.} public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHostDeployer.install().// Validate the format and state of our argumentsif (config == null)throw new IllegalArgumentException(sm.getString("standardHost.configRequired"));if (!host.isDeployXML())throw new IllegalArgumentException(sm.getString("standardHost.configNotAllowed"));// Calculate the document base for the new web application (if needed)String docBase = null; // Optional override for value in config fileif (war != null) {String url = war.toString();host.log(sm.getString("standardHost.installingWAR", url));// Calculate the WAR file absolute pathnameif (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));}// Install the new web applicationthis.context = null;this.overrideDocBase = docBase;InputStream stream = null;try {stream = config.openStream();Digester digester = createDigester();digester.setDebug(host.getDebug());digester.clear();digester.push(this);digester.parse(stream);stream.close();stream = null;} catch (Exception e) {host.log(sm.getString("standardHost.installError", docBase), e);throw new IOException(e.toString());} finally {if (stream != null) {try {stream.close();} catch (Throwable t) {;}}}}
【3.2】安裝一個WAR 文件或目錄 1)org.apache.catalina.core.StandardHostDeployer.install(String contextPath, URL war)方法: 接收一個表示上下文路徑的字符串和一個表示WAR 文件的URL; public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHost.install().deployer.install(config, war); // highlight line.}public synchronized void install(URL config, URL war) throws IOException { // org.apache.catalina.core.StandardHostDeployer.install().// Validate the format and state of our argumentsif (config == null)throw new IllegalArgumentException(sm.getString("standardHost.configRequired"));if (!host.isDeployXML())throw new IllegalArgumentException(sm.getString("standardHost.configNotAllowed"));// Calculate the document base for the new web application (if needed)String docBase = null; // Optional override for value in config fileif (war != null) {String url = war.toString();host.log(sm.getString("standardHost.installingWAR", url));// Calculate the WAR file absolute pathnameif (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));}// Install the new web applicationthis.context = null;this.overrideDocBase = docBase;InputStream stream = null;try {stream = config.openStream();Digester digester = createDigester();digester.setDebug(host.getDebug());digester.clear();digester.push(this);digester.parse(stream);stream.close();stream = null;} catch (Exception e) {host.log(sm.getString("standardHost.installError", docBase), e);throw new IOException(e.toString());} finally {if (stream != null) {try {stream.close();} catch (Throwable t) {;}}}}public synchronized void install(String contextPath, URL war) throws IOException { // Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));if (findDeployedApp(contextPath) != null)throw new IllegalStateException(sm.getString("standardHost.pathUsed", contextPath));if (war == null)throw new IllegalArgumentException(sm.getString("standardHost.warRequired"));// Calculate the document base for the new web applicationhost.log(sm.getString("standardHost.installing",contextPath, war.toString()));String url = war.toString();String docBase = null;boolean isWAR = false;if (url.startsWith("jar:")) {url = url.substring(4, url.length() - 2);if (!url.toLowerCase().endsWith(".war")) {throw new IllegalArgumentException(sm.getString("standardHost.warURL", url));}isWAR = true;}if (url.startsWith("file://"))docBase = url.substring(7);else if (url.startsWith("file:"))docBase = url.substring(5);elsethrow new IllegalArgumentException(sm.getString("standardHost.warURL", url));// Determine if directory/war to install is in the host appBaseboolean isAppBase = false;File appBase = new File(host.getAppBase());if (!appBase.isAbsolute())appBase = new File(System.getProperty("catalina.base"),host.getAppBase());File contextFile = new File(docBase);File baseDir = contextFile.getParentFile();if (appBase.getCanonicalPath().equals(baseDir.getCanonicalPath())) {isAppBase = true;}// For security, if deployXML is false only allow directories// and war files from the hosts appBaseif (!host.isDeployXML() && !isAppBase) {throw new IllegalArgumentException(sm.getString("standardHost.installBase", url));}// Make sure contextPath and directory/war names match when// installing from the host appBaseif (isAppBase && (host.getAutoDeploy() || host.getLiveDeploy())) {String filename = contextFile.getName();if (isWAR) {filename = filename.substring(0,filename.length()-4);}if (contextPath.length() == 0) {if (!filename.equals("ROOT")) {throw new IllegalArgumentException(sm.getString("standardHost.pathMatch", "/", "ROOT"));}} else if (!filename.equals(contextPath.substring(1))) {throw new IllegalArgumentException(sm.getString("standardHost.pathMatch", contextPath, filename));}}// Expand war file if host wants wars unpackedif (isWAR && host.isUnpackWARs()) {if (contextPath.equals("")) {docBase = ExpandWar.expand(host,war,"/ROOT");} else {docBase = ExpandWar.expand(host,war,contextPath);}}// Install the new web applicationtry {Class clazz = Class.forName(host.getContextClass());Context context = (Context) clazz.newInstance();context.setPath(contextPath);context.setDocBase(docBase);if (context instanceof Lifecycle) {clazz = Class.forName(host.getConfigClass());LifecycleListener listener =(LifecycleListener) clazz.newInstance();((Lifecycle) context).addLifecycleListener(listener);}host.fireContainerEvent(PRE_INSTALL_EVENT, context);host.addChild(context);host.fireContainerEvent(INSTALL_EVENT, context);} catch (Exception e) {host.log(sm.getString("standardHost.installError", contextPath),e);throw new IOException(e.toString());}} Attention)當安裝一個Context后,就會將其添加到 StandardHost實例中;
【3.3】啟動Context實例 1)org.apache.catalina.core.StandardHostDeployer.start()方法:用于啟動Context實例,以下代碼給出了start()方法的實現; public void start(String contextPath) throws IOException {// Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));Context context = findDeployedApp(contextPath);if (context == null)throw new IllegalArgumentException(sm.getString("standardHost.pathMissing", contextPath));host.log("standardHost.start " + contextPath);try {((Lifecycle) context).start();} catch (LifecycleException e) {host.log("standardHost.start " + contextPath + ": ", e);throw new IllegalStateException("standardHost.start " + contextPath + ": " + e);}} 【3.4】停止一個Context實例 1)org.apache.catalina.core.StandardHostDeployer.stop()方法用于停止 Context實例,以下代碼給出了stop()方法的實現; public void stop(String contextPath) throws IOException {// Validate the format and state of our argumentsif (contextPath == null)throw new IllegalArgumentException(sm.getString("standardHost.pathRequired"));if (!contextPath.equals("") && !contextPath.startsWith("/"))throw new IllegalArgumentException(sm.getString("standardHost.pathFormat", contextPath));Context context = findDeployedApp(contextPath);if (context == null)throw new IllegalArgumentException(sm.getString("standardHost.pathMissing", contextPath));host.log("standardHost.stop " + contextPath);try {((Lifecycle) context).stop();} catch (LifecycleException e) {host.log("standardHost.stop " + contextPath + ": ", e);throw new IllegalStateException("standardHost.stop " + contextPath + ": " + e);}} Conclusion) C1)部署器:是用來部署和安裝web 應用程序的組件,是org.apache.catalina.Deployer接口的實例; C2)StandardHost:是Deployer接口的一個實例,使其成為一個 可以向其中部署web 應用程序的特殊容器; C3)StandardHost類會將部署和安裝web 應用程序的任務委托給其輔助類 org.apache.catalina.core.StandardHostDeployer類完成。 C4)StandardHostDeployer類:提供了部署和安裝web 應用程序以及啟動/ 關閉 Context實例的代碼;
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎
總結
以上是生活随笔為你收集整理的tomcat(18)部署器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring(3)高级装配
- 下一篇: tomcat(19)Manager应用程