深入理解Servlet(上)

news2024/10/6 10:29:59
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要了解Servlet

现如今各种框架层出不穷,SpringMVC早已经成过去式,几乎所有公司都直接使用SpringBoot、微服务了。我甚至怀疑,还有多少人知道Servlet这玩意...

但正所谓基础不牢地动山摇,如果你对Servlet那一套机制不熟悉的话,往后学习SpringMVC、SpringBoot时常常会感到困惑,甚至束手无策。举个简单的例子:

很多人都知道SpringMVC的Interceptor可以用来拦截请求,符合条件的返回true,请求继续往后走

对于这个问题,符合条件的自然好处理,直接返回true即可,关键是不符合条件的怎么办?直接return false吗?

@Component
public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 由于返回值限定为boolean,所以你没法做return Result.error(...)之类的操作
        return false;
    }
}

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor).addPathPatterns("/**");
    }
}

你会发现,如果Interceptor直接return false,客户端将收不到任何消息(响应体为空),这对客户端而言是极其不友好的,因为它无法知道被拦截的原因:

此时,我们可以抛异常出去,在@RestControllerAdvice统一处理:

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理运行时异常
     * 比较正规的处理方式是返回统一结果,为了方便,这里直接返回String
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(RuntimeException e) {
        return e.getMessage();
    }

}

@Component
public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 模拟判断逻辑,如果不符合条件,直接抛异常
        if (ThreadLocalRandom.current().nextBoolean()) {
            throw new RuntimeException("未登录");
        }

        return true;
    }
}

但其实还可以利用Writer直接输出响应流:

@Component
public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 模拟判断逻辑,如果不符合条件,响应错误信息
        if (ThreadLocalRandom.current().nextBoolean()) {
            responseErrorMsg(response);
        }

        return true;
    }

    private void responseErrorMsg(HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.write("need login");
        writer.flush();
        writer.close();
    }
}

如果你对Servlet那一套不了解,也没好好研究过SpringMVC,上面的问题可能会让你手足无措。

所以,无论出于什么原因,我认为Java程序员都有必要了解一下Servlet,学习Servlet其实就是学习JavaWeb的请求响应机制。

很多人可能和我当初一样,把Servlet和太多东西联系起来。其实Servlet本身在Tomcat中是“非常被动”的一个角色,处理的事情也很简单。网络请求与响应,不是他的主要职责,它其实更偏向于业务代码。所谓的Request和Response是Tomcat传给它,用来处理请求和响应的工具,但它本身不处理这些。

文章会比较长,但是看完会拔高你看待Servlet的视角。

Servlet的前世今生

类似于Servlet是Server Applet(运行在服务端的小程序)等其他博文已经提过的内容,这里就不重复了。它就是用来处理请求的业务逻辑的。

之前在Tomcat外传中我们聊过,所谓Tomcat其实是Web服务器和Servlet容器的结合体。

什么是Web服务器?

比如,我当前在杭州,你能否用自己的电脑访问我桌面上的一张图片?恐怕不行。我们太习惯通过URL访问一个网站、下载一部电影了。一个资源,如果没有URL映射,那么外界几乎很难访问。而Web服务器的作用说穿了就是:将某个主机上的资源映射为一个URL供外界访问。

什么是Servlet容器?

Servlet容器,顾名思义里面存放着Servlet对象。我们为什么能通过Web服务器映射的URL访问资源?肯定需要写程序处理请求,主要3个过程:

  • 接收请求
  • 处理请求
  • 响应请求

任何一个应用程序,必然包括这三个步骤。其中接收请求和响应请求是共性功能,且没有差异性。比如访问淘宝和访问京东,都是请求一个URL,然后服务器会响应一段JSON数据给浏览器。于是,大家就把接收和响应两个通用步骤抽取成Web服务器:

处理请求的逻辑是不同的。没关系,抽取出来做成Servlet,交给程序员自己编写。

当然,随着后期互联网发展,出现了三层架构,所以一些逻辑就从Servlet抽取出来,分担到Service和Dao。

但是Servlet并不擅长往浏览器输出HTML页面,所以出现了JSP。

等Spring家族出现后,Servlet开始退居幕后,取而代之的是方便的SpringMVC。SpringMVC的核心组件DispatcherServlet其实本质就是一个Servlet,只不过它已经自立门户,在原来HttpServlet的基础上,又封装了一些复杂的逻辑。

总之,很多新手程序员框架用久了反而迷糊,觉得SpringMVC是凭空实现的,和Servlet没半毛钱关系。

我所理解的JavaWeb三大组件

不知道从什么时候开始,我们已经不再关心、甚至根本不知道到底谁调用了我写的这个程序,反正我写了一个类,甚至从来没new过,它就跑起来了...

