参考:

 一:Tomcat核心组件及应用架构详解

Hong EuiSung –@gowoonosori :  요청처리 내부구조

码哥字节Tomcat 架构原理解析到架构设计借鉴

编程随笔: 细说tomcat之类加载器

tomcatDoc: Class Loader How-To

 

Http 请求响应实例

 

 

Servlet 接口

为了解耦 【http服务器代码】和【业务代码】,Java就定义了一个接口,各种业务类都必须实现这个接口,这个接口就叫 Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet。

Servlet 接口定义了下面五个方法

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}
  • 其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。
    • ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。
    • 比如 HTTP 协议中的请求和响应就是对应了 HttpServletRequest 和 HttpServletResponse 这两个类。你可以通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等。还可以通过 HttpServletRequest 来创建和获取 Session。而 HttpServletResponse 是用来封装 HTTP 响应的。
  • 两个跟生命周期有关的方法 init 和 destroy
    • Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法。我们可能会在 init 方法里初始化一些资源,并在 destroy 方法里释放这些资源,
    • 比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Servlet WebApplicationContext
  • 还有 ServletConfig 这个类,ServletConfig 的作用就是封装 Servlet 的初始化参数。
    • 你可以在 web.xml 给 Servlet 配置参数,并在程序里通过 getServletConfig 方法拿到这些参数。

有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑。

(接口和抽象类都不能实例化,接口只能有方法的声明不能有实现,抽象类可以有方法的实现。接口用来抽象功能,抽象类用来抽象类型)

因此 Servlet 规范提供了 GenericServlet 抽象类,我们可以通过扩展它来实现 Servlet。虽然 Servlet 规范并不在乎通信协议是什么,但是大多数的 Servlet 都是在 HTTP 环境中处理的,因此 Servet 规范还提供了 HttpServlet 来继承 GenericServlet,并且加入了 HTTP 特性。这样我们通过继承 HttpServlet 类来实现自己的 Servlet,只需要重写两个方法:doGet 和 doPost。

HttpServlet 实现的 Servlet 声明的业务方法接口 service 方法里会调用 doGet 和 doPost

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                .......省略
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }

 

Servlet 容器

当客户请求某个资源时:

  • HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法
  • Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系找到相应的 Servlet如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器
  • HTTP 服务器会把响应发送给客户端

扩展机制

Servlet 规范提供了两种扩展机制:Filter 和 Listener。

Filter 

允许你对请求和响应做一些统一的定制化处理。过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter。

比如你可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。还有 SpringSecurity 的 SessionRepositoryFilter 就 implements 了 Filter,用来校验请求头中的 session

Listener 

当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。

比如 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。

 

Servlet容器 与 SpringMVC 的关系

在 SpringMVC 中,有一个名为 DispatcherServlet 的 Servlet,它充当 FrontController,转发、匹配、将请求委托给其他控制器来处理请求。

执行service(),并在方法中执行 dispatch.doService() -> doDispatch(),doDispatch 负责 从 HandlerMapping 中通过 URL 获取handler

在应用启动的时候,Spring 容器会加载这些 Controller 类,并且解析出 URL 对应的处理函数,封装成 Handler 对象,存储到 HandlerMapping 对象中。当有请求到来的时候,DispatcherServlet 从 HanderMapping 中,查找请求 URL 对应的 Handler,然后调用执行 Handler 对应的函数代码,最后将执行结果返回给客户端。

Root WebApplicationContext

  • 就是 org.springframework.context.ApplicationContext
  • 就是 Spring 的 IOC 容器,包括 Service、datasource、repositories 等 bean
  • 通过 Servlet 容器的 Listener 机制Spring 的 ContextLoaderListener 类实现 ServletContextListener,监听 Servlet 容器的启动事件,创建并初始化这个 spring 全局上下文。
  • 管理bean的生命周期并提供 IOC/DI
  • Root WebApplicationContext 不能访问 Servlet WebApplicationContext 中的 bean(Controller 里可以访问 Service 对象,Service 对象不可以访问 Controller 对象)

