coding……
但行好事 莫问前程

Tomcat源码解读『基础类介绍』

通过之前两篇关于Tomcat的介绍,我们已经清楚知道Tomcat的作用及基本的工作原理。本篇文章开始,我们来从解读Tomcat源码。本篇先来介绍一下Tomcat的基础类,以便我们后续介绍Tomcat启动流程,工作流程。

我们知道,Tomcat的能作为web容器正常工作,依赖于Server.xml配置文件,如下:

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />
      </Host>
      
    </Engine>
  </Service>
</Server>

上篇文章我们也讲过,上述xml配置节点,基本都是Tomcat的基础组件。比如Server、Service、Connector、Container、Engine、Host、Context等。也在上篇文章大致介绍了上述组件的基本功能。我们今天要介绍的Tomcat基础类,就是Tomcat源码中对上述组件的抽象和实现。

1. Server

Server 组件的具体实现类是 StandardServer。它的子组件是Service,因此它还需要管理Service的生命周期,也就是说在启动时调用Service组件的启动方法,在停止时调用Service组件的停止方法。Server在内部维护了若干 Service 组件,它是以数组来保存的。

StandardServer内部提供了一个addService方法,用于将Service子组件添加到数组services[]中,如下:

public void addService(Service service) {

    service.setServer(this);

    synchronized (servicesLock) {
        Service results[] = new Service[services.length + 1];
        System.arraycopy(services, 0, results, 0, services.length);
        results[services.length] = service;
        services = results;

        if (getState().isAvailable()) {
            try {
                service.start();
            } catch (LifecycleException e) {
                // Ignore
            }
        }

        // Report this property change to interested listeners
        support.firePropertyChange("service", null, service);
    }

}

它并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组。

除了Service子组件的维护,Server另一个重要的功能就是来实现Tomcat的关闭,这也是为什么你可以通过shutdown命令来关闭Tomcat的原因。具体实现在StandardServer的await方法中,在await方法中会启动一个Socket来监听8005(停止端口),并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据,如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程。

public void await() {
    // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
    if (getPortWithOffset() == -2) {
        // undocumented yet - for embedding apps that are around, alive.
        return;
    }
    if (getPortWithOffset() == -1) {
        try {
            awaitThread = Thread.currentThread();
            while(!stopAwait) {
                try {
                    Thread.sleep( 10000 );
                } catch( InterruptedException ex ) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
        return;
    }

    // Set up a server socket to wait on
    try {
        // 创建socket,默认端口8005
        awaitSocket = new ServerSocket(getPortWithOffset(), 1,
                InetAddress.getByName(address));
    } catch (IOException e) {
        // log ……
        return;
    }

    try {
        awaitThread = Thread.currentThread();

        // 死循环,等待停止命令
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }

            // 等待连接,从连接中获取请求命令
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (SocketTimeoutException ste) {
                    // log ……
                    continue;
                } catch (AccessControlException ace) {
                    // log ……
                    continue;
                } catch (IOException e) {
                    if (stopAwait) {
                        // Wait was aborted with socket.close()
                        break;
                    }
                    // log ……
                    break;
                }

                // Read a set of characters from the socket
                int expected = 1024; // Cut off to avoid DoS attack
                while (expected < shutdown.length()) {
                    if (random == null)
                        random = new Random();
                    expected += (random.nextInt() % 1024);
                }
                while (expected > 0) {
                    int ch = -1;
                    try {
                        ch = stream.read();
                    } catch (IOException e) {
                        log.warn(sm.getString("standardServer.accept.readError"), e);
                        ch = -1;
                    }
                    // Control character or EOF (-1) terminates loop
                    if (ch < 32 || ch == 127) {
                        break;
                    }
                    command.append((char) ch);
                    expected--;
                }
            } finally {
                // Close the socket now that we are done with it
                try {
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    // Ignore
                }
            }

            // 如果连接是SHUTDOWN命令,结束循环
            boolean match = command.toString().equals(shutdown);
            if (match) {
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else
                log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
        }
    } finally {
        ServerSocket serverSocket = awaitSocket;
        awaitThread = null;
        awaitSocket = null;

        // Close the server socket and return
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                // Ignore
            }
        }
    }
}

2. Service

Service组件的具体实现类是StandardService。通过之前的文章我们知道,Service组件中有一个或多个Connector和一个Container。所以Service需要负责管理Connector和Container的生命周期。