我们把模糊的记忆往前推一推,没错,就是在学了Tomcat后!从Tomcat开始,我们再也没写过main方法。以前,一个main方法启动,程序间的调用井然有序,我们知道程序所有流转过程。但是到了Javaweb后,Servlet/Filter/Listener一路下来我们越学越沮丧。没有main,也没有new,写一个类然后在web.xml中配个标签,它们就这么兀自运行了。

其实,这一切的一切,简单来说就是“注入”和“回调”。想象一下吧朋友们,Tomcat里有个main方法,假设是这样的:

其实,编程学习越往后越是如此,我们能做的其实很有限。大部分工作,框架都已经帮我们做了。只要我们实现xxx接口,它会帮我们创建实例,然后搬运(接口注入)到它合适的位置,然后一套既定的流程下来,肯定会执行到。我只能用中国一个古老的成语形容这种开发模式:闭门造车,出门合辙(闭门造车完整的意思其实是形容一个人技艺高超,家里瞎捣鼓,完事就可以上路了)。

很多时候,框架就像一个傀儡师,我们写的程序是傀儡,顶多就是给傀儡化化妆、打扮打扮,实际的运作全是傀儡师搞的。

了解到这个层面后,JavaWeb三大组件任何生命周期相关的方法、以及调用时Tomcat传入的形参,这里就不再强调。肯定是程序的某处,在创建实例后紧接着就传入参数调用了呗。没啥神秘的。

如何编写一个Servlet

Servlet的定位

首先,我们心里必须有一个信念:我们都是菜鸡,框架肯定不会让我们写很难的代码,所以Servlet既然交给我们实现,肯定是很简单的!

(没有网络请求和响应需要我们处理,都封装好了!)

你别不服。如果你观察培训班的教材,会发现进入Tomcat阶段后,我们开始全面面向接口编程,而且最早出现在JDBC阶段。我就问你,JDBC接口是你自己实现的吗?别闹了,你导入MySQL的驱动包,它给你搞定了一切。

真正的连接过程太难写了,朋友们。底层就是TCP连接数据库啊,你会吗?写Socket,然后进行数据库校验,最后返回Connection?这显然超出我们的能力范围了。我们这么菜,JDBC不可能让我们自己动手的。所以各大数据库厂商体贴地推出了驱动包,里面有个Driver类,调用:

driver.connect(url, username, password);

即可得到Connection。

BUT,这一次难得Tomcat竟然这么瞧得起我黄某 ,仅仅提供了javax.servlet接口,这是打算让我自己去实现?

不,不可能的,肯定是因为太简单了。

查看接口方法:

五个方法,最难的地方在于形参,然而Tomcat会事先把形参对象封装好传给我...除此以外,既不需要我写TCP连接数据库,也不需要我解析HTTP请求,更不需要我把结果转成HTTP响应,request对象和response对象帮我搞定了。

看吧,Tomcat是不是把我们当成智障啊。

Tomcat之所以放心地交给我们实现,是因为Servlet里主要写的代码都是业务逻辑代码。和原始的、底层的解析、连接等没有丝毫关系。最难的几个操作,人家已经给你封装成形参传进来了。也就是说,Servlet虽然是个接口,但实现类只是个空壳,我们写点业务逻辑就好了。

总的来说,Tomcat已经替我们完成了所有“菜鸡程序员搞不定的骚操作”,并且传入三个对象:ServletConfig、ServletRequest、ServletResponse。

ServletConfig

翻译过来就是“Servlet配置”。我们在哪配置Servlet来着?web.xml嘛。请问你会用dom4j解析xml得到对象吗?

可能...会吧,就是不熟练,嘿嘿嘿。

所以,Tomcat还真没错怪我们,已经帮“菜鸡们”搞掂啦:

也就是说,servletConfig对象封装了servlet的一些参数信息。如果需要,我们可以从它获取。

如何编写Servlet

Request/Response两位老朋友,不用多介绍了,也是大家最在意的网络请求相关的内容。但这其实也是Tomcat处理并封装好了的,不需要Servlet(我们)操心。很多人看待HTTP和Request/Response的眼光过于分裂,其实它们的关系就像菜园里的大白菜和餐桌上的酸辣白菜一样亲近!HTTP请求到了Tomcat后,Tomcat通过字符串解析,把各个请求头(Header),请求地址(URL),请求参数(QueryString)都封装进了Request对象中。通过调用:

request.getHeader();
request.getUrl();
request.getQueryString();
...

等等方法,都可以得到浏览器当初发送的请求信息。

至于Response,Tomcat传给Servlet时,它还是空的对象。Servlet逻辑处理后得到结果,最终通过response.write()方法,将结果写入response内部的缓冲区。Tomcat会在servlet处理结束后,拿到response,遍历里面的信息,组装成HTTP响应发给客户端。