Servlet WebApplicationContext

  • 就是 org.springframework.web.context.WebApplicationContext
  • 与标准 javax.servlet.ServletContext 配合使用。ServletContext 实例可以做很多事情,例如通过调用 getResourceAsStream() 方法访问 WEB-INF 资源(xml 配置等)。
  • 继承自 RootWebApplicationContext 实现的 Context,主要包含 Controller、Intercepter、ViewResolver、HandlerMapping 等bean。
  • 之所以有这样的继承关系,是因为一个Servlet容器内部可能会出现多个Servlet,它们要共用 Root WebApplicationContext 中的服务和数据源。
  • 延迟加载,当第一个请求到来,Tomcat 发现 DispatcherServlet 还没被实例化,就会调用它的 init 方法,建立容器初始化相关 bean
  • 如果一个servlet 上下文的 bean注册了与Application Context和context相同的ID,则使用Servlet Context中声明的bean,查找 bean 时按照 Servlet Context -> Application Context的顺序查找。
public interface WebApplicationContext extends ApplicationContext {
    ServletContext getServletContext();
}

从服务器启动到请求的步骤

  1. Web 服务器初始化
  2. 监听到 Servlet 容器启动。加载 Root WebApplicationContext
  3. 网络服务器启动
  4. 客户端第一次向 Web 服务器发出请求
  5. Web Server 交付给 Servlet 容器
  6. 创建 Servlet线程
  7. 如果 DispatcherServlet 没有创建,DispatcherServlet init 初始化 Servlet WebApplicationContext
  8. 在生成的线程中调用 DisapatcherServlet 的 service() 方法。
  9. 通过 HandlerMapping 查找 Controller
  10. 通过 HandlerAdapter 传递给 Controller
  11. Controller->Service->Response

Filter 与 Intercepter

Filter 是 Servlet 规范的一部分,Servlet 容器 Tomcat 实现了它;Intercepter 是 Spring 实现的。它们的执行顺序是

  • Filter.doFilter();
  • HandlerInterceptor.preHandle();
  • Controller
  • HandlerInterceptor.postHandle();
  • HandlerInterceptor.afterCompletion();
  • Filter.doFilter()
  • Servlet 方法返回

 

Tomcat 配置

 

Tomcat 总体架构

Tomcat 实现的 2 个核心功能:

  • 连接器(Connector):处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化
  • 容器(Container):加载并管理 Servlet ,以及处理具体的 Request 请求。

所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部处理

  • Service 默认只有一个,也就是一个 Tomcat 实例默认一个 Service。
  • Connector:一个 Service 可能多个连接器,接受不同连接协议。
  • Container: 多个连接器对应一个容器,顶层容器其实就是 Engine。

Tomcat 为了实现支持多种 I/O  模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。

连接器

连接器主要完成三个功能:

  • EndPoint :网络通信负责提供字节流给 Processor
  • Processor(Coyote):应用层协议解析。负责提供 Tomcat Request 对象给 Adapter
  • Adapter:Tomcat request/response 与 ServletRequest/ ServeletReponse 的转换。负责提供 ServletRequest对象给容器。

主要处理 网络连接应用层协议 的两个重要部件 EndPoint 和 Processor 组合形成 ProtocoHandler

Tomcat 支持的 I/O   模型有:

  • NIO:非阻塞 I/O,采用 Java NIO 类库实现。
  • NIO2:异步I/O,采用 JDK 7 最新的 NIO2 类库实现。
  • APR:采用 Apache可移植运行库实现,是 C/C++ 编写的本地库。

Tomcat 支持的应用层协议有:

  • HTTP/1.1:这是大部分 Web 应用采用的访问协议。
  • AJP:用于和 Web 服务器集成(如 Apache)。
  • HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。

容器(Engine)

Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。因为这里正好符合一个 Host 多个 Context, 一个 Context 也包含多个 Servlet,而每个组件都需要统一生命周期管理,所以组合模式设计这些容器

server.xml

<Server port="8005" shutdown="SHUTDOWN"> // 顶层组件,可包含多个 Service,代表一个 Tomcat 实例
    <Service name="Catalina">  // 顶层组件,包含一个 Engine ,多个连接器
      // 连接器1:http 协议
        <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
      // 连接器2:AJP 协议
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
      // 容器组件:一个 Engine 处理 Service 所有请求,包含多个 Host
        <Engine name="Catalina" defaultHost="localhost">
          //容器组件:处理指定Host下的客户端请求, 可包含多个 Context
            <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
              // 容器组件:处理特定 Context Web应用的所有客户端请求
                <Context>
                </Context>
            </Host>
        </Engine>
    </Service>
</Server>

Wrapper 就是 Servlet。Servlet 的映射可以通过配置文件配置 web.xml。这个里面也可以配置 Filter

