上一节我们研究了部分组件的启动,在host启动的时候,HostConfig监听了Host容器的启动事件。
Host的start事件
public void org.apache.catalina.startup.HostConfig.start() {
if (log.isDebugEnabled())
log.debug(sm.getString("hostConfig.start"));
try {
//获取host的ObjectName,这个ObjectName是用于mbean的对象名
ObjectName hostON = host.getObjectName();
oname = new ObjectName
(hostON.getDomain() + ":type=Deployer,host=" + host.getName());
//注册到mbean
Registry.getRegistry(null, null).registerComponent
(this, oname, this.getClass().getName());
} catch (Exception e) {
log.error(sm.getString("hostConfig.jmx.register", oname), e);
}
//如果指定的appBase不是一个目录,那么打印错误日志,并设置自动部署为false,
//启动即部署为false
if (!host.getAppBaseFile().isDirectory()) {
log.error(sm.getString("hostConfig.appBase", host.getName(),
host.getAppBaseFile().getPath()));
host.setDeployOnStartup(false);
host.setAutoDeploy(false);
}
//是否在启动时就发布,默认为true
if (host.getDeployOnStartup())
//发布应用
deployApps();
}
发布应用
protected void org.apache.catalina.startup.HostConfig.deployApps() {
//获取appBase,如果你的tomcat安装在D:\tomcat,并且appBasee配置为webapps,那么这个地址就是D:\tomcat\webapps
File appBase = host.getAppBaseFile();
//当前host的基本配置文件路径,一般就是D:\tomcat\conf\Catalina\host名(如果是本机,那么就是localhost)
File configBase = host.getConfigBaseFile();
//过滤掉不需要进行自动部署的文件目录
//(*1*)
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
//从host的基本配置路径发布xml描述
deployDescriptors(configBase, configBase.list());
// Deploy WARs
//从应用基本路径发布war包
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
//发布扩展路径下的应用
deployDirectories(appBase, filteredAppPaths);
}
//(*1*)
protected String[] org.apache.catalina.startup.HostConfig.filterAppPaths(String[] unfilteredAppPaths) {
//获取过滤的正则表达式
Pattern filter = host.getDeployIgnorePattern();
//如果没有设置那么就直接返回,可以在配置文件中指定deployIgnorePattern属性
if (filter == null || unfilteredAppPaths == null) {
return unfilteredAppPaths;
}
List<String> filteredList = new ArrayList<>();
Matcher matcher = null;
for (String appPath : unfilteredAppPaths) {
if (matcher == null) {
matcher = filter.matcher(appPath);
} else {
//重置匹配
matcher.reset(appPath);
}
if (matcher.matches()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("hostConfig.ignorePath", appPath));
}
} else {
//记录不需要过滤的
filteredList.add(appPath);
}
}
return filteredList.toArray(new String[filteredList.size()]);
}
从上面的代码中可以看到tomcat发布应用分成了三种情况,下面我们也分三步走
1、Deploy XML descriptors from configBase
第一步:发布基本配置路径的context的xml描述
protected void org.apache.catalina.startup.HostConfig.deployDescriptors(File configBase, String[] files) {
//假设路径:tomcat的安装目录/conf/catalina/localhost/context.xml
if (files == null)
return;
//获取host容器的线程池
ExecutorService es = host.getStartStopExecutor();//获取线程池
List<Future<?>> results = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
File contextXml = new File(configBase, files[i]);
//只处理后缀为xml的xml文件
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
//包装成ContextName对象,用于维护tomcat的名字,版本
//(*1*)
ContextName cn = new ContextName(files[i], true);
//当前context是否已经被发布过了
if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;
//使用线程池进行发布
results.add(
es.submit(new DeployDescriptor(this, cn, contextXml)));
}
}
for (Future<?> result : results) {
try {
//阻塞,等待子线程处理完毕
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.threaded.error"), e);
}
}
}
//(*1*)
public ContextName(String name, boolean stripFileExtension) {
//对于configBase路径下的context名默认使用context配置文件的名字
//比如Tomcat/Context.xml
String tmp1 = name;
// Convert Context names and display names to base names
// Strip off any leading "/"
if (tmp1.startsWith("/")) {
//去掉开头的/
tmp1 = tmp1.substring(1);
}
// Replace any remaining /
//替换调用余下/,此时tmp1 = Tomcat#Context.xml
tmp1 = tmp1.replaceAll("/", FWD_SLASH_REPLACEMENT);
// Insert the ROOT name if required
//如果是以##开头,或者已经没有名字了,那么tmp1就会标称ROOT+tmp的形式
//假设tmp1的值为空字符,那么tmp1就会变成ROOT,如果tmp1原来是##1.5,那么就变成了ROOT##1.5
if (tmp1.startsWith(VERSION_MARKER) || "".equals(tmp1)) {
tmp1 = ROOT_NAME + tmp1;
}
// Remove any file extensions
//是否是严格的文件后缀,如果是,那么截取掉后缀,比如tomcatContext.xml或者是tomcatContext.war
//那么就会变成tomcatContext
if (stripFileExtension &&
(tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") ||
tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) {
tmp1 = tmp1.substring(0, tmp1.length() -4);
}
//设置为基础名
baseName = tmp1;
String tmp2;
// Extract version number
//是否存在##字符串
int versionIndex = baseName.indexOf(VERSION_MARKER);
if (versionIndex > -1) {
//如果存在##,那么##后面的字符串作为版本号,比如ROOT##1.5,那么版本号就为1.5
version = baseName.substring(versionIndex + 2);
//如果baseName = ROOT##1.5,那么tmp2就变成了ROOT
tmp2 = baseName.substring(0, versionIndex);
} else {
version = "";
tmp2 = baseName;
}
//如果tmp2是ROOT(表示根路径)
//那么应用路径设置为空,也就是我们常在项目中配置/
if (ROOT_NAME.equals(tmp2)) {
path = "";
} else {
//如果不是根路径,那么将#替换成/作为项目路径
path = "/" + tmp2.replaceAll(FWD_SLASH_REPLACEMENT, "/");
}
//设置context名字
///比如context##1.5
if (versionIndex > -1) {
this.name = path + VERSION_MARKER + version;
} else {
this.name = path;
}
}
上面有段设置context名字,路径,版本的代码,可以分为以下几种情况
- 假设传入的contextName是context.xml,那么最终的basename=context,version="",path=/context,name=/context
- /tomcat/context.war 那么最终的basename=tomcat#context,version="",path=/tomcat/context,name=/tomcat/context
- ##tomcat/context.xml 那么最终的basename=ROOT##tomcat#context,version=tomcat#context,path="",name=##tomcat#context
- / 那么最终的basename=ROOT,version="",path="",name=""
- tomcat#context##1.5.xml 那么最终的basename=tomcat#context##1.5,version=1.5,path=/tomcat/context,name=/tomcat/context##1.5
-ROOT##1.5 那么最终的basename=ROOT##1.5,version=1.5,path="",name=##1.5
可以认为baseName是tomcat对web应用目录的规范(后面你会看到tomcat对war还有拷贝的context.xml文件的路径名就是baseName),version表示可以用于表示相同的path下的不同版本,name一看就是path与版本通过##连接的字符串,这个name在tomcat中是唯一的,如果出现相同的name的context,tomcat就不会加载,并打印错误日志告诉你哪一个context发生了冲突。
发布描述
//这是HostConfig的一个内部类
private static class DeployDescriptor implements Runnable {
private HostConfig config;
private ContextName cn;
private File descriptor;
public DeployDescriptor(HostConfig config, ContextName cn,
File descriptor) {
this.config = config;
this.cn = cn;
this.descriptor= descriptor;
}
@Override
public void run() {
//调用了HostConfig的发布描述方法
config.deployDescriptor(cn, descriptor);
}
}
protected void org.apache.catalina.startup.HostConfig.deployDescriptor(ContextName cn, File contextXml) {
//发布应用,用于记录发布的context名,和是否有描述文件,一些可能被修改的文件的修改时间,用于日后检测是否需要重新加载
//第一个参数表示这个context的名字,第二参数表示是否存在描述文件
DeployedApplication deployedApp =
new DeployedApplication(cn.getName(), true);
long startTime = 0;
// Assume this is a configuration descriptor and deploy it
if(log.isInfoEnabled()) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployDescriptor",
contextXml.getAbsolutePath()));
}
Context context = null;
//是否外部的context war包
boolean isExternalWar = false;
//是否为外部context
boolean isExternal = false;
File expandedDocBase = null;
//JDK7新增的语法糖,用于自动释放资源
try (FileInputStream fis = new FileInputStream(contextXml)) {
synchronized (digesterLock) {
try {
//解析,并返回根对象Context,这个Digester在第二小节的时候进行分析,这是一个sax的xml处理器
//这个处理器在创建HostConfig的时候就随着构造器一起构造,内部自动添加了Context标签的规则,用于
//创建StandardContext对象,已经设置属性规则
context = (Context) digester.parse(fis);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
contextXml.getAbsolutePath()), e);
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
//获取配置的类对象,这个配置对象默认就是ContextConfig
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
//将监听器添加到这个创建出来的context容器中
context.addLifecycleListener(listener);
//设置context配置文件的url路径
context.setConfigFile(contextXml.toURI().toURL());
//设置它的名字,请求路径,版本
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
// Add the associated docBase to the redeployed list if it's a WAR
//是否设置docBase,docBase就是某个web应用的所在目录,就像是host指定的webapps目录下某个具体的文件夹
if (context.getDocBase() != null) {
File docBase = new File(context.getDocBase());
//如果这个目录不是绝对路径,那么就把host指定的appBase路径拼接到这个docBase的前面
if (!docBase.isAbsolute()) {
docBase = new File(host.getAppBaseFile(), context.getDocBase());
}
// If external docBase, register .xml as redeploy first
//如果这个docBase不是以host指定的appBase作为开头的,那么认为这个docBase为外部的路径
//对应的应用为外部应用
if (!docBase.getCanonicalPath().startsWith(
host.getAppBaseFile().getAbsolutePath() + File.separator)) {
isExternal = true;
//将当前context的配置描述文件添加到重新部署资源map中
//资源绝对路径-》资源最后修改时间,在发生更改的情况下可以进行重新部署
deployedApp.redeployResources.put(
contextXml.getAbsolutePath(),
Long.valueOf(contextXml.lastModified()));
//将docBase目录记录为可重新发布资源
deployedApp.redeployResources.put(docBase.getAbsolutePath(),
Long.valueOf(docBase.lastModified()));
//要是这个docBase指定是一个context的war包,那么把isExternalWar置为true
if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
isExternalWar = true;
}
} else {
log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
docBase));
// Ignore specified docBase
//如果这个docBase就是Host下的web应用,即使context描述文件中指定了docBase也不会设置docBase,因为这个Context将会
//在appBase中发布
context.setDocBase(null);
}
}
//添加子容器
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployDescriptor.error",
contextXml.getAbsolutePath()), t);
} finally {
// Get paths for WAR and expanded WAR in appBase
//这里之所以要进行扩展路径的监控,是因为tomcat可以配置把外部包解压到appBase路径下
// default to appBase dir + name
//主动设置扩展的docBase,假设我们简单点,我们在host基本配置路径下放了一个叫做TomcatContext.xml的context描述文件
//并且appBase设置的目录为D:/tomcat/webapps,那么这个延伸的路径为D:/tomcat/webapps/TomcatContext
expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName());
//当前context不为空,也就是目录为外部目录(不在host指定的appBase之下的路径),并且不是war包
if (context.getDocBase() != null
&& !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
// first assume docBase is absolute
//那么使用配置文件指定的docBase覆盖扩展路径
//因为对于这种不是war包的外部web项目,tomcat不会复制其内容到wabAppBase目录中
expandedDocBase = new File(context.getDocBase());
//如果不是绝对路径,那么用host的appBase路径进行拼接
if (!expandedDocBase.isAbsolute()) {
// if docBase specified and relative, it must be relative to appBase
expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase());
}
}
//是否解压war包,默认是false,但是通常我们在server.xml中会配置为true
boolean unpackWAR = unpackWARs;
if (unpackWAR && context instanceof StandardContext) {
//使用context的配置覆盖host的配置
unpackWAR = ((StandardContext) context).getUnpackWAR();
}
// Add the eventual unpacked WAR and all the resources which will be
// watched inside it
//是否是外部的war包
if (isExternalWar) {
//是否进行解压
if (unpackWAR) {
//监控扩展的路径(通常就是解压后的路径)
deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
Long.valueOf(expandedDocBase.lastModified()));
//添加需要监控的资源到deployedApp.redeployResources中,包括context标签中添加的watchedResource
addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
} else {
//如果不允许发布时解压,那么只监控context标签中指定的路径的资源
//因为文件不解压,tomcat无法监控到war内部文件的变动,不过也没必要,只要监控了
//这个war包是否改动即可
addWatchedResources(deployedApp, null, context);
}
} else {
// Find an existing matching war and expanded folder
//如果不是外部的war也不是外部的web应用
if (!isExternal) {
//那么添加war后缀
File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war");
//如果存在,监控这个war包
if (warDocBase.exists()) {
deployedApp.redeployResources.put(warDocBase.getAbsolutePath(),
Long.valueOf(warDocBase.lastModified()));
} else {
// Trigger a redeploy if a WAR is added
//如果不存在,也会添加这个路径的资源监控(因为有可能日后会将对应的war添加到这个目录),但是上次修改时间为0(不存在,当然是零)
deployedApp.redeployResources.put(
warDocBase.getAbsolutePath(),
Long.valueOf(0));
}
}
//这个操作和上面那一段是一样的
if (unpackWAR) {
//监控扩展的路径
deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
Long.valueOf(expandedDocBase.lastModified()));
addWatchedResources(deployedApp,
expandedDocBase.getAbsolutePath(), context);
} else {
addWatchedResources(deployedApp, null, context);
}
//如果不是外部web应用,补充监控context描述文件,对于放在appBase目录下的web应用,其docBase由tomcat根据context描述文件名进行拼接。
if (!isExternal) {
// For external docBases, the context.xml will have been
// added above.
deployedApp.redeployResources.put(
contextXml.getAbsolutePath(),
Long.valueOf(contextXml.lastModified()));
}
}
// Add the global redeploy resources (which are never deleted) at
// the end so they don't interfere with the deletion process
//添加全局的监控资源,configBase下的context.xml.default和conf下的Context.xml context描述文件
addGlobalRedeployResources(deployedApp);
}
//如果不存在,那么记录当前context已被发布
if (host.findChild(context.getName()) != null) {
deployed.put(context.getName(), deployedApp);
}
if (log.isInfoEnabled()) {
log.info(sm.getString("hostConfig.deployDescriptor.finished",
contextXml.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}
上面的一段代码比较长,它主要做的事情就是将对应的外部context资源加入监控,对于外部的资源,tomcat会对这个外部资源的context,docBase进行监控,同时,如果这个外部资源被配置为解压后/压缩后的目录在appBase下面,那么同样要进行监控。那这里就有个问题了,如果我指定了一个外部的war包,并且指定了器解压后放入appBase中,看上面的代码压根就没有进行解压啊,那finally块的解压目录的监控地址的上次修改时间就是零,tomcat肯定是进行了解压才这么做的,那么解压的逻辑放到哪了呢?没错,就是给host添加子容器的时候,会启动StandardContext,启动时会对docBase进行一次fix,如果是war包,那么就会进行解压
public void org.apache.catalina.core.StandardHost.addChild(Container child) {
//添加内存泄露生命周期监听器
child.addLifecycleListener(new MemoryLeakTrackingListener());
if (!(child instanceof Context))
throw new IllegalArgumentException
(sm.getString("standardHost.notContext"));
super.addChild(child);
}
public void org.apache.catalina.core.ContainerBase.addChild(Container child) {
//是否开启了JDK的安全验证,如果开启了,那么需要通过方位控制器处理接下来的逻辑,
//否则将可能被安全拦截
if (Globals.IS_SECURITY_ENABLED) {
PrivilegedAction<Void> dp =
new PrivilegedAddChild(child);
AccessController.doPrivileged(dp);
} else {
addChildInternal(child);
}
}
private void org.apache.catalina.core.ContainerBase.addChildInternal(Container child) {
if( log.isDebugEnabled() )
log.debug("Add child " + child + " " + this);
synchronized(children) {
//如果已经存在相同名字的context,那么抛错
if (children.get(child.getName()) != null)
throw new IllegalArgumentException("addChild: Child name '" +
child.getName() +
"' is not unique");
//给子容器设置父容器
child.setParent(this); // May throw IAE
//记录到父容器的集合中
children.put(child.getName(), child);
}
// Start child
// Don't do this inside sync block - start can be a slow process and
// locking the children object can cause problems elsewhere
try {
//调用context的start方法
if ((getState().isAvailable() ||
LifecycleState.STARTING_PREP.equals(getState())) &&
startChildren) {
child.start();
}
} catch (LifecycleException e) {
log.error("ContainerBase.addChild: start: ", e);
throw new IllegalStateException("ContainerBase.addChild: start: " + e);
} finally {
//触发子容器添加事件
fireContainerEvent(ADD_CHILD_EVENT, child);
}
}
描述文件的发布就分析到这里,容器的启动非常复发,将用一小节来分析context的启动过程。
2、Deploy WARs
接下来进行第二步,host基础应用路径下的war包
protected void deployWARs(File appBase, String[] files) {
//appBase下的文件
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
//跳过META-INF文件夹
if (files[i].equalsIgnoreCase("META-INF"))
continue;
//跳过WEB-INF文件夹
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
//获取war包
File war = new File(appBase, files[i]);
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
war.isFile() && !invalidWars.contains(files[i]) ) {
//不同于发布xml描述,这里采用的context名字是通过文件名进行解析
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;
//子容器覆盖父容器的unpackWAR属性
if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
}
//如果允许解压的,直接跳过,说明已经解压过了,不允许解压,并且存在旧的发布应用
//目录不存的话,也是跳过,存在就打个日志告诉你在某个目录下存在冲突的context
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
//检查路径是否存在../和./这种字符串,如果存在,那么视为无效的war包
if (!validateContextPath(appBase, cn.getBaseName())) {
log.error(sm.getString(
"hostConfig.illegalWarName", files[i]));
invalidWars.add(files[i]);
continue;
}
//发布war包
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);
}
}
}
protected void deployWAR(ContextName cn, File war) {
//设置解压后context的context.xml的路径META-INF/context.xml
File xml = new File(host.getAppBaseFile(),
cn.getBaseName() + "/" + Constants.ApplicationContextXml);
//设置war包轨迹跟踪器路径/META-INF/war-tracker
File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker);
boolean xmlInWar = false;
try (JarFile jar = new JarFile(war)) {
//读取war里面的META-INF/context.xml文件
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
if (entry != null) {
//如果存在,那么xmlInWar设置为true,表示在war里面找到了context描述文件
xmlInWar = true;
}
} catch (IOException e) {
/* Ignore */
}
// If there is an expanded directory then any xml in that directory
// should only be used if the directory is not out of date and
// unpackWARs is true. Note the code below may apply further limits
boolean useXml = false;
// If the xml file exists then expandedDir must exists so no need to
// test that here
//如果在已经存在这个扩展的目录了(可能以前tomcat启动时解压过,后者认为的手动解压)并且允许解压的,还
//有就是不存在tomcat设置的war包追踪器,或者追踪器没有发生修改,那么使用这个已经存在的context描述文件
if (xml.exists() && unpackWARs &&
(!warTracker.exists() || warTracker.lastModified() == war.lastModified())) {
useXml = true;
}
Context context = null;
//是否允许发布当前这个xml描述,如果配置了不允许,并且存在安全校验,那么通过JDK的安全校验读取
//这个xml是否允许发布
boolean deployThisXML = isDeployThisXML(war, cn);
try {
//copyXML表示是否拷贝这个描述文件到$CATALINA_BASE/conf/catalina/host/下面,也就是前面发布xml描述那小节读取的那个目录
//是否发布xml并且使用已经存在的扩展目录中的context描述文件
if (deployThisXML && useXml && !copyXML) {
synchronized (digesterLock) {
try {
//解析
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
war.getAbsolutePath()), e);
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
//保存描述文件来源
context.setConfigFile(xml.toURI().toURL());
//是否发布context描述并且war中是否存在这样的描述
} else if (deployThisXML && xmlInWar) {
synchronized (digesterLock) {
try (JarFile jar = new JarFile(war)) {
//读取war包中的context描述文件
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
try (InputStream istream = jar.getInputStream(entry)) {
//解析
context = (Context) digester.parse(istream);
}
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
war.getAbsolutePath()), e);
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
//构建context描述文件的url在war中的地址
context.setConfigFile(
UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
}
}
//如果不允许发布,并且这个war中确实又存在xml描述文件
} else if (!deployThisXML && xmlInWar) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
//打印日志,表示发布某个xml描述受阻
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), Constants.ApplicationContextXml,
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")));
} else {
//其他情况,直接反射创建context对象
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();
}
}
boolean copyThisXml = false;
if (deployThisXML) {
if (host instanceof StandardHost) {
copyThisXml = ((StandardHost) host).isCopyXML();
}
// If Host is using default value Context can override it.
//子容器覆盖父容器属性
if (!copyThisXml && context instanceof StandardContext) {
copyThisXml = ((StandardContext) context).getCopyXML();
}
//如果允许拷贝描述文件,并且描述文件在war包中
if (xmlInWar && copyThisXml) {
// Change location of XML file to config base
//设置将要拷贝的路径以及描述文件的名字
xml = new File(host.getConfigBaseFile(),
cn.getBaseName() + ".xml");
try (JarFile jar = new JarFile(war)) {
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
try (InputStream istream = jar.getInputStream(entry);
FileOutputStream fos = new FileOutputStream(xml);
BufferedOutputStream ostream = new BufferedOutputStream(fos, 1024)) {
byte buffer[] = new byte[1024];
while (true) {
int n = istream.read(buffer);
if (n < 0) {
break;
}
//拷贝文件
ostream.write(buffer, 0, n);
}
ostream.flush();
}
} catch (IOException e) {
/* Ignore */
}
}
}
//构建发布应用
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
//监控war包
deployedApp.redeployResources.put
(war.getAbsolutePath(), Long.valueOf(war.lastModified()));
//监控拷贝后的xml描述文件
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));
}
//获取ContextConfig
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());
//后期进行解压后,目录名将叫这个baseName
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 {
/**
* 与发布描述文件一样,添加解压后可能存在的扩展目录进行监控
*/
// If we're unpacking WARs, the docBase will be mutated after
// starting the context
boolean unpackWAR = unpackWARs;
if (unpackWAR && context instanceof StandardContext) {
unpackWAR = ((StandardContext) context).getUnpackWAR();
}
if (unpackWAR && context.getDocBase() != null) {
File docBase = new File(host.getAppBaseFile(), cn.getBaseName());
deployedApp.redeployResources.put(docBase.getAbsolutePath(),
Long.valueOf(docBase.lastModified()));
addWatchedResources(deployedApp, docBase.getAbsolutePath(),
context);
if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) {
deployedApp.redeployResources.put(xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
}
} else {
// Passing null for docBase means that no resources will be
// watched. This will be logged at debug level.
addWatchedResources(deployedApp, null, context);
}
// 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);
if (log.isInfoEnabled()) {
log.info(sm.getString("hostConfig.deployWar.finished",
war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}
3、Deploy expanded folders
发布目录,我们部署web应用的时候除了扔一个war到tomcat的appBase目录下之外,我们还可能直接将一个解压好的web应用扔到appBase目录下。
protected void org.apache.catalina.startup.HostConfig.deployDirectories(File appBase, String[] files) {
//appBase下的目录
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 dir = new File(appBase, files[i]);
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i], false);
//如果已经发布过了,就直接跳过
if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;
//异步发布目录
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}
for (Future<?> result : results) {
try {
//阻塞等待发布完成
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDir.threaded.error"), e);
}
}
}
下面是具体的发布代码
protected void deployDirectory(ContextName cn, File dir) {
long startTime = 0;
// Deploy the application in this directory
if( log.isInfoEnabled() ) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployDir",
dir.getAbsolutePath()));
}
Context context = null;
//META-INF/context.xml
File xml = new File(dir, Constants.ApplicationContextXml);
//context.xml将要拷贝到的目录
File xmlCopy =
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
DeployedApplication deployedApp;
//是否允许拷贝xml描述文件
boolean copyThisXml = isCopyXML();
//是否允许发布xml描述文件
boolean deployThisXML = isDeployThisXML(dir, cn);
try {
//如果对应的xml描述文件存在,并且允许发布的话,那么通过xml描述文件进行发布
if (deployThisXML && xml.exists()) {
synchronized (digesterLock) {
try {
//解析xml描述文件
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml), e);
//如果创建失败,那么就创建一个FailedContext对象,表示这个容器创建失败
context = new FailedContext();
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
//子容器覆盖父容器的copyThisXml属性
if (copyThisXml == false && context instanceof StandardContext) {
// Host is using default value. Context may override it.
copyThisXml = ((StandardContext) context).getCopyXML();
}
//拷贝对应的描述文件
if (copyThisXml) {
Files.copy(xml.toPath(), xmlCopy.toPath());
context.setConfigFile(xmlCopy.toURI().toURL());
} else {
context.setConfigFile(xml.toURI().toURL());
}
} else if (!deployThisXML && 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) Class.forName(contextClass).getConstructor().newInstance();
}
//设置ContextConfig生命周期监听器
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());
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployDir.error",
dir.getAbsolutePath()), t);
} finally {
//和发布描述,发布war包是一样的操作,不再赘述
deployedApp = new DeployedApplication(cn.getName(),
xml.exists() && deployThisXML && 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 (deployThisXML && xml.exists()) {
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);
// 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);
if( log.isInfoEnabled() ) {
log.info(sm.getString("hostConfig.deployDir.finished",
dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}
到这里我们已经分析完了HostConfig这个生命周期监听器进行context的发布了,下面我进入context的启动,在进入context的启动之前,我们顺便提下HostConfig最后一个感兴趣的事件就是stop事件,这个stop事件主要工作就是把自己从mbeanserver中注销。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?