概述
本章要讨论的2个主题是主机(host)和引擎(Engine)。如果需要在一个 Tomcat部署中部署多个上下文,需要使用一个主机。理论上,当只有一个上下文容器时不需要主机,正如下面 org.apache.catalina.Context 接口中描述:
“上下文容器的父容器通常是主机,但是可能有一些其它实现,没有必要的时候也可以忽略”
然而在实践中,一个 Tomcat 部署往往需要一个主机。至于为什么,将会在本章后面的“为什么需要Host” 一节中看到。
Engine表示整个 Catalina 的 Servlet 引擎。如果使用的话,它通常位于容器等级的最高层。可以添加到Engine上的子容器通常实现于org.apache.catalina.Host 或org.apache.catalina.Context。在一个 Tomcat 部署中,默认是使用引擎容器的。在该部署中,Engine有一个Host,默认主机。
本章讨论了跟 Host 和 Engine 接口相关的类。首先介绍了 Host 相关的StandardHost、StandardHostMapper(Tomcat4)以及 StandardHostValve 类。接下来是用一个Demo来示范演示了Host作为顶层容器的情况。Engine是本章讨论的第二个主题,介绍了StandardEngine和StandardEngineValve类。接下来是本章的第2个应用Demo,说明了如何将Engine作为顶层容器使用。
13.1 Host接口
主机是用 org.apache.catalina.Host 接口表示的。本接口继承了 Container 接口,如 Listing13.1 所示:
Listing 13.1: The Host interface
package org.apache.catalina;
public interface Host extends Container {
// ----------------------------------------------------- Manifest Constants
/**
* The ContainerEvent event type sent when a new alias is added
* by <code>addAlias()</code>.
*/
public static final String ADD_ALIAS_EVENT = "addAlias";
/**
* The ContainerEvent event type sent when an old alias is removed
* by <code>removeAlias()</code>.
*/
public static final String REMOVE_ALIAS_EVENT = "removeAlias";
// ------------------------------------------------------------- Properties
/**
* Return the application root for this Host. This can be an absolute
* pathname, a relative pathname, or a URL.
*/
public String getAppBase();
/**
* Set the application root for this Host. This can be an absolute
* pathname, a relative pathname, or a URL.
*
* @param appBase The new application root
*/
public void setAppBase(String appBase);
/**
* Return the value of the auto deploy flag. If true, it indicates that
* this host's child webapps should be discovred and automatically
* deployed.
*/
public boolean getAutoDeploy();
/**
* Set the auto deploy flag value for this host.
*
* @param autoDeploy The new auto deploy flag
*/
public void setAutoDeploy(boolean autoDeploy);
/**
* Set the DefaultContext
* for new web applications.
*
* @param defaultContext The new DefaultContext
*/
public void addDefaultContext(DefaultContext defaultContext);
/**
* Retrieve the DefaultContext for new web applications.
*/
public DefaultContext getDefaultContext();
/**
* Return the canonical, fully qualified, name of the virtual host
* this Container represents.
*/
public String getName();
/**
* Set the canonical, fully qualified, name of the virtual host
* this Container represents.
*
* @param name Virtual host name
*
* @exception IllegalArgumentException if name is null
*/
public void setName(String name);
// --------------------------------------------------------- Public Methods
/**
* Import the DefaultContext config into a web application context.
*
* @param context web application context to import default context
*/
public void importDefaultContext(Context context);
/**
* Add an alias name that should be mapped to this same Host.
*
* @param alias The alias to be added
*/
public void addAlias(String alias);
/**
* Return the set of alias names for this Host. If none are defined,
* a zero length array is returned.
*/
public String[] findAliases();
/**
* Return the Context that would be used to process the specified
* host-relative request URI, if any; otherwise return <code>null</code>.
*
* @param uri Request URI to be mapped
*/
public Context map(String uri);
/**
* Remove the specified alias name from the aliases for this Host.
*
* @param alias Alias name to be removed
*/
public void removeAlias(String alias);
}
特别重要的是map ()方法,它返回合适的上下文来处理请求,该方法的实现可以在StandardHost 类中找到,将在下一小节中讨论。
13.2 StandardHost类
org.apache.catalina.core.StandardHost 类是对 Host 接口的标准实现。该类继承了 org.apache.catalina.core.ContainerBase 类并实现了 Host和Deployer 接口。Deployer 接口将在第 17 章讨论。
跟 StandardContext 和 StandardWrapper 类相似,StandardHost 类的构造函数在它的管道中添加一基础阀门:
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}
如你所见,该基础阀门类型为 org.apache.catalina.core.StandardHostValve。
当启动时,如 start()方法被调用时,StandardHost 上面添加2个阀门:ErrorReportValve 和 ErrorDispatcherValve。它们都在org.apache.catalina.valves 包中。Tomcat4 中 StandardHost 的 start()方法如Listing13.2 所示:
Listing 13.2: The start method of StandardHost
public synchronized void start() throws LifecycleException {
// Set error report valve
if ((errorReportValveClass != null)&& (!errorReportValveClass.equals(""))) {
try {
Valve valve =(Valve) Class.forName(errorReportValveClass).newInstance();
addValve(valve);
}catch (Throwable t) {
log(sm.getString("StandardHost.invalidErrorReportValveClass",
errorReportValveClass));
}
}
// Set dispatcher valve
addValve(new ErrorDispatcherValve());
super.start();
}
注意:在 Tomcat5 中,start()方法相似,不同点在于包括了构建 JMX 对象的代码,JMX 将在第 20 章讨论。
errorReportValveClass 的值定义如下:
private String errorReportValveClass ="org.apache.catalina.valves.ErrorReportValve";
对于每一个请求,都会调用主机的 invoke()方法。由于 StanardHost 类并没有实现invoke()方法,所以会调用它的父类ContainerBase类的invoke()方法。该invoke()方法会转而调用 StandardHost 的基础阀门 StandardHostValve 的 invoke() 方法。StandardHostValve 阀门的 invoke()方法将在 “StandardHostValve类”小节中讨论。特别是StandardHostValve 的 invoke()方法调用 StandardHost 类的 map()方法来获得一个合适的上下文容器进行请求处理。StandardHost 的 map()方法如 Listing13.3 所示:
Listing 13.3: The map method in the StandardHost class
public Context map(String uri) {
if (debug > 0)
log("Mapping request URI '" + uri + "'");
if (uri == null)
return (null);
// Match on the longest possible context path prefix
if (debug > 1)
log(" Trying the longest context path prefix");
Context context = null;
String mapuri = uri;
while (true) {
context = (Context) findChild(mapuri);
if (context != null)
break;
int slash = mapuri.lastIndexOf('/');
if (slash < 0)
break;
mapuri = mapuri.substring(0, slash);
}
// If no Context matches, select the default Context
if (context == null) {
if (debug > 1)
log(" Trying the default context");
context = (Context) findChild("");
}
// Complain if no Context has been selected
if (context == null) {
log(sm.getString("standardHost.mappingError", uri));
return (null);
}
// Return the mapped Context (if any)
if (debug > 0)
log(" Mapped to context '" + context.getPath() + "'");
return (context);
}
注意在 Tomcat4 中,ContainerBase 类也声明了一个 map()方法如下签名:
public Container map(Request request, boolean update);
在 Tomcat4 中,StandardHostVavle 的 invoke()方法调用 ContainerBase 的 map()方法,它转而调用 StandardHost 的 map()方法。在 Tomcat5 中,没有映射器组件,适当的上下文将从请求对象中获得。
13.3 StandardHostMapper类
在 Tomcat4 中,StandardHost 的父类 ContainerBase 在start()方法中调用 addDefaultMapper()方法创建一个默认映射器。默认映射器的类型由 mapperClass 属性指定。如下是ContainerBase 的 addDefaulstMapper()方法:
protected void addDefaultMapper(String mapperClass) {
// Do we need a default Mapper?
if (mapperClass == null)
return;
if (mappers.size() >= 1)
return;
// Instantiate and add a default Mapper
try {
Class clazz = Class.forName(mapperClass);
Mapper mapper = (Mapper) clazz.newInstance();
mapper.setProtocol("http");
addMapper(mapper);
}catch (Exception e) {
log(sm.getString("containerBase.addDefaultMapper", mapperClass),e);
}
}
StandardHost 定义 mapperClass 变量如下:
private String mapperClass = "org.apache.catalina.core.StandardHostMapper";
另外,StandardHost 类的 start()方法在它的最后调用super.start()来保证创建一个默认的映射器。
注意:Tomcat4 中的 standardContext 使用了略微不同的方法来创建一个默认映射器。它的 start() 方法中并没有调用 super.start()。相反 StandardContext 的start()方法调用 addDefaultMapper()来传递 mapperClass 变量。
StandardHostMapper 中最重要的是 map()方法,下面是它的实现:
public Container map(Request request, boolean update) {
// Has this request already been mapped?
if (update && (request.getContext() != null))
return (request.getContext());
// Perform mapping on our request URI
String uri = ((HttpRequest) request).getDecodedRequestURI();
Context context = host.map(uri);
// Update the request (if requested) and return the selected Context
if (update) {
request.setContext(context);
if (context != null)
((HttpRequest) request).setContextPath(context.getPath());
else
((HttpRequest) request).setContextPath(null);
}
return (context);
}
注意,map() 方法仅仅是简单地调用了 Host 的 map()方法。
13.4 StandardHostValve类
org.apache.catalina.core.StandardHostValve 类是 StandardHost 的基础阀门。当有 HTTP 请求时会调用它的 invoke()方法,代码如Listing 13.4:
Listing 13.4: The invoke method of StandardHostValve
public void invoke(Request request, Response response,
ValveContext valveContext)
throws IOException, ServletException {
// Validate the request and response object types
if (!(request.getRequest() instanceof HttpServletRequest) ||
!(response.getResponse() instanceof HttpServletResponse)) {
return; // NOTE - Not much else we can do generically
}
// Select the Context to be used for this Request
StandardHost host = (StandardHost) getContainer();
Context context = (Context) host.map(request, true);
if (context == null) {
((HttpServletResponse) response.getResponse()).sendError
(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
sm.getString("standardHost.noContext"));
return;
}
// Bind the context CL to the current thread
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
// Update the session last access time for our session (if any)
HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
String sessionId = hreq.getRequestedSessionId();
if (sessionId != null) {
Manager manager = context.getManager();
if (manager != null) {
Session session = manager.findSession(sessionId);
if ((session != null) && session.isValid())
session.access();
}
}
// Ask this Context to process this request
context.invoke(request, response);
}
在 Tomcat4 中invoke()方法中调用 StandardHost 的map()方法来获得一个合适的上下文:
// Select the Context to be used for this Request
StandardHost host = (StandardHost) getContainer();
Context context = (Context) host.map(request, true);
注意:在得到上下对象的时候需要一个往返过程。map() 方法接收2个参数,该方法是在 ContainerBase 中的。然后 ContainerBase 类又在它的子对象(这里是StandardHost对象)中查找合适的映射器并调用它的 map()方法。
invoke()方法解析得到一个 Session 对象并调用它的 access()方法更新它的最后进入访问时间。如下是org.apache.catalina.session.StandardSession 类中的 access() 方法实现:
public void access() {
this.isNew = false;
this.lastAccessedTime = this.thisAccessedTime;
this.thisAccessedTime = System.currentTimeMillis();
}
最后,invoke()方法调用上下文容器的 invoke()方法,让上下文来处理请求。
13.5为什么需要Host
一Tomcat 部署必须有一个Host,如果该Context使用 ContextConfig 来配置。原因如下:
ContextConfig 需要应用文件 web.xml 的位置,它在applicationConfig()方法中尝试打开该文件,下面是该方法的片段:
synchronized (webDigester) {
try {
URL url =
servletContext.getResource(Constants.ApplicationWebXml);
InputSource is = new InputSource(url.toExternalForm());
is.setByteStream(stream);
...
webDigester.parse(is);
...
Constants.ApplicationWebXml 定义的是 /WEB-INF/web.xml文件的相对地址,servletContext是一个 org.apache.catalina.core.ApplicationContext 类型的对象(它实现了javax.servlet.ServletContext)。
下面是 ApplicationContext 类中的 getResource()方法:
public URL getResource(String path)throws MalformedURLException {
DirContext resources = context.getResources();
if (resources != null) {
String fullPath = context.getName() + path;
// this is the problem. Host must not be null
String hostName = context.getParent().getName();
最后一行清楚表明context的父容器(host)是必须的,如果使用 ContextConfig来配置的话。在第 15 章中将会介绍如何解析 web.xml 文件。简单的说,除非自己写 ContextConfig 类,否则你必须有一个主机。
13.6 应用Demo1
本章第一个应用Demo演示了如何将一个主机作为顶层容器使用。该程序有2个类组成:ex13.pyrmont.core.SimpleContextConfig 和ex13.pyrmont.startup.Bootstrap1 类。SimpleContextConfig 类跟第 11 章中的相同,Boostrap2 类如 Listing13.5 所示:
Listing 13.5: The Bootstrap1 Class
package ex13.pyrmont.startup;
//explain Host
import ex13.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappLoader;
public final class Bootstrap1 {
public static void main(String[] args) {
//invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern
System.setProperty("catalina.base", System.getProperty("user.dir"));
Connector connector = new HttpConnector();
Wrapper wrapper1 = new StandardWrapper();
wrapper1.setName("Primitive");
wrapper1.setServletClass("PrimitiveServlet");
Wrapper wrapper2 = new StandardWrapper();
wrapper2.setName("Modern");
wrapper2.setServletClass("ModernServlet");
Context context = new StandardContext();
// StandardContext's start method adds a default mapper
context.setPath("/app1");
context.setDocBase("app1");
context.addChild(wrapper1);
context.addChild(wrapper2);
LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);
Host host = new StandardHost();
host.addChild(context);
host.setName("localhost");
host.setAppBase("webapps");
Loader loader = new WebappLoader();
context.setLoader(loader);
// context.addServletMapping(pattern, name);
context.addServletMapping("/Primitive", "Primitive");
context.addServletMapping("/Modern", "Modern");
connector.setContainer(host);
try {
connector.initialize();
((Lifecycle) connector).start();
((Lifecycle) host).start();
// make the application wait until we press a key.
System.in.read();
((Lifecycle) host).stop();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
13.6.1 运行Demo
在 Windows 下面可以在工作目录下输入如下命令运行该程序:
java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./lib/commons—digester.jar;./
ex13.pyrmont.startup.Bootstrap1
在 Linux 下面需要使用分号来分隔开两个库
java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./lib/commons-digester.jar:./
ex13.pyrmont.startup.Bootstrap1
使用如下 URL 可以调用 PrimitiveServlet和ModernServlet:
http://localhost:8080/app1/Primitive
http://localhost:8080/app1/Modern
13.7 Engine接口
org.apache.catalina.Engine 接口用来表示一个引擎。Engine表示整个 Catalina的 Servlet 引擎。当我们想要支持多个虚拟主机时,需要一个引擎,实际上Tomcat 部署正是使用了引擎。Engine 接口如 Listing 13.6 所示:
Listing 13.6: The Engine Interface
public interface Engine extends Container {
// ------------------------------------------------------------- Properties
/**
* Return the default hostname for this Engine.
*/
public String getDefaultHost();
/**
* Set the default hostname for this Engine.
*
* @param defaultHost The new default host
*/
public void setDefaultHost(String defaultHost);
/**
* Retrieve the JvmRouteId for this engine.
*/
public String getJvmRoute();
/**
* Set the JvmRouteId for this engine.
*
* @param jvmRouteId the (new) JVM Route ID. Each Engine within a cluster
* must have a unique JVM Route ID.
*/
public void setJvmRoute(String jvmRouteId);
/**
* Return the <code>Service</code> with which we are associated (if any).
*/
public Service getService();
/**
* Set the <code>Service</code> with which we are associated (if any).
*
* @param service The service that owns this Engine
*/
public void setService(Service service);
/**
* Set the DefaultContext
* for new web applications.
*
* @param defaultContext The new DefaultContext
*/
public void addDefaultContext(DefaultContext defaultContext);
/**
* Retrieve the DefaultContext for new web applications.
*/
public DefaultContext getDefaultContext();
// --------------------------------------------------------- Public Methods
/**
* Import the DefaultContext config into a web application context.
*
* @param context web application context to import default context
*/
public void importDefaultContext(Context context);
}
可以给引擎设置默认主机或上下文。注意引擎也跟服务(service)相关联。将在第 14 章介绍Services 相关内容。
13.8 StandardEngine类
类 org.apache.catalina.core.StandardEngine 是 Engine 接口的标准实现,跟StandardContext 和 StandardHost 相比,StandardEngine 类相对较小。初始化时,StandardEngine 类需要添加一个基础阀门,下面是该类构造函数:
public StandardEngine() {
super();
pipeline.setBasic(new StandardEngineValve());
}
作为顶层容器,StandardEngine 可以有子容器,它的子容器必须是主机(host)。如果你尝试给它添加一个非主机容器,会产生异常。如下是StandardEngine 类的 addChile()方法:
public void addChild(Container child) {
if (!(child instanceof Host))
throw new IllegalArgumentException(sm.getString("StandardEngine.notHost"));
super.addChild(child);
}
由于位于容器顶层,所以引擎不能有父容器,当你尝试给引擎设置父容器时会产生异常,下面是 StandardEngine 类的 setParent()方法:
public void setParent(Container container) {
throw new IllegalArgumentException(sm.getString("standardEngine.notParent"));
}
13.9 StandardEngineValve类
org.apache.catalina.core.StandardEngineValve 是StandardEngine 的基础阀门,它的 invoke() 方法如 Listing13.7。
Listing 13.7: The invoke method of StandardEngineValve
public void invoke(Request request, Response response,
ValveContext valveContext)
throws IOException, ServletException {
// Validate the request and response object types
if (!(request.getRequest() instanceof HttpServletRequest) ||
!(response.getResponse() instanceof HttpServletResponse)) {
return; // NOTE - Not much else we can do generically
}
// Validate that any HTTP/1.1 request included a host header
HttpServletRequest hrequest = (HttpServletRequest) request;
if ("HTTP/1.1".equals(hrequest.getProtocol()) &&
(hrequest.getServerName() == null)) {
((HttpServletResponse) response.getResponse()).sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHostHeader",
request.getRequest().getServerName()));
return;
}
// Select the Host to be used for this Request
StandardEngine engine = (StandardEngine) getContainer();
Host host = (Host) engine.map(request, true);
if (host == null) {
((HttpServletResponse) response.getResponse()).sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getRequest().getServerName()));
return;
}
// Ask this Host to process this request
host.invoke(request, response);
}
在验证了请求对象和响应对象之后,invoke()方法获得一个 Host 实例来处理请求。是通过调用引擎的 map()方法得到主机。一旦获得了一个主机,它的 invoke()方法将会被调用。
13.10 应用Demo2
本章的第二个应用Demo用于演示引擎作为顶层容器。该Demo使用了2个类,ex13.pyrmont.core.SimpleContextConfig 和ex13.pyrmont.startup.Bootstrap2 类。Bootstrap2 类如 Listing13.8 所示。
Listing 13.8: The Bootstrap2 class
package ex13.pyrmont.startup;
//Use engine
import ex13.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappLoader;
public final class Bootstrap2 {
public static void main(String[] args) {
//invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern
System.setProperty("catalina.base", System.getProperty("user.dir"));
Connector connector = new HttpConnector();
Wrapper wrapper1 = new StandardWrapper();
wrapper1.setName("Primitive");
wrapper1.setServletClass("PrimitiveServlet");
Wrapper wrapper2 = new StandardWrapper();
wrapper2.setName("Modern");
wrapper2.setServletClass("ModernServlet");
Context context = new StandardContext();
// StandardContext's start method adds a default mapper
context.setPath("/app1");
context.setDocBase("app1");
context.addChild(wrapper1);
context.addChild(wrapper2);
LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);
Host host = new StandardHost();
host.addChild(context);
host.setName("localhost");
host.setAppBase("webapps");
Loader loader = new WebappLoader();
context.setLoader(loader);
// context.addServletMapping(pattern, name);
context.addServletMapping("/Primitive", "Primitive");
context.addServletMapping("/Modern", "Modern");
Engine engine = new StandardEngine();
engine.addChild(host);
engine.setDefaultHost("localhost");
connector.setContainer(engine);
try {
connector.initialize();
((Lifecycle) connector).start();
((Lifecycle) engine).start();
// make the application wait until we press a key.
System.in.read();
((Lifecycle) engine).stop();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
13.10.1 运行Demo
在 Windows 下面可以在工作目录下输入如下命令运行该程序:
java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./lib/commons—digester.jar;./
ex13.pyrmont.startup.Bootstrap2
在 Linux 下面需要使用分号来分隔开两个库
java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./lib/commons-digester.jar:./
ex13.pyrmont.startup.Bootstrap2
使用如下 URL 可以调用 PrimitiveServlet和ModernServlet:
http://localhost:8080/app1/Primitive
http://localhost:8080/app1/Modern
13.11小结
在本章中,我们讨论了两种类型的容器:主机(Host)和引擎(Engine)。本章还介绍了这两种容器的相关类。并且用2个Demo演示了如何让这两种容器作为顶层容器来工作。