这偏文章是为了弄明白一个问题,就是tomcat部署war包的时候是部署已经解压过多目录文件还是部署war包。
tomcat启动部署的整个来龙去脉,要理清楚,先要从StandandHost的初始化开始。
StandandHost 在启动执行start的时候,自己没有做什么事情,关键的代码在父类ContainerBase的startInternal方法如下的代码:
setState(LifecycleState.STARTING);
LifecycleState.STARTING为START_EVENT 事件,StandandHost有一个事件监听器HostConfig,setState方法会通知HostConfig执行start方法。
HostConfig的start方法如下:
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.warn(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();
}
如果host的deployOnStartup
属性默认为true,所以tomcat启动的时候就是部署webapps目录下的应用,下面我们看下tomcat的部署顺序,因为webapps目录下可有war包文件,应用的目录文件等,先部署那个,或者两个都存在的情况下,tomcat是怎么处理的,我们经常遇到webapps目录下即有一个app的war包,也有对应的目录。
deployApps代码如下:
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);
}
通过上面的代码可以看出,tomcat 开始部署时,先部署descriptors,在部署war包,最后是目录,下面看下面具体每个部署是怎么进行的
并发部署
我们看下上面deployWARs方法的代码如下,参数appBase是webapps的路径,files是该目录下的文件
protected void deployWARs(File appBase, String[] files) {
if (files == null)
return;
//服务并行启动的线程池,默认是一个线程,即按顺序部署多个服务
ExecutorService es = host.getStartStopExecutor();
List> results = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
//过滤掉META-INF和WEB-INF文件
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File war = new File(appBase, files[i]);
//如果是war包文件,则继续处理
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包应用,如果符合条件,就提交一个任务到线程池去执行,这里个注意的部署应用线程池线程的个数,默认是1,通过内部实现的InlineExecutorService
来执行,则是同步启动, 通过startStopThreads
设置,如果大于1则可以真正并行部署。
创建StandardContext
DeployWar 任务的逻辑在HostConfig的deployWAR方法,这个方法比较长,就不贴代码了,关键点是tomcat为每个war包会创建一个对应的StandardContext,并设置对应的listener为ContextConfig,这个ContextConfig是固定的,代码如下:
//指定standardContext 的 contextConfig,后面部署war包时会不用到
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");
//开始触发StandardContext的初始化
host.addChild(context);
解压WAR包
一个StandardContext 对应一个服务实例,要启动服务,就需要先解压war吧,这个逻辑在StandardContext的ContextConfig实现
StandardContext在start前,会产生一个before 事件,ContextConfig会根据事件执行启动前,启动后相关的逻辑。
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
}
ContextConfig 有两个重要的事件,对应configureStart和beforeStart,
beforeStart是初始化StandardContext前调用的, 即解压war包,configureStart是StandardContext初始化好后,调用的,是解析web xml 文件的入口,这里beforeStart()的就是调用了fixDocBase方法,核心逻辑在fixDocBase里面,核心代码就是判断是否解压,执行解压,如下:
protected void fixDocBase() throws IOException {
// At this point we need to determine if we have a WAR file in the
// appBase that needs to be expanded. Therefore we consider the absolute
// docBase NOT the canonical docBase. This is because some users symlink
// WAR files into the appBase and we want this to work correctly.
boolean docBaseAbsoluteInAppBase = docBaseAbsolute.startsWith(appBase.getPath() + File.separatorChar);
if (docBaseAbsolute.toLowerCase(Locale.ENGLISH).endsWith(".war") && !docBaseAbsoluteFile.isDirectory()) {
URL war = UriUtil.buildJarUrl(docBaseAbsoluteFile);
if (unpackWARs) {
docBaseAbsolute = ExpandWar.expand(host, war, pathName);
docBaseAbsoluteFile = new File(docBaseAbsolute);
if (context instanceof StandardContext) {
((StandardContext) context).setOriginalDocBase(originalDocBase);
}
} else {
ExpandWar.validate(host, war, pathName);
}
}
}
通过ExpandWar.expand方法去解压war包,expand 才是最终解压war包的地方,而且要不要解压都在这里,下面是检查是否要解压的代码,解压部分代码去掉了
public static String expand(Host host, URL war, String pathname)
throws IOException {
/* Obtaining the last modified time opens an InputStream and there is no
* explicit close method. We have to obtain and then close the
* InputStream to avoid a file leak and the associated locked file.
*/
JarURLConnection juc = (JarURLConnection) war.openConnection();
juc.setUseCaches(false);
URL jarFileUrl = juc.getJarFileURL();
URLConnection jfuc = jarFileUrl.openConnection();
boolean success = false;
File docBase = new File(host.getAppBaseFile(), pathname);
File warTracker = new File(host.getAppBaseFile(), pathname + Constants.WarTracker);
long warLastModified = -1;
try (InputStream is = jfuc.getInputStream()) {
// Get the last modified time for the WAR
warLastModified = jfuc.getLastModified();
}
//如果war包文件对应的目录也存在,则检查对应目录下的/META-INF/warTracker文件的修改日期。
// Check to see of the WAR has been expanded previously
if (docBase.exists()) {
// A WAR was expanded. Tomcat will have set the last modified
// time of warTracker file to the last modified time of the WAR so
// changes to the WAR while Tomcat is stopped can be detected
//如果不存在,或者日期和war包文件的修改时间相等,则直接返回。
if (!warTracker.exists() || warTracker.lastModified() == warLastModified) {
// No (detectable) changes to the WAR
success = true;
return docBase.getAbsolutePath();
}
// WAR must have been modified. Remove expanded directory.
log.info(sm.getString("expandWar.deleteOld", docBase));
//删除应用对应的目录,需要重新解压。
if (!delete(docBase)) {
throw new IOException(sm.getString("expandWar.deleteFailed", docBase));
}
}
// Create the new document base directory
if(!docBase.mkdir() && !docBase.isDirectory()) {
throw new IOException(sm.getString("expandWar.createFailed", docBase));
}
// Expand the WAR into the new document base directory
String canonicalDocBasePrefix = docBase.getCanonicalPath();
if (!canonicalDocBasePrefix.endsWith(File.separator)) {
canonicalDocBasePrefix += File.separator;
}
// Creating war tracker parent (normally META-INF)
File warTrackerParent = warTracker.getParentFile();
if (!warTrackerParent.isDirectory() && !warTrackerParent.mkdirs()) {
throw new IOException(sm.getString("expandWar.createFailed", warTrackerParent.getAbsolutePath()));
}
// Create the warTracker file and align the last modified time
// with the last modified time of the WAR
if (!warTracker.createNewFile()) {
throw new IOException(sm.getString("expandWar.createFileFailed", warTracker));
}
if (!warTracker.setLastModified(warLastModified)) {
throw new IOException(sm.getString("expandWar.lastModifiedFailed", warTracker));
}
success = true;
} catch (IOException e) {
throw e;
} finally {
if (!success) {
// If something went wrong, delete expanded dir to keep things
// clean
deleteDir(docBase);
}
}
// Return the absolute path to our new document base directory
return docBase.getAbsolutePath();
}
warTracker 文件
tomcat 解压war时,会生成一个warTracker
文件,在对应服务目录下的/META-INF/目录下,并设置修改时间,后面再部署时,通过检查warTracker 这个文件的修改时间,查看war包是否有变更。
是否需要解压war包
通过判断docBase.exists(),即war包对应的目录是否存在,我们平时只要部署过一次,war包下面的文件是存在的,即已经存在了,就根据warTracker
不存在,即有可能被删除了,也不解压,如果存在则比较war包和warTracker
的修改时间,如果相等,则不解压,代表war包没有变化,总结就是只有在warTracker
存在而且war包的修改时间有变化的情况下,比如我们重新打了war包,放到这里,就会用新的war包部署,如果你把原来目录的warTracker
删了,那也不会部署新的了。
WebAppClassload准备
启动Listener和Servlet
应用的文件准备好了,应用的webappclassload也准备好了后,可以开始解析web应用的标准启动文件web.xml了,也就是上面提到的ContextConfig的configureStart方法,这里主要是有webConfig方法实现,这里不做详细分析,主要说下tomcat对servlet的解析,tomcat 在解析完web.xml后,会配置context,servlet配置代码如下:
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// jsp-file gets passed to the JSP Servlet as an init-param
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map params = servlet.getParameterMap();
for (Entry entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
if (multipartdef.getMaxFileSize() != null &&
multipartdef.getMaxRequestSize()!= null &&
multipartdef.getFileSizeThreshold() != null) {
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
Long.parseLong(multipartdef.getMaxFileSize()),
Long.parseLong(multipartdef.getMaxRequestSize()),
Integer.parseInt(
multipartdef.getFileSizeThreshold())));
} else {
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation()));
}
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
可以看到,tomcat对每个servlet的创建一个了StandardWrapper的实例,并设置我们配置的相关参数,你应该很眼熟。
我们用到的listener,servlet,filter 这些都设置完了后,就要用我们的webappclassload来加载这些类了,这些代码在StandardContext 初始化方法startInternal结尾实现,代码如下:
// Configure and call application event listeners
if (ok) {
//初始化我们定义的listener在web.xml里面
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}
try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}
// Configure and call application filters
if (ok) {
//初始化我们定义的filter在web.xml里面
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
//初始化我们定义的Servlet在web.xml里面
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
我们平时在web.xml里面都会有listener,servlet,filter,listener在启动的时候就会创建实例,比如spring上下文的初始化,这个没有疑问,servlet的就是通过配置指定即loadOnStartup的配置,如果小于0则运行时再创建实例,否则都会在初始化启动的时候就创建实例。
总结
弄明白一个问题,又写了这么多,tomcat 启动部署web应用时,先部署war包文件,如果对应的目录下有warTrack 文件而且两者的更新时间是一样的,则不解压,直接用已经解压过的目录文件部署。否则删掉老的目录,重新解压,同时支持并行部署,默认是一个线程,可以通过配置多个线程实现并行部署
war包部署完后,开始部署目录的服务,如果war包已经部署过的,肯定就不执行了,只是部署哪些没有war包文件的应用这里就不研究了。