Servlet接口5个方法,其中init、service、destroy是生命周期方法。init和destroy各自只执行一次,即servlet创建和销毁时。而service会在每次有新请求到来时被调用。也就是说,我们主要的业务代码需要写在service中。

但是,浏览器发送请求最基本的有两种:Get/Post,于是我们必须这样写:

很烦啊。有没有办法简化这个操作?我不想直接实现javax.servlet接口啊。于是,菜鸡程序员找了下,发现了GenericServlet,是个抽象类

我们发现GenericServlet做了以下改良:

  • 提升了init方法中原本是形参的servletConfig对象的作用域(成员变量),方便其他方法使用
  • init方法中还调用了一个init空参方法,如果我们希望在servlet创建时做一些什么初始化操作,可以继承GenericServlet后,覆盖init空参方法
  • 由于其他方法内也可以使用servletConfig,于是写了一个getServletContext方法
  • service竟然没实现...要它何用

放弃GenericServlet。

于是我们继续寻找,又发现了HttpServlet:

它继承了GenericServlet。

GenericServlet本身是一个抽象类,有一个抽象方法service。查看源码发现,HttpServlet已经实现了service方法:

好了,也就是说HttpServlet的service方法已经替我们完成了复杂的请求方法判断。

大家注意了,HttpServlet是个抽象类。但我把HttpServlet这个类的源码翻了个底朝天,都没有找出一个抽象方法...所以它为啥要声明为abstract呢?

看一下HttpServlet的文档注释:

把一个类声明成抽象的,一般有两个原因:

  • 有抽象方法
  • 没有抽象方法,但是不希望被实例化

HttpServlet做成抽象类,是处于第二个原因:仅仅是为了不让new。

它为什么不希望被实例化,且要求子类重写doGet、doPost等方法呢?

我们来看一下源码:

如果我们没重写会怎样?

浏览器页面会显示:405(http.method_get_not_supported)

也就是说,HttpServlet虽然在service中帮我们写了请求方式的判断。但是针对每一种请求,业务逻辑代码是不同的,HttpServlet无法知晓子类想干嘛,所以就抽出七个方法,并且提供了默认实现:报405、400错误,提示请求不支持。

但这种实现本身非常鸡肋,简单来说就是等于没有。所以,不能让它被实例化,不然调用doXxx方法是无用功。Filter用到了责任链模式,Listener用到了观察者模式,Servlet也不会放过使用设计模式的机会:模板方法模式。

小结:

  • 如何写一个Servet?
  • 不用实现javax.servlet接口
  • 不用继承GenericServlet抽象类
  • 只需继承HttpServlet并重写doGet()/doPost()
  • 父类把能写的逻辑都写完,把不确定的业务代码抽成一个方法,调用它。当子类重写该方法,整个业务代码就活了。这就是模板方法模式

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1276917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用JavaScript的管道方法简化代码复杂性

用JavaScript的管道方法简化代码复杂性 在现代 web 开发中,维护干净有效的代码是必不可少的。随着项目的增加,我们功能的复杂性也在增加。然而,javaScript为我们提供了一个强大的工具,可以将这些复杂的函数分解为更小的、可管理的…

什么是Anaconda

Anaconda的安装也很方便。打开这个网站Anaconda下载,然后安装即可。 Anaconda可以帮助我们解决团队之间合作的包依赖管理问题。在没有使用Anaconda之前,如果你的Python程序想让你的同事运行,那么你的同事可能会遇到很多包依赖问题&#xff0…

【 RTTI 】

RTTI 概念: RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检 查着这些指针或引用所指的对象的实际派生类型。 原因: C是一种静态类 型语言。其数据类型是在编译期就确定的,不能在运…

2023年中国消费金融行业研究报告

第一章 行业概况 1.1 定义 中国消费金融行业,作为国家金融体系的重要组成部分,旨在为消费者提供多样化的金融产品和服务,以满足其消费需求。这一行业包括银行、消费金融公司、小额贷款公司等多种金融机构,涵盖了包括消费贷款在内…

力扣15题 三数之和 双指针算法

15. 三数之和 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意:答案中不可以包含重复的三…

mysql的InnoDB存储引擎

详情请参考:https://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html InnoDB 是一个通用目的的存储引擎,它在高可用性、高性能方面做了平衡。MySQL 8.0,InnoDB 是默认的存储引擎。在创建表的时候,如果没有使用ENGIN…

1+x网络系统建设与运维(中级)-练习题

一.给设备重命名 同理可得&#xff0c;所有交换机和路由器都用一下命令配置 <Huawei>sys [Huawei]sysn LSW1 二.配置VLAN LSW1&#xff1a; [LSW1]vlan batch 10 20 [LSW1]int e0/0/1 [LSW1-Ethernet0/0/1]port link-type access [LSW1-Ethernet0/0/1]port default vlan…

用户反馈组件实现(Vue3+ElementPlus)含图片拖拽上传

