tomcat-2.原理

76 阅读21分钟

需求

The Apache Tomcat® software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket specifications are developed under the Java Community Process.

Tomcat被设计为Servlet规范的一种快速而高效的实现。Tomcat是作为这个规范的参考实现出现的,并且一直严格遵守该规范。与此同时,Tomcat的性能得到了很大的关注,现在它已经可以与其他servlet容器(包括商业容器)相提并论。

架构

核心

image.png

Tomcat的核心功能

  • 处理Socket连接,负责网络字节流与Request和Response对象的转换
  • 加载和管理Servlet,具体处理Request请求

Tomcat核心组件

  • 连接器(Connector)-负责对外连接
  • 容器(Container)-负责内部处理

Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,每个连接器对应不同的监听端口,比如默认的HTTP连接器监听8080端口,默认的AJP连接器监听8009端口,配置不同的I/O模型及应用层协议,就好比一个房间有多个门。

单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。Tomcat 内可能有多个 Service,这样的设计也是出于灵活性的考虑。通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

概念

  • Server 在Tomcat世界中,Server代表整个容器。Tomcat提供了服务器接口的默认实现,很少由用户定制。
  • Service Service是位于Server内部的起媒介作用的中间组件,它将一个或多个连接器绑定到一个Engine。Service很少由用户定制,因为默认实现简单而充分:org.apache.catalina.service接口。
  • Engine Engine表示特定service的请求处理pipeline。由于Service可能有多个Connectors,因此Engine接收并处理来自这些Connnectors的所有请求,并将响应传递回适当的connector,以便传输到客户端。可以实现org.apache.catalina.Engine接口来提供定制的引擎,尽管这种情况并不常见。
  • Host 表示一个虚拟主机,它是一个服务器的网络名称(www.mycompany.com) 与运行Tomcat的特定服务器的关联。一个Engine可以包含多个主机,Host还支持网络别名,如yourcompany.com和abc.yourcompany.com。用户很少创建自定义Host,因为org.apache.catalina.core.StandardHost实现提供了重要的附加功能。
  • Connector Connector处理与客户端的通信。Tomcat有多个Connector可用。其中包括用于大多数HTTP通信的HTTP连接器(特别是在将Tomcat作为独立服务器运行时),以及实现将Tomcat连接到web服务器(如Apache HTTPD服务器)时使用的AJP协议的AJP连接器。创建一个定制的Connector是一项重要的工作。 Tomcat 支持三种协议:HTTP/1.1、HTTP/2.0、AJP。
  • Context Context表示web应用程序。Host可以包含多个contexts,每个Context都有一个唯一的path。可以实现org.apache.catalina.Context接口来创建自定义上下文,但这种情况很少发生,因为org.apache.catalina.core.StandardContext提供了重要的附加功能。

组件

connector

  1. Tomcat支持的IO模型
  • BIO-阻塞式IO(Tomcat7及之前版本的默认方式)
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
  • NIO-非阻塞式IO(Tomcat8及之后版本的默认方式) 17-Aug-2019 17:56:08.717 信息 [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]

  • NIO2-异步 I/O,采用 JDK 7 最新的 NIO.2 类库实现

<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />
  • APR-采用 Apache 可移植运行库实现,是 C/C++ 编写的本地类库
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="20000" redirectPort="8443" />
  1. Tomcat支持的协议 HTTP/1.1 AJP HTTP/2

  2. 连接器功能

  • 网络通信-EndPoint(I/O模型)

  • 应用层协议解析-Processor(应用层协议处理)

  • Tomcat Request/Response 与 ServletRequest/ServletResponse的转化-Adaptor

Endpoint负责提供字节流给Processor,Processor负责提供Tomcat Request对象给Adaptor,Adaptor负责提供ServletRequest对象给容器

Http11NioProtocol

Http11NioProtocol是ProtocolHandler接口的NIO实现-包含I/O模型+应用层协议