除此之外,Service内部还有一个比较重要的成员就是映射器Mapper及其监听器,映射器及其监听器用于将用户请求的 URL 定位到一个 Servlet。它的工作原理是: Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及 Wrapper容器里Servlet映射的路径。映射器Mapper的监听器用于支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,它监听容器的变化,并把信息更新到Mapper 中,这是典型的观察者模式。

Service组件最重要的功能对下层容器的管理,比如启动时,要负责启动Connector和Container组件。Service的启动方法在startIntel中,如下:

protected void startInternal() throws LifecycleException {

    if(log.isInfoEnabled())
        log.info(sm.getString("standardService.start.name", this.name));
    // 1. 触发启动监听器
    setState(LifecycleState.STARTING);

    // 2. 先启动Engine,Engine会启动它子容器
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }

    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }

    // 3. 再启动Mapper监听器
    mapperListener.start();

    // 4. 最后启动连接器,连接器会启动它子组件,比如Endpoint
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            // If it has already failed, don't try and start it
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

从启动方法可以看到,Service先启动了Engine组件,再启动Mapper监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此Mapper和MapperListener在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。

3. Connector

通过之前的文章我们知道,连接器的作用是处理 Socket 连接,将网络字节流转化为Request和Response对象。也就是说连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,对容器而言,获取到的都是一个标准的ServletRequest对象。连接器的工作流程可以细分为如下几个部分:

  • 监听网络端口
  • 接受网络连接请求
  • 读取请求网络字节流
  • 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象
  • 将 Tomcat Request 对象转成标准的 ServletRequest
  • 调用 Servlet 容器,得到 ServletResponse
  • 将 ServletResponse 转成 Tomcat Response 对象
  • 将 Tomcat Response 转成网络字节流
  • 将响应字节流写回给浏览器

Tomcat设计者将上述功能进一步细分为以下三部分:

  • 网络通信
  • 应用层协议解析
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化

Tomcat 的设计者设计了3个组件来实现这3个功能,分别是EndPoint、Processor和Adapter

网络通信的I/O模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint负责提供字节流给 Processor,Processor负责提供Tomcat Request对象给Adapter,Adapter 负责提供 ServletRequest对象给容器。

如果要支持新的I/O方案、新的应用层协议,只需要实现相关的具体子类。上层通用的处理逻辑是不变的。

由于I/O模型和应用层协议可以自由组合,比如NIO + HTTP或者NIO2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler的接口来封装这两点变化点。各种协议和通信模型的组合有相应的具体实现类,比如: Http11NioProtocol 和 AjpNioProtocol。

除了这些变化点,系统也存在一些相对稳定的部分,因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类AbstractProtocol实现了 ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。

这样设计的目的是尽量将稳定的部分放到抽象基类,同时每一种 I/O 模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。所以Tomcat连接器可以细分为如下几个部分:

3.1 ProtocolHandler

ProtocolHandler成员变量初始化是在Connector构造函数中完成的,如果调用Connector无参构造函数,ProtocolHandler默认为HTTP/1.1 NIO类型,即Http11NioProtocol。这里我们也以Http11NioProtocol为例,介绍ProtocolHandler。

ProtocolHandler用来处理网络连接和应用层协议,包含了2个重要部件:EndPoint和Processor

3.1.1 Endpoint

EndPoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此EndPoint是用来实现TCP/IP协议的。

EndPoint是一个接口,对应的抽象实现类是AbstractEndpoint,而 AbstractEndpoint的具体子类,比如在 NioEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor。

其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的 Socket请求,它实现Runnable接口,在run()方法里调用协议处理组件Processor 进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。

3.1.2 Processor

EndPoint用来实现TCP/IP协议,Processor则用来实现应用层协议的(HTTP协议、AJP协议等),负责接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。

Processor是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AJPProcessor、HTTP11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。

上图的流程代码逻辑大致如下:

NioEndPoint.startIntel -> 
Poller -> run() 
-> processKey(SelectionKey sk, NioSocketWrapper attachment) 
-> processSocket(attachment, SocketEvent.OPEN_READ, true) 
-> createSocketProcessor(socketWrapper, event) 
-> SocketProcessor run 
-> getHandler().process 
-> processor.process(wrapper, status) 
-> service(socketWrapper) 
-> getAdapter().service(request, response)

3.2 Adapter

由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的 Request类来存放这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter 的 Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用Engine容器的pipline方法(之前文章介绍的pipline-valve机制),实现对servlet的调用。

