本文主要讲解Tomcat是如何在设计中使用设计模式提高代码应对变化的能力。会有几个层面的论述:1. 此处的应用场景是什么? 2. 这个应用场景会产生什么样的变化,又需要怎样的可扩展性 3. 为什么在此处使用某种设计模式,又是如何关联起来的?
// TODO : 继续更新一些常用设计模式,并完善相关场景描述
宏观了解Tomcat的应用场景
Tomcat需要依据HTTP或AJP等请求报文中的请求信息生成相应的Request对象,之于Http而言即为诸如
- 当前请求的请求行-
RequestLine: 包括请求方法诸如:GET、POST、HEAD, 请求的资源,诸如静态页面、动态处理程序(Servlet)等的URL; - 当前请求所携带的请求头中的信息,诸如
Cookie、content-type等信息...
将这些信息放入Request对象中,并通过静态资源或动态处理程序填充到返回对象Response对象中返回给请求的客户端;如此即为在宏观层面上Tomcat处理一个请求的方式。
接着我们讲Tomcat在设计上遇到的一些挑战,并分析Tomcat的设计者是如何规避的。
基于服务器状态的变化产生事件 -观察者模式
对象间存在一对多的关系,并且多个观察对象(Listener, Observer) 关心被观察对象的状态的变更(事件产生的条件),并依据状态的变更执行相应的动作,这个时候我们可以使用到观察者模式。
在Tomcat中,服务器存在多种状态,包括Init、Starting、Started、Stoped等等,如果我们需要在服务器状态变化的过程中进行一些相应的扩展,比如状态变更时 需要创建对象、需要记录关键日志、需要进行资源的控制时,我们可以设置多个Observer在被观察者的状态变更时进行扩展。
StandardServer即为Tomcat中对应服务器的类,当我们的服务器状态由Init变为Start时,StandardServer会借助依赖类LifeCycleSupport中的FireLifeCycleEvent()将当前服务器启动事件通知到listeners[]列表中的对应观察者对象,观察者对象对相应事件的行为进行响应。
我们来看实际代码:
/**
* The lifecycle event support for this component.
* LifecycleSupport类封装了观察者模式的实现-> 1. 存储观察者列表 2. 动态注册与删除观察者
* 3. 提供方法在被观察者与观察者之间传递信息
*/
private LifecycleSupport lifecycle = new LifecycleSupport(this);
/**
* Server组件启动方法
* It send a LifecycleEvent of type START_EVENT to any registered listeners.
*/
public void start() throws LifecycleException {
// Notify our interested LifecycleListeners
// 触发Server组件Before Start 事件
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
// 触发Server组件Start事件,启动组件
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
// Start our defined Services
// 启动下层组件,无需关心细节
synchronized (services) {
for (int i = 0; i < services.length; i++) {
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).start();
}
}
// Notify our interested LifecycleListeners
// 触发Server组件After Start 事件
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}
public final class LifecycleSupport {
/**
* The set of registered LifecycleListeners for event notifications.
* 观察者列表
*/
private LifecycleListener listeners[] = new LifecycleListener[0];
/**
* 将被观察者产生事件的类型与相关的数据推送到监听者中
*/
public void fireLifecycleEvent(String type, Object data) {
// 构建事件对象,此处的LifeCycle即为Server对象
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = null;
synchronized (listeners) {
interested = (LifecycleListener[]) listeners.clone();
}
for (int i = 0; i < interested.length; i++)
// 调用观察者对于事件的响应方法
interested[i].lifecycleEvent(event);
}
}
public class ServerLifecycleListener implements LifecycleListener {
/**
* Primary entry point for startup and shutdown events.
* @param event The event that has occurred
* 观察者响应事件的方法,通过LifecycleSupport.fireLifecycleEvent()触发
*/
public void lifecycleEvent(LifecycleEvent event) {
Lifecycle lifecycle = event.getLifecycle();
// 判断事件的类型是否为当前观察者所关心的-START_EVENT
if (Lifecycle.START_EVENT.equals(event.getType())) {
if (lifecycle instanceof Server) {
// 在EJB中创建相关对象
createMBeans();
}
// We are embedded.
if( lifecycle instanceof Service ) {
MBeanFactory factory = new MBeanFactory();
createMBeans(factory);
createMBeans((Service)lifecycle);
}
}
}
}
基于上述的代码,Tomcat通过观察者模式实现了多个观察者对于服务启动这个事件的监听与响应,使代码满足了三种设计原则:
- 封装变化-找出应用中可能需要变化之处并封装起来,好让其他部分不受到影响
- 多用组合,少用继承
- 使得交互对象之间的交互松耦合;
Tomcat启动过程中的变化在于:1. 服务状态会有增减,如Tomcat在之后的版本迭代中在START_EVENT的前后分别增加了BEFORE_START_EVENT和AFTER_START_EVENT; 2. 新增或减少对于每个服务状态变更的处理,诸如资源的控制,这时我们只需要增加一个Listener(监听者)即可。
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
当然,如果观察者事件执行间有相互依赖关系时,就不太适合使用观察者模式了,使用责任链是更好的选择。
多种网络通信方式与应用层协议-策略模式
通信方式包括BIO、NIO、NIO2等,应用层协议包括HTTP1.1 HTTP2、AJP等等,而用户可以通过组合的方式进行使用,因此Tomcat中存在多种实现网络通信功能的策略,诸如NIO + HTTP1.1 Strategy、NIO + AJP Strategy,因此在网络通信层面上,Tomcat需要组织代码结构以支持通信协议的动态变化(如某种协议的新增)。
我们可以先按照OO设计原则去设计。
-
封装变化,该场景下代码最核心的需要支持的变化是网络通信方式的变化,因此我们需要将此功能与其他功能分隔开;
-
针对接口编程而不是针对实现编程,因此我们需要定义
interface ProtocolHandler,并让不同协议实现该接口; -
多用组合 上层组件则委托上面定义的接口以使用不同协议实现的网络通信功能。
按照上述思路,我们可以先画出大概的类图如下所示:
其中Connector(Tomcat中的连接器组件)依赖ProtocolHandler以实现不同的网络通信方式,如此当我们有协议的增改时,我们只需要增加或修改对应的 Protocol实现类即可,无需修改Connector中的逻辑。
接着我们再来看策略模式的定义:策略模式定义了算法簇,分别封装起来,让它们之间可以相互替换,此模式可以让算法的变化独立于使用算法的客户。
可以看到使用策略模式设计与遵循相应的设计原则设计一致,因此设计模式可以帮助我们可以更快更好的使用设计原则。
接着最核心的是我们需要在Connector中支持动态设置不同的协议实现类,Tomcat中是依据用户所使用的协议类型通过if-else实例化(new)出对应的协议实现类放在Connector中。
public Connector(String protocol) {
// 设置不同的协议实现类
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol");
}
if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName("org.apache.jk.server.JkCoyoteHandler");
}
// 实例化
Class clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler) clazz.newInstance();
}
当然我们也可以通过复合使用工厂模式使得实现相同功能对象簇的实例化与Connector类隔离开。
PS:因为Tomcat中处理网络通信的逻辑比较复杂,不展开叙述接口的设计。
模块间调用 -适配器模式
服务器处理一个请求分为 建立连接、按照协议解析请求、处理请求三个部分,因此Tomcat将其分成了两个模块,分别为Connector和Engine,为了方便两个模块之间彼此解耦,通过实现适配器模式达到两个模块的解耦。
切面逻辑 -过滤链模式
过滤链模式适用于当我们想让一个或以上的对象有机会能够处理某个请求的时候。
如上图所示,在Tomcat中,请求正真到达Servlet时,我们需要动态增加一些切面逻辑,如Log、Auth,此时便可以使用过滤链模式来处理这部分切面逻辑,将切面代码与核心业务代码分隔开。
比如当我们想在Engine层面为每个进入服务器的请求增加请求的入参和出参的快照日志时,我们可以在server.xml中进行如下配置:
<!-- The request dumper valve dumps useful debugging information about
the request and response data received and sent by Tomcat.
Documentation at: /docs/config/valve.html -->
<Valve className="org.apache.catalina.valves.RequestDumperValve"/>
Tomcat便可以通过Digester(xml解析器)读取配置文件生成相应的事件进行过滤器链的相关配置。
Tomcat中的类图设计如下:
一条过滤链中,比较重要的是,First Valve、Basic Valve、Next Valve,通过这三个信息,我们便可以遍历一条过滤链,由链首开始,通过invoke()将请求传递下去即可。
总结
至此,Tomcat主要的大体图像便构建完成了,当然Tomcat中还使用到很多的设计模式,比如工厂、单例、门面等等。