image.png

  • EndPoint Endpoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 Endpoint 是用来实现 TCP/IP 协议的。 Endpoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,有两个重要的子组件:Acceptor 和 SocketProcessor。

    • Acceptor Acceptor 用于监听 Socket 连接请求。
    • SocketProcessor SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。
  • Processor Http11Processor 用来实现 HTTP 协议,Http11Processor 接收来自 Endpoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Http11Processor 是对应用层协议的抽象。

  • Adapter 组件 protocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,Tomcat 设计者的解决方案是引入 CoyoteAdapter,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方法。

container

在 Tomcat 中一共设计了 4 种容器,它们分别为 Engine、Host、Context、Wrapper,它们的关系如下图所示:

image.png

Context 表示一个 Web 应用程序;

Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;

Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;

Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。

  • 各组件连接关系

image.png

  1. 根据协议和端口号选定 Service 和 Engine。 Tomcat 的每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。

  2. 根据域名选定 Host。 Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的域名是user.shopping.com,因此 Mapper 会找到 Host2 这个容器。

  3. 根据 URL 路径找到 Context 组件。 Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是/order,因此找到了 Context4 这个 Context 容器。

  4. 根据 URL 路径找到 Wrapper(Servlet) Context 确定后,Mapper 再根据web.xml中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet

流程

启动

image.png

  1. startup.sh脚本会启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。

  2. Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。

  3. Catalina 是一个启动类,它通过解析server.xml、创建相应的组件,并调用 Server 的 start 方法。

  • Catalina 的主要任务就是创建 Server,通过解析server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用 Server 组件的 init 方法和 start 方法,这样整个 Tomcat 就启动起来了。
  • Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”
  1. Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法。
  • Server 组件的具体实现类是 StandardServer,负责管理 Service 的生命周期,启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的。
  • Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,Catalina的启动方法中会调用Server的await方法, await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程
  1. Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。
  • Service 组件的具体实现类是 StandardService,StandardService 继承了 LifecycleBase 抽象类,并包含其他我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。
  • 包含MapperListener,因为 Tomcat 支持热部署,当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。
  • Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动

请求

sequenceDiagram
Acceptor->>Poller: accept一个channel之后通知PollerEvent
Poller-->>Poller: 将连接channel OP_READ注册nio selector
Poller-->>SocketProcessor: nio selector读到SelectionKey,创建SocketProcessor交由executor线程池处理
SocketProcessor-->>ConnectionHandler: 将事件交给handler处理
ConnectionHandler-->>Http11Processor: 交给对应的协议processor
Http11Processor-->>CoyoteAdapter: 委托处理
CoyoteAdapter-->>StandardEngineValve: pipeline开始处理
StandardEngineValve-->>StandardHostValve: pipeline处理
StandardHostValve-->>StandardContextValve: pipeline处理
StandardContextValve-->>StandardWrapperValve: pipeline处理
StandardWrapperValve-->>ApplicationFilterChain: FilterChain过滤
ApplicationFilterChain-->>HttpServlet: service处理req,resp
HttpServlet-->>MyServlet: 业务逻辑

NioEndpoint为例

非阻塞I/O,多路复用模式

Java 的多路复用器

  1. 创建一个 Selector,在它身上注册各种感兴趣的事件,然后调用 select 方法,等待感兴趣的事情发生(select和poll是轮训,epoll是回调通知)
  2. 感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从 Channel 中读数据

Tomcat 的 NioEndpoint 组件虽然实现比较复杂,但基本原理就是上面两步。我们先来看看它有哪些组件,它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 个组件,它们的工作过程如下图所示。

image.png

LimitLatch

LimitLatch 是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝 功能类似于CountDownLatch-通过自定义Sync,并扩展AQS来实现

Acceptor

跑在一个单独的线程里,它在一个死循环里调用accept方法来接收新连接,一旦有新的连接请求到来,accept方法返回一个Channel对象,接着把Channel对象交给Poller去处理

