盘点Tomcat中常见的13种设计模式
Tomcat的源码深处蕴含着一系列精妙的设计模式,它们共同支撑起了这个高性能、高灵活性的服务器平台
本文旨在深入探索Tomcat架构的底层逻辑,揭示隐藏其中的13种设计模式,从适配器模式到享元模式,从责任链模式到模板方法模式,我们将一一揭开这些设计模式的神秘面纱,展示它们如何协同工作,成就了Tomcat的稳定与高效
Tomcat设计模式思维导图:
创建型
单例模式
单例模式能够让对象全局唯一共享使用,适合生命周期长(与应用相同)、全局共享访问的对象,减少开销提高利用率
但是为了让对象全局唯一,防止并发访问而导致对象变成"多例",通常需要使用加锁的方式保证唯一
无论是饿汉式(类加载时synchronized锁),还是懒汉式,都会通过加锁的方式创建对象
单例模式实现的多种方式就不过多赘述,Tomcat中会使用StringManager
分离错误信息的存储与处理
StringManager
是Tomcat中实现错误消息和日志消息国际化管理的核心组件,使用的是单例模式,通过静态工厂方法获取对象
java
protected static final StringManager sm = StringManager.getManager(NioChannel.class);
在获取对象时,最终也会加锁防止并发创建对象
java
public static final synchronized StringManager getManager(
String packageName, Locale locale) {
Map<Locale,StringManager> map = managers.get(packageName);
if (map == null) {
map = new LinkedHashMap<Locale,StringManager>(LOCALE_CACHE_SIZE, 1, true) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(
Map.Entry<Locale,StringManager> eldest) {
if (size() > (LOCALE_CACHE_SIZE - 1)) {
return true;
}
return false;
}
};
managers.put(packageName, map);
}
StringManager mgr = map.get(locale);
if (mgr == null) {
mgr = new StringManager(packageName, locale);
map.put(locale, mgr);
}
return mgr;
}
注意:Tomcat中一些如线程池Executor可能不是单例,因为Executor属于Connector连接器,而Tomcat设计上运行存在多连接器,这就可能导致变为多例
如果平时业务中也有负责、全局共享的组件也可以设计为单例
工厂模式
工厂模式能够通过传入一个定义的参数CODE,来获取对应对象实例,无需关心其内部实现
Tomcat中获取日志的日志工厂LogFactory.getLog(StandardContext.class)
获取过滤器链的过滤器链工厂ApplicationFilterFactory.createFilterChain(request, wrapper, servlet)
工厂模式常用于创建复杂对象,根据入参构建复杂对象返回,使用工厂屏蔽内部实现细节
Tomcat中还使用大量的工厂方法进行创建对象,而创建型模式中剩下的建造者模式和原型模式并不常见
结构型
适配器模式
适配器模式能够将接口转换为期望的接口,使得原本不兼容的类可以一起工作,提高兼容性,但转换过程复杂可能会导致开销太大
在Tomcat中,连接器与容器之间会使用适配器对请求/响应进行适配
连接器Connector中的请求/响应是Tomcat定义的org.apache.coyote.Request
, org.apache.coyote.Response
而容器中的请求/响应需要遵循servlet规范,要求实现servlet规定的接口org.apache.catalina.connector.implements HttpServletRequest
Tomcat中的CoyoteAdapter
作为适配器则来将请求/响应进行适配,提高兼容性
装饰者模式
装饰者模式能够通过组合的方式,动态的给对象添加额外的功能,相比继承会更加灵活
装饰者模式又叫包装器模式,Tomcat中大量使用包装器,给对象多套一层增加功能
比如Tomcat网络通信中曾说到过处理完网络通信将socket封装为 SocketWrapperBase 再去交给线程池处理
其子类NioSocketWrapper在其基础上又增加Nio处理相关功能
java
public static class NioSocketWrapper extends SocketWrapperBase<NioChannel> {
private final SynchronizedStack<NioChannel> nioChannels;
private final Poller poller;
private int interestOps = 0;
private volatile SendfileData sendfileData = null;
private volatile long lastRead = System.currentTimeMillis();
private volatile long lastWrite = lastRead;
private final Object readLock;
private volatile boolean readBlocking = false;
private final Object writeLock;
private volatile boolean writeBlocking = false;
}
Nio2SocketWrapper在其基础上增加读写回调相关功能
java
public static class Nio2SocketWrapper extends SocketWrapperBase<Nio2Channel> {
private final SynchronizedStack<Nio2Channel> nioChannels;
private SendfileData sendfileData = null;
private final CompletionHandler<Integer, ByteBuffer> readCompletionHandler;
private boolean readInterest = false; // Guarded by readCompletionHandler
private boolean readNotify = false;
private final CompletionHandler<Integer, ByteBuffer> writeCompletionHandler;
private final CompletionHandler<Long, ByteBuffer[]> gatheringWriteCompletionHandler;
private boolean writeInterest = false; // Guarded by writeCompletionHandler
private boolean writeNotify = false;
private CompletionHandler<Integer, SendfileData> sendfileHandler;
}
组合模式
组合模式能够将各个对象组合为树形结构的组件,易于扩展(想加新功能继续组合其他组件)、复用性好、层次清晰
21张图解析Tomcat运行原理与架构全貌中曾分析过Tomcat组件关系,自顶向下可以分为:
Tomcat只能有一个Server,Server下允许存在多个Service,Service中又允许多个Connector和一个Container
Connector和Container中又存在各自的组件,这种就是使用组合模式将各个组件组合为树形结构
外观模式
外观模型对子系统定义一个更高层的接口,使用高层接口简化操作,屏蔽内部实现,相当于中间加一层
前文说过Tomcat中连接器与容器的适配器Adapter会将Tomcat定义org.apache.coyote.Request/Response
转化为遵循servlet规范的org.apache.catalina.connector.Request/Response
(实现HttpServletRequest/HttpServletResponse接口)
org.apache.catalina.connector.Request
内部会使用 org.apache.coyote.Request
来实现servlet规范接口HttpServletRequest
为了屏蔽内部实现,防止使用内部其他细节,使用外观模式中间加一层,我们平常使用的HttpServletRequest都是外观类RequestFacade
(RequestFacade implements HttpServletRequest
也实现servlet规范HttpServletRequest接口)
当调用HttpServletRequest接口时,RequestFacade
会再去调用org.apache.catalina.connector.Request
的实现
(Response同理)
享元模式
享元模式通过"共享"的方式,让对象进行复用,旨在减少频繁创建、销毁对象,减少开销提高性能
线程池、连接池、对象池等池化技术是实现享元模式的方式之一,Tomcat中主要使用线程池、对象池来实现享元模式
线程池用于管理线程,避免频繁创建、销毁线程,减少内核开销,提高性能
对象池用于管理常用的复杂对象,也是避免频繁创建、销毁复杂对象,从而减少GC,提高性能
在享元模式中会把对象共享的与单独变化的数据进行隔离 ,其中共享的数据叫内部状态 ,而复用对象时动态变化的数据叫外部状态
那么享元模式有没有什么缺点呢?
说到对象的复用,那么使用对象时,对象中外部状态数据还是上次使用时遗留的数据
因此复用对象时要清理对象这些外部状态的数据,否则会出现脏数据,享元模式的缺点就是需要手动维护外部状态
线程池以前的文章说过,这篇文章就不再说明,感兴趣的同学可以查看Tomcat线程池如何进行扩展?
Tomcat自定义SynchronizedStack数据结构用作对象池,SynchronizedStack的实现比较简单,从名称看就知道是栈,并且使用Synchronized保证多线程下入栈、出栈等方法的原子性
Tomcat中对象池在很多地方进行使用:
SynchronizedStack<NioChannel> nioChannels
NioChannel对象池
SynchronizedStack<PollerEvent> eventCache
PollerEvent对象池
SynchronizedStack<SocketProcessorBase<S>> processorCache
SocketProcessor对象池
SynchronizedStack<Nio2Channel> nioChannels
Nio2EndPoint中Nio2Channel对象池
通过源码举几个例子:
在分析Tomcat网络通信源码的文章中曾说到过NioEndPoint
它获取到客户端连接后,会尝试从使用NioChannel对象池拿出NioChannel对象进行复用
NioChannel对象池
在使用前调用reset清除上次对象使用过(动态变化)的数据 (即清理外部状态)
java
//连接的对象池
private SynchronizedStack<NioChannel> nioChannels;
// Allocate channel and wrapper
NioChannel channel = null;
//对象池不为空从对象池里拿
if (nioChannels != null) {
channel = nioChannels.pop();
}
//拿到还为空则创建
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) {
channel = new SecureNioChannel(bufhandler, this);
} else {
channel = new NioChannel(bufhandler);
}
}
NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
//防止复用到脏数据
channel.reset(socket, newWrapper);
当连接关闭时,再放入对象池中
java
//EndPoint还在跑
if (getEndpoint().running) {
//不为空尝试push
if (nioChannels == null || !nioChannels.push(getSocket())) {
getSocket().free();
}
}
使用对象池进行复用时,一般都会reset清理脏数据,防止用到以前的数据导致程序错误
java
public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException {
this.sc = channel;
this.socketWrapper = socketWrapper;
bufHandler.reset();
}
从reset方法也可以看出,NioChannel中的连接SocketChannel、连接包装NioSocketWrapper和bufHandler缓冲池相关是外部状态,需要清理
PollerEvent对象池
在NioEndPoint与Poller进行通信时,封装PollerEvent也会使用到对象池
java
//PollerEvent对象池
private SynchronizedStack<PollerEvent> eventCache;
private PollerEvent createPollerEvent(NioSocketWrapper socketWrapper, int interestOps) {
PollerEvent r = null;
if (eventCache != null) {
r = eventCache.pop();
}
//为空就创建,复用就reset
if (r == null) {
r = new PollerEvent(socketWrapper, interestOps);
} else {
r.reset(socketWrapper, interestOps);
}
return r;
}
当Poller在event(将连接注册到selector上)使用完后,又把对象返回池中
java
if (running && eventCache != null) {
pe.reset();
eventCache.push(pe);
}
Tomcat中的对象池为了复用对象,还需要使用同步手段(Synchronized)来保证原子性,从而会导致一些性能开销
思考:有没有又能复用对象,又不需要使用同步手段的方式呢?
如果让我们来实现,能否使用线程局部变量ThreadLocal来实现对象池呢?
比如 mybatis使用sqlsession
java
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy;
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
}
这样就不涉及多线程操作,不需要使用同步手段,避免这部分的开销;但是每个线程占用的空间都会变大,由于对象池存在每个线程上,也不太方便进行管理
如果平时某个业务GC比较频繁,可以看看是不是频繁创建、销毁复杂对象导致的,如果复杂对象能将固定、动态变化的数据分离,考虑使用享元模式
行为型
责任链模式
责任链模式将处理者串联成链,请求只需要交给责任链上的第一个处理者,依次被处理者进行处理,对于请求者无需关心处理者是谁,并且处理者只需要关心自己要处理的那部分,无法处理就交给下一个处理者
前文曾经说过在多级容器的调用链路中每个容器都使用职责链模式
Pipeline接口为职责链中的管道,Valve接口为管道中负责处理的节点,其中作为Basic的Valve将会去调用下一级容器
包括最后执行的过滤器链也是责任链模式
业务上如果有需要进行一系列的操作/验证也可以考虑使用责任链模式
命令模式
命令模式将请求命令与执行命令分离,对两者进行解耦,提高灵活/扩展性
Processor解析请求向Container容器请求可以看作命令模式,其中Processor为请求命令,容器为处理命令
针对不同的协议HTTP1.1、APR、HTTP2.0,不同的Processor实现类(Http11Processor
/AjpProcessor
/StreamProcessor
)相当于不同命令
迭代器模式
迭代器模式提供方法访问集合中的元素,而不暴露集合内部元素,封装性好(无需关心内部实现),灵活性高(可以提过多种迭代顺序)
Tomcat触发生命周期的事件时就会去通过增强for循环调用监听器(增强for循环就是使用的迭代器)
java
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
迭代器模式是业务开发最常见的模式(增强for),可能平时没怎么注意
观察者模式
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新
前文说过,Tomcat中使用监听器来实现观察者模式,生命周期的监听器,当生命周期事件发生时通知所有监听器
java
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
//观察者+迭代器
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
在通知观察者时常用迭代器模式进行通知
当业务中需要进行监听时可以考虑观察者模式
策略模式
策略模式定义多种不同的算法,算法之间可以互相替换,使用算法无需关心实现,提高扩展性
策略模式在Tomcat中可以广义理解为具有不同的实现策略
比如前文说过网络通信时实现的IO模型:APR、NIO、NIO2,在面对不同场景时可以使用不同的实现策略进行替换
将来如果新出了什么IO模型则又可以增加一种实现策略,在对应场景时进行替换
在业务开发中如果某块需求动态变化的情况多,要考虑扩展性,可以考虑策略模式
模板方法模式
模板方法模式常用于定义算法骨架,用来实现固定的流程,而动态变化的流程往往通过策略模式中的算法来实现
处理完网络通信向后执行时,调用抽象父类AbstractEndpoint.processSocket
的模板方法,无需关心SocketProcessorBase的实现是NIO、NIO2还是APR
java
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
if (socketWrapper == null) {
return false;
}
//不同的算法实现
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
//调用后续执行
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
return true;
}
模板方法常用于抽象父类中,用于定义通用且固定的流程,业务开发中结合策略模式一起使用
总结
单例模式全局维护单一对象,适合生命周期长(与应用生命周期相同)、全局访问的对象,避免创建/销毁开销,但为了全局唯一,创建对象时需要使用"同步"的机制;业务中全局共享、生命周期长的组件考虑设计为单例
工厂模式根据入参构建对象,屏蔽内部实现细节,常用于构建/复用复杂对象;业务中根据不同参数创建/获取不同实现组件考虑使用工厂
适配器模式将原本不兼容的接口转换为期望的接口,提高兼容性,但转换过程存在开销;业务中对两个不适配的组件兼容时考虑适配器
装饰者模式能够对原始对象进行包装,动态的给对象添加新功能(更加灵活),但如果包装多层可能导致对象功能杂乱;业务中需要在原对象上增加功能时考虑装饰者
组合模式将各个组件组合为树级结构,更易于扩展、体现层级、增强复用性,但结构会变得更加复杂;业务中需要体现树形结构,局部与整体结构,考虑使用组合
外观模式提高高层接口,简化操作使用,屏蔽内部实现;业务中需要向调用者提高更简单、防止调用者操作其他方法时考虑外观模式
享元模式将固定的(内部状态)与动态变化的(外部状态)数据进行隔离,内部状态由复用对象共享,每次复用对象前需要清理外部状态,能够避免频繁创建、销毁复杂对象,但需要手动维护外部状态;业务中频繁创建、销毁复杂对象,对象固定值多时考虑享元池化
责任链模式将请求与处理解耦,处理者串行成链表依次处理请求,支持动态修改链表中的处理者(易于扩展),但如果处理者耗时或链路较长都会影响性能;业务中需要校验、依次处理考虑责任链
迭代器模式提供多种不同顺序访问元素的方法,不暴露内部元素,灵活性高、封装性好;业务中处理数据常用增强for(迭代器)
观察者模式能够在对象状态改变时通知观察者自动更新,观察者与被观察者都可以独立变化(低耦合);业务中需要监听考虑观察者
策略模式定义多种算法,算法间可以互相替换,不同场景使用不同算法,提高扩展性;业务中需要多种动态实现考虑策略
模板方法模式在抽象父类中定义固定流程,常与实现动态变化的策略模式一起使用;业务中大量通用固定流程考虑模板方法
🌠最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Tomcat全解析:架构设计与核心组件实现,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