浅看Tomcat & Servlet

# 前言

本文适合有过Servlet编程经验的半小白观看~~

# 一、Servlet简介

Servlet 全称是Java Server Applet。从命名来看就知道名字起名来自于Java Applet(曾经也不没有风靡一时的java 小应用程序,主要应用于嵌入HTML),那么Servlet 顾名思义就是应用于服务器端的程序(小服务程序)了。

Servlet 的含义比较多,一句话概括的话我倾向于理解为——Servlet 是Java语言的一套接口和标准。他是目前最主流的实现动态页面的技术之一,开发者可以由此编写java web 服务端应用。而且截止目前,Java 服务端生态圈已经发展处众多的应用框架,如Spring MVC、Struts2……

尽管已经有众多框架可以让开发者免于手动编写Servlet开发程序,但是对于初学者来讲,学习并使用Servlet开发一次Web项目是非常有必要的。

# 二、Servlet容器简介

既然有了这么一套标准,开发者也就无需要自己那么辛苦撸一套基于socket包和http协议的web服务器了。

(P.S 自己撸是不可能的撸的,曾经做计网作业写一个web服务器DEMO,能跑起来很简单,但是实现更多的协议细节和一堆技术的话……)

除了实现这套接口之外,还需要一个“容器”来运行编写好的Servlet,Tomcat就是主流的Servlet 容器之一,将编写好的Servlet 打包成web应用程序就可以在Tomcat上运行了。

# 三、Servlet接口

撇开入门时候就用上的Tomcat,Servlet到底是什么,狭义的Servlet仅仅是一个接口!

// 删掉注释版本,实际上注释很有用
package javax.servlet;

import java.io.IOException;

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    public String getServletInfo();

    public void destroy();
}

如图,揭示了Servlet的生命周期和作用。

  • init(ServletConfig config)

    为该Servlet初始化时候被调用的。具体会根据 web.xml(或Java Config)中的load-on-startup 属性决定初始化时刻。

    • service(ServletRequest req, ServletResponse res)

    为业务代码,在这里可以执行业务,读取请求,和修改响应。

    • destroy()

    当Servlet实例将要被销毁时,该方法将会被调用

    最基本的Servlet知识仅为如上,然后各公司提供的实现都是从这个接口实现得到,而同包(javax.servlet)下已经有了 GenericServletHttpServlet 的实现。

其中 HttpServlet 继承自 GenericServlet 新增了更多如 doGetdoPut 等Http操作,进一步方便开发者开发。

另外,Servlet的默认工作方式是单例多线程的,所以需要注意线程安全问题,不好把一些状态保存到一个Servlet实例变量上。

如果需要多例模式,请继承SingleThreadModel,为了性能非常不推荐

public abstract class HttpServlet extends GenericServlet {

    private static final long serialVersionUID = 1L;

    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";

    private static final String HEADER_IFMODSINCE = "If-Modified-Since";
    private static final String HEADER_LASTMOD = "Last-Modified";

    private static final String LSTRING_FILE =
        "javax.servlet.http.LocalStrings";
    private static final ResourceBundle lStrings =
        ResourceBundle.getBundle(LSTRING_FILE);

    // ...Servlet Method...doGet,doPost,doPut....
}

# 四、 围绕Servlet接口的类

围绕着Servet接口有四个类,分别是ServletConfigServletContextServletRequestServletResponse,依次讲。

# ServletConfig

ServletConfig 是在 Servlet 初始化时就传给 Servlet 了,保存了相关Servlet初始化信息,并且提供了getServletContext() 方法从容器中获得Servlet上下文。

package javax.servlet;

import java.util.Enumeration;

public interface ServletConfig {

    public String getServletName();

    public ServletContext getServletContext();

    public String getInitParameter(String name);

    public Enumeration<String> getInitParameterNames();
}

我们看看有哪些类实现了该接口。

1

getServletContext() 可以追溯到Tomcat源码查看实现

    /**
     * @return the servlet context for which this Context is a facade.
     */
    @Override
    public ServletContext getServletContext() {

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

}

如图,获取了 Tomcat 容器中 应用上下文的门面类。

# ServletContext

上文已经讲到, ServletContext 其实就是从 ApplicationContext 中获取其门面类 ApplicationContextFacade (均实现 ServletContext 接口,屏蔽了部分内部使用的属性)。

每一个独立的Web项目创建时, Tomcat 都会为其新建一个 ServletContext 实例,亦即应用上下文。