4. Engine

Engine是一个容器(或者讲Engine是Tomcat的顶级容器,Tomcat设计了4 种容器,分别是 Engine、Host、Context 和 Wrapper),因此它继承了ContainerBase基类,并且实现了Engine接口,具体实现类是StandardEngine。

Engine的子容器是Host,负责子容器Host的启动暂停。所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase中,ContainerBase中有这样一个数据结构:

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如 ContainerBase会用专门的线程池来启动子容器。

Engine在启动Host子容器时就直接重用了这个方法(startIntel)。在StandardEngine中,我们只看到了容器启动、暂停相关的方法。我们知道,Engine作为容器,最重要的肯定是处理请求了,那么Engine是如何处理请求的?上面介绍Adapter的时候我们知道,Adapter中会调用Engine的pipline的invoke方法,如下:

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

从而实现将请求提交给容器处理,可以看到调用的就是Engine的Pipeline。之前介绍Pipline-valve机制的时候,我们介绍过,每一个容器组件都有一个Pipeline,而 Pipeline中有一个基础阀(Basic Valve),基础阀负责调用下层容器Pipeline。Engine容器的基础阀StandardEngineValve定义如下:

final class StandardEngineValve extends ValveBase {

    //------------------------------------------------------ Constructor
    public StandardEngineValve() {
        super(true);
    }


    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // 拿到请求中的host容器
        Host host = request.getHost();
        if (host == null) {
            // HTTP 0.9 or HTTP 1.0 request without a host when no default host
            // is defined. This is handled by the CoyoteAdapter.
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // 调用host容器pipline中第一个valve,将请求提交给host处理
        host.getPipeline().getFirst().invoke(request, response);
    }
}

可以看到,Engine对请求的处理,其实就是把请求转发给某一个 Host 子容器来处理。

5. Host

Host是Tomcat顶层容器Engine的子容器,通过我们之前web.xml配置不难发现,Host是跟虚拟域名绑定的,同时负责维护其子容器Context的生命周期。Host容器在Tomcat中的实现类是StandardHost,它也继承了ContainerBase基类同时实现了Host接口。

public class StandardHost extends ContainerBase implements Host{
    //
}

因为继承了ContainerBase类,所以对于Context子容器的维护,也是基于ContainerBase的统一结构(上面介绍Engine的时候介绍过,HashMap children)。在StandardHost中也没有重写startInternal方法,所以StandardHost的启动功能也很明确——用于启动子容器Context。

6. Context

Context对应一个web应用,是用于存储管理Servlet的容器,在Tomcat中的默认实现是StandardContext。StandardContext也继承了ContainerBase类,子容器也是通过ContainerBase类的成员变量children维护的,但是StandardContext中重写了startInternal方法,在该方法中,完成WebAppClassLoader的初始化,子容器Wrapper的初始化及启动,web.xml配置文件解析,Context监听器维护与触发等。

6.1 基础属性

比如我们在配置文件中配置的Context的path属性,用于指定Host下(域名下)web应用的访问路径:

<Host appBase="webapps" autoDeploy="true" name="www.lidol.top" unpackWARs="true">

    <!--访问路径:www.lidol.top/demo1-->
    <Context docBase="/Users/zhuoli/Documents/demo/demo1" path="/demo1" reloadable="true"/> 
 
    <!--访问路径:www.lidol.top/demo2-->
    <Context docBase="/Users/zhuoli/Documents/demo/demo2" path="/demo2" reloadable="true"/> 
</Host>  

在StandardContext中,我们发现存在这样两个成员变量:

/**
 * The document root for this web application.
 */
private String docBase = null;

/**
 * Unencoded path for this web application.
 */
private String path = null;

Context是所有的Servlet的父容器,我们知道Servlet中一个重要的组件就是ServletContext(Servlet上下文),该组件就是在StandardContext中初始化的:

/**
 * The ServletContext implementation associated with this Context.
 */
protected ApplicationContext context = null;
@Override
public ServletContext getServletContext() {

    if (context == null) {
        context = new ApplicationContext(this);
        if (altDDName != null)
            context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
    }
    return (context.getFacade());

}

6.2 Context和Java Web三大组件

我们之前介绍过Servlet三大组件,分别是Servlet、Filter和Listener。这三个组件的作用于都是整个Web应用。

Servlet其实就是Context的子容器Wrapper,用继承的children成员变量来维护。

