Tomcat Host组件在Tomcat中代表一个“Virtual Host”,使Tomcat可以在单个Tomcat实例中支持多个“Virtual Host”,这样,我们也就可以知道一个Engine可以包含多个Host组件。Host组件包含两个主要的Valve,一个Valve决定请求由哪一个Context处理,另一个Valve负责处理在Context中未被捕获的异常。除了两个Valve以外,Host组件还包含了一个Configurator,它的作用是负责应用的部署,加载等事情。

一:Virtual Host

在了解Virtual Host之前,我们先来了解下什么是Host。我们知道,在我们访问一个网站时,我们需要在浏览器的地址栏输入一个网页地址,浏览器会试图将域名解析成IP,这个IP代表了连接到互联网的一台主机(Host)。在浏览器向主机发送的HTTP请求中,也包含了请求的Host信息:
Host
在最简单的情况下,一台主机只需要对应一个IP,提供一个web服务即可,这种情况下一个IP就对应一台物理主机(Physical Host)。然而,在多数情况下,一台主机不会只提供一个web服务,因而一台物理主机就需要虚拟出多台主机来,这就是Virtual Host。Virtual Host根据实现技术的不同可以分为基于名称的Virtual Host和基于IP的Virtual Host。

基于名称的Virtual Host

基于名称的Virtual Host,对于基于名称的Virtual Host来说,每一个Virtual Host对应一个域名,这些域名都解析到同一个IP下去,这样,这些Virtual Host就共享了这个IP对应的物理主机的资源,在Tomcat中,主要配置以下信息就配置了一个基于名称的Virtual Host:

1
2
3
4
< Host name = "localhosts"  appBase = "webapps"
     unpackWARs = "true" autoDeploy = "true"
     xmlValidation = "false" xmlNamespaceAware = "false" >
</ Host >

当然,在Tomcat中,如果多个Virtual Host仅仅名称是不一样的,其他都是一样的,那么就可以使用别名来配置,如下图:

1
2
3
4
5
< Host name = "localhosts"  appBase = "webapps"
         unpackWARs = "true" autoDeploy = "true"
         xmlValidation = "false" xmlNamespaceAware = "false" >
     < Alias >khotyn.org</ Alias >
</ Host >
基于IP的Virtual Host

不同于基于名称的Virtual Host,在基于IP的Virtual Host中,可以将多个IP地址绑定到同一台物理主机上去,这个是怎么做到的呢?一个方式在物理主机上配置多块网卡,另一个就是通过Virtual Network Interfaces来实现,在Tomcat中配置基于IP的Virtual Host,可以参考下图中的配置:

1
2
< Connector port = "8080" protocol = "org.apache.coyote.http11.Http11NioProtocol" connectionTimeout = "20000"
         redirectPort = "8443" executor = "tomcatThreadPool" address = "127.0.0.2" useIPVHosts = "true" />

注意,需要将Connector的useIPVHosts设置成true,默认情况为false,才能够使用基于IP的Virtual Host。

二:StandardHost和HostConfig

StandardHost

Tomcat对Host的默认实现是StandardHost,StandardHost是一个非常简单地类,其属性和方法基本上和server.xml中对应的host元素的属性一一对应,但是值得注意的是,StandardHost在初始化的时候,会初始化一个HostConfig,并把它注册成一个Lifecycle Listener,用于负责应用的部署。StandardHost另外一个特别的地方就是在初始化的时候不仅仅加入它的Basic Valve:StandardHostValve到Pipeline中,而且加入了一个ErrorReportValve到Pipeline中去。

HostConfig

在Host调用其start方法的时候,它也生成了一个HostConfig实例,并让它去监听Host的启动,关闭以及定时事件。

在Host中每一个应用由一个Context和一个DeployedApplication来表示,你可以在server.xml或者CATALINA_BASE/conf/[engineName]/[hostName]下或者在应用的META-INF目录下配置context。

前一种在解析server.xml的时候就会被解析成Context,后两种会在HostConfig部署应用的时候被解析并生成对应的Context。

DeployedApplication是HostConfig的一个内部类,是用来监视应用的文件的修改的,它有两个重要的属性:

  • redeployResources:这里面的文件被删除或者被修改会造成应用的重新部署。
  • reloadResources:这里面的文件被删除或者被修改会造成应用的重新加载。具体哪些文件需要被监视可以在Context的WatchedResource里面配置。

在这里需要区分应用的重新部署和重新加载是不一样的行为:重新部署一个Context只需要调用Context的stop方法,然后在调用start方法就可以了,但是重新加载一个Context则需要将Context完全从Host中移除,然后重新生成一个Context,并加入到Host中去。

HostConfig的一个重要的作用就是负责应用的部署,在HostConfig中,“应用”被分成3种不用的类型:context.xml描述文件,war包,文件夹,其部署方式都是类似的,下面仅以war包的部署为例来讲一讲在HostConfig中大概的一个应用部署过程:

  1. 判断是否已经部署了该war,如果已经部署就直接返回。
  2. 看下war包里面有没有META-INF/context.xml文件,有就将其复制一份到CATALINA_BASE/conf/[engineName]/[hostName]目录下并解析成一个Context
  3. 生成一个DeloyedApplication实例并将需要监视的文件放入其中。
  4. 设置2中解析到的Context(如果2中没有context.xml文件,就实例化一个Context)的一些属性,比如DocBase等。
  5. 将Context加入到host中去。
  6. 如果war包是需要被解开来被部署的,那么就需要更新Context的Docbase属性(具体的解包过程在步骤5中由Context的start方法去完成)。

三:StandardHostValve和ErrorReportValve

对于每一个组件,我们可以通过观察其包含的Valve的实现来了解其功能,在Host组件中,有两个重要的Valve:StandardHostValve和ErrorReportValve;

StandardHostValve

StandardHostValve的工作一个是决定请求由Host内的哪一个Context去处理,另一个处理应用没有捕获的异常,把处理未捕获异常的工作放在Host中是因为Host包含的子组件就是Context了,对应一个应用,理所当然,这个工作就放在了应用的上一层组件Host中处理。前一个决定哪一个Context来处理请求的代码相当简单,无非就是从Request中取出Context,并交给它处理:

1
context.getPipeline().getFirst().invoke(request, response);

我们重点来看一下StandardHostValve对异常的处理,但是在这个之前,我们还是了解下Servlet规范(JSR154)对error page的描述,因为StandardHostValve对异常的处理就是按照JSR154来的:

为了让开发者能够在servlet产生错误的时候返回给客户端一些自定义的信息,我们可以在部署描述符(web.xml)中定义error page。这个配置可以让容器在servlet、filter调用response的sendError方法或者servlet产生错误、异常传播到容器的时候,向客户端返回一个自定义的错误页面。

也就是说Servlet容器需要能够根据web.xml中定义的ErrorPage的配置来处理由应用进入容器的错误。

下面通过一幅流程图来描述StandardHostValve对错误的处理过程:
Tomcat Error Process

ErrorReportValve

这个Valve的作用更像是给StandardHostValve擦屁股的,如果请求没有被提交,并且包含了javax.servlet.error.exception,ErrorReportValve就调用response.sendError,然后产生一个默认的错误页面给客户端。