Host组件表示一个虚拟主机,在Host元素内可以有多个Context元素与之关联以表示不同的Web应用,Engine元素内可以配置多个Host,但其中一个的名称必须与Engine的defaultHost属性值相匹配。
Host组件
Host接口继承Container接口,StandardHost类是Host组件的默认实现,它继承ContainerBase基类并实现了Host接口,其构造函数和部分成员变量如下所示:
public class StandardHost extends ContainerBase implements Host {
// ----------------------------------------------------------- Constructors
/**
* Create a new StandardHost component with the default basic Valve.
*/
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}
// ----------------------------------------------------- Instance Variables
/**
* The set of aliases for this Host.
*/
private String[] aliases = new String[0];
private final Object aliasesLock = new Object();
/**
* The application root for this Host.
*/
private String appBase = "webapps";
private volatile File appBaseFile = null;
/**
* The XML root for this Host.
*/
private String xmlBase = null;
/**
* host's default config path
*/
private volatile File hostConfigBase = null;
/**
* The auto deploy flag for this Host.
*/
private boolean autoDeploy = true;
/**
* The Java class name of the default context configuration class
* for deployed web applications.
*/
private String configClass =
"org.apache.catalina.startup.ContextConfig";
/**
* The Java class name of the default Context implementation class for
* deployed web applications.
*/
private String contextClass = "org.apache.catalina.core.StandardContext";
/**
* The deploy on startup flag for this Host.
*/
private boolean deployOnStartup = true;
/**
* deploy Context XML config files property.
*/
private boolean deployXML = !Globals.IS_SECURITY_ENABLED;
/**
* Should XML files be copied to
* $CATALINA_BASE/conf/<engine>/<host> by default when
* a web application is deployed?
*/
private boolean copyXML = false;
/**
* The Java class name of the default error reporter implementation class
* for deployed web applications.
*/
private String errorReportValveClass =
"org.apache.catalina.valves.ErrorReportValve";
/**
* Unpack WARs property.
*/
private boolean unpackWARs = true;
/**
* Work Directory base for applications.
*/
private String workDir = null;
/**
* Should we create directories upon startup for appBase and xmlBase
*/
private boolean createDirs = true;
/**
* Track the class loaders for the child web applications so memory leaks
* can be detected.
*/
private final Map<ClassLoader, String> childClassLoaders =
new WeakHashMap<>();
/**
* Any file or directory in {@link #appBase} that this pattern matches will
* be ignored by the automatic deployment process (both
* {@link #deployOnStartup} and {@link #autoDeploy}).
*/
private Pattern deployIgnore = null;
private boolean undeployOldVersions = false;
private boolean failCtxIfServletStartFails = false;
@Override
public void addChild(Container child) {
child.addLifecycleListener(new MemoryLeakTrackingListener());
if (!(child instanceof Context))
throw new IllegalArgumentException
(sm.getString("standardHost.notContext"));
super.addChild(child);
}
// 省略一些代码
}
重要的成员变量如下:
- appBase表示本Host的Web应用根路径;
- xmlBase表示本Host的XML根路径;
- autoDeploy表示是否应周期性地部署新的或者更新过的Web应用;
- configClass和contextClass分别表示部署Web应用时Context组件的配置类和实现类;
- deployOnStartup表示Tomcat启动时是否自动部署本Host的应用;
- unpackWARs表示是否要将WAR文件解压成目录;
- createDirs表示启动阶段是否要创建appBase和xmlBase表示的目录;
- deployIgnore表示当autoDeploy和deployOnStartup属性被设置时忽略的文件模式;
- 其余变量的含义可以参考Host的配置文档。
Host组件的构造函数为自己的Pipeline添加基本阀StandardHostValve,addChild方法只能添加Context组件。
组件初始化
StandardHost类并没有重写initInternal方法,因此它的初始化过程只是为自己创建了一个线程池用于启动和停止自己的子容器。
组件启动
StandardHost类的startInternal方法如下所示:
@Override
protected synchronized void startInternal() throws LifecycleException {
// Set error report valve
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
found = true;
break;
}
}
if(!found) {
Valve valve =
(Valve) Class.forName(errorValve).getConstructor().newInstance();
getPipeline().addValve(valve);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardHost.invalidErrorReportValveClass",
errorValve), t);
}
}
super.startInternal();
}
- 如果所关联的Pipeline中没有错误报告阀(errorReportValveClass),那么就给Pipeline添加一个,否则什么也不做;
- 调用基类ContainerBase类的startInternal方法,先启动子容器组件,然后启动Pipeline,最后发布LifecycleState.STARTING事件给添加到Host组件自身的生命周期事件监听器。
HostConfig监听器
Host组件里比较重要的生命周期事件监听器之一是HostConfig监听器,Tomcat在解析server.xml时为Digester添加了HostRuleSet规则,进而为StandardHost添加HostConfig生命周期监听器(请参见本系列的Tomcat启动分析(二))。HostConfig的主要作用是在Host组件启动时响应Host发布的事件。
成员变量
HostConfig类实现了生命周期事件监听器接口LifecycleListener,成员变量如下所示:
public class HostConfig implements LifecycleListener {
private static final Log log = LogFactory.getLog(HostConfig.class);
protected static final StringManager sm = StringManager.getManager(HostConfig.class);
/**
* The resolution, in milliseconds, of file modification times.
*/
protected static final long FILE_MODIFICATION_RESOLUTION_MS = 1000;
protected String contextClass = "org.apache.catalina.core.StandardContext";
/**
* The Host we are associated with.
*/
protected Host host = null;
protected ObjectName oname = null;
protected boolean deployXML = false;
protected boolean copyXML = false;
protected boolean unpackWARs = false;
/**
* Map of deployed applications.
*/
protected final Map<String, DeployedApplication> deployed =
new ConcurrentHashMap<>();
public boolean isCopyXML() {
return (this.copyXML);
}
public void setCopyXML(boolean copyXML) {
this.copyXML= copyXML;
}
public boolean isUnpackWARs() {
return (this.unpackWARs);
}
public void setUnpackWARs(boolean unpackWARs) {
this.unpackWARs = unpackWARs;
}
}
- host变量引用该监听器关联的Host组件;
- 其他成员变量如deployXML、copyXML和unpackWARs等与Host组件的对应变量含义相同。
响应生命周期事件
HostConfig类实现的lifecycleEvent方法如下:
@Override
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
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.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
- 首先将发布事件的Host组件的copyXML、deployXML、unpackWARs和contextClass属性绑定到该监听器自身;
- 然后针对不同事件执行不同的事件处理方法,以启动事件Lifecycle.START_EVENT为例,当启动事件到来时start方法被调用,若Host组件设置了deployOnStartup则部署Web应用。
public void start() { if (log.isDebugEnabled()) log.debug(sm.getString("hostConfig.start")); try { ObjectName hostON = host.getObjectName(); oname = new ObjectName (hostON.getDomain() + ":type=Deployer,host=" + host.getName()); Registry.getRegistry(null, null).registerComponent (this, oname, this.getClass().getName()); } catch (Exception e) { log.error(sm.getString("hostConfig.jmx.register", oname), e); } if (!host.getAppBaseFile().isDirectory()) { log.error(sm.getString("hostConfig.appBase", host.getName(), host.getAppBaseFile().getPath())); host.setDeployOnStartup(false); host.setAutoDeploy(false); } if (host.getDeployOnStartup()) deployApps(); }
部署Web应用
部署Web应用是由deployApps方法完成的,其代码如下所示,内部首先调用filterAppPaths过滤掉与deployIgnore模式匹配的文件,然后才部署剩余的应用。
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
下面以部署WAR文件为例分析Web应用是如何被部署的。
部署WAR文件
部署WAR文件是由deployWARs方法完成的,其代码如下所示:
protected void deployWARs(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
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 war = new File(appBase, files[i]);
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
war.isFile() && !invalidWars.contains(files[i]) ) {
ContextName cn = new ContextName(files[i], true);
if (isServiced(cn.getName())) {
continue;
}
if (deploymentExists(cn.getName())) {
DeployedApplication app = deployed.get(cn.getName());
boolean unpackWAR = unpackWARs;
if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
}
if (!unpackWAR && app != null) {
// Need to check for a directory that should not be
// there
File dir = new File(appBase, cn.getBaseName());
if (dir.exists()) {
if (!app.loggedDirWarning) {
log.warn(sm.getString(
"hostConfig.deployWar.hiddenDir",
dir.getAbsoluteFile(),
war.getAbsoluteFile()));
app.loggedDirWarning = true;
}
} else {
app.loggedDirWarning = false;
}
}
continue;
}
// Check for WARs with /../ /./ or similar sequences in the name
if (!validateContextPath(appBase, cn.getBaseName())) {
log.error(sm.getString(
"hostConfig.illegalWarName", files[i]));
invalidWars.add(files[i]);
continue;
}
results.add(es.submit(new DeployWar(this, cn, war)));
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployWar.threaded.error"), e);
}
}
}
每个WAR文件、XML文件或者目录都对应一个Context,根据它们的文件名可以得到基本文件名、Context名称、Context版本和Context路径,详见Context配置文档的Naming一节。
对appBase目录下的每个没被过滤掉的且文件名以.war结尾的文件:
- 如果文件名忽略大小写后与META-INF或者WEB-INF相等则跳过不处理;
- 如果该文件对应的Context已经开始服务或者已经在Host组件里部署,那么跳过不处理;
- 如果该文件对应的Context路径包含非法字符,那么跳过不处理;
- 在Host组件的startStopExecutor中部署符合要求的WAR文件,startStopExecutor的作用请见上一篇文章。
部署Web应用时,关于文件名有以下几点需要注意:
- 文件名不能包含*和?两个字符(Servlet规范并没有这个要求),因为它们会被JMX认成通配符模式。如果使用则会报与JMX相关的错误,可参考ObjectName文档;
- 除了*和?两个字符外,其他字符(包括中文字符)均可以作为文件名的一部分。
为Web应用创建Context
上文提到HostConfig会在与之关联的Host组件的startStopExecutor中部署符合要求的WAR文件,这是通过HostConfig的静态内部类DeployWar实现的,该类实现了Runnable接口,在run方法中调用了HostConfig类自身的deployWAR方法,该方法部分代码如下所示:
/**
* Deploy packed WAR.
* @param cn The context name
* @param war The WAR file
*/
protected void deployWAR(ContextName cn, File war) {
File xml = new File(host.getAppBaseFile(),
cn.getBaseName() + "/" + Constants.ApplicationContextXml);
// 省略部分代码
Context context = null;
boolean deployThisXML = isDeployThisXML(war, cn);
try {
if (deployThisXML && useXml && !copyXML) {
// 省略部分代码
} else if (deployThisXML && xmlInWar) {
// 省略部分代码
} else if (!deployThisXML && xmlInWar) {
// 省略部分代码
} else {
context = (Context) Class.forName(contextClass).getConstructor().newInstance();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployWar.error",
war.getAbsolutePath()), t);
} finally {
if (context == null) {
context = new FailedContext();
}
}
// 省略部分代码
DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
xml.exists() && deployThisXML && copyThisXml);
long startTime = 0;
// Deploy the application in this WAR file
if(log.isInfoEnabled()) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployWar",
war.getAbsolutePath()));
}
try {
// Populate redeploy resources with the WAR file
deployedApp.redeployResources.put
(war.getAbsolutePath(), Long.valueOf(war.lastModified()));
if (deployThisXML && xml.exists() && copyThisXml) {
deployedApp.redeployResources.put(xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
} else {
// In case an XML file is added to the config base later
deployedApp.redeployResources.put(
(new File(host.getConfigBaseFile(),
cn.getBaseName() + ".xml")).getAbsolutePath(),
Long.valueOf(0));
}
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName() + ".war");
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployWar.error",
war.getAbsolutePath()), t);
} finally {
// 省略部分代码
}
deployed.put(cn.getName(), deployedApp);
if (log.isInfoEnabled()) {
log.info(sm.getString("hostConfig.deployWar.finished",
war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}
该方法有以下几点需要注意:
- 对一般WAR文件部署的过程,与该文件对应的Context对象会被创建,具体的类型是HostConfig的contextClass属性表示的类型,默认是StandardContext类型;
- 在为Context对象设置其他属性如名称、路径和版本等之后,Host组件通过addChild方法将该Context对象加入到自己的子容器组件集合中。在ContainerBase类的addChild方法可以看到,添加之后便启动了子容器组件。
调试部署WAR文件
为了方便调试部署WAR文件的流程,可以使用嵌入式Tomcat。调试的基本代码如下,指定WAR文件所在目录即可。
import org.apache.catalina.Host;
import org.apache.catalina.startup.HostConfig;
import org.apache.catalina.startup.Tomcat;
public class DebugTomcat {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8081);
Host host = tomcat.getHost();
host.setAppBase("path for directory with wars"); // WAR文件的路径
host.addLifecycleListener(new HostConfig());
tomcat.start();
tomcat.getServer().await();
}
}