// org.apache.tomcat.util.net.NioEndpoint->initServerSocket()
serverSock = ServerSocketChannel.open();
serverSock.socket().bind(addr,getAcceptCount());
serverSock.configureBlocking(true);

ServerSocketChannel 通过 accept() 接受新的连接,accept()方法返回获得SocketChannel对象,然后将SocketChannel对象封装在一个PollerEvent对象中,并将PollerEvent 对象压入Poller的Queue里,这是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过Queue通信

//org.apache.tomcat.util.net.Acceptor.run()-> endpoint.serverSocketAccept();-> poller.register(socketWrapper);
socket = endpoint.serverSocketAccept();//实现:serverSock.accept()
endpoint.setSocketOptions(socket); 
--->
//NioEndpoint.setSocketOptions(socket)
// Set socket properties
// Disable blocking, polling will be used
socket.configureBlocking(false);
socketProperties.setProperties(socket.socket());

socketWrapper.setReadTimeout(getConnectionTimeout());
socketWrapper.setWriteTimeout(getConnectionTimeout());
socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
poller.register(socketWrapper);
return true;
Poller

Poller 的本质是一个Selector,也跑在单独线程里。Poller内部在一个死循环里不断检测Acceptor有没有新发送的event并将其注册到sellector里, 然后监听selector里的channel数据就绪状态,一旦有Channel可读,就生成一个SocketProcessor任务对象扔给 Executor 去处理。

//org.apache.tomcat.util.net.NioEndpoint.Poller
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();

public void run() {
  while(true) {
    hasEvents = events();// 接收到event之后注册到sellector: sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
    Iterator<SelectionKey> iterator =
        keyCount > 0 ? selector.selectedKeys().iterator() : null;
    // Walk through the collection of ready keys and dispatch
    // any active event.
    while (iterator != null && iterator.hasNext()) {
        SelectionKey sk = iterator.next();
        iterator.remove();
        NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
        // Attachment may be null if another thread has called
        // cancelledKey()
        if (socketWrapper != null) {
            processKey(sk, socketWrapper);//调用下面逻辑
        }
    }
}
SocketProcessor

Executor 就是线程池,负责运行 SocketProcessor 任务类,SocketProcessor 的 run 方法会调用 Http11Processor 来读取和解析请求数据。Http11Processor 是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel 写出。 Poller 会创建 SocketProcessor 任务类交给线程池处理,而 SocketProcessor 实现了 Runnable 接口,用来定义 Executor 中线程所执行的任务,主要就是调用 Http11Processor 组件来处理请求。Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象

//org.apache.tomcat.util.net.AbstractEndpoint
//上面processKey(sk, socketWrapper)的处理逻辑
if (sc == null) {
    sc = createSocketProcessor(socketWrapper, event);//new SocketProcessor(socketWrapper, event);
} else {
    sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
    executor.execute(sc);
} else {
    sc.run();
}
Http11Processor
getAdapter().service(request, response);

CoyoteAdapter
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
...
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
        request, response);

原理

servlet规范的实现

Servlet 规范中最重要的就是 Servlet、Filter 和 Listener“三兄弟”。Web 容器最重要的职能就是把它们创建出来,并在适当的时候调用它们的方法。 Tomcat 通过 Wrapper 容器来管理 Servlet,Wrapper 包装了 Servlet 本身以及相应的参数,这体现了面向对象中“封装”的设计原则。 Tomcat 会给每个请求生成一个 Filter 链,Filter 链中的最后一个 Filter 会负责调用 Servlet 的 service 方法。 对于 Listener 来说,我们可以定制自己的监听器来监听 Tomcat 内部发生的各种事件:包括 Web 应用级别的、Session 级别的和请求级别的。Tomcat 中的 Context 容器统一维护了这些监听器,并负责触发。

connector线程池的高并发

Executor

