Servlet 规范

HTTP 服务器

过程: HTTP -> HTTP Server -> Servlet 容器 -> Servlet 接口 -> Servlet 连接的业务类

解耦

网页发送的HTTP 格式的请求, 服务端收到请求, 需要业务类来处理, 业务类也就是我们写的service类; 但这样业务类就和 HTTP服务器强耦合了.

  • 为了解决耦合, 于是一群人就定义了一个接口, 业务类都实现这个接口, 也就是 Servlet 接口, 实现类也叫作 Servlet.

  • 但还有一个问题, 不同请求需要不同的类处理, http服务器如何知道调用哪个 Servlet 呢, Servlet 又是由谁来实例化呢, 显然交给 HTTP 服务器还是耦合了

为了解决实例化和派发问题, 就发明了 Servlet 容器, 容器用来加载和管理业务类. 这样 HTTP服务器就不直接跟业务类打交道, 把请求交给Servlet容器, 容器再转发给具体的Servlet, 如果没创建, 就加载并实例化这个 Servlet, 然后去调用 Servlet 接口方法去执行业务

Servlet 规范

因此 Servlet 接口和 Servlet 容器的出现, 达到了 HTTP 服务器与业务类解耦的目的. Servlet 接口Servlet 容器 这一整套规范就叫作 Servlet 规范. Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器, 同时也具有 HTTP 服务器的功能. 如果我们要实现新的业务功能, 只需要实现一个 Servlet 并注册到 Tomcat(Servlet 容器)中, 剩下的事情就由 Tomcat 帮我们处理了

Servlet 接口

public interface Servlet {
    void init(ServletConfig config) throws ServletException;
    
    ServletConfig getServletConfig();
    
    void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
    
    String getServletInfo();
    
    void destroy();
}

service 方法

其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法的两个参数 ServletRequest 用来封装请求信息 ServletResponse 用来封装响应信息, 本质上是对通信协议的封装.

在系统设计里, 有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑,因此 Servlet 规范提供了 GenericServlet 抽象类,我们可以通过扩展它来实现 Servlet。

虽然 Servlet 规范并不在乎通信协议是什么,但是大多数的 Servlet 都是在 HTTP 环境中处理的, 因此 Servet 规范还提供了 HttpServlet 来继承 GenericServlet, 并且加入了 HTTP 特性. 这样我们通过继承 HttpServlet 类来实现自己的 Servlet, 只需要重写两个方法: doGet 和 doPost. 里面的方法参数对应了 HTTP 协议 (HttpServletRequest、HttpServletResponse 类)

  • HttpServletRequest 来获取所有请求相关的信息,包括 1请求路径、2Cookie、3HTTP 头、4请求参数等, 还可以 5创建和获取 Session
  • HttpServletResponse 则是用来封装 HTTP 响应的

init 方法 & destroy 方法

Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法. 可以通过init 方法初始化一些资源, destroy 方法里面释放这些资源.

Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Spring 容器.

getServletConfig 方法

主要是得到 ServletConfig 这个返回值, ServletConfig 类的作用就是封装 Servlet 的初始化参数。可以在 web.xml 给 Servlet 配置参数,并在程序里通过 getServletConfig 方法拿到这些参数。

Servlet 容器

HTTP Server 将请求交给容器, 容器如何工作呢?
在这里插入图片描述

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

Web 应用

Servlet 容器会实例化和调用 Servlet,那 Servlet 是怎么注册到 Servlet 容器中的呢?

一般来说,我们是以 Web 应用程序的方式来部署 Servlet 的,而根据 Servlet 规范,Web 应用程序有一定的目录结构,在这个目录下分别放置了 Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet

Web 应用的目录结构:

| -  MyWebApp
      | -  WEB-INF/web.xml        -- 配置文件,用来配置Servlet等
      | -  WEB-INF/lib/           -- 存放Web应用所需各种JAR包
      | -  WEB-INF/classes/       -- 存放你的应用类,比如Servlet类
      | -  META-INF/              -- 目录存放工程的一些信息

