盘点Tomcat中常见的13种设计模式

2024-08-09 09:03:56 浏览数 (1)

盘点Tomcat中常见的13种设计模式

Tomcat的源码深处蕴含着一系列精妙的设计模式,它们共同支撑起了这个高性能、高灵活性的服务器平台

本文旨在深入探索Tomcat架构的底层逻辑,揭示隐藏其中的13种设计模式,从适配器模式到享元模式,从责任链模式到模板方法模式,我们将一一揭开这些设计模式的神秘面纱,展示它们如何协同工作,成就了Tomcat的稳定与高效

Tomcat设计模式思维导图:

image.pngimage.png

创建型

单例模式

单例模式能够让对象全局唯一共享使用,适合生命周期长(与应用相同)、全局共享访问的对象,减少开销提高利用率

但是为了让对象全局唯一,防止并发访问而导致对象变成“多例”,通常需要使用加锁的方式保证唯一

无论是饿汉式(类加载时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中又存在各自的组件,这种就是使用组合模式将各个组件组合为树形结构

image.pngimage.png
外观模式

外观模型对子系统定义一个更高层的接口,使用高层接口简化操作,屏蔽内部实现,相当于中间加一层

前文说过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将会去调用下一级容器

包括最后执行的过滤器链也是责任链模式

image.pngimage.png

业务上如果有需要进行一系列的操作/验证也可以考虑使用责任链模式

命令模式

命令模式将请求命令与执行命令分离,对两者进行解耦,提高灵活/扩展性

Processor解析请求向Container容器请求可以看作命令模式,其中Processor为请求命令,容器为处理命令

针对不同的协议HTTP1.1、APR、HTTP2.0,不同的Processor实现类(Http11Processor/AjpProcessor/StreamProcessor)相当于不同命令

imgimg
迭代器模式

迭代器模式提供方法访问集合中的元素,而不暴露集合内部元素,封装性好(无需关心内部实现),灵活性高(可以提过多种迭代顺序)

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(迭代器)

观察者模式能够在对象状态改变时通知观察者自动更新,观察者与被观察者都可以独立变化(低耦合);业务中需要监听考虑观察者

策略模式定义多种算法,算法间可以互相替换,不同场景使用不同算法,提高扩展性;业务中需要多种动态实现考虑策略

模板方法模式在抽象父类中定义固定流程,常与实现动态变化的策略模式一起使用;业务中大量通用固定流程考虑模板方法

0 人点赞