Executor-Executor 是 Tomcat 定制版的线程池,它负责创建真正干活的工作线程,就是执行 SocketProcessor 的 run 方法,也就是解析请求并通过容器来处理请求,最终会调用到我们的 Servlet。

//org.apache.tomcat.util.net.AbstractEndpoint
public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

高并发思路

NioEndpoint 要完成三件事情:接收连接、检测 I/O 事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程数去处理,比如用专门的线程组去跑 Acceptor,并且 Acceptor 的个数可以配置;用专门的线程组去跑 Poller,Poller 的个数也可以配置;最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小

定制版的ThreadPoolExcutor

Java 原生线程池的任务处理逻辑比较简单:

  • 前 corePoolSize 个任务时,来一个任务就创建一个新线程。
  • 后面再来任务,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。
  • 如果总线程数达到 maximumPoolSize,执行拒绝策略。 Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑:
  • 前 corePoolSize 个任务时,来一个任务就创建一个新线程。
  • 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。
  • 如果总线程数达到 maximumPoolSize,则继续尝试把任务添加到任务队列中去。
  • 如果缓冲队列也满了,插入失败,执行拒绝策略。
public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        super.execute(command);
    } catch (RejectedExecutionException rx) {
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                if (!queue.force(command, timeout, unit)) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                }
            } catch (InterruptedException x) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }

    }
}

定制版的任务队列

Tomcat 线程池是用这个变量 submittedCount 来维护已经提交到了线程池,但是还没有执行完的任务个数。Tomcat 的任务队列 TaskQueue 扩展了 Java 中的 LinkedBlockingQueue,我们知道 LinkedBlockingQueue 默认情况下长度是没有限制的,除非给它一个 capacity。因此 Tomcat 给了它一个 capacity,TaskQueue 的构造函数中有个整型的参数 capacity,TaskQueue 将 capacity 传给父类 LinkedBlockingQueue 的构造函数。

capacity 参数是通过 Tomcat 的 maxQueueSize 参数来设置的,默认情况下 maxQueueSize 的值是Integer.MAX_VALUE,等于没有限制,这样就带来一个问题:当前线程数达到核心线程数之后,再来任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会有机会创建新线程了。

TaskQueue 重写了 LinkedBlockingQueue 的 offer 方法,在合适的时机返回 false, 只有当前线程数大于核心线程数、小于最大线程数,并且已提交的任务个数大于当前线程数时,也就是说线程不够用了,但是线程数又没达到极限,才会去创建新的线程。这就是为什么 Tomcat 需要维护已提交任务数这个变量,它的目的就是在任务队列的长度无限制的情况下,让线程池有机会创建新的线程