Servlet 规范里定义了 ServletContext 这个接口来对应一个 Web 应用

Web 应用部署好后,Servlet 容器在启动时会加载 Web 应用, 并为每个 Web 应用创建唯一的 ServletContext 对象

你可以把 ServletContext 看成是一个全局对象,一个 Web 应用可能有多个 Servlet,这些 Servlet 可以通过全局的 ServletContext 来共享数据,这些数据包括 Web 应用的初始化参数、Web 应用目录下的文件资源等

由于 ServletContext 持有所有 Servlet 实例,你还可以通过它来实现 Servlet 请求的转发

扩展机制

引入了 Servlet 规范后, 不需要关心 Socket 网络通信, 不需要关心 HTTP 协议, 也不需要关心你的业务类是如何被实例化和调用的, 因为这些都被 Servlet 规范标准化了, 你只要关心怎么实现的你的业务逻辑. 但设计一个规范或者一个中间件,要充分考虑到可扩展性。

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

  • Filter 是干预过程的,它是过程的一部分,是基于过程行为的。
    Filter 过滤器接口, 对请求和响应做一些统一的定制化处理, 比如根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容.
    工作原理: FilterChain链. 在 web 应用部署完成后, Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain. 当请求进来时, 获取第一个 Filter 并调用 doFilter 方法, doFilter 方法负责调用这个 FilterChain 中的下一个 Filter.
  • Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致的。
    Listener 监听器接口, 当 web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件, 如 Web 应用的启动和停止, 用户请求到达等. Servlet 容器提供了一些默认的监听器来监听这些事件, 当事件发生时 Servlet 容器会负责调用监听器的方法. 当然也可以自定义监听器去监听感兴趣的事件, 将监听器配置在 web.xml 中. 比如 Spring 实现了自己的监听器, 来监听 ServletContext 的启动事件, 目的是当 Servlet 容器启动时, 创建并初始化全局的 Spring 容器.

ps: Spring 中提供了一个 interceptor 拦截器, 不属于 Servlet 规范;
Spring中,web应用启动的顺序是:listener->filter->servlet.
关键类有 DelegatingFilterProxy、DispathServlet;

Tomcat 服务器

Tomcat 的整体架构包含了两个核心组件连接器容器

连接器 负责 对外交流: 处理 Socket 连接, 负责网络字节流与 Request 和 Response 对象的转化
容器 负责 内部处理: 加载和管理 Servlet, 以及具体处理 Request 请求

网络通讯需要解决两大问题: 网络I/O、解析协议

Tomcat 支持的 I/O 模型有:

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

Tomcat 支持的应用层协议有:

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

为了支持多种 I/O 模型应用层协议, tomcat 实现为一个容器 对接 多个连接器的方式. 其中 连接器 或者 容器 不单独对外服务, 需要组装一起才工作, 组装后整体叫作 Service 组件 (注意 Service 组件本身不做事, 仅将连接器和容器组装在一起包裹一层)

  • 一个 Server 组件代表一个 Tomcat 实例
  • 一个 Service 组件中有多个连接器和一个容器
  • 连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通信

出于灵活性考虑, 设计一个 Tomcat 实例包含多个 Service 组件. 通过在 Tomcat 中配置多个 Service 组件, 能实现通过不同端口号 访问 同一台服务器 上不同应用.

图片

连接器

  • 作用: 连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别, 无论是 HTTP 还是 AJP, 在容器中获取到的都是一个标准的 ServletRequest 对象。

将连接器需求进一步细化:

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

设计: 连接器 应该有哪些子模块?优秀模块化设计 应考虑高内聚低耦合

  • 高内聚是指相关度高的功能要尽可能集中, 不要分散
  • 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度, 不让两个模块产生强依赖

分析: 通过分析连接器的详细功能列表, 我们发现连接器需要完成 3 个高内聚的功能

  1. 网络通信
  2. 应用层协议解析
  3. Tomcat Request (Response) 与 ServletRequest (ServletResponse) 的转化

