03 | 你应该知道的 Servlet 规范和 Servlet 容器_servlet规范和servlet容器-程序员宅基地

技术标签: 深入拆解Tomcat  java  容器  服务器  


浏览器发给服务端一个 HTTP 格式的请求,HTTP 服务器收到请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的 Java 类,一般来说不同的请求需要由不同的 Java 类来处理。

HTTP 服务器怎么知道要调用哪个 Java 类的哪个方法呢。最直接的做法是在 HTTP 服务器代码里写一大堆 if else 逻辑判断:如果是 A 请求就调 X 类的 M1 方法,如果是 B 请求就调 Y 类的 M2 方法。但这样做明显有问题,因为 HTTP 服务器的代码跟业务逻辑耦合在一起了,如果新加一个业务方法还要改 HTTP 服务器的代码。

怎么解决这个问题呢?面向接口编程是解决耦合问题的法宝,于是有一伙人就定义了一个接口,各种业务类都必须实现这个接口,这个接口就叫 Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet

对于特定的请求,HTTP 服务器如何知道由哪个 Servlet 处理呢?Servlet 又是由谁来实例化呢?显然 HTTP 服务器不适合做这个工作,否则又和业务类耦合了。

于是又发明了 Servlet 容器,Servlet 容器用来加载和管理业务类。HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器处理,Servlet 容器将请求转发到具体的 Servlet,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。因此 Servlet 接口其实是 Servlet 容器跟具体业务类之间的接口。下面我们通过一张图来加深理解。
在这里插入图片描述
图的左边表示 HTTP 服务器直接调用具体业务类,它们是紧耦合的。图的右边,HTTP 服务器不直接调用业务类,而是把请求交给容器,容器加载并调用业务类的Servlet接口方法。Servlet 接口和 Servlet 容器的出现,达到了 HTTP 服务器与业务类解耦的目的。

Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范。Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器,同时它们也具有 HTTP 服务器的功能。作为 Java 程序员,如果我们要实现新的业务功能,只需要实现一个 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 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。

HTTP 协议中的请求和响应就是对应了 HttpServletRequestHttpServletResponse 这两个类。你可以通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等。此外,我在专栏上一期提到过,我们还可以通过 HttpServletRequest 来创建和获取 Session。而 HttpServletResponse 用来封装 HTTP 响应

接口中还有两个跟生命周期有关的方法 init 和 destroy,这是一个比较贴心的设计,Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法。我们可能会在 init 方法里初始化一些资源,并在 destroy 方法里释放这些资源,比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建自己的 Spring 容器

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

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

Servlet 容器

Servlet 容器是怎么工作的呢?接下来我会介绍 Servlet 容器大体的工作流程,一起来聊聊我们非常关心的两个话题:Web 应用的目录格式是什么样的,以及我该怎样扩展和定制化 Servlet 容器的功能

工作流程

当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 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 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理,比如你可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter

Listener 是监听器,这是另一种扩展机制。当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。如 Spring 就实现了自己的监听器ContextLoaderListener,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。缺省情况时在WEB-INF/applicationContext.xml文件找Spring的配置。通过定义一个元素名字为”contextConfigLocation”来改变Spring配置文件的位置(重命名applicationContext)。

最后我给你总结一下 Filter 和 Listener 的本质区别:Filter 是干预过程的,它是过程的一部分,是基于过程行为的。Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致的。

到这里相信你对 Servlet 容器的工作原理有了深入的了解,只有理解了这些原理,我们才能更好的理解 Tomcat 和 Jetty,因为它们都是 Servlet 容器的具体实现。后面我还会详细谈到 Tomcat 和 Jetty 是如何设计和实现 Servlet 容器的,虽然它们的实现方法各有特点,但是都遵守了 Servlet 规范,因此你的 Web 应用可以在这两个 Servlet 容器中方便的切换

解答

文章中提到 1.SpringMVC 容器实在DispatcherServlet中init方法里创建的。 2.Spring 容器是通过Listener创建的 a、就是说SpringMVC容器和Spring容器还不一样,那么他们是什么关系? b、他们和Servlet容器又是啥关系?