public boolean offer(Runnable o) {
  //we can't do any checks
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

Context容器的双亲委派机制

Tomcat自定义类加载器打破双亲委托机制

JVM 的类加载

  • Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法。

  • JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。

  • defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。

  • findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。

  • loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。

JVM类加载器的分类

image.png

  • BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jar、resources.jar等。

  • ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。

  • AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。

  • 自定义类加载器,用来加载自定义路径下的类。

  • 类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

  • 类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制

Tomcat的类加载器

  • Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。

  • 在 findClass 方法:

    • 先在 Web 应用本地目录下查找要加载的类。

    • 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 SharedClassLoader。

    • 如何父加载器也没找到这个类,抛出 ClassNotFound 异常

  • loadClass 方法:

    • 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。

    • 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。

    • 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。

    • 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。

    • 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。

    • 如果上述加载过程全部失败,抛出 ClassNotFound 异常

//org.apache.catalina.loader.WebappLoader.createClassLoader()
new ParallelWebappClassLoader(context.getParentClassLoader());//StandardContext.getParentClassLoader->StandardHost.getParentClassLoader即Shared ClassLoader.

//org.apache.catalina.loader.WebappClassLoaderBase

Tomcat中WEB应用的隔离

问题:

  1. 假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。

  2. 假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。

  3. 跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。

Tomcat 类加载器的层次结构

image.png

  • 问题一:两个应用的隔离

假如我们使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。

Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。

  • 问题二:两个应用之间共享类库

Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

  • 问题三:隔离Tomcat的依赖类与应用的类

要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassLoader,专门来加载 Tomcat 自身的类。

增加一个 CommonClassLoader,作为 CatalinaClassLoader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

//org.apache.catalina.startup.Bootstrap
private void initClassLoaders() {
    try {
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader = this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);

容器组件的组合设计模式

Tomcat的组件生命周期管理

  1. Tomcat组件关系
  • 组件有大有小,大组件管理小组件,比如 Server 管理 Service,Service 又管理连接器和容器
  • 组件有外有内,外层组件控制内层组件,比如连接器是外层组件,负责对外交流,外层组件调用内层组件完成业务功能。也就是说,请求的处理过程是由外层组件来驱动的。
  1. 组件的创建顺序原则
  • 先创建子组件,再创建父组件,子组件需要被“注入”到父组件中。
  • 先创建内层组件,再创建外层组件,内层组建需要被“注入”到外层组件。
  1. 一键式启停:Lifecycle 接口 Lifecycle 接口里应该定义这么几个方法:init、start、stop 和 destroy,每个具体的组件去实现这些方法。
  • 不变点-这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。
  • 变化点-每个具体组件的初始化方法,也就是启动方法是不一样的。
  • 在父组件的 init 方法里需要创建子组件并调用子组件的 init 方法。同样,在父组件的 start 方法里也需要调用子组件的 start 方法,因此调用者可以无差别的调用各组件的 init 方法和 start 方法,这就是组合模式的使用,并且只要调用最顶层组件,也就是 Server 组件的 init 和 start 方法,整个 Tomcat 就被启动起来了。
  1. 可扩展性:Lifecycle 事件
  • 各个组件 init 和 start 方法的具体实现是复杂多变的,比如在 Host 容器的启动方法里需要扫描 webapps 目录下的 Web 应用,创建相应的 Context 容器,如果将来需要增加新的逻辑,直接修改 start 方法?这样会违反开闭原则,那如何解决这个问题呢?开闭原则说的是为了扩展系统的功能,你不能直接修改系统中已有的类,但是你可以定义新的类。
  • 组件的 init 和 start 调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动,因此我们把组件的生命周期定义成一个个状态,把状态的转变看作是一个事件。而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式。

重用性:LifecycleBase 抽象基类

具备一些公共的逻辑,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等

容器组件的责任链设计模式

连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。

Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理,Valve 表示一个处理点(也就是一个处理阀门),因此 invoke方法就是来处理请求的。

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}
public interface Pipeline {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

Pipeline中有 addValve方法。Pipeline 中维护了 Valve链表,Valve可以插入到 Pipeline中,对请求做某些处理。Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve完成自己的处理后,调用 getNext.invoke() 来触发下一个 Valve 调用。

其实每个容器都有一个 Pipeline 对象,只要触发了这个 Pipeline 的第一个 Valve,这个容器里 Pipeline中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。

这是因为 Pipeline中还有个 getBasic方法。这个 BasicValve处于 Valve链表的末端,它是 Pipeline中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。

image.png

整个过程分是通过连接器中的 CoyoteAdapter 触发,它会调用 Engine 的第一个 Valve:

@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
    // 省略其他代码
    // Calling the container
    connector.getService().getContainer().getPipeline().getFirst().invoke(
        request, response);
    ...
}

Wrapper 容器的最后一个 Valve 会创建一个 Filter 链,并调用 doFilter() 方法,最终会调到 Servlet的 service方法。

源码环境搭建

git clone https://github.com/apache/tomcat.git
cd tomcat
ant
ant ide-eclipse
//intellij idea -> create project from eclipse

参考

mp.weixin.qq.com/s/fU5Jj9tQv…