因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能
分别是: 1. Endpoint、2. Processor 和 3. 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 或者 NIO.2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑, 设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。

比如:Http11NioProtocolAjpNioProtocol。除了这些变化点, 系统也存在一些相对稳定的部分, 因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分, 抽象基类 AbstractProtocol 实现了 ProtocolHandler 接口。

此外, 每一种「应用层协议」都有自己的抽象基类 (比如 AbstractAjpProtocol 和 AbstractHttp11Protocol) 具体协议的实现类扩展了协议层抽象基类

  • 它们的继承关系:
    图片
    将稳定的部分放到抽象基类, 同时每一种 I/O 模型和协议的组合都有相应的具体实现类, 我们在使用时可以自由选择

  • 设计时 Endpoint 组件 和 Processor 组件 放在一起抽象成 ProtocolHandler 组件

  • 两个组件 ProtocolHandlerAdapter 组成顶层组件

图

小总结

ProtocolHandler 组件

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

  1. Endpoint

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

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

  • Acceptor 用于监听 Socket 连接请求。

  • SocketProcessor 用于处理接收到的 Socket 请求, 实现了 Runnable 接口, 在 run 方法里调用协议处理组件 Processor 进行处理. 为了提高处理能力, SocketProcessor 被提交到线程池来执行, 而这个线程池叫作执行器(Executor)

图片

  1. Processor

对应用层协议的抽象, Processor 接收来自 Endpoint 的 Socket, 读取字节流解析成 Tomcat Request 和 Response 对象 (最后通过 Adapter 将其提交到容器处理)

Processor 是一个接口, 定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装, 没有对方法进行实现。具体的实现有 AjpProcessor、Http11Processor 等, 这些具体实现类实现了特定协议的解析方法和请求处理方式
图
Endpoint 接收到 Socket 连接后, 生成一个 SocketProcessor 任务提交到线程池去处理, SocketProcessor (run 方法) 调用 Processor 组件去解析应用层协议, Processor 通过解析生成 Request 对象后, 调用 Adapter 的 Service 方法

Adapter 组件

Tomcat 定义了自己的 Request 类来存放请求信息, ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类, 但是这个 Request 对象不是标准的 ServletRequest, 就需要进行一遍转换.

Tomcat 的解决方案就是引入 CoyoteAdapter, 连接器调用 CoyoteAdapter 的 sevice 方法, 传入的是 Tomcat Request 对象, CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest, 再调用容器的 service 方法

连接器直接创建 ServletRequest 和 ServletResponse 对象就和 Servlet 协议耦合了, 设计者认为连接器尽量保持独立性, 它不一定要跟Servlet容器工作.

另外, 对象转化的性能消耗较大, Tomcat 对 HTTP 请求体采取延迟解析策略, 也就是说, TomcatRequest 对象转化成 ServletRequest 的时候, 请求体内容还未读取, 直到容器处理请求时才读取

连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异, ProtocolHandler 内部又分为 Endpoint 和 Processor 模块, Endpoint 负责底层 Socket 通信, Processor 负责应用层协议解析。连接器通过适配器 Adapter 调用容器。

容器

容器则负责处理 Servlet 请求,

层次结构

Tomcat 设计了 4 种容器: EngineHostContext Wrapper 分别是包含关系
在这里插入图片描述

通过分层的架构,使得 Servlet 容器具有很好的灵活性

  • Wrapper => 一个 Servlet (可能会有多个 Servlet)

  • Context => 一个 Web 应用程序;

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

  • Engine => 管理多个虚拟站点 (一个 Service 最多只能有一个 Engine)

通过 server.xml配置文件来理解, 采用的组件化思想,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中

<Server> 包含 多个Service
    <Service> 包含 一个 Engine、多个连接器
连接器组件<Connector> 代表通信接口
        </Connector>