public class ApplicationContextFacade implements org.apache.catalina.servlet4preview.ServletContext {
    public Object getAttribute(String name) {}
    public String getInitParameter(String name) {} 
    public FilterRegistration.Dynamic addFilter {}

    // methods
}

通过该 ServletContext 可以

  • 获得Web项目中的共享数据 Attributes...
  • 获得初始化参数 InitParameters ,InitParameterNames
  • 获得项目资源 Resources
  • ……

# ServletRequest

该接口为Servlet对Request请求的封装。

public interface ServletRequest {
    public Object getAttribute(String name);

    public String getParameter(String name);

    public Map<String, String[]> getParameterMap();

    public String getRemoteAddr();

    public String getRemoteHost();

    public void setAttribute(String name, Object o);

    public void removeAttribute(String name);

    public ServletContext getServletContext();

    // ...
    }

具体在一次Tomcat Servlet 请求中的实现类为 RequestFacade 包装了 org.apache.catalina.servlet4preview.http.HttpServletRequest 这个类

都是从 容器Tomcat 中传过来的对象,实现了 HttpServletRequest 接口,亦即是在 Servlet 开发中处理 Request 是通常使用的接口。

2

继续追溯 Request 类 的关系

3

大概可以得到如下关联,其中最后两个为Tomcat中的Connect 提供的类,Facade为其门面类,均继承自 javax.servlet.ServletRequest 接口, 而另外一个 org.apache.coyote.Request 为 Tomcat Processor 收到请求时候对请求的 初次封装(处理 socket 则由另外的包工具进行,再封装成该 Request 类)。

这样的互相转化,在 Tomcat org.apache.coyote.Adapter 中进行。


