【Tomcat】Tomcat原始碼閱讀之StandardHost與HostConfig的分析
前面的文章分析了
StandardEngine
,這裡來分析一下由
Engine
物件來管理的另外一種物件吧:
Host
。
StandardHost物件的建立
一般情況下,在tomcat中都是預設使用StandardHost
型別。。。這裡先來看看在catalina是如何配置建立StandardHost物件的吧:
//建立host物件
digester.addObjectCreate(prefix + "Host",
"org.apache.catalina.core.StandardHost", //建立host物件的配置
"className");
digester.addSetProperties(prefix + "Host");
digester.addRule(prefix + "Host",
new CopyParentClassLoaderRule()); //會將host的parentClassloader設定為engine的,engine被設定為sharedloader
digester.addRule(prefix + "Host", //為host設定配置的監聽
new LifecycleListenerRule
("org.apache.catalina.startup.HostConfig", //這個算是比較重要的吧,在裡面會具體的建立context啥的
"hostConfigClass"));
digester.addSetNext(prefix + "Host",
"addChild",
"org.apache.catalina.Container"); // 在engine上面呼叫addChild方法,用於新增當前的host到engine上面去
這部分具體的涉及到Host物件的建立,第一個是具體的建立要用的host物件,也就是StandardHost物件,接著是指定為host設定的parentclassLoader為engine的parentClassLoader,在前面的文章中,我們知道engine的parentClassLoader會被設定為sharedLoader,所以這裡host物件的parentClassLoader也會被設定為sharedLoader。。
然後還有比較重要的配置項吧,為StandardHost
物件新增lifecycle
的監聽器,為HostConfig
型別的物件。。它是比較重要的吧,因為它會具體的負責對context的建立,啟動啥的。。這個一會再來說吧。。
然後就最後呼叫engine的addChild方法將當前host物件新增到engine上面去了。。。
host介面
好啦,到這裡怎樣建立host物件應該算是知道了吧。。。那麼接下來來看看host介面是如何定義的吧:
public interface Host extends Container {
//host的一些事件的定義,新增別名,移除別名啥的
public static final String ADD_ALIAS_EVENT = "addAlias";
public static final String REMOVE_ALIAS_EVENT = "removeAlias";
public String getXmlBase(); //當前host物件的配置檔案的路徑,這個檔案不一定存在吧 /conf/enginename/hostname/
public void setXmlBase(String xmlBase);
public File getConfigBaseFile(); //當前host的配置xml檔案
public String getAppBase(); //當前host的app在什麼地方
public File getAppBaseFile(); //獲取app的放的目錄的檔案引用
public void setAppBase(String appBase); //設定app存放的路徑
public boolean getAutoDeploy(); //是否自動部署
public void setAutoDeploy(boolean autoDeploy); //是否自動部署
public String getConfigClass(); //用於監聽context的listener的型別
public void setConfigClass(String configClass);
public boolean getDeployOnStartup(); //啟動的時候部署?
public void setDeployOnStartup(boolean deployOnStartup);
public String getDeployIgnore();
public Pattern getDeployIgnorePattern(); //context名字匹配用的正規表示式
public void setDeployIgnore(String deployIgnore);
public ExecutorService getStartStopExecutor(); //用於啟動和停止子container(也就是context)的executor
public boolean getCreateDirs(); //如果是ture的話,那麼會嘗試為應用程式和host的配置建立資料夾
public void setCreateDirs(boolean createDirs);
public boolean getUndeployOldVersions(); //是否自動解除安裝程式的老版本
public void setUndeployOldVersions(boolean undeployOldVersions);
public void addAlias(String alias); //為當前host新增別名
public String[] findAliases(); //獲取當前host的所有別名
public void removeAlias(String alias); //移除一個別名
}
介面定義稍微長一些吧,不過也還挺簡單的,主要是一些配置,別名什麼的管理,這個具體看上面的註釋應該能比較的清楚吧。
StandardHost
好啦,接下來來看看StandardHost是怎麼實現的吧,先來看看簡單的繼承體系:
這個應該算是很簡單的吧,首先也是一個容器。。。然後實現了host介面。。。這裡來看看它的建構函式吧:
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve()); //設定basic
}
沒做什麼事情吧,無非是在pipeline上面新增了一個basic的valve物件。。。接下來再來看看一些重要的屬性的定義吧:
private String[] aliases = new String[0]; //當前host物件的別名的陣列
private final Object aliasesLock = new Object(); //鎖
private String appBase = "webapps"; //預設的app的路徑是tomcat根路徑下的webapps資料夾
private volatile File appBaseFile = null; //引用這個資料夾
private String xmlBase = null; //xml配置檔案所在的目錄
private volatile File hostConfigBase = null; //host的預設配置路徑 conf/ + enginename + / + hostname
private boolean autoDeploy = true; //預設是自動部署的
private String configClass =
"org.apache.catalina.startup.ContextConfig"; // 預設的config型別,是個listener,通過監聽當前host的狀態來部署context啥的
private String contextClass =
"org.apache.catalina.core.StandardContext"; //預設用到的context物件的型別
private boolean deployOnStartup = true; //預設在啟動的時候部署應用
private boolean deployXML = !Globals.IS_SECURITY_ENABLED;
private boolean copyXML = false; //
private String errorReportValveClass =
"org.apache.catalina.valves.ErrorReportValve"; //用於儲存的valve預設的型別
private boolean unpackWARs = true; //預設要解壓war包
private String workDir = null; //app的work路徑
private boolean createDirs = true; //預設在啟動的時候建立資料夾
private final Map<ClassLoader, String> childClassLoaders = //跟蹤每個app的classLoaer,用於定位記憶體洩露
new WeakHashMap<>();
private Pattern deployIgnore = null;
private boolean undeployOldVersions = false; //預設不解除安裝老版本
嗯,具體這些屬性的用處在註釋上應該比較的清楚了。。其實host物件本身無非就是對這些屬性的管理。。自己並沒有太多的要做的事情
。。方法也基本上都是對這些屬性的設定什麼的。。。這裡就不具體的來分析這些方法了,有興趣自己看看就是了。。挺簡單的。。。
那麼這裡來看看host物件的pipeline上的basic的valve幹了的invoke做了什麼事情吧。。我們在前面知道。。在engine的basic的vavle上將會呼叫請求所屬的host的pipeline來處理請求。。
//其實這裡主要是是呼叫當前請求的context的pipeline來處理
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Context to be used for this Request
Context context = request.getContext(); //獲取當前請求所屬的context
if (context == null) { //如果沒法找到context,那麼可以直接返回錯誤了
response.sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
sm.getString("standardHost.noContext"));
return;
}
context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
if (request.isAsyncSupported()) { //設定當前請求是否支援非同步
request.setAsyncSupported(context.getPipeline().isAsyncSupported());
}
// Don't fire listeners during async processing
// If a request init listener throws an exception, the request is
// aborted
boolean asyncAtStart = request.isAsync();
// An async error page may dispatch to another resource. This flag helps
// ensure an infinite error handling loop is not entered
boolean errorAtStart = response.isError();
if (asyncAtStart || context.fireRequestInitEvent(request)) { //用於讓ServletRequestListener,表示有請求進來了
// Ask this Context to process this request
try {
context.getPipeline().getFirst().invoke(request, response); //呼叫所屬的context來處理了
} catch (Throwable t) { //如果有異常的話,那麼需要返回錯誤
ExceptionUtils.handleThrowable(t);
if (errorAtStart) {
container.getLogger().error("Exception Processing " +
request.getRequestURI(), t);
} else {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
throwable(request, response, t);
}
}
// If the request was async at the start and an error occurred then
// the async error handling will kick-in and that will fire the
// request destroyed event *after* the error handling has taken
// place
if (!(request.isAsync() || (asyncAtStart &&
request.getAttribute(
RequestDispatcher.ERROR_EXCEPTION) != null))) {
// Protect against NPEs if context was destroyed during a
// long running request.
if (context.getState().isAvailable()) {
if (!errorAtStart) {
// Error page processing
response.setSuspended(false);
Throwable t = (Throwable) request.getAttribute(
RequestDispatcher.ERROR_EXCEPTION);
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}
context.fireRequestDestroyEvent(request);
}
}
}
// Access a session (if present) to update last accessed time, based on a
// strict interpretation of the specification
if (ACCESS_SESSION) {
request.getSession(false);
}
context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
}
嗯,程式碼雖然挺長的,其實主要也就是要呼叫當前請求所屬的context的pipeline來處理這個請求的。。。
嗯,這裡其實StandardHost部分的內容也就差不多了吧。。
HostConfig
接下來來看看前面提到的HostConfig物件。。它將會用於監聽host物件的生命週期事件,例如啟動,停止
是什麼的。。。
繼承體系還是蠻簡單的吧,實現了LifecycleListener
介面,那麼表示當前物件可以響應lifecycle物件的生命週期事件,例如啟動停止
。。。接下來來看看它的一些重要的屬性以及建構函式吧:
protected String contextClass = "org.apache.catalina.core.StandardContext"; //用到的context的型別的名字
protected Host host = null; //監聽的host物件
protected ObjectName oname = null; //在jmx上面註冊的名字
protected static final StringManager sm =
StringManager.getManager(Constants.Package);
protected boolean deployXML = false; //是否要處理app的context的xml配置檔案
protected boolean copyXML = false; //是否要將xml配置檔案移動到/conf/enginename/hostname/下面
protected boolean unpackWARs = false; //是否要解壓war
protected final Map<String, DeployedApplication> deployed = //所有已經部署的應用,key是context的名字
new ConcurrentHashMap<>();
protected final ArrayList<String> serviced = new ArrayList<>();
protected Digester digester = createDigester(contextClass); //用於即系xml檔案的
private final Object digesterLock = new Object();
protected final Set<String> invalidWars = new HashSet<>(); //忽略的war包
public String getContextClass() { //獲取用到的context的型別
return (this.contextClass);
}
這裡屬性有些還是非常很總要的,例如deployed,用於代表每一個已經部署的web應用程式。。。接下來來看看它的lifecycleEvent方法的定義吧,也就是它是如何響應事件的:
//相應監聽的host的生命週期的事件
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle(); //當前所監聽的lifecycle物件,這裡監聽的是host物件
if (host instanceof StandardHost) { //根據host的資訊,來設定一些配置
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { //週期事件
check();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start(); //開始進行部署
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop(); //停止
}
}
這裡首先是在host物件上面來拿一些配置的資訊,然後根據事件的型別進行相應的處理。。這裡就來看看對於啟動的事件是怎麼相應的吧:
//當監聽的host啟動的時候會執行這個方法,其實主要是context的部署
public void start() {
if (log.isDebugEnabled())
log.debug(sm.getString("hostConfig.start"));
try {
ObjectName hostON = host.getObjectName(); //獲取host的jmx上面註冊的名字
oname = new ObjectName
(hostON.getDomain() + ":type=Deployer,host=" + host.getName()); //根據host的名字生成當前物件的名字
Registry.getRegistry(null, null).registerComponent
(this, oname, this.getClass().getName()); //在jmx上面註冊當前物件
} catch (Exception e) {
log.error(sm.getString("hostConfig.jmx.register", oname), e);
}
if (host.getCreateDirs()) { //如果要建立資料夾,這個是用於存放host的配置檔案。/conf/enginename/hostname,還有就是app的目錄
File[] dirs = new File[] {host.getAppBaseFile(),host.getConfigBaseFile()};
for (int i=0; i<dirs.length; i++) {
if (!dirs[i].mkdirs() && !dirs[i].isDirectory()) { //如果目錄不存在的話,那麼建立目錄
log.error(sm.getString("hostConfig.createDirs",dirs[i]));
}
}
}
if (!host.getAppBaseFile().isDirectory()) { //如果app的目錄不是資料夾,那麼錯誤了
log.error(sm.getString("hostConfig.appBase", host.getName(),
host.getAppBaseFile().getPath()));
host.setDeployOnStartup(false);
host.setAutoDeploy(false);
}
if (host.getDeployOnStartup()) //一般都是
deployApps(); //開始部署應用
}
這裡要做的其實主要是在jmx上的註冊
,然後對配置檔案目錄的處理,接著再是呼叫deployApps
方法來具體的部署應用web應用程式
:
//app的部署
protected void deployApps() {
File appBase = host.getAppBaseFile(); //獲取app的路徑目錄的資料夾引用 /webapps
File configBase = host.getConfigBaseFile(); //獲取host的配置檔案的路徑 /conf/enginename/hostname/
String[] filteredAppPaths = filterAppPaths(appBase.list()); //這裡過濾一下app的路徑,用host裡的正規表示式來判斷資料夾的名字是否符合規定
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list()); //先處理host的配置
// Deploy WARs
deployWARs(appBase, filteredAppPaths); //部署app資料夾的war包
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths); //部署app資料夾裡面的檔案
}
首先獲取了host的配置檔案的目錄以及存放app的資料夾的目錄,然後對app的檔案的名字進行一些過濾,畢竟context的名字不能隨便取的嘛。。接著就是開始部署應用程式了,這裡分為兩種吧,一種是不是war包型別的,另外一種就是部署資料夾型別的。。。
這裡就來看看部署資料夾型別的吧,war包無非就是多了一層解壓而已:
// 部署應用,資料夾型別的,前面是所有app所在的目錄的引用,後面是要部署的資料夾的名字
protected void deployDirectories(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor(); //後去host物件用於啟動停止子container的executor
List<Future<?>> results = new ArrayList<>();
for (int i = 0; i < files.length; i++) { //遍歷每一個資料夾
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File dir = new File(appBase, files[i]); //建立檔案的file引用
if (dir.isDirectory()) { //這裡需要是一個資料夾
ContextName cn = new ContextName(files[i], false); //根據資料夾的名字來設定context的名字
if (isServiced(cn.getName()) || deploymentExists(cn.getName())) //是否有同名的
continue;
results.add(es.submit(new DeployDirectory(this, cn, dir))); //新增一個deploy資料夾的任務,派遣到executor裡面進行 其實是config.deployDirectory
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDir.threaded.error"), e);
}
}
}
這裡是遍歷當前app所在資料夾,然後根據資料夾的名字來建立context的名字,然後在host的executor上面提交部署的任務,具體的執行如下:
//第一個引數是context的名字,第二個引數是資料夾的引用
protected void deployDirectory(ContextName cn, File dir) {
// Deploy the application in this directory
if( log.isInfoEnabled() ) //列印正在部署啥
log.info(sm.getString("hostConfig.deployDir",
dir.getAbsolutePath()));
Context context = null;
//有的web應用可能有定義context的配置
File xml = new File(dir, Constants.ApplicationContextXml); //"META-INF/context.xml"; 當前context的配置檔案,這個也不一一定有
File xmlCopy =
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); //獲取host的配置資料夾裡面當前context的配置,這個不一定有
DeployedApplication deployedApp; //用於引用已經部署的app
boolean copyThisXml = copyXML;
try {
if (deployXML && xml.exists()) { //根據應用的配置來建立
synchronized (digesterLock) {
try {
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml), e);
context = new FailedContext();
} finally {
if (context == null) {
context = new FailedContext();
}
digester.reset();
}
}
if (copyThisXml == false && context instanceof StandardContext) {
// Host is using default value. Context may override it.
copyThisXml = ((StandardContext) context).getCopyXML();
}
if (copyThisXml) {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(xml);
os = new FileOutputStream(xmlCopy);
IOTools.flow(is, os);
// Don't catch IOE - let the outer try/catch handle it
} finally {
try {
if (is != null) is.close();
} catch (IOException e){
// Ignore
}
try {
if (os != null) os.close();
} catch (IOException e){
// Ignore
}
}
context.setConfigFile(xmlCopy.toURI().toURL());
} else {
context.setConfigFile(xml.toURI().toURL());
}
} else if (!deployXML && xml.exists()) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), xml, xmlCopy));
context = new FailedContext();
} else { //一般沒有context的配置的話,就在這裡建立context org.apache.catalina.core.StandardContext
context = (Context) Class.forName(contextClass).newInstance();
}
Class<?> clazz = Class.forName(host.getConfigClass()); //為context建立config物件 org.apache.catalina.startup.ContextConfig
LifecycleListener listener =
(LifecycleListener) clazz.newInstance(); // 建立listener的物件,然後新增到context上面去
context.addLifecycleListener(listener);
context.setName(cn.getName()); //設定當前context的名字
context.setPath(cn.getPath()); //應用所在的路徑
context.setWebappVersion(cn.getVersion()); //當前版本
context.setDocBase(cn.getBaseName());
host.addChild(context); //在host新增context,在host裡面,會將context的name與context對應起來,而且這裡還會進行context的啟動
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployDir.error",
dir.getAbsolutePath()), t);
} finally {
deployedApp = new DeployedApplication(cn.getName(), //建立DeployedApplication物件,表設一個部署的應用
xml.exists() && deployXML && copyThisXml);
// Fake re-deploy resource to detect if a WAR is added at a later
// point
//重新熱部署的東西
deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war",
Long.valueOf(0));
deployedApp.redeployResources.put(dir.getAbsolutePath(),
Long.valueOf(dir.lastModified()));
if (deployXML && xml.exists()) { //如果有context的配置檔案
if (copyThisXml) {
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(xmlCopy.lastModified()));
} else {
deployedApp.redeployResources.put(
xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
// Fake re-deploy resource to detect if a context.xml file is
// added at a later point
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(0));
}
} else {
// Fake re-deploy resource to detect if a context.xml file is
// added at a later point
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(0));
if (!xml.exists()) {
deployedApp.redeployResources.put(
xml.getAbsolutePath(),
Long.valueOf(0));
}
}
addWatchedResources(deployedApp, dir.getAbsolutePath(), context); //新增web應用程式資源的監控
// Add the global redeploy resources (which are never deleted) at
// the end so they don't interfere with the deletion process
addGlobalRedeployResources(deployedApp); //新增全域性的資源
}
deployed.put(cn.getName(), deployedApp); //表示這個app已經部署了,key是當前context的名字,後面是
}
程式碼也還算是比較長的吧,其實主要要做得到事情就是處理當前web應用程式的context的配置,然後建立context的物件,然後呼叫host物件的addChild方法將當前建立的context加入到host裡面去。。。
好啦,到這裡就算差不多了。。乾貨不多吧,主要就集中在context的建立和部署上了。。。
相關文章
- Tomcat原始碼閱讀筆記Tomcat原始碼筆記
- Tomcat 原始碼閱讀記錄(1)Tomcat原始碼
- Tomcat中session詳解(原始碼閱讀)TomcatSession原始碼
- ARTS第十三週(閱讀Tomcat原始碼)Tomcat原始碼
- Tomcat中Lifecycle詳解(原始碼閱讀)Tomcat原始碼
- tomcat8.5.57原始碼閱讀筆記1Tomcat原始碼筆記
- Tomcat原始碼分析2 之 Protocol實現分析Tomcat原始碼Protocol
- Tomcat聯結器執行過程(原始碼閱讀)Tomcat原始碼
- Tomcat 中的 NIO 原始碼分析Tomcat原始碼
- [Web Server]Tomcat調優之SpringBoot內嵌Tomcat原始碼分析WebServerTomcatSpring Boot原始碼
- 【Tomcat 原始碼系列】原始碼構建 TomcatTomcat原始碼
- 詳解Tomcat系列(一)-從原始碼分析Tomcat的啟動Tomcat原始碼
- tomcat nio2原始碼分析Tomcat原始碼
- Tomcat原始碼分析--啟動流程Tomcat原始碼
- TOMCAT原始碼分析(啟動框架)Tomcat原始碼框架
- tomcat原始碼分析(第三篇 tomcat請求原理解析--Connector原始碼分析)Tomcat原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- Vuex原始碼閱讀分析Vue原始碼
- snabbdom 原始碼閱讀分析原始碼
- 【Tomcat 原始碼系列】認識 TomcatTomcat原始碼
- [Tomcat原始碼系列] Tomcat ConnectorTomcat原始碼
- 閱讀《Tomcat 系統架構與設計模式》Tomcat架構設計模式
- tomcat原始碼分析(第四篇 tomcat請求處理原理解析--Container原始碼分析)Tomcat原始碼AI
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- tomcat原始碼分析(第二篇 tomcat啟動過程詳解)Tomcat原始碼
- 【Tomcat 原始碼系列】Tomcat 整體結構Tomcat原始碼
- Tomcat長輪詢原理與原始碼解析Tomcat原始碼
- Docker原始碼分析,附閱讀地址Docker原始碼
- knockout原始碼分析之訂閱原始碼
- 閱讀原始碼的意義與方法原始碼
- JDK1.8原始碼分析03之idea搭建原始碼閱讀環境JDK原始碼Idea
- 深入理解 Tomcat(一)原始碼環境搭建和 How Tomcat works 原始碼Tomcat原始碼
- 閱讀原始碼---與高手對話原始碼
- 原始碼閱讀之Java棧的實現原始碼Java
- Tomcat原始碼分析 (四)----- Pipeline和ValveTomcat原始碼
- Tomcat詳解系列(3) - 原始碼分析準備和分析入口Tomcat原始碼