容器组件 <Engine> 一个 Engine组件 处理 Service 所有请求, 包含多个Host
            <Host> 处理特定 Host 下客户请求, 包含多个 Context
                <Context> 为特定 Web应用 处理所有客户端请求
                </Context>
            </Host>
        </Engine>
     </Service>
</Server>

这些层级形成一个树形结构

Container 接口定义如下:

public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet

一个后台管理系统,一个前台系统, 同时跑在同一个 Tomcat 上, 为了隔离它们的访问域名,配置了两个虚拟域名:manage.shopping.com和user.shopping.com,网站管理人员通过manage.shopping.com域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用

终端客户通过user.shopping.com域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。针对这样的部署,Tomcat 会创建一个 Service 组件和一个 Engine 容器组件,在 Engine 容器下创建两个 Host 子容器,在每个 Host 容器下创建两个 Context 子容器

由于一个 Web 应用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创建多个 Wrapper 子容器,每个容器都有对应的访问路径
在这里插入图片描述

如何定位 URL 到一个 Servlet (http://user.shopping.com:8080/order/buy):

  1. 根据协议和端口号选定 Service 和 Engine
  2. Tomcat 的每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。URL 访问的是 8080 端口, 因此会被HTTP连接器接收. 一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了
  3. 我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件 (具体就是一个 Engine 容器) 因此 Service 确定了也就意味着 Engine 也确定了。然后根据域名选定 Host
  4. Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比如例子中的 URL 访问的是user.shopping.com,因此 Mapper 会找到 Host2 这个容器
  5. 之后根据 URL 路径找到 Context 组件
  6. Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访问的是/order,因此找到了 Context4 这个 Context 容器
  7. 最后,根据 URL 路径找到 Wrapper(Servlet)。Context 确定后,Mapper 再根据web.xml中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。

需要注意的是,并不是说只有 Servlet 才会去处理请求,实际上这个查找路径上的父子容器都会对请求做一些处理。

连接器中的Adapter会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。

  • 调用过程具体是使用 Pipeline-Valve 管道实现的. Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程
public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。继续看 Pipeline 接口:

public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}
  1. Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理
  2. Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用getNext.invoke来触发下一个 Valve 调用
  3. 每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到
  4. 但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。这是因为 Pipeline 中还有个 getBasic 方法
  5. 这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve

在这里插入图片描述

整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve:

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

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

如果需要扩展容器本身的功能,只需要增加相应的 Valve 即可

  • 与 filter 的区别

    • Valve 是 Tomcat 的私有机制,与 Tomcat 的基础架构 API 是紧耦合的。Servlet API 是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。
    • 另一个重要的区别是 Valve 工作在 Web 容器级别,拦截所有应用的请求, 而 Servlet Filter 工作在应用级别,只能拦截某个 Web 应用的所有请求. 如果想做整个 Web 容器的拦截器,必须通过 Valve 来实现
  • Tomcat 的 Context 组件跟 Servlet 规范中的 ServletContext 接口有什么区别, 跟 Spring 中的 ApplicationContext 又有什么关系

    • Servlet 规范中 ServletContext 表示web应用的上下文环境,而 web 应用对应 tomcat 的概念是 Context,所以从设计上,ServletContext 自然会成为 tomcat 的 Context 具体实现的一个成员变量

      • tomcat 内部实现也是这样完成的,ServletContext 对应 tomcat 实现是org.apache.catalina.core.ApplicationContext,Context 容器对应 tomcat 实现是org.apache.catalina.core.StandardContext
        ApplicationContextStandardContext 的一个成员变量
    • tomcat 启动过程中 ContextLoaderListener 会监听到容器初始化事件,它的 contextInitialized方法 中,Spring 会初始化全局的 Spring 根容器 ApplicationContext,初始化完毕后,Spring将其存储到 ServletContext

  • 总而言之,Servlet 规范中 ServletContext 是 tomcat 的 Context 实现的一个成员变量,而Spring 的 ApplicationContext 是 Servlet 规范中 ServletContext 的一个属性 ApplicationContext < ServletContext (ApplicationContext) < Context (StandardContext)

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值