public void service(org.apache.coyote.Request req,org.apache.coyote.Response res)throws Exception {
\tRequest request = (Request) req.getNote(ADAPTER_NOTES);
\tResponse response = (Response) res.getNote(ADAPTER_NOTES);

\tif (request == null) {
\t\trequest = connector.createRequest();
\t\trequest.setCoyoteRequest(req);
\t\tresponse = connector.createResponse();
\t\tresponse.setCoyoteResponse(res);

\t\trequest.setResponse(response);
\t\tresponse.setRequest(request);

\t\treq.setNote(ADAPTER_NOTES, request);
\t\tres.setNote(ADAPTER_NOTES, response);
}

// ......

Coyote 为 Tomcat 处理从 SocketRequest 的框架,并包装成 Tomcat 自定义的 Request 类,没有继承和实现任何接口,供 Tomcat 自身使用,同时 Tomcat 还讲其包装成实现于 javax.servlet.ServletRequest 接口的类供开发者使用。关于Tomcat 更底层的内容,本篇尚不讨论。

# ServletResponse

Response 类的原理和Request不会差的太远,继承和实现的关系也差不都类似的结构。

# 五、再说 Tomcat 容器

Servlet 容器和 Servlet 互相独立发展又彼此依存,通过标准化的接口互相协作,我们已经把接口的关系差不多了解清楚了,再回到刚开始介绍的 Tomcat 容器的内部结构。

4

如图,从外到里,整个 Tomcat 生命周期由 Server 层控制, 包含多层 Service ,然后就是 各个 Connector ,再到一个Container 核心组件。Service 可以有多个 Connectors ,但是只能有一个 Container 。

为了更易理解,我们先不从源码阅读,我们从初始配置文件 server.xml 开始阅读。

<Server port="8005" shutdown="SHUTDOWN">
  <!-- ... -->
</Server>

最外层 Service 为 Tomcat 关闭服务 使用 8005 端口,所以一种关闭 Tomcat 的方法为 telnet 到 8005 端口执行 SHUTDOWN 命令。然后为各 Listener、GlobalNamingResources 等……不多介绍

<Service name="Catalina">
    <!-- ... -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"/>

    <!-- ... -->
    <Engine name="Catalina" defaultHost="localhost">
    <!-- ... -->
    </Engine>
</Service>

第二层为熟悉的 Catalina Service ,包含了多个 Connector 和 一个 Container 。Tomcat将Engine,Host,Context,Wrapper统一抽象成Container。

Connector 从配置文件看就了解到它负责接收浏览器的发过来的 tcp 连接请求。除此,还将创建一个 Request 和 Response 对象分别用于和请求端交换数据。

5

再到了 Engine 层

<Engine name="Catalina" defaultHost="localhost">
    <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 &quot;%r&quot; %s %b"/>
    </Host>
    </Engine>

一个 Engine 代表一个完整的 Servlet 引擎,并将传过来的请求选择到一个 Host 处理。一个 Engine 可以有多个Host,没个 Host 亦即虚拟主机,初始配置文件只有一个 localhost 的主句, 通常来讲每个 Host 对应一个域名。

Host不是必须的,但是要解析 web.xml 必须要带有HOST

Host 往下为 Context

<Context
    path="/library"
    docBase="/.../library.war"
    reloadable="true"
    />

初始配置文件没有添加 web应用 所以没有 <Context/> 标签, 而一个 Context 对应一个 Web应用,也就是开头提到的 ServletContext

Context 以下就是 Wrapper 层,Wrapper 是对 Servlet 的包装,所以一个 Context 可以有多个 Wrapper , Wrapper 已经是 Tomcat 最底层的容器的。

为了管理 Tomcat 组件的生命周期,Tomcat 将其抽象为接口 Lifecycle

package org.apache.catalina;

/**
 * <pre>
 *            start()
 *  -----------------------------
 *  |                           |
 *  | init()                    |
 * NEW -»-- INITIALIZING        |
 * | |           |              |     ------------------«-----------------------
 * | |           |auto          |     |                                        |
 * | |          \\\\|/    start() \\\\|/   \\\\|/     auto          auto         stop() |
 * | |      INITIALIZED --»-- STARTING_PREP --»- STARTING --»- STARTED --»---  |
 * | |         |                                                            |  |
 * | |destroy()|                                                            |  |
 * | --»-----«--    ------------------------«--------------------------------  ^
 * |     |          |                                                          |
 * |     |         \\\\|/          auto                 auto              start() |
 * |     |     STOPPING_PREP ----»---- STOPPING ------»----- STOPPED -----»-----
 * |    \\\\|/                               ^                     |  ^
 * |     |               stop()           |                     |  |
 * |     |       --------------------------                     |  |
 * |     |       |                                              |  |
 * |     |       |    destroy()                       destroy() |  |
 * |     |    FAILED ----»------ DESTROYING ---«-----------------  |
 * |     |                        ^     |                          |
 * |     |     destroy()          |     |auto                      |
 * |     --------»-----------------    \\\\|/                         |
 * |                                 DESTROYED                     |
 * |                                                               |
 * |                            stop()                             |
 * ----»-----------------------------»------------------------------
 *
 * @author Craig R. McClanahan
 */
 public interface Lifecycle {
    public void init() throws LifecycleException;

    public void start() throws LifecycleException;

    public void stop() throws LifecycleException;

    public void destroy() throws LifecycleException;

    // ...
}

# 六、 web.xml

web.xml 称为部署描述符文件,在Servlet规范中定义的,是web应用的配置文件,虽然如今已经不是必须的了(基于注解),但是依然有必要提到一下。

该文件定义了web应用的基本配置,例如 servlet、filter、mapping。Tomcat 在 context.xml 中 定义了该文件的寻找路径,寻找到后将会被解析和添加到Context。

如下为一个标准的 spring web.xml 文件实例

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:web="http://java.sun.com/xml/ns/javaee" xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    id="WebApp_ID" version="3.0">

    <display-name>console</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
           classpath:spring.xml,classpath:spring-mybatis.xml,classpath:spring-shiro.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>字符集编码</description>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <description>spring mvc servlet</description>
        <servlet-name>springMvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <description>spring mvc 配置文件</description>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springMvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>60</session-timeout>
    </session-config>
</web-app>

如图,

  • Context 初始化时候加载了 Spring IOC 容器
  • 添加了字符集编码的 filter
  • 添加了Spring MVC 的 DispatcherServlet 并将 Spring IOC 容器作为其父容器
  • load-on-startup 的值为 1 ,该 Servlet 随着项目初始化而初始化init()

# 总结

本文主要目的是,从 Servlet 云里雾里的开发,到对 Servlet 有基本的认识, Tomcat 容器的讲解一直不是重点,只是帮助理解 Servlet 的工作状态。至于从源码分析一切的实现,亦不是本文讨论的重点。

所以本文涉及的内容不会深奥,而且 Tomcat 如此庞大的源码, Servlet 的细节非常多,本人知识能力还不足以一一阐述清楚。

Tomcat 的设计是非常遵守不少设计模式,例如 listener——观察者模式,Engine-->Host-->Context——责任链模式,学习设计模式,会对阅读 Tomcat 源码有很大帮助。


参考链接:

Servlet 工作原理解析 (许 令波) (opens new window)

Tomcat 系统架构与设计模式 (许 令波) (opens new window)