前面的文章分析了StandardEngine,这里来分析一下由Engine对象来管理的另外一种对象吧:Host。。
一般情况下,在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接口是如何定义的吧:
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是怎么实现的吧,先来看看简单的继承体系:
这个应该算是很简单的吧,首先也是一个容器。。。然后实现了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对象。。它将会用于监听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的创建和部署上了。。。