<servlet>
    <servlet-name>DownloadServlet</servlet-name>
    <servlet-class>
        com.test.servlet.DownloadServlet
    </servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>DownloadServlet</servlet-name>
    <url-pattern>/download</url-pattern>
</servlet-mapping>    

也可以通过注解

@WebServlet(urlPatterns = "/download")
public class FetchMessageServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //......
    }

Mapper组件里保存了 Web 应用的配置信息。当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。

 

启动流程

  • Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本会启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。
  • Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。关于 Tomcat 为什么需要自己的类加载器,后面详细介绍。
  • Catalina 是一个启动类,它通过解析 server.xml、创建相应的组件,并调用 Server 的 start 方法。
  • Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法。
  • Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。

 

 

Tomcat 类加载器

类加载前情知识

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

对于任意一个类,都必须由 加载它的类加载器 和 这个类本身一起共同确立其在Java虚拟机中的唯一性。即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分
    • 负责加载存放在 <java_home>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,用来加载 JVM 启动时所需要的核心类,比如rt.jar、resources.jar等。
  • 其他所有的类加载器,这些类加载器都由Java语言实现独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
    • 扩展类加载器(Extension ClassLoader)负责加载<java_home>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
    • 用户类加载器(Application ClassLoader)加载用户类路径 (ClassPath)上所有的类库

自JDK 1.2以来,Java一直保 持着三层类加载器、双亲委派的类加载架构。

  • 双亲委派模型的工作过程是:
    • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
  • 好处
    • 很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载
    • Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
    • 例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类程序的各种类加载器环境中都能够保证是同一个类
  • 反之
    • 如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。

 

Tomcat 类加载器

tomcat6 之前

从图中的委派关系中可以看出(父类加载器类库的可以被子类加载器使用,同级类加载器的类库相互隔离)

  • CommonClassLoader 用来加载放置在/common目录中的。
    • 类库可被Tomcat和所有的Web应用程序共同使用。
  • CatalinaClassLoader(也称为 ServerClassLoader)用来加载放置在/server目录中的。
    • 类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • SharedClassLoader 用来加载放置在/shared目录中的。
    • 类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。(实现了两个Web应用程序 所使用的 Java公共类库可以互相共享
  • WebappClassLoader 用来加载放置在/WebApp/WEB-INF目录中的。
    • 类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应 用程序都不可见。(实现了两个Web应用程序 所使用的 Java类库可以实现相互隔离

 tomcat6 以后

Java ClassLoader:                         
    Bootstrap ClassLoader(加载$JAVA_HOME/jre/lib/目录下核心类库:resources.jar,rt.jar,sunrsasign.jar,jsse.jar,jce.jar,charsets.jar,jfr.jar,以及jre/classes目录下的class)                 
       /|\
        |                                                            Tomcat ClassLoader:      
    ExtClassLoader(加载$JAVA_HOME/jre/lib/ext/目下的所有jar) -------- Bootstrap(加载$JAVA_HOME/jre/lib/ext/目录下的所有jar)
       /|\                                                                 /|\
        |                                                                   |     
    AppClassLoader(加载应用程序classpath目录下的所有jar和class文件)    System(加载$CATALINA_HOME/bin/bootstrap.jar,$CATALINA_BASE/bin/tomcat-juli.jar,$CATALINA_HOME/bin/commons-daemon.jar)
                                                                           /|\
                                                                            |
                                                                         Common(加载$CATALINA_BASE/lib和$CATALINA_HOME/lib下的class,资源和jar文件)
                                                                           /|\
                                                                            |
                                                                         WebAppClassLoader(多个平级的,加载 WebApp/WEB-INF/classes,WebApp/WEB-INF/lib

WebAppClassLoade默认不使用“双亲委派机制”(根据 Servlet 规范 2.4 版第 9.7.2 Web 应用程序类加载器第 9.7.2 节中的建议),查找class和资源的顺序如下:

  • Bootstrap classes of your JVM
  • /WEB-INF/classes of your web application
  • /WEB-INF/lib/*.jar of your web application
  • System class loader classes
  • Common class loader classes

如果 Web 应用程序类加载器配置为 <Loader delegate="true"/> ,则顺序变为双亲委派的顺序

第二段:建议应用类加载器优先加载 Web 应用自己定义的类

 

 

 

Tomcat 实现细节

 

异步 Servlet