用户反馈组件实现&#xff08;Vue3ElementPlus&#xff09;含图片拖拽上传 1. 页面效果1.1 正常展示1.2 鼠标悬浮1.3 表单 2. 代码部分1.2 html、ts1.2 less部分 3. 编码过程遇到的问题 1. 页面效果 1.1 正常展示 1.2 鼠标悬浮 1.3 表单 2. 代码部分 1.2 html、ts <templ…

gorm修改操作中两个update方法的小细节

在使用gorm进行修改操作时&#xff0c;修改操作中如下两个方法&#xff1a; Update() Updates() 都可以实现修改&#xff0c;根据名称可以看出Update是针对单个字段&#xff0c;而后者应该是多个。 下面是主要实际操作&#xff1a; ​​ Updates() 即&#xff0c;前者确实是…

vector是如何扩容的

vector容器扩容 vector是成倍扩容的&#xff0c;一般是2倍。 vector管理内存的成员函数 开始填值 没有填值之前&#xff0c;vector元素个数和容量大小都为0 加入一个值之后&#xff1a; 加入两个值&#xff1a;重点在加入三个值&#xff0c;此时容量变为4&#xff1a;加入第…

开源图床Qchan本地部署远程访问,轻松打造个人专属轻量级图床

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…

优彩云采集器最新版免费下载,优彩云采集器免费

随着网络时代的发展&#xff0c;SEO&#xff08;Search Engine Optimization&#xff0c;搜索引擎优化&#xff09;已经成为网站推广和营销的关键一环。在SEO的世界里&#xff0c;原创内容的重要性愈发凸显。想要做到每天更新大量原创文章&#xff0c;并不是一件轻松的事情。优…

RocketMQ主从同步原理

一. 主从同步概述 主从同步这个概念相信大家在平时的工作中&#xff0c;多少都会听到。其目的主要是用于做一备份类操作&#xff0c;以及一些读写分离场景。比如我们常用的关系型数据库mysql&#xff0c;就有主从同步功能在。 主从同步&#xff0c;就是将主服务器上的数据同步…

接口测试基础知识

一、接口测试简介 什么是接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试&#xff0c;主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。 测试的重点&#xff1a; 检查数据的交换&#xff0c;传递和控制管理过程&#xff1b;检查系统间的相互…

人工智能对我们的生活影响有多大?

一、标题解析 本文标题为“人工智能对我们的生活影响有多大&#xff1f;”&#xff0c;这是一个典型的知乎风格SEO文案标题&#xff0c;既能够吸引读者&#xff0c;又能够体现文章的核心内容。 二、内容创作 1. 引言&#xff1a;在开头&#xff0c;我们可以简要介绍人工智能…

PVE系列-LVM安装MacOS的各个版本

PVE系列-LVM安装MacOS的各个版本 环境配置大概过程&#xff1a;详细步骤&#xff1a;1.建立安装环境和下载安装工具2. 重启后&#xff0c;执行osx-setup配置虚拟机3. 安装到硬盘&#xff0c;4.设定引导盘&#xff0c;以方便自动开机启动5.打开屏幕共享和系统VNC最后的结果 引子…

【Node.js】基础梳理 6 - MongoDB

写在最前&#xff1a;跟着视频学习只是为了在新手期快速入门。想要学习全面、进阶的知识&#xff0c;需要格外注重实战和官方技术文档&#xff0c;文档建议作为手册使用 系列文章 【Node.js】笔记整理 1 - 基础知识【Node.js】笔记整理 2 - 常用模块【Node.js】笔记整理 3 - n…

第三节:提供者、消费者、Eureka

一、 提供者 消费者&#xff08;就是个说法、定义&#xff0c;以防别人叭叭时听不懂&#xff09; 服务提供者&#xff1a;业务中被其他微服务调用的服务。&#xff08;提供接口给其他服务调用&#xff09;服务消费者&#xff1a;业务中调用其他微服务的服务。&#xff08;调用…

java基于springboot框架的中小企业人力资源管理系统的设计及实现+jsp

&#xff08;1&#xff09;员工信息管理&#xff1a;员工的基本信息&#xff0c;人员编制&#xff0c;岗位管理&#xff0c;人员流动管理&#xff08;老员工转出&#xff0c;辞职&#xff0c;退休等&#xff09;&#xff0c;职工业绩考核归公管理&#xff0c;工人工种管理。 &…

负电源电压转换-TP7660H

负电源电压转换-TP7660H 简介引脚说明典型应用电路倍压与反压的应用电路 简介 TP7660H 是一款 DC/DC 电荷泵电压反转器专用集成电路。芯片能将输入范围为 2.5V&#xff5e;11V 的电压转换成相应的-2.5V&#xff5e;-11V 的输出&#xff0c;电压转换精度可达99.9%&#xff0c;电…