Tomcat&Jetty启动时给每个Web应用创建一个全局的上下文环境,这个上下文就是ServletContext,为后面的Spring容器提供宿主环境。
Tomcat&Jetty在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器,IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后来获取。
Tomcat&Jetty在启动过程中还会扫描Servlet,一个Web应用中的Servlet可以有多个,以SpringMVC中的DispatcherServlet为例,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。
Servlet一般会延迟加载,当第一个请求达到时,Tomcat&Jetty发现DispatcherServlet还没有被实例化,就调用DispatcherServlet的init方法,DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC容器,用来持有Spring MVC相关的Bean。同时,SpringMVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,SpringMVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean,也就是说Spring根容器不能访问SpringMVC容器里的Bean。说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象
https://blog.csdn.net/qq_35571554/article/details/82453684
https://www.runoob.com/servlet/servlet-tutorial.html

请问service方法为什么把request和response都当作输入参数,而不是输入参数只有request,response放到返回值里呢?

方便责任链模式下层层传递

老题,问下。springmvc如何实现url到方法的映射

通过扫描注解,将所有带有@Controller和@RequestMapping注解的类收集起来,统一保存到map里,请求来了通过查找这个map找到相应的类。

老师您好!请问您的置顶回复中“IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后来获取”,对于这句话我不是很理解,想请老师解答一下IoC容器是如何被存储到ServletContext中的,以及为什么要这么做,相应的源码在哪里看。非常感谢老师~

将全局的Spring容器做为ServletContext的一个属性,代码在ContextLoader类中的initWebApplicationContext方法:servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,this.context);ServletContext就是用来共享数据的,比如SpringMVC需要从ServletContext拿到全局的Spring容器,把它设置成自己的父容器。

老师你好,请问在SpringBoot项目中,为什么没有web.xml了?

SpringBoot是以嵌入式的方式来启动Tomcat。对于SpringBoot来说,Tomcat只是个JAR包。
SpringBoot通过Servlet3.0规范中@WebServlet注解或者API直接向Servlet容器添加Servlet,无需web.xml。像纯手工打造Servlet那篇演示的那样。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/name_sakura/article/details/121744049

智能推荐

%e5 转换汉字 php,汉字转Unicode编码,Unicode编码转汉字-程序员宅基地

文章浏览阅读2.3k次。/*** 汉字转Unicode编码* @param string $str 原始汉字的字符串* @param string $encoding 原始汉字的编码* @param boot $ishex 是否为十六进制表示(支持十六进制和十进制)* @param string $prefix 编码后的前缀* @param string $postfix 编码后的后缀*/function unicode..._%e5

东汉十三州以及各个郡的说明_三国地图十三州精确到城池-程序员宅基地

文章浏览阅读6.4w次,点赞7次,收藏10次。东汉十三州以及州下辖的郡县。_三国地图十三州精确到城池

响应式编程实现异步RPC,提升xxl-job调度吞吐量-程序员宅基地

文章浏览阅读1.1k次。在xxl-job中,RPC即用于调度中心请求执行器执行job、kill job,也用于执行器请求调度中心主动注册、执行结果上报。xxl-job实现的RPC类似Feign框架,是基于http..._xxljob 用的什么协议

C++ Json到对象的自动序列化和反序列化工作_c++ json序列化和反序列化-程序员宅基地

文章浏览阅读555次,点赞17次,收藏22次。JSERIALIZE_DEF_OBJECTLIST(Person,Object,objectList) //接受json中的objectList对象数组,对象数组使用此宏定义。JSERIALIZE_DEF_OBJECTTYPE(Person,Son,son) //接受json中的son对象,对象成员使用此宏定义。//输出反序列化结果。

DOSBOX 0.74模拟器安装Windows 95_dosbox imgmount-程序员宅基地

文章浏览阅读7.8k次,点赞2次,收藏6次。DosBox本身带有5.0版的DOS系统,启动后虚拟一个Z盘存放有Dosbox特有的外部指令,如config.com、imgmount.com等,经测试,可以顺利安装各版本的windows 3.1系统,但是不能安装win95,需要用原版的dos镜像启动才能安装。1. 获取启动盘镜像文件 下载Win95启动软盘镜像文件,名为boot.img,放到DosBox 0.74的目录下。2. 制作硬盘镜像文件_dosbox imgmount

呼叫转移的普适性及编程实现_电话自动转移程序开发-程序员宅基地