Filter是一种Servlet的扩展机制,在web.xml中配置,在Context容器中进行管理的:

private Map<String, FilterDef> filterDefs = new HashMap<>();

需要注意的是StandardContext中只是将我们配置的Filter解析成FilterDef,并不是每个请求都需要将所有的Filter走一遍。在web.xml中,我们通过<filter-mapping>来配置servlet生效的Filter集合,Tomcat容器会为每个请求生成一个FilterChain,用于表示该请求相关联的Filter,在每个Filter执行结束后,才会调用Servlet的service方法。

Listener跟Filter一样,也是一种扩展机制,你可以监听容器内部发生的事件,主要有两类事件:

  • 生命状态的变化,比如Context容器启动和停止、Session的创建和销毁
  • 属性的变化,比如Context容器某个属性值变了、Session的某个属性值变了以及新的请求来了等

我们在web.xml配置了监听器,在监听器里实现我们的业务逻辑。对于Tomcat来说,它需要读取配置文件,拿到监听器类的名字,实例化这些类,并且在合适的时机调用这些监听器的方法。

Tomcat是通过Context容器来管理这些监听器的。Context容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器:

// 监听属性值变化的监听器
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();

// 监听生命事件的监听器
private Object applicationLifecycleListenersObjects[] = new Object[0];

剩下的事情就是触发监听器了,比如在Context容器的启动方法里,就触发了所有的ServletContextListener:

for (int i = 0; i < instances.length; i++) {
    if (!(instances[i] instanceof ServletContextListener))
        continue;
    ServletContextListener listener =
        (ServletContextListener) instances[i];
    try {
        fireContainerEvent("beforeContextInitialized", listener);
        if (noPluggabilityListeners.contains(listener)) {
            listener.contextInitialized(tldEvent);
        } else {
            listener.contextInitialized(event);
        }
        fireContainerEvent("afterContextInitialized", listener);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        fireContainerEvent("afterContextInitialized", listener);
        getLogger().error
            (sm.getString("standardContext.listenerStart",
                          instances[i].getClass().getName()), t);
        ok = false;
    }
}

7. Wrapper

Wrapper也是一种容器,用来管理Servlet,每一个Servlet都对应一个Wrapper,在Tomcat中的实现是StandardWrapper。所以StandardWrapper中有一个比较重要的成员变量:

protected volatile Servlet instance = null;

用于表示该Wrapper容器对应的Servlet实例。在StandardWrapper中提供了allocate()方法,用于实例化并初始化Servlet。而allocate方法最终调用了StandardWrapper中的loadServlet()方法来实例化并初始化Servlet。

public synchronized Servlet loadServlet() throws ServletException {

    // Nothing to do if we already have an instance or an instance pool
    if (!singleThreadModel && (instance != null))
        return instance;

    // 1. 创建Servlet实例
    servlet = (Servlet) instanceManager.newInstance(servletClass);

    // 2. 调用Servlet的init方法,初始化Servlet
    initServlet(servlet);

    return servlet;

}

loadServlet()方法并不会在Tomcat启动的时候调用,为了加快启动速度,Tomcat采取了资源延迟加载的策略。默认情况下Tomcat在启动时不会加载你的 Servlet,除非把Servlet的loadOnStartup属性设置为true。但是在Tomcat容器启动时,是会创建Wrapper容器的。

那么loadServlet()方法到底是什么时候调用的?答案是StandardWrapperValve中。上面我们介绍到,连接器的Adapter组件会把请求提交给容器Engine的Pipline,之后通过Pipline-Valve机制,会调用到StandardWrapperValve的invoke方法,就是在该方法中,完成Servlet的初始化和实例化。如下:

public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // 1. 实例化Servlet
    servlet = wrapper.allocate();

    // 2. 为当前请求创建一个FilterChain
    ApplicationFilterChain filterChain =
            ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

    // 3. 调用这个FilterChain的doFilter方法,FilterChain中最后一个Filter执行结束后,会调用Servlet的service方法
    filterChain.doFilter(request.getRequest(), response.getResponse());
}

到这里,我们也可以解答一个疑问,为什么Servlet会在第一次被调用时实例化。因为在上述loadServlet方法中,只有Wrapper容器中的Servlet实例为null时(第一次调用时),才会创建Servlet实例。

参考链接:

1. Tomcat源码

2. 深入了解Tomcat&Jetty

赞(0) 打赏
Zhuoli's Blog » Tomcat源码解读『基础类介绍』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址