Tomcat很复杂,不是一章内容就能完全说清楚的,本章主要从Tomcat如何分发请求、如何处理多用户同时请求、它的多级容器是如何协调工作的角度来分析它的工作原理,这也是一个Web服务器首先要解决的关键问题。同时在Tomcat中运用了很多设计模式,本章也分析了几个经典的模式,它能对我们以后的软件设计起到一定的借鉴作用。
Tomcat总体设计
本章以Tomcat 5为基础,也兼顾最新的Tomcat 6。Tomcat的基本设计思路和架构是有一定连续性的。
Tomcat总体结构
Tomcat的结构很复杂,但是Tomcat也非常模块化,找到了 Tomcat最核心的模块,你就抓住Tomcat的“七寸”了。图11-1是Tomcat的总体结构图。
从图11-1中可以看出,Tomcat的心脏有两个组件:Connector和Container,这两个组件将在后面详细介绍。Connector组件是可以被替换的,这样可以给服务器设计者提供更多的选择,因为这个组件是如此重要,不仅跟服务器本身的设计相关,而且和不同的应用场景也十分相关,所以一个Container可以选择对应多个Connector。多Connector和一个Container就形成了一个Service, Service的概念大家都很熟悉了,有了 Service就可以对外提供服务了。但是Service还要有一个生存的环境,必须能给其生命、掌握其生死大权,这时就非Server莫属了。所以整个Tomcat的生命周期由Server控制。
1. 以Service作为“婚姻”
我们将Tomcat中的Connector、Container作为一个整体,比作一对情侣,Connector主要负责对外交流,可以比作男孩,Container主要处理Connector接受的请求,主要处理内部事务,可以比作女孩。那么这个Service就是连接这对男女的结婚证了,是Service将这对男女连接在一起,共同组成一个家庭。当然要组成一个家庭还要有很多其他元素。
说白了,Service只是在Connector和Container外面多包一层,把它们组装在一起,向外面提供服务。一个Service可以设置多个Connector,但是只能有一个Container容器:这个Service接口的方法列表如图11-2所示。
从Service接口中定义的方法可以看出,它主要是为了关联Connector和Container,同时会初始化它下面的其他组件。注意,在接口中并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个Lifecycle的接口中控制,这里用到了一个重要的设计模式,这个接口将在后面介绍。
在Tomcat中Service接口的标准实现类是StandardService,它不仅实现了 Service接口,同时还实现了 Lifecycle接口,这样它就可以控制下面组件的生命周期了。StandardService类结构图如图11-3所示。
从图11-3中可以看出,除了 Service接口的方法的实现以及控制组件生命周期的Lifecycle接口的实现,还有几个方法用于实现事件监听方法。不仅是这个Service组件,在Tomcat中其他组件也同样有这几个方法,这也是一个典型的设计模式。
下面看一下在StandardService中的几个主要方法的实现代码,首先看setContainer方法的源码:
这段代码很简单,其实就是先判断当前的这个Service有没有己经关联了 Container,如果已经关联了,那么去掉这个关联关系 oldContainer.setService(null)。如果这个oldContainer已经被启动了,则结束它的生命周期,然后再替换新的关联,再初始化并开始这个新的Container的生命周期,最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改Container时要将新的Container关联到每个Connector,还好Container和Connector没有双向关联,不然这个关联关系将会很难维护。
AddConnector方法的源码如下:
public void setContainer(Container container)
{
Container oldContainer = this.container;
if ((oldContainer != null) && (oldContainer instanceof Engine))
((Engine) oldContainer).setService(null);
this.container = container;
if ( (this.container != null) && (this.container instanceof Engine))
((Engine) this.container).setService(this);
if (started && (this.container != null) && (this.container instanceof Lifecycle))
{
try
{
((Lifecycle) this.container).start();
}
catch (LifecycleException e)
{
;
}
}
synchronized (connectors)
{
for (int i = 0; i < connectors.length; i++)
connectors[i].setContainer(this.container);
}
if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle))
{
try
{
((Lifecycle) oldContainer).stop();
}
catch (LifecycleException e)
{
;
}
}
support.firePropertyChange("containerM", oldContainer, this.container);
}
这段代码很简单,其实就是先判断当前的这个Service有没有己经关联了 Container,如果已经关联了,那么去掉这个关联关系 oldContainer.setService(null)。如果这个oldContainer已经被启动了,则结束它的生命周期,然后再替换新的关联,再初始化并开始这个新的Container的生命周期,最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改Container时要将新的Container关联到每个Connector,还好Container和Connector没有双向关联,不然这个关联关系将会很难维护。
AddConnector方法的源码如下:
public void addConnector(Connector connector)
{
synchronized (connectors)
{
connector.setContainer(this.container);
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
if (initialized)
{
try
{
connector.initialize();
}
catch (LifecycleException e)
{
e.printStackTrace(System.err);
}
}
if (started && (connector instanceof Lifecycle))
{
try
{
((Lifecycle) connector).start();
}
catch (LifecycleException e)
{
;
}
}
support.firePropertyChange("connector", null, connector);
}
}
这个方法也很简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,Connector用的是数组而不是List集合,这从性能角度考虑可以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始就分配一个固定大小的数组,它的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象复制到新的数组中,这种方式实现了类似动态数组的功能,值得我们以后拿来借鉴。
最新的Tomcat 6中的StandardService也基本没有变化,但是从Tomcat 5开始,Service、Server和容器类都继承了 MBeanRegistration接口,Mbeans的管理更加合理了。
2. 以 Server 为“居”
前面说一对情侣因为Service而成为一对夫妻,有了能够组成一个家庭的基本条件,但是他们还要有个实体的家,这是他们在社会上的生存之本,有了家他们就可以安心地为人民服务并一起为社会创造财富了。Server要完成的任务很简单,就是提供一个接口让其他程序能够访问到这个Service集合,同时要维护它所包含的所有Service的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的Service。还有其他的一些次要的任务,类似于你住在这个地方要向当地政府去登记,可能还要配合当地公安机关日常的安全检查等。
Server的类结构图如图11-4所示。
它的标准实现类StandardServer实现了上面这些方法,同时也实现了 Lifecycle、MbeanRegistration两个接口的所有方法。下面看一下StandardServer的一个重要方法addService 的实现:
从代码的第一句就知道了 Service和Server是相互关联的,Server也是和Service管理Connector—样管理它,也是将Service放在一个数组中,后面的代码也是管理这个新加进来的Service的生命周期。在Tomcat 6中也没有什么变化。
public void addService(Service service)
{
service.setServer(this);
synchronized (services)
{
Service results[] = new Service[services.length + 1];
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;
if (initialized)
{
try
{
service.initialize ();
}
catch (LifecycleException e)
{
e .printStackTrace (System.err);
}
}
if (started && (service instanceof Lifecycle))
{
try
{
((Lifecycle) service).start();
}
catch (LifecycleException e)
{
;
}
}
support.firePropertyChange("service", null, service);
}
}
从代码的第一句就知道了 Service和Server是相互关联的,Server也是和Service管理Connector—样管理它,也是将Service放在一个数组中,后面的代码也是管理这个新加进来的Service的生命周期。在Tomcat 6中也没有什么变化。
3. 组件的生命线“Lifecycle”
前面一直在说Service和Server管理它下面组件的生命周期,那它们是如何管理的呢?
在Tomcat中组件的生命周期是通过Lifecycle接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了。这样一层一层地直到一个最高级的组件就可以控制Tomcat中所有组件的生命周期了,这个最高级的组件就是Server,而控制Server的是Startup,也就是启动和关闭Tomcat:
图11-5是Lifecycle接口的类结构图。
除了控制生命周期的Start和Stop方法外,还有一个监听机制,在生命周期开始和结束时做一些额外的操作。这个机制在其他框架中也被使用,如在Spring中。
Lifecycle接口的方法的实现都在其他组件中,就像前面说的,组件的生命周期由包含它的父组件控制,所以它的Start方法自然就是调用它下面的组件的Start方法,Stop方法也是一样。如在Server中Start方法就会调用Service组件的Start方法,Server的Start方法代码如下:
监听的代码会包围Service组件的启动过程,即简单地循环启动所有Service组件的Start方法,但是所有的Service必须要实现Lifecycle接口,这样做会更加灵活。
Lifecycle接口的方法的实现都在其他组件中,就像前面说的,组件的生命周期由包含它的父组件控制,所以它的Start方法自然就是调用它下面的组件的Start方法,Stop方法也是一样。如在Server中Start方法就会调用Service组件的Start方法,Server的Start方法代码如下:
public void start() throws LifecycleException
{
if (started)
{
log.debug (sm.getString("standardServer.start.started"));
return;
}
lifecycle.fireLifecycleEvent{BEFORE_START_EVENT, null);
lifecycle.fireLifecycleEvent{START_EVENTr null);
started = true;
synchronized (services)
{
for (int i = 0; i < services.length; i++)
{
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).start();
}
}
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}
监听的代码会包围Service组件的启动过程,即简单地循环启动所有Service组件的Start方法,但是所有的Service必须要实现Lifecycle接口,这样做会更加灵活。
Server的Stop方法代码如下:
public void stop() throws LifecycleException
{
if (!started)
return;
lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENTf null);
lifecycle.fireLifecycleEvent(STOP_£VENT, null);
started = false;
for (int i = 0; i < services.length; i++)
{
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).stop();
}
lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);
}
Connector 组件
Connector组件是Tomcat中的两个核心组件之一,它的主要任务是负责接收浏览器发过来的TCP连接请求,创建一个Request和Response对象分别用于和请求端交换数据。然后会产生一个线程来处理这个请求并把产生的Request和Response对象传给处理这个请求的线程,处理这个请求的线程就是Container组件要做的事了。
由于这个过程比较复杂,大体的流程可以用如图11-6所示的顺序图来解释。
在Tomcat 5中默认的Connector是Coyote,这个Connector是可以选择替换的。Connector最重要的功能就是接收连接请求,然后分配线程让Container来处理这个请求,所以这必然是多线程的,多线程的处理是Connector设计的核心。Tomcat 5将这个过程更加细化,它将Connector划分成Connector、Processor、Protocol,另外Coyote也定义自己的 Request 和 Response 对象。
下面主要看一下在Tomcat中如何处理多线程的连接请求,先看一下Connector的主要类图,如图11-7所示。
下面主要看一下在Tomcat中如何处理多线程的连接请求,先看一下Connector的主要类图,如图11-7所示。
HttpConnector 的 Start 方法如下:
public void start() throws LifecycleException
{
if (started)
throw new LifecycleException(sm.getString ("httpConnector.alreadyStarted"));
threadName = "HttpConnector[" + port + "]";
lifecycle.fireLifecycleEvent(START_EVENT null);
started = true;
threadStart();
while (curProcessors < minProcessors)
{
if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
break;
HttpProcessor processor = newProcessor〇;
recycle(processor);
}
}
当执行到threadStart()方法时,就会进入等待请求的状态,直到一个新的请求到来才会激活它继续执行,这个激活是在HttpProcessor的assign方法中的,这个方法的代码如下:
synchronized void assign(Socket socket)
{
while (available)
{
try
{
wait ();
}
catch (InterruptedException e)
{
}
}
this.socket = socket;
available = true;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");
}
创建HttpProcessor对象时会把available设为false,所以当请求到来时不会进入while循环,将请求的Socket赋给当前处理的Socket,并将available设为true,当available没置为true时,HttpProcessor的run方法将被激活,接下来将会处理这次请求。
run的方法代码如下:
public void run()
{
while (!stopped)
{
Socket socket = await();
if (socket == null)
continue;
try
{
process(socket);
}
catch (Throwable t)
{
log("process.invoke", t);
}
connector.recycle(this);
}
synchronized (threadSync)
{
threadSync.notifyAll();
}
}
解析Socket的过程在process方法中,process方法的代码片段如下:
private void process(Socket socket)
{
boolean ok = true;
boolean finishResponse = true;
SocketInputStream input = null;
OutputStream output = null;
try
{
input = new SocketInputStream(socket.getlnputStream(),connector.getBufferSize());
}
catch (Exception e)
{
log("process•create", e);
ok = false;
}
keepAlive = true;
while (Istopped && ok && keepAlive)
{
finishResponse = true;
try
{
request.setStream(input);
request.setResponse(response);
output = socket.getOutputStream();
response.setStream(output);
response.setRequest(request);
((HttpServletResponse) response.getResponse()).setHeader("Server", SERVER_INFO);
}
catch (Exception e)
{
log("process.create", e);
ok = false;
}
try
{
if (ok)
{
parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol().startsWith("HTTP/O"));
parseHeaders(input);
if (httpll)
{
ackRequest(output);
if (connector.isChunkingAllowed())
response.setAllowChunking(true);
}
}
。。。。。。。。
try
{
((HttpServletResponse) response).setHeader("Date", FastHttpDateFormat.getCurrentDate());
if (ok)
{
connector.getContainer().invoke(request, response);
}
。。。。。。。。
}
try
{
shutdownInput(input);
socket.close();
}
catch (IOException e)
{
;
}
catch (Throwable e)
{
log("process.invoke", e);
}
socket = null;
}
当Connector将Socket连接封装成Request和Response对象后,接下来的事情就交给Container 来处理了。
Servlet 容器 Container
Container是容器的父接口,所有子容器都必须实现这个接口,Container容器的设计用的是典型的责任链的设计模式,它由4个子容器组件构成,分别是Engine、Host、Context和Wrapper,这4个组件不是平行的,而是父子关系,Engine包含Host,Host包含Context,Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet,则可以定义多个Wrapper;如果有多个Wrapper,则要定义一个更高的Container,如Context,Context通常对应下面的配置:
<Context path="/library" docBase="D:\projects\library\deploy\target\library .war" reloadable="true"/>
1.容器的总体设计
Context还可以定义在父容器Host中,Host不是必需的,但是要运行war程序,就必须要用Host,因为在war中必有web.xml文件,这个文件的解析就需要Host。如果要有多个Host就要定义一个top容器Engine。而Engine没有父容器了,一个Engine代表一个完整的Servlet引擎。
那么这些容器是如何协同工作的呢?它们之间的关系图如图11-8所示。
那么这些容器是如何协同工作的呢?它们之间的关系图如图11-8所示。
当Connector接受一个连接请求时,会将请求交给Container,Container是如何处理这个请求的?这4个组件是怎么分工的?怎么把请求传给特定的子容器的?又是如何将最终的请求交给Servlet处理的?图11-9是这个过程的时序图。
<Engine defaultHost="localhost" name="Catalina">
<Valve className="org.apache.catalina.valves.RequestDumperValven"/>
.......
<Host appBase="webappsn autoDeploy="true" name="localhost" unpackWARs="true” xmlNamespaceAware="false" xmlValidation="false"〉
<Valve className=»org.apache.catalina.valves.FastCommonAccessLogValve" directory="logs" prefix="localhost_access_log. " suffix=".txt" pattern="common" resolveHosts="false"/>
.......
</Host>
</Engine>
StandardEngineValve、StandardHostValve 是 Engine 和 Host 默认的 Valve,最后一个Valve负责将请求传给它们的子容器,以继续往下执行。
前面是Engine和Host容器的请求过程,下面看Context和Wrapper容器是如何处理请求的。图11-10是处理请求的时序图。
从丁Tomcat5开始,子容器的路由放在了request中,在request保存了了当前请求正在处理的 Host、Context 和 Wrapper。
2. Engine 容器
Engine容器比较简单,它只定义了一些基本的关联关系,按口类囝如图11-11所示。
它的标准实现类是StandardEngine,注意Engine没有父容器,如果调用setParent方法将会报错。添加的子容器也只能是Host类型的,代码如下:
public void addChild(Container child)
{
if (!(child instanceof Host)
throw new IllegalArgumentException(sm.getString("StandardEngine. notHost"));
super.addChild(child);
}
public void setParent(Container container)
{
throw new IIlegalArgumentException(sm.getString("standardEngine.notParent"));
}
它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。
3. Host容器
Host是Engine的子容器,一*个Host在Engine中代表一个虚拟主机’这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。 它的子容器通常是Context,它除了关联子容器外,还保存一个主机应有的信息。
图11-12是和Host相关的类图。
从图中可以看出,除了所有容器都继承的ContainerBase外,StandardHost还实现了Deployer接口,图11-12清楚地列出了这个接口的主要方法,这些方法都可以安装、展开、启动和结束每个Web应用。
Deployer接口的实现是StandardHostDeployer,这个类实现了最主要的几个方法,Host可以调用这些方法完成应用的部署等。
Deployer接口的实现是StandardHostDeployer,这个类实现了最主要的几个方法,Host可以调用这些方法完成应用的部署等。
4. Context 容器
Context代表Servlet的Context,它具备了 Servlet运行的基本环境,理论上只要有Context就能运行Servlet 了。简单的Tomcat可以没有Engine和Host。Context最重要的功能就是管理它里面的Servlet实例,Servlet实例在Context中是以Wrapper出现的。还有一点就是Context如何才能找到正确的Servlet来执行它呢? Tomcat 5以前是通过一个Mapper类来管理的,在Tomcat5以后这个功能被移到了 Request中,在前面的时序图中就可以发现获取子容器都是通过Request来分配的。
Context准备Servlet的运行环境是从Start方法开始的,这个方法的代码片段如下:
public synchronized void start () throws LifecycleException
{
......
if(!initialized )
{
try
{
init ();
}
catch ( Exception ex )
{
throw new LifecycleException ("Error initializaing ",ex);
}
......
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
setAvailable(false);
setConfigured(false);
boolean ok = true;
File configBase = getConfigBase();
if (configBase != null)
{
if (getConfigFile() == null)
{
File file = new File(configBase, getDefaultConfigFile());
setConfigFile(file.getPath());
try
{
File appBaseFile = new File(getAppBase());
if (!appBaseFile.isAbsolute())
{
appBaseFile = new File(engineBase()f getAppBase());
}
String appBase = appBaseFile.getCanonicalPath();
String basePath = (new File(getBasePath())).getCanonicalPath();
if (!basePath.startsWith(appBase))
{
Server server = ServerFactory.getServer();
((StandardServer) server).storeContext(this);
}
}
catch (Exception e)
{
log.warn("Error storing config file", e);
}
}
else
{
try
{
String canConfigFile = (new File(getConfigFile())).getCanonicalPath();
if (!canConfigFile.startsWith (configBase.getCanonicalPath()))
{
File file = new File (configBase, getDefaultConfigFile());
if (copy(new File(canConfigFile), file))
{
setConfigFile(file.getPath());
}
}
}
catch (Exception e)
{
log.warn ("Error setting config file", e);
}
}
}
......
Container children[] = findChildren();
for (int i = 0; i < children.length; i++)
{
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).start ();
......
}
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
它的主要作用是设置各种资源属性和管理组件,还有一个非常重要的作用就是启动子容器和Pipeline。
我们知道Context的配置文件中有个reloadable属性,如下面的配置:
<Context path="/library" docBase="D:\projects\library\deploy\target\library.war" reloadable="true" />
当这个reloadable设为true时,war被修改后Tomcat会自动重新加载这个应用。如何做到这点呢?这个功能是在StandardContext的backgroundProcess方法中实现的,这个方法的代码如下:
public void backgroundProcess()
{
if (!started)
return;
count = (count + 1) % managerChecksFrequency;
if ((getManager() != null) && (count == 0))
{
try
{
getManager().backgroundProcess();
}
catch ( Exception x )
{
log.warn("Unable to perform background process on manager",x);
}
}
if (getLoader() != null)
{
if (reloadable && (getLoader().modified()))
{
try
{
Thread.currentThread().setContextClassLoader(StandardContext.class.getClassLoader());
reload();
} finally
{
if (getLoader() != null)
{
Thread.currentThread{).setContextClassLoader(getLoader().getClassLoader());
}
}
}
if (getLoader() instanceof WebappLoader)
{
((WebappLoader) getLoader()).closeJARs(false);
}
}
}
它会调用reload方法,而reload方法会先调用stop方法,然后再调用Start方法,完成Context的一次重新加载。可以看出,执行reload方法的条件是reloadable为true和应用被修改,那么这个backgroundProcess方法是怎么被调用的呢?
这个方法是在ContainerBase类中定义的内部类ContainerBackgroundProcessor中被周期调用的,这个类运行在一个后台线程中。它会周期地执行run方法,它的nm方法会周期地调用所有容器的backgroundProcess方法,因为所有容器都会继承ContainerBase类,所以所有容器都能够在backgroundProcess方法中定义周期执行的事件。
5.Wrapper 容器
Wrapper代表一个Servlet,它负责管理一个Servlet,包括Servlet的装载、初始化、执行及资源回收。Wrapper是最底层的容器,它没有子容器了,所以调用它的addChild将会报错。
Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了拥有一个 Servlet 初始化信息的ServletConfig,由此看出StandardWrapper将直接和Servlet的各种信息打交道。
LoadServlet是一个非常重要的方法,代码片段如下:
public synchronized Servlet loadServlet() throws ServletException
{
......
Servlet servlet;
try {
......
ClassLoader classLoader = loader.getClassLoader();
......
Class classClass = null;
......
servlet = (Servlet) classClass.newInstance();
if ((servlet instanceof ContainerServlet) && (isContainerProvidedSfirvlet(actualClass) || ((Context)getParent()).getPrivileged()))
{
((ContainerServlet) servlet).setWrapper(this);
}
classLoadTime=(int) (System.currentTimeMillis() -tl);
try
{
instanceSupport.firelnstanceEvent(InstanceEvent.REFORE_INIT_EVENT,servlet);
if( System.getSecurityManager() != null)
{
Class[] classType = new Class[]{ServletConfig.class};
Object[] args = new Object[]{((ServletConfig)facade)};
SecurityUtil. doAsPrivilege(f,initf,f servlet, classType, args);
}
else
{
servlet.init(facade);
}
if((loadOnstartup >= 0) && (jspFile != nill))
{
......
if (System.getSecurityManager() != nill)
{
Class[] classType = new Class[]{ServletRequest.class,ServletResponse.class};
Object[] args = new Object[]{req,res};
SecurityUtil.doAsPrivilege("servlet",servlet,classType,args);
}
else
{
servlet.service(req,res);
}
}
instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);
......
return servlet;
}
它基本上描述了对Servlet的操作,装载了Servlet后就会调用Servlet的init方法,同时会传一个StandardWrapperFacade对象给Servlet,这个对象包装了StandardWrapper,ServletConfig与它们的关系如图11-13所示。
Servlet可以获得的信息都在StandardWrapperFacade里封装,这些信息又是在StandardWrapper对象中拿到的,所以Servlet可以通过ServletConfig拿到有限的容器的信息
当Servlet被初始化完成后,就等着StandardWrapperValue去调用它的Service方法了,调用Service方法之前要调用Servlet所有的filter。
Tomcat中的其他组件
Tomcat还有其他重要的组件,如安全组件security、日志组件logger、session、mbeans、naming等。这些组件共同为Connector和Container提供必要的服务。Tomcat中的设计模式
在Tomcat中用了很多设计模式,如模板模式、工厂模式和单例模式等一些常用的设计模式,对这些模式大家都比较熟悉,下面介绍一些在Tomcat中用到的其他设计模式。门面设计模式
门面设计模式在Tomcat中有多处使用,在Request和Response对象封装、从StandardWrapper 到 ServletConfig 封装、从 ApplicationContext 到 ServletContext 封装中都用到了这种设计模式。1.门面设计模式的原理
这么多场合都用到了这种设计模式,那这种设计模式究竟能有什么作用呢?顾名思义,就是将一个东西封装成一个门面,好与大家更容易地进行交流,就像一个国家的外交部一样。
这种设计模式主要用在在一个大的系统中有多个子系统时,这时多个子系统肯定要相互通信,但是每个子系统又不能将自己的内部数据过多地暴露给其他系统,不然就没有必要划分子系统了。每个子系统都会设计一个门面,把别的系统感兴趣的数据封装起来,通过这个门面来进行访问。这就是门面设计模式存在的意义。
门面设计模式的示意图如图11-14所示。
Client只能访问Facade中提供的数据是门面设计模式的关键,至于Client如何访问Fapade和Subsystem、如何提供Facade门面设计模式并没有规定得很严格。
2. Tomcat的门面设计模式示例
在Tomcat中门面设计模式使用得很多,因为在Tomcat中有很多组件,每个组件要相互互交互数据,用门面设计模式隔离数据是个很好的方法。在Request上使用的门面设计模式类图如图11-15所示。
从图11-15中可以看出,HttpRequestFacade类封装了 HttpRequest接口,能够提供数据,通过HttpRequestFacade访问到的数据都被代理到HttpRequest中,通常被封装的对象都被设为Private或者Protected,以防止在Facade中被直接访问。
观察者设计模式
这种设计模式也是常用的设计方法,通常也叫发布-订阅模式,也就足事件监听机制。通常在某个事件发生的前后会触发一些操作。1. 观察者模式的原理
观察者模式的原理也很简单,就是你在做事时旁边总有一个人盯着你,当你做的功情是他感兴趣的亊情时,他就会跟着做另外一些事情。但是盯着你的人必须要到你那里登记,不然你无法通知他。观察者模式通常包含下面几个角色。- Subject抽象主题:它负责管理所有观察者的引用,同时定义主要的事件操作。
- ConcretcSubject具体主题:它实现了抽象主题定义的所有接口,当自己发生变化时,会通知所有观察者。
- Observer观察者:监听主题发生变化的操作接口。
2. Tomcat的观察者模式示例
在Tomcat中观察者模式也有多处使用,前面讲的控制组件生命周期的Lifecyde就是这种模式的体现,还有对Servlet实例的创建、Session的管理、Container等都是同样的原理。下面主要看一卜Lifecycle的具体实现。Lifecycle的观察者模式结构如图11-16所示。
在上面的结构图中,LifecycleListener代表的是抽象观察者,它定义了一个lifecycleEvent方法,这个方法就是当主题变化时要执行的方法。ServerLifecycleListener代表的是具体的观察者,它实现了 LifecycleListener接口的方法,就是这个具体的观察者具体的实现方式。
Lifecycle接口代表的是抽象主题,它定义了管理观察者的方法和它要做的其他方法。而StandardServer代表的是具体主题,它实现了抽象主题的所有方法。这里Tomcat对观察者做了扩展,增加了另外两个类:LifecycleSupport和LifecycleEvent,它们作为辅助类扩展了观察者的功能。LifecycleEvent可以定义事件类別,对不同的半件可区别处理,更加灵活.LifecycleSupport类代理了主题对多观察者的管理,将这个管理抽出来统一实现,以后如果修改只要修改LifecycleSupport类就可以了,不需要去修改所有的具体主题,因为所有具体主题对观察者的操作都被代理给LifecycleSupport类了。这可以认为是观察者模式的改进版。
LifecycleSupport调用观察者的方法代码如下:
主题是怎么通知观察者的呢?代码如下:
public void fireLifecycleEvent(String type. Object data)
{
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interestedf] = null;
synchronized (listeners)
{
interested = (LifecycleListener[]) listeners.clone ();
}
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}
主题是怎么通知观察者的呢?代码如下:
public void start() throws LifecycleException
{
lifecycle.fireLifecycleEvent(BEFORE_START_EVENTT, null);
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
synchronized (services)
{
for (int i = 0; i < services.length; i++)
{
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).start();
}
}
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}
命令设计模式
Connector是通过命令模式调用Container的。1. 命令模式的原理
命令模式的主要作用就是封装命令,把发出命令的责任和执行命令的责任分开,也是一种功能的分工。不同的模块可以对同一个命令做出不同的解释。
命令模式通常包含下面几个角色。
◎ Client:创建一个命令,并决定接受者。◎ Command:命令接口,定义一个抽象方法。◎ ConcreteCommand:具体命令,负责调用接受者的相应操作。◎ Invoker:请求者,负责调用命令对象执行请求。◎ Receiver:接受者,负责具体实施和执行一次请求。
2. Tomcat中命令模式的示例
在Tomcat中命令模式在Connector和Container组件之间有体现,Tomcat作为一个应用服务器,无疑会接收到很多请求,如何分配和执行这些请求是必须的功能。
下面看一下Tomcat是如何实现命令模式的,图1M7是Tomcat命令模式的结构图。
Connector作为抽象请求者,HttpConnector作为具体请求者,HttpProcessor作为命令.Container作为命令的抽象接受者,ContainerBase作为具体的接受者,客户端就是应用服务器Server组件了。Server首先创建命令请求者HttpConnector对象,然后创建命令HttpProcessor对象,再把命令对象交给命令接受者ContainerBase容器来处理。命令最终是被Tomcat的Container执行的。命令可以以队列的方式进来,Container也可以以不同的方式来处理请求,如HTTP1.0和HTTP1.1的处理方式就不同。
责任链设计模式
在Tomcat中一个最容易发现的设计模式就是责任链设计模式,这个设计模式也是在Tomcat中Container设计的基础,整个容器就是通过一个链连接在一起的,这个链一直将请求正确地传递给最终处理请求的那个Servlet。1.责任链模式的原理
责任链模式就是很多对象由每个对象对其下家的引用而连接起来形成一条链,请求在这条链上传递,直到链上的某个对象处理此请求,或者每个对象都可以处理请求,并传给“下家”,直到最终链上每个对象都处理完。这样可以不影响客户端而能够在链上增加任意的处理节点。
通常责任链模式包含下面几个角色。
- Handler (抽象处理者):定义一个处理请求的接口。
- ConcreteHandler (具体处理者):处理请求的具体类,或者传给“下家”。
2. Tomcat中的责任链模式示例
在Tomcat中这种设计模式几乎被完整地使用了,Tomcat的容器设置就是责任链模式,从Engine到Host再到Context —直到Wrapper都通过一个链传递请求。Tomcat中的责任链模式的类结构图如图11-18所示。
上图基本上描述了 4个子容器使用责任链模式的类结构图,在对应的责任链模式的角色屮Container扮演抽象处理者角色,具体处理者由StandardEngine等子容器扮演。与标准的责任链不同的是,这里引入了Pipeline和Valve接门,它们有什么作用呢?
实际上Pipeline和Valve扩展了这个链的功能,使得在链向下传递的过秤中,能够接收外界的干预。Pipeline就是连接每个子容器的管子,里面传递的Request和Response对象好比管子里流的水,而Valve就是在这个管子上开的一个个小U子,让你有机会接触到里面的水,做一些额外的事情。
为了防止水被引出来而不流到下一个容器中,在每一段管子最后总有一个节点保证它一定能流到下一个子容器,所以每个容器都有一个StandardXXXValve。只要涉及这样一种链式的处理流程,这便是一个非常值得借鉴的模式。
总结
本章将主要从Tomcat如何分发请求、如何处理多用户同时请求,还有它的多级容器 是如何协调工作的角度来分析Tomcat的工作原理,这也是一个Web服务器首要解决的关 键问题。另外分析在Tomcat中运用的许多经典设计模式,如模版模式、工厂模式和单例模式等。通过学习它们的实践运用能给我们以后的软件设计起到—定的借鉴作用。