文章浏览阅读53次。总结来说,呼叫转移是一种方便的电话通信功能,在编程中可以通过使用电话服务提供商的API来实现。然而,实际的实现可能因具体的服务提供商而有所不同,你需要参考相应的文档或与服务提供商联系以获取准确的实现细节。在函数内部,我们构建了一个API请求的有效载荷(payload),其中包含了原始电话号码和目标电话号码。在编程中,呼叫转移的实现涉及使用电话通信协议和相应的编程语言。需要注意的是,实际的呼叫转移功能的实现可能因电话服务提供商的不同而有所差异。首先,我们需要确保已经安装了Python的开发环境和相应的库。_电话自动转移程序开发

随便推点

C++--继承基本概念、对象赋值转换、作用域_什么是赋值转换-程序员宅基地

文章浏览阅读254次,点赞5次,收藏2次。继承1. 继承的基本概念1.1 继承的定义1.2 继承基类成员访问方式的变化2. 基类和派生类对象赋值转换3. 继承中的作用域1. 继承的基本概念继承是面向对象程序设计使代码复用的最重要的手段,允许在保持原有类特性的基础上进行扩展,增加功能,产生新的类,称为派生类/子类。继承是类设计层次的复用。1.1 继承的定义派生类 : 继承方式 基类class Student : public Person1.2 继承基类成员访问方式的变化父类成员在子类中的访问权限(除过父类中的私有成员):_什么是赋值转换

模式识别(2)KNN分类_usps数据集是在哪里提出的-程序员宅基地

文章浏览阅读2.3k次,点赞10次,收藏36次。基于USPS和UCI数据集的近邻法分类一、问题描述 使用近邻算法进行分类问题的研究,并在USPS手写体数据集和UCI数据集上的iris和sonar数据上验证算法的有效性,并分别对近邻法中k近邻算法、最近邻算法和Fisher线性判别进行对比分析。二、数据集说明2.1 USPS手写体 USPS,美国邮政署,是美国联邦政府的独立机构,其中的手_usps数据集是在哪里提出的

Access根据出生日期计算年龄_Excel表格中怎么用出生日期计算年龄?这些方法好用哟...-程序员宅基地

文章浏览阅读1.9k次。  平时工作中用到Excel表格的几率特别大,也积累了一些小技巧,今天就给大家分享一下计算年龄的方法。  在Excel表格中利用“系统时间”和“出生年月”来计算“周岁年龄”、“虚岁年龄”和“实际年龄”是非常方便的,特别是人事管理和工资的统计中遇到的可能性比较大,一起来看一下计算年龄的方法吧。  方法一  第一步,如下图所示,先把需要计算年龄的出生日期输入到表格中。   第二步,然后在B2单元格中输..._access计算年龄

【EJB】异步方法调用-程序员宅基地

文章浏览阅读641次,点赞23次,收藏18次。虽然我个人也经常自嘲,十年之后要去成为外卖专员,但实际上依靠自身的努力,是能够减少三十五岁之后的焦虑的,毕竟好的架构师并不多。架构师,是我们大部分技术人的职业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

如何构建知识体系_网络智能知识体系的构建方法-程序员宅基地

文章浏览阅读286次。分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请轻击http://www.captainbed.net先说一件值得思考的事情:高考的时候大家都是一样的教科书,同一个教室,同样的老师辅导,时间精力基本差不多,可是最后别人考的是清华北大或者一本,而你的实力只能考个三本,为什么?当然这里主要是智商的影响,那么其他因素呢?智商解决的问题能不能后天用其他方式来补位一下?大家平时都看过很多方法论的文章,看的时候很爽觉得非常有用,但是一两周后基本还是老样子了。其中有很大_网络智能知识体系的构建方法

超全的数组去重12种方法_数组去重方法-程序员宅基地

文章浏览阅读2.7w次,点赞33次,收藏349次。前言数组去重,可以说是一个比较常见的面试题,今天来盘点一下都有哪些方法可以实现数组去重。方法1、双重for循环这是一个最笨的方法,双重循环。var arr = [1, 2, 3,4 ,5,6, 4, 3, 8, 1] // 数组去重: // 方法1: 双重for 循环 function newArrFn (arr) { // 创建一个新的空数组 let newArr = [] for(let i = 0;i<arr.length;i+_数组去重方法

推荐文章

热门文章

相关标签