Servlet - JavaEE初阶 - 细节狂魔

文章目录

前言

前文说到:
其实 web 开发主要的工作就是在 动态页面 这边。
因此接下来重点学习的,就是 动态页面的构造。
动态页面的构造:其实就是学习 Tomcat 给程序员提供的这组用来操作 HTTP 的 API。
我们给 这组 API,单独起了个名字:Servlet。

Servlet,可以说是在Java世界中,进行 web 开发的一个基石。
我们未来用到一些像 Spring 这样的框架,其实它也是基于 Servlet 这样的机制,进行了进一步的封装。

Servlet,好不好学?
下面讲 Servlet 的时候,你就会发现:
这里的 API,其实 和 HTTP协议,是关系非常紧密的。
如果你非常熟悉 HTTP协议,那么这个API,你会学的非常轻松!

但是整体来说,Servlet 这里的学习难度还是比较大的。
难度大的原因:
不是因为 代码,而是因为这个环境不好搞。


前置知识:Maven

正式学习 Servlet 之前,我们需要先学习点前置知识。

前面的 HTTP 和 Tomcat 也算是 Servlet 的前置知识

那就是 maven

maven,这是我们在 写 Servlet 代码 的时候,所需要用到的一个工具。

Maven, 是Java 世界中的一个非常知名的 “工程管理工具 / 构建工具”。
它的核心功能:
1、管理依赖
2、构建 / 编译
3、打包

有的人可能会产生疑问:编译,不是编译器的工作吗?
和 Maven 有什么关系呢?
其实 Maven 的 构建 / 编译,也是在调用 JDK 来去进行 编译 和 打包 的工作。
但是呢,如果你光用 JDK ,就好像是:
执行这个操作,你去进行编译。
执行下一个操作,你去进行打包。
。。。

但是,现在 Maven 把 这一系列的操作,都给你串起来了。
用一个成语来形容:一气呵成
因此,Maven 存在的意义:
就是能够直接把这些操作(管理依赖,构建 / 编译,打包)串起来。(一气呵成)

尤其是一些比较大的程序,它里面有很多模块。
你要是每一个模块,都去手动去敲一个命令去编译;或者每一个模块,都去点一下进行打包,这就很麻烦了。

但是,我们如果使用 Maven,就可以一键式的来帮我们把这里的这些操作,全部完成。

这里需要注意的是:
打包:就是把 Java代码 给构造成 jar 包,或者是 war 包。

jar包:其实就是一个特殊的压缩包,类似于 rar。里面就是把 各种 .class 文件,放到一起来。然后,进行压缩得到的包,就是 jar 包。
war包,也是同理。只是与 jar 包,在细节上存在差别。

这是 Java 中常见的程序发布方式。
一个项目中的 字节码文件是会非常多的,你一个个发,是想恶心死人嘛。。。
肯定是全部放在一起,直接全部压缩,打包发给你不香吗?
这样做,显然是更加轻松的!

另外,打包,也是 Maven 调用 JDK 里面的功能来实现的。(强调一下)

打包,我们现在了解了。
那么,管理依赖又如何去理解呢?

依赖:
你进行一个 A 操作之前,要先进行一个 B 操作。
举个例子:
你想有个贴心小棉袄(girl),就需要有个老婆。
此时,老婆就是 B,也就是依赖的对象;孩纸就是 A.
要想得到 A,就必须要依赖 B 。
杠精不要抬杠!先上车后补票,这种行为。
我是强烈谴责的!


以此类推:
要想有老婆,就必须先要有结婚的对象。
要想有结婚的对象,就必须先要有 女朋友。
要想有女朋友,就必须要有自己中意的对象。
其中 结婚的对象,女朋友,中意的对象,都是进行前者操作的必要条件。
两者之间的关系,就被称为 “依赖”。

而且,有时候依赖的条件还不止一个!
结婚,你光有女朋友还不行。
还有得有房子,车子,彩礼。。。
男同志,看到这里是不是老扎心了。
其实上面这些操作,除了生娃之外,都可以认为是 结婚的 “依赖” 条件。

不难得出结论:
依赖,有时候是很复杂的,而且是嵌套的!
上面这个生娃例子,就是一个多重嵌套的依赖关系。
你必须要满足前面的所有的条件,才能进行最终的 目的。
这就是 “依赖”。

前面在 博客设计博文中,设计博客编辑页,引入 markdown 编辑器的时候,editor.md文件,就依赖了 jQuery,不是嘛!
得先让页面加载 jQuery,再加载 editor.md(markdown的核心文件)。
得需要先解决依赖,才能进行本体的任务。


咱们写代码的时候,也是有很多依赖的。
只是当前阶段,依赖的东西不多。
1、经常会依赖标准库(集合类:Scanner,顺序表,链表等等…)
但是,更严格来说:标准库并不算是依赖。
因为,你只有安装了JDK,这些东西就都会有。【自带的】
但是,要想执行 Java 程序,肯定是需要依赖 JDK 的。
因此,Java 程序 和 JDK 是属于 依赖关系,
标准库,太勉强了。

2、经常依赖第三方库
第三方库:就是我们写代码的时候,需要引入的一些其它的jar包。
就像前面讲 JDBC编程 的时候,当时就下载了一个 mysql 的驱动包。
当时,我们要想进行 JDBC 编程,这个 驱动包 是必不可少的!
这也就属于依赖。

其实写代码的时候,有时候的依赖也会非常复杂。
你引入了一个第三方库A,而这个 A 又依赖于 库 B,B 由依赖于 C,C又依赖于D,
类似这样的套娃操作,我们要想使用 A,就必要把它前面所依赖的库,全部引入。

如果我们是手动去管理这个依赖,那就会相当的麻烦!!
不光你得研究清楚,每个库都依赖哪些其他库。
而且,还得要研究清楚,这些依赖之间的版本是否匹配的问题。
如果版本不匹配,搭配起来使用,就会有很多莫名其妙的bug存在。


为了解决上述的依赖问题,很多编程员都引入了自己的包管理工具(自动解决依赖)。
Java:Maven,Gradle
Python:pip
JS:npm
各种语言都有着自己的包管理工具。
除了 C++。。。
这就是一个比较悲伤的故事了。
目前为止,C++官方还没有提供这样的一个包管理工具。
第三方的包管理工具是有,但是问题多。
就没有一个像 Maven 这种这么成熟的工具。

我们当前用的 Maven,隔壁 C++ 的朋友都馋哭了!
我们想要下载第三方库,直接Maven中一个命令就搞定了。
而 C++ 的朋友,就像我们刚才说的那样。
得研究清楚,每个库都依赖哪些其他库。
而且,还得要研究清楚,这些依赖之间的版本是否匹配的问题。
可能还需要自己去折腾编译等操作…
折腾一下午,还不一定搞得定。。
所以说,这才一个真的悲伤的故事。
不过也没有办法,这就是 C++ 本省的生态缺陷。

C++方面的东西,不讨论了。
总之明确一件事:Maven,这个东西对于我们来说,绝不是负担!它是一个非常非常香的存在!它能帮我们解决很多很多的问题。


如何下载安装 Maven?

网上有很多下载安装 Maven 的教程。
我的建议是:大家千万不要去参考!!!
我们的做法:就是啥也不干就行了,因为 idea 中内置了 现成 Maven。

这就好比打电子竞技类游戏:“不打等于上分”。
你不打,排位分不变。别人一直在打,一直掉分。这就相当于你在变向上分。

如果你自己去按照网上教程去安装下在Maven,环境配置…一顿操作下,可能 Maven 没有安装成功,反而可能会把现有的JDK环境给破坏了。

因此,我们直接使用线程的 Maven 即可。


如何使用 Maven?

主要介绍,搭配 idea 的使用方式。

先来在 idea上 创建一个 Maven项目
在这里插入图片描述

有的朋友可能会发现,自己的操作界面,和博主这里不太一样。
原因很简单:和 idea的版本有关。
虽然有差别,但是不影响。而且差别也不会很大。
在这里插入图片描述
接着来看:项目中的一些关键文件。
在这里插入图片描述

项目创建好了。
下面,我们来看看如何使用?

国外中央仓库的链接https://mvnrepository.com
在这里插入图片描述

如果你默认的源使用不了,就可以采用下面的方式来解决。

将maven源改为国内阿里云镜像中央仓库,参考文章链接https://zhuanlan.zhihu.com/p/71998219
在这里插入图片描述

如果你们下载成功了。
就会出现下面这种情况:
在这里插入图片描述
无论是 Maven,还是其他的库,都是通过同样的方式来引入的。
只要把对应的 xml 片段(坐标)拷贝到 你的 pom.xml 文件中的 dependencies 标签中即可。
接下来的操作,包管理工具都会帮你处理。

以上就是 Maven 最基础的操作,更多的内容,遇到了再讲。
此时,目的是先了解 Maven 的基础用法即可。


Servlet

Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app(网页).

这里我就不再进行过多的赘述了,因为前言里面,已经讲得差不多了。


第一个 Servlet 程序:hello world

完成一个 servlet 的 hello world,需要七个步骤。


一、创建以一个 Maven 项目

在这里插入图片描述

对了这里,还有一个小细节。
在这里插入图片描述


二、引入依赖

需要在 代码中 引入 Servlet API,这个 API 不是 JDK 内置,而是第三方(Tomcat )提供的。

Tomcat 相对于 Java 官方来说,仍然属于第三方。
换个说法:Tomcat 和 Java 的官方(Oracle),不是“一家人” !

你如果是属于标准库的内容,也就是 JDK 内置的。
那你只要装了 Java(JDK),它就自带了这些内容。

但是,Tomcat 是属于第三方的,不是说一家人的。
因此,这个时候 JDK 就不会自带这个库,想要使用就只能去额外的引入 Servlet(依赖)。

故,引入 Servlet 依赖,是一个必要的操作。

这时候,Maven 的作用就体现出来了。
下面,我们就借助 Maven 来引入 servlet 依赖。
中央仓库的链接:https://mvnrepository.com
在这里插入图片描述
在这里插入图片描述


三、创建目录结构

虽然当下 Maven 帮我们创建了一些目录,但是还不够。

当前这个目录还不足以支撑我们写一个 Servlet 项目。
因此我们需要手动的再创建一个目录和文件出来。
这个操作,显然是每回写 Servlet 项目的时候,都需要进行的操作。
这是不是就麻烦。
虽然也有更简单的办法,但是此时先不介绍。
先用最朴素的方式:纯手动创建。
在这里插入图片描述
至于为什么要去写?
这因为人家Tomcat默认的一种的约定。
当然,这里也不一定非得就这么去写,也是能改的!
但是!如果要改,就需要修改更多的设置了,也就更加麻烦了。

web.xml 文件是不能为空的!
需要写点东西进去。
东西:也是固定了。你可以把这个东西保存起来,下回用到的时候,直接复制粘贴即可。

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>
        Archetype Created Web Application
    </display-name>
</web-app>

在这里插入图片描述
至此,第三步骤:创建目录结构就完成了。
前面这三个步骤,都是属于准备工作。

另外,可能有些朋友会出现下面这个问题:
在这里插入图片描述
下标这一段标红。

这是因为在idea里面,针对 XML / CSS / HTML等非 Java 的代码,idea的提示不一定就是准确的。

主要还是 idea 针对 Java 这块太准了,以至于大家就形成了一个默认的条件反射。
不标红,代码一定没有问题(至少在编译期间是这样的);标红,一定是有问题的。

实际上除了 Java 之外,idea 不能保证自己的提示就是那么准确的。
至少要经过一些配置操作,才能比较准。

所以上述标红,并不影响我们的程序正确执行!!!

如果你实在受不了标红,你就点击一下标红的地方,然后 alt + 回车键,选择第一个即可。
在这里插入图片描述
起到的效果,就是能够让 idea 针对 你的 XML来进行 语法补全 功能。
【这样做,只是说提升你的代码体验而已。不改,也不影响代码的执行】


四、编写 servlet 代码

在这里插入图片描述

通过上面的解析,我们对 服务器所需要做的工作有了一点了解。
同时也明白,我们需要做的事情就是:根据响应,来计算响应。
在书写 doGet 方法*(根据响应,来计算响应)之前,我们需要做一件事!
在这里插入图片描述
下面,我们就要正式开始 为 doGet添加内容。
在这里插入图片描述

我们需要 注意最后一句代码的写法。
在这里插入图片描述

但是,写到这里还没有完!
代码目前还跑不起来!

虽然代码写到这里,感觉已经有不少新的知识涌过来了。
但是还不够,还差最后一个画龙点睛之笔。
加上一个 @WebServlet(“/路径”)
在这里插入图片描述
此时,我们的hello world代码才真正完成了!

接下来,这个代码又该怎么去运行呢?

按照以前的习惯,我们都是直接运行 main 方法的。
但是!现在没了!!!

下一件麻烦的事情正在向我们招手。。。


五、打包

当前的代码,是不能单独运行的。
因为没有main方法
那么怎么才能执行代码呢?

需要把当前的代码进行打包,然后部署到 Tomcat 上,由 Tomcat 来进行调用。
打个比方:
如果把一个完整的应用程序,理解成一个能跑的汽车。
那么,当前写的 Servlet 代码,就相当于车斗
【货车后面用来装载物品的容器】
在这里插入图片描述
车斗没有轮子,没有发动机,只是一个载物的容器。
移动需要借助车辆来进行,【车辆驮着它形式,达到另类的移动】”

Servlet 程序也是一样的,光靠程序本身是执行不起来的。
需要借助 Tomcat,此时的Tomcat,就相当于是车头。
在这里插入图片描述
可以拖着 Servlet 程序执行,从而达到执行 Servlet 程序的目的。
在这里插入图片描述

打包:就是把 Servlet 程序,装进“集装箱 / 车斗”里面,
后面,再让Tomcat(车头)拖着它执行。

下面我们来看一下,打包是怎么实现的。
在这里插入图片描述


六、部署

把 war 包拷贝到 Tomcat 的 webapps 目录下

让Tomcat(车头)拖着它执行。
在这里插入图片描述


七、验证程序

借助浏览器来去验证程序

说白了,就是借助浏览器来访问一下 我们刚才写的代码,是不是有效果。
在这里插入图片描述
大家要注意:
一个Tomcat上是可以同时部署多个 webapp(网站),一个网站上又有多个页面。
所以,一个请求中的第一级路径,就告诉了Tomcat,我们要访问那个网站。
再通过第二级路径,就告诉了Tomcat,我们要访问这个网站具体的页面是哪个。

代码的效果,很简单!
就是返回 HTTP 响应的 body部分,就是 显示 hello world
在这里插入图片描述

考虑到 特殊状况。
可能会存在一种情况:
有些朋友的程序 和 步骤没有问题,但是就是没有效果。
解决的的办法:在Tomcat 窗口上的,按一下回车键,就好了。

造成这种情况的原因:是属于 cmd 的一个大坑!!!
cmd,就是我们电脑上自带的命令行窗口
在这里插入图片描述
CMD 有一个“选择文本” 这样的模式
在这里插入图片描述
当 CMD 被选中文本的时候,CMD 就会把里面运行的程序给 挂起。
挂起:程序暂停了,不继续往下执行了。
程序都不往下继续执行了,自然也就不会有 内容反馈了。
因此,网页就没有效果显示。


讨论

一、现在这个页面内容是通过 Java 代码生成的,但是这和我们直接创建一个HTML,里面写个 hello world ,有什么区别呢?

HTML页面的内容 是固定死的,是静态的,是不变的。
Java 这边的页面内容是可变的,根据用户不同的输入,可以得到不同的结果!!
也就是说:Java 编写的页面是动态的。

为了让大家更直观的感受“动态”,我们来对代码进行修改。
在页面显示的代码中加上 时间戳。
多执行几次,打印的时间戳是变化的。
因为时间时刻都在流失。
在这里插入图片描述

虽然看起来,1-7挺麻烦的,但是熟练之后,这些操作不到1分钟,基本就能搞定。

虽然每次部署之前,都需要将新的war,复制到对应webapps目录下,显然这个操作是不科学的!太麻烦了!
但是,却是 最朴素的操作。因此也是最低效的操作。

我们程序员是最讨厌低效的操作的!!!
因此也有一些方法,来提高上述流程的效率。

我们已知7个步骤中,前三个(创建项目,引入依赖,构建目录)步骤是雷打不动。
因为前三个操作都是一次性的,就是不用重复进行同样的操作。
而第四步,写代码这是真的无法省略的过程,绝对要写的。
关键就在于 5 和 6 步骤。
【5和6,就是将新的war,复制到对应webapps目录】
最后的第7步也是固定的。

下面我们就来借助 第三方工具来简化 5 和 6 步骤。


简化 5 和 6 步骤

因为每次修改代码,都需要重新打包,重新部署。
两三次还行,但是来个五六次,甚至更多,这就很麻烦了。
因此,我们来使用第三方工具,来简化这两个步骤。

将Tomcat部署到 idea 中
更准确的来说:
我们是通过 idea 上的插件,来直接把我们 Tomcat 给集成进来了。
做到“一键式”完成打包和部署操作。

但是要明确一点:Tomcat 虽然可以通过 idea 插件,来集成安装到 idea中。
但是 idea 和 Tomcat 是两个独立的程序!!!
Tomcat 不是 idea 功能的一部分!!!!
Tomcat,只是通过 idea 插件,与 idea 建立一座“合作的桥梁”。

为什么要提这一点?
这是因为后面开发,主要还是通过 idea 调用 Tomcat的方式来进行。
用的时间长了之后,大家就对于 Tomcat的印象,就开始模糊。
甚至会产生 Tomcat 是 idea 功能的一部分,这样的错觉!

以后大家在工作中,会涉及到几个不同的环境。
在这里插入图片描述

这个第三方工具,是idea上的一个插件。
我们称为 Smart Tomcat。

虽然idea本身虽然已经有很多功能了,但是也不能做到 面面俱到!
因此,idea 还提供了一系列的扩展能力,允许第三方来开发一些程序,交给 idea运行。
相当于对 idea 进行了扩展。
我们把这些扩展,称为 第三方插件。
插件就是对程序的一些特定场景, 做出一些特定的功能的扩展
在这里插入图片描述

正如前面所说,idea 的插件有很多,Smart Tomcat 只是其中的一个!


安装 Smart Tomcat

在这里插入图片描述


使用 Smart Tomcat

在每一个项目中,首次使用 Smart Tomcat,需要先简单配置,后续就不必再配置了。
在这里插入图片描述


常见出错问题汇总

初学的时候,大家都很容易出错。
这都是很正常的情况,下面我们就来认识 出错的原因是什么。


1、404

404:表示你要访问的资源在服务器上 不存在/ 没找到

出现这种状态的原因:
1、你请求的资源路径不对
2、路径虽然是对的,但是服务器没有正确的把资源加载起来。


1.1、少写了 Comtext Path(上下文路径)

在这里插入图片描述


1.2、少写了 Servlet Path (servlet 路径>>代码注释中标注的路径)

在这里插入图片描述


1,3、 Servlet Path 写的和 URL 不匹配

在这里插入图片描述


小结

我们要想正确访问页面,就必须要保证 context path 和 servlet path 是正确的。
在这里插入图片描述


1.4、web.xml 写错了 / 忘记给 web.xml 添加内容了 / 误把 web.xml的内容删除

上述的三种情况,总得来说:就是 web.xml 的内容,不符合书写规定。
在这里插入图片描述
也就是说:出错的原因不在于 路径,而是因为 web.xml 的书写不规范。
Tomcat 是不能正确加载到 webapp 的,因此浏览器会出现 404 报错。

换句话来说:
Tomcat 凭什么可以去加载页面数据?
至少它得有一个凭证吧?
我们在 web.xml 就相当于是一个凭证。
光有凭证还不行!你还得有 内容!
内容还必须符合规范的,也就是合法的。
此时,Tomcat 才能加载这个 webapp(网站)。【前提是:路径没有问题】


2、405

405:Method Not Allowed

Method: 指的就是 HTTP中方法。
405,整体的意思就是 HTTP 方法不匹配,无法获取到数据。
下面我们就来演示一下:
在这里插入图片描述
注意!
什么时候浏览器发送的是GET 请求?
1、直接在地址栏里,输入 URL
2、通过 a 标签跳转
3、通过 img / link / script… 标签,进行访问
4、通过 form 表单,同时 Method 指定为 GET
5、通过 ajax,将 type 的值 设置为 GET

那么,什么时候浏览器发送的是 POST 请求?
1、通过 form 表单,将 method属性的值指定为 POST。
2、通过 ajax,将 type 的 value值设置为 POST。


除了上述 浏览器请求中的方法 和 服务器 处理方法 不匹配 的 情况。
还有一种情况:
浏览器请求中的方法 和 服务器 处理方法 是 匹配的!
但是!还是会出现 405 的情况
我们来看一下具体情况
在这里插入图片描述


3、500

这个错误,也是属于出现频率很高的。

这么说吧:除了404之外,500就是出现频率最高的。

以 5 开头的状态码,是服务器出了问题。

一般 500 就意味着 服务器代码里抛出异常了,并且我们的代码没处理这个异常。
因此 异常就抛到 Tomcat 这里了。

如果代码中出现异常,可以通过 catch 来捕获到。
如果 catch 没捕获到:类型不匹配,压根没有catch到。
此时异常就会沿着 异常信息栈,向上传递。
异常的处理过程,可参考这篇文章 Exception - 异常

什么是向上传递呢?
举个例子:上培训客户课程。
甲 在教室里丢失了一手机,找不到了!【自己处理不了】
于是,找到班级老师反馈这个问题。【向上传递】
老师也帮忙找,也问了其他同学,都说不清楚。
于是 老师也没办法,于是反馈 “校长”。【老师解决不了,继续向上专递】
“校长”就说:教室里都是有监控,我们去调监控看看情况。
最终发现:甲的手机被他的手肘推到桌子之间的缝隙里面去了,夹着在。
这就是就是向上传递。
当前的问题解决不了,就交给上一级处理,层层向上传递。
直到问题被解决了,或者 传到头了 问题都没有被解决。
如果是 传到头,问题都没有得到解决,最终 以栈的形式,表现出来。
这也就是 异常信息栈 产生的原因。
如果问题被处理了,那就不会报错了,代码就跑起来呀!

这么说吧:代码出现问题,我们没发现(运行时异常)。
这个异常,就会被向上传递到 Tomcat,Tomcat 处理不了。
接着,就会向上传递给 JVM.
JVM 也处理不了,直接程序就崩溃了!
就会以 栈 的形式来表现了。

下面,我们就来构造一个异常,来“品味一下”。
在这里插入图片描述


4、出现 “空白页面”

该错误就如同字面意思一样:页面上什么显示都没有。
在这里插入图片描述


5、出现 “无法访问此网站”

在这里插入图片描述
一般出现这种情况,其中最大的可能性,就是 Tomcat 没有正确启动。

1、Tomcat 压根没有启动
2、Tomcat 不完全启动 / 没有正确启动
3、Tomcat 端口号被占用,导致启动失败。(但是你没有发现)

这好比,你去餐馆吃饭。
人家都没开门,你吃什么?
啃别人的卷帘门吗??


小结

作为程序员,会写代码固然重要。
但是 排查 代码问题,也是非常重要的!
千万不要因为出现了错误,而“不耐烦”!
要静心!
才能更好的 调式代码 和 维护代码,而且 这就是我们参加工作后,最主要的工作内容。


Servlet 运行原理

在 Servlet 的代码中我们并没有写 main 方法, 那么对应的 doGet 代码是如何被调用的呢? 响应又是如何返回给浏览器的?

首先我们要明确: Servlet 终究是处在 应用层里。
Servlet 是 HTTP 应用层里进行的一些操作。
那么,它的底层仍然是依赖着 传输层,网络层,数据链路层,物理层,这样的一些基础网络通信。

换句话来说:Servlet是属于上层建筑,下面的传输层,网络层,数据链路层…属于经济基础 / 底层。
毕竟:经济基础 决定上层建筑。
也就是说:下层 与 上层之间的关系是非常紧密的!


Tomcat 的定位

我们自己的实现是在 Tomcat 基础上运行的。
在这里插入图片描述
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求.

HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示:
在这里插入图片描述
更详细的交互过程可以参考下图:
可参考这两篇文章 网络初识网络协议之TCP/IP协议
在这里插入图片描述


Tomcat 的伪代码

就是说:如果让我们自己写一个 Tomcat ,或者让我们去写一个HTTP服务器,大概要这么写?
下面我就以 伪代码的形式来给大家介绍一下。

伪代码:
这个代码并不能真正的编译运行,因此我们不必拘泥于语法的具体形式。
我们主要通过伪代码来理解:实现某个功能的大致逻辑体系。
借助伪代码来疏通某部分的“逻辑阻塞”,构造出较为完整的实现逻辑。
&ensp;
其实伪代码这种,在很多资料书中都会用到。
比如:
与算法相关的一些书,很多都是使用伪代码来描述的。
例如《算法导论》,它里面几乎都是通过伪代码的方式 来描述 算法的具体实现。

下面是 关于 Tomcat的初始化流程 的 伪代码。
大家简单扫一遍,我会在代码下面进行解析。

class Tomcat {
    
    
    // 用来存储所有的 Servlet 对象
    private List<Servlet> instanceList = new ArrayList<>();
    public void start() {
    
    
// 根据约定,读取 WEB-INF/web.xml 配置文件;
// 并解析被 @WebServlet 注解修饰的类
// 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
        Class<Servlet>[] allServletClasses = ...;
// 这里要做的的是实例化出所有的 Servlet 对象出来;
        for (Class<Servlet> cls : allServletClasses) {
    
    
// 这里是利用 java 中的反射特性做的
// 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
// 方式(全部在 WEB-INF/classes 文件夹下)存放的,所以 tomcat 内部是
// 实现了一个自定义的类加载器(ClassLoader)用来负责这部分工作。
            Servlet ins = cls.newInstance();
            instanceList.add(ins);
        }
// 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
    
    
            ins.init();
        }
// 利用我们之前学过的知识,启动一个 HTTP 服务器
// 并用线程池的方式分别处理每一个 Request
        ServerSocket serverSocket = new ServerSocket(8080);
// 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
        ExecuteService pool = Executors.newFixedThreadPool(100);
        while (true) {
    
    
            Socket socket = ServerSocket.accept();
// 每个请求都是用一个线程独立支持,这里体现了我们 Servlet 是运行在多线程环境下的
            pool.execute(new Runnable() {
    
    
                doHttpRequest(socket);
            });
        }
// 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次;
        for (Servlet ins : instanceList) {
    
    
            ins.destroy();
        }
    }
    public static void main(String[] args) {
    
    
        new Tomcat().start();
    }
}

参考文章多线程基础篇reflection - 反射
在这里插入图片描述


Tomcat 处理请求流程 部分

    class Tomcat {
    
    
        void doHttpRequest(Socket socket) {
    
    
// 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析,和响应构建
            HttpServletRequest req = HttpServletRequest.parse(socket);
            HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
// 直接使用我们学习过的 IO 进行内容输出
            if (file.exists()) {
    
    
// 返回静态内容
                return;
            }
// 走到这里的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
            Servlet ins = findInstance(req.getURL());
// 调用 Servlet 对象的 service 方法
// 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
            try {
    
    
                ins.service(req, resp);
            } catch (Exception e) {
    
    
// 返回 500 页面,表示服务器内部错误
            }
        }
    }

在这里插入图片描述


Servlet 的 service 方法的实现 部分

    class Servlet {
    
    
        public void service(HttpServletRequest req, HttpServletResponse resp) {
    
    
            String method = req.getMethod();
            if (method.equals("GET")) {
    
    
                doGet(req, resp);
            } else if (method.equals("POST")) {
    
    
                doPost(req, resp);
            } else if (method.equals("PUT")) {
    
    
                doPut(req, resp);
            } else if (method.equals("DELETE")) {
    
    
                doDelete(req, resp);
            }
......
        }
    }

在这里插入图片描述


总结:

这一块的核心逻辑,主要分成两个部分来理解。

1、初始化 / 准备工作
   准备工作:把 Servlet 实例给加载起来。

2、处理请求
根据 URL找到匹配的 Servlet 类,再来去调用 Servlet 里面对应的方法、

另外,在讨论到这整套流程过程中,涉及到了 关于 Servlet 的 关键方法,主要由三个:

1、init:初始化阶段,对象创建好了之后,就会执行到。另外,用户可以重写这个方法,来执行一些初始化的操作。

2、destroy:退出主循环,Tomcat 结束运行之前回调用。主要是用来 释放资源。

3、service:在处理请求的阶段来调用,每次来个请求都需要调用一次 service。

注意! init 和 destroy 方法,都是一个 Servlet 对象调用一次。
而 service 可能会被一个对象,给调用很多次。
毕竟我们大部分时候,进入一个网站都是有目的,至少UI浏览一下,在浏览的过程中就需要进行多次交互,也就会发送多次请求,service 自然也就会被调用多次。

这几个方法的调用时机,都是比较固定的。
我们就把这几个关键方法,以及它们的调用时机,称为 Servlet 的 生命周期。

生命周期,这个词是计算机里一个挺常见的术语。
它的意思就是:什么时候该做什么事情。
比如:
我们在上学,该做的事情就是 学习。
我们在工作,该做的事情就是 挣钱。
稳定之后,该做大的事情就是找对象,结婚生子了。

也就是说,在人生的每一个阶段,我们都是有事可做的。
这就是 一个普通人 的 生命周期。

我们的代码,也是类似的。
很多的对象,也是会划分出几个阶段。
这个对象,第一阶段做什么,第二阶段做什么…
所以这里划分出的这些阶段,以及每个阶段执行的时机,就就叫做 声明周期。


Servlet API 详解

我们当前主要就掌握这里面提供的 三个类,就差不多了。

1、HttpServlet
2、HttpServletRequest
3、HttpServletResponse
我们把这三个类,它们是干什么的。怎么用的。
以及它们里面都有哪些属性和方法,以及这些属性和方法都是用来干什么的。

只要我们把这些搞清楚了,那么 Servlet 代码,我们就会了。


HttpServlet

我们自己写的代码,就是通过继承这个类,重写其中的方法,来被 Tomcat 调用执行的。

这个过程 和 继承 有关,就会涉及到Java中的一个核心语法 “多态”。
可参考 面向对象的编程(多态、抽象类、接口)博文 面向对象的编程(包、继承、多态的一部分:向下/向下转型,重写)
在我们学习Java的时候,多态就是我们一个难啃的骨头。
如果以后在面试的时候,被面试官问到。
让你讲讲对多态的理解,最好方式就是 举例子。
最好举一些有意义的例子,并且最好是关于代码的。
比如:
集合类: List list = new ArrayList<>();
多线程:class Mythred extends Thread{ 重写 run方法 }
像这种平常刷题经常会用到的代码,用来举例是最好的。

另外,Servlet 也是一个很好的例子!
因为我们自己写的代码也是通过继承重写的方式来实现的。
因此在执行的过程,是一定会涉及到 “多态的”!


HttpServlet 的 核心方法

方法名称 调用时机
init 在 HttpServlet 实例化之后被调用一次
destroy 在 HttpServlet 实例不再使用的时候调用一次【不靠谱】
service 收到 HTTP 请求的时候调用
doGet 收到 GET 请求的时候调用(由 service 方法调用)
doPost 收到 POST 请求的时候调用(由 service 方法调用)
doPut/doDelete/doOptions/… 收到其他请求的时候调用(由 service 方法调用)

我们实际开发的时候主要重写 doXXX 方法, 很少会重写 init / destory / service

.这些方法的调用时机, 就称为 “Servlet 生命周期”. (也就是描述了一个 Servlet 实例从生到死的过程).
在这里插入图片描述
注意: HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例.


doPost 代码实例

这里 doGet 就不演示了。
一开始重写的就是 doGet 方法。
我们直接演示 doPost 方法
在这里插入图片描述
那么,问题来了:通过 ajax 来构造请求太麻烦了,阿还需要去引入依赖jQuery。
是否有更简单的。不用写代码,就能构造 POST 请求的方式呢?

有一系列的第三方工具,可以构造任意的 HTTP 请求。
其中一个比较知名的,就是 PostMan。
在这里插入图片描述
postman 最初是 Chrome 的一个插件。
它的定位:帮助开发人员进行构造请求的调试,就是一个功能插件。
结果后来,不小心就火了。
于是官方就把 postman给提取出来了,另起炉灶。
专门搞了一个 postman 的程序,不再依赖Chrome了。
postman 和其它软件。还不一样!
它还有了一个对象叫做 postwoman,功能上和postman几乎一致。

下面,我们就来使用postman来构造一个请求。
在这里插入图片描述


HttpServletRequest

当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成HttpServletRequest 对象.

也就是说:HttpServletRequest 对应到一个 HTTP 请求。
HTTP 请求中有什么。HttpServletRequest 里面就有什么。


HttpServletRequest 的 核心方法

方法 描述
String getProtocol() 返回请求协议的名称和版本。
String getMethod() 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。
String getRequestURI() 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分。
String getContextPath() 返回指示请求上下文的请求 URI 部分。
String getQueryString() 返回包含在路径后的请求 URL 中的查询字符串。
Enumeration getParameterNames() 返回一个 String 对象的枚举,包含在该请求中包含的参数的名称。
String getParameter(String name) 以字符串形式返回请求参数的值,或者如果参数不存在则返回null。
String[] getParameterValues(String name) 返回一个字符串对象的数组,包含所有给定的请求参数的值,如果参数不存在则返回 null。
Enumeration getHeaderNames() 返回一个枚举,包含在该请求中包含的所有的头名。
String getHeader(String name) 以字符串形式返回指定的请求头的值。
String getCharacterEncoding() 返回请求主体中使用的字符编码的名称。
String getContentType() 返回请求主体的 MIME 类型,如果不知道类型则返回 null。
int getContentLength() 以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1。
InputStream getInputStream() 用于读取请求的 body 内容. 返回一个 InputStream 对象.

这里需要注意一下!

String getRequestURI()|从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分。
其中 方法的名称是 getRequestURI,不是getRequestURL。
URL 和 URI ,这其实是两个概念,但是这两个概念非常相似,甚至可以混用。
URL:全球资源定位器(Uniform Resource Locator)
URI(小写 u r i ):唯一资源标识符(Uniform Resource Identifier)
其实两个都是表示网上唯一资源的,只是说 URL 表示的是这个资源的位置,而 URI 表示的是这个资源的 ID。
因此,确实有时候,两者是可以混着用的!

下面再来关注三个方法

在这里插入图片描述

接着来看一组方法

在这里插入图片描述

后面剩下的方法,直接一起看。

在这里插入图片描述

大家需要注意:

关于 header,URL,这些方法,我们拿到的结果都是 String类型 的 数据。
至于有关 body 的方法,由于 body的长度可能会更长一些,我们并没有通过字符串方式来获取。
而是通过 流对象的方式,来获取。

在了解这些方法之后,其实我们就可以针对我们的请求进行一些处理了。

那么,接下来我们就来通过一些简单的示例,来加深对上述方法的理解。


代码示例一:打印请求信息

在这里插入图片描述


代码示例二:获取 GET 请求中的参数

有的人可能会有疑问:这个会不会跟上面的冲突了。

不会!这个实例,我们主要是针对 getParameter。
至于 getParameterNames 和 getParameterValues 用的很少,就不演示了。
在这里插入图片描述
当然如果我们不再URL中添加 queryString,输出的值就是一个null值。
在这里插入图片描述
有了这样的 API,我们后续就可以借助这样的一个操作,来通过 queryString 携带一些相关的参数交给服务器。
服务器就可以做出更加灵活的处理了。
其实 getParameter 才是我们写 servlet代码中 最常使用的 API 之一。


代码示例三: 获取 POST 请求中的参数

首先,我们来回顾一下,POST 请求 的body 格式

1、x-www-form-urlencoded
2、form-data
3、json
由于 第二种(form-data),暂时不做演示。
我们主要 演示 第一种(x-www-form-urlencoded) 和 第三种(json) 格式。


1、x-www-form-urlencoded

如果请求是这种格式,服务器如何获取参数呢?

获取参数的方式 和 GET一样,也是 getParameter !!!

如何在前端构造一个 x-www-form-urlencoded 格式的请求?
1、form 表单
2、postman


1.1、form表单

在这里插入图片描述


1.2、postman

在这里插入图片描述


3、 那如果是第三种 json 形式的POST请求,又该如何处理?

在这里插入图片描述
我们不难发现 json 的格式 ,与前面的格式是完全不同的!
它采用 JS 中对象的格式。

对于 中body 为 json的格式来说:
如果手动 解析,其实并不容易!
因为 json 里面字段是能嵌套的,如下面这种情况
在这里插入图片描述
说白了就是说:json 支持 套娃操作!
而且,除了json对象之外,还可以是数组等其他的数据;并且可以套多层“娃”。
所以手动解析json格式的请求很麻烦!

因此,想这种情况,手动处理比较麻烦,我们可以使用第三方的库来直接处理 json 格式的数据。
另外,在 java生态中,用来处理 json 的第三库的种类也很多!
我们主要使用的库,叫做 Jackson。(Spring 官方推荐的库)
Spring 默认就是使用 Jackson 来处理 json 格式的数据。

此时,我们就可以通过 Maven 把 Jackson 这个库下载到本地,并引入项目中。
中央仓库:https://mvnrepository.com/
在这里插入图片描述

现在 Jackson 就可以使用了!
在使用之前,我们需要准备一分 json 形式的请求。

我们需要分两个角度去看:
1、在浏览器前端代码中,先通过 JS 构造出 body 为 json 格式的请求。
2、在Java后端代码中。通过 Jackson 来进行处理。

在浏览器前端代码中,先通过 JS 构造出 body 为 json 格式的请求。

在这里插入图片描述

2、在 Java 后端代码中,通过 jackson 来处理。

需要使用 Jackson,把请求body中的数据读取出来,并且解析成 Java 对象。
在这里插入图片描述


我们来看一下 postman 怎么去构建一个 json请求

在这里插入图片描述


HttpServletResponse

同样的,HttpServletResponse 对应到 一个 HTTP 响应。
HTTP 响应中有什么,这里就有什么。

Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到
HttpServletResponse 对象中.
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过Socket 写回给浏览器


HttpServletResponse 核心方法

方法 描述
void setStatus(int sc) 为该响应设置状态码。
void setHeader(String name,String value) 设置一个带有给定的名称和值的 header. 如果 name 已经存在,则覆盖旧的值.
void addHeader(String name, String value) 添加一个带有给定的名称和值的 header. 如果 name 已经存在,不覆盖旧的值, 并列添加新的键值对
void setContentType(Stringtype) 设置被发送到客户端的响应的内容类型。
void setCharacterEncoding(String charset) 设置被发送到客户端的响应的字符编码(MIME 字符集)例如,UTF-8。
void sendRedirect(String location) 使用指定的重定向位置 URL 发送临时重定向响应到客户端。(用于构造一个302重定向响应)
PrintWriter getWriter() 用于往 body 中写入文本格式数据.(常用)
OutputStream 、getOutputStream() 用于往 body 中写入二进制格式数据

代码示例: 设置状态码

在这里插入图片描述


代码示例: 自动刷新

实现一个程序, 让浏览器每秒钟自动刷新一次. 并显示当前的时间戳.

实现一个“自动刷新”这样的页面效果
只需要给 HTTP响应中设置一个 header: Refresh 即可。
在这里插入图片描述


代码示例: 重定向

实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面.

说白了,就是构造一个重定向的响应
下面,我们来构造一个302,让浏览器触发 跳转页面的工作。
在这里插入图片描述


总结

光理解了 Servlet API,还不足以支撑我们写出一个功能完整的网站。
还需要理解一个网站的开发过程大概是什么样子的。
理解这里的一些基本的编程思维 和 设计思路。

这就需要通过更多的案例,来进行强化了。


实战案例


实现服务器版本的表白墙 - 服务器内存版-(非持久化存储)

这个其实在前面讲JavaScript的时候,已经讲过了的.
就是最后一个案例。
在这里插入图片描述

我就直接拿过来用了。建议点击链接,看看这个页面是怎么实现的!
要不然下面有些东西,你是看不懂的!
就是说:你会不明白代码为什么这么写!
在这里插入图片描述

前面我们实现这个页面的时候,只要我们一刷新,下面的信息记录就会被清空。

因为当下的信息记录,只是保存在 页面 / 内存 里,因此 信息记录是及其容易丢失的。
想要做到真正持久化存储,我们需要将其信息记录 存入到 服务器当中。

首先我们要明白 这个页面(客户端) 该如何 与 服务器 进行交互。

进一步来说:约定前后端交互的接口。

这个操作过程,其实就是在自定义应用层协议。
我们既然是搞一个服务器,服务器就得提供一些服务。
那么,具体是提供什么样的服务?以及这些服务该如何触发?
这些都需要我们去考虑清楚!

对于当下这个表白墙来说,我们主要提供两个接口

1、告诉服务器,当前留言了一条什么样的数据
2、从服务器获取到:当前都有那些留言数据

第一个接口服务的触发条件:

当用户点击 “提交按钮” 的时候,就会给 发送一个 HTTP 请求。
让服务器 把这个信息 存下来。

约定好,为了实现这个效果,客户端发送一个什么样的 HTTP 请求,服务器返回一个什么样的 HTTP 响应。
在这里插入图片描述

第二个接口服务的触发条件:
当页面加载的时候,就需要从服务器获取到曾经存储的的消息内容。

在这里插入图片描述

确定好接口之后,就可以编写代码了。

现在的我们即需要编写后端代码,也需要编写群短代码。
在实际工作中,一个项目,前端和后端是“两个人 / 两个小组”分别负责的。
此时两个人就是在并行的开发,前端的负责前端,后端的负责后端。
两边代码一写完,放在一起,测试一下,俗称 “联调” 。
如果 联调的结果,没有问题,那么这个项目也就完成了。

此时我们就是“全栈”,不过也没有办法,毕竟卷死别人,总比被别人卷死的好!

废话不多说!为了避免程序杂乱。我们新建一个项目来完成“表白墙”。
还记得那7个步骤嘛?

1、创建Maven项目
在这里插入图片描述
2、引入依赖
在这里插入图片描述
3、构建目录
在这里插入图片描述
4、编写代码
在这里插入图片描述

5、打包
6、部署
在这里插入图片描述

这里我们在补充一个点!
Smart Tomcat 并不是真的把 项目 打包了!!!
其实相当于把 我们创建的 webapp 目录 作为 Tomcat 启动的一个参数,给设定进去了。
让Tomcat 直接从这个指定目录中加载 webapp。

这么说吧:让Tomcat直接从 webapps 中 加载 war包,但这不是唯一的出路!
Smart Tomcat 相当于是走了一个捷径,从而节省了打包的过程。

就比如现在,我使用了 Smart Tomcat 进行 打包 和 部署。
照理来说:此时 cw 的 war包,应该处于 Tomcat目录中的 webapps 路径底下存在?
在这里插入图片描述
你会发现 cw 的 war 包,压根就不在这个 目录底下!!!
像我们看到的 servlet_hello 和 blogSystem 都是我们手动拷贝进去的!!!

其实 这两步是可以省略的
在这里插入图片描述
至于 Context Path,我们配置 Smart Tomcat 的时候,不是有默认的路径吗?
直接用就行了!只要你输入上下文路径的时候,别输入错误就行!!
至于 第一行的名字,这个你可以随便去个名字,它不影响代码的执行!
在这里插入图片描述

7、验证代码
在这里插入图片描述


既然代码执行起来没有问题,我们实现具体的代码逻辑。
在这里插入图片描述
此时,服务器的两个服务接口,我们就完成了。


下面,我们就来编写前端页面的代码。
在这里插入图片描述


下面我们来看一下前后端代码结合执行的效果。
在这里插入图片描述
只要服务器不重启,数据一直都会在。
如果重启服务器,数据就会被清空
在这里插入图片描述


表白墙 - 进阶(MySQL版本) - 持久化存储

想要解决上面那个版本存在的问题:服务器重启,数据不丢失。

最好的办法:将数据存储到硬盘上。
存储的方式:可以是文件。也可以数据库。

可参考MySQL专栏博客。

我们采取的方式:是 MySQL 数据库的方式 来实现 持久化存储。

首先,既然我们要使用数据库来实现持久化存储。
那么,我们在服务器代码中,需要引入依赖(MySQL)。

中央仓库链接:https://mvnrepository.com/
在这里插入图片描述

既然我们使用MySQL 作为存储数据的手段。
那么原先的代码有些东西,就不在需要了!

为了“嵌入”MySQL,我们需要添加一些方法去辅助我们。在这里插入图片描述

在实现 save 和 load 方法之前,我们先来在 MySQL上创建一个数据表来存储告白墙的信息。
在这里插入图片描述

下面,我们就来为完善 save 和 load 方法做准备。
在这里插入图片描述

注意!上面的代码存在一个巨大的问题:线程安全!
可参考 博文 多线程基础篇
在这里插入图片描述

下面,我们可以来完善 MessageServlet 当中的 save 和 load 方法。

先来看 save 方法
在这里插入图片描述
load 方法
在这里插入图片描述


DButil - 数据库访问程序

主要是简化 MessageServlet 中的程序。

将 数据库连接,创建数据源,资源释放,具体实现细节给封装成一个类。
来供 MessageServlet 中的程序使用。

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DButil {
    
    
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false";
    private static final String User = "root";
    private static final String Password = "123456";// 输入你们自己MySQL密码
    // 创建数据源 - 懒汉模式(单例模式的一种)
    // 加上 volatile 防止编译器对其进行优化。 
    private static volatile DataSource dataSource = null;

    // 获取数据源
    private static  DataSource getDataSource(){
    
    
        if(dataSource == null){
    
    // 提升效率
            synchronized (DButil.class){
    
    //保证线程安全
                if (dataSource == null){
    
    
                    dataSource = new MysqlDataSource();// 创建数据源
                    // 描述/设置 服务器主机地址,用户名,以及密码
                    ((MysqlDataSource)dataSource).setURL(URL);
                    ((MysqlDataSource)dataSource).setUser(User);
                    ((MysqlDataSource)dataSource).setPassword(Password);
                }
            }
        }
        return dataSource;
    }
   // 与数据库建立连接
    public static Connection getConnection() throws SQLException {
    
    
        return getDataSource().getConnection();
    }
    //关闭连接,释放资源
    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){
    
    
        if(resultSet != null){
    
    
            try {
    
    
                resultSet.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if (statement != null){
    
    
            try {
    
    
                statement.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if (connection != null){
    
    
            try {
    
    
                connection.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}


MessageServlet 服务器总程序

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

class Message{
    
    
    public String from;
    public String to;
    public String message;
}

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    
    
    // 处理:提交消息的请求
    private ObjectMapper objectMapper = new ObjectMapper();//用于读取请求,并解析
    // 保存信息
    private void save(Message message){
    
    
        Connection connection = null;
        PreparedStatement statement = null;
        // 1、和数据库建立连接
        try {
    
    
            connection = DButil.getConnection();
            //2、构造SQL语句
            String sql = "insert into message values(?,?,?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1,message.from);// 替换第一个?号
            statement.setString(2,message.to);// 替换第二个?号
            statement.setString(3,message.message);// 替换第三个?号
            //3、执行 SQL
            statement.executeUpdate();

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //4、释放资源
            DButil.close(connection,statement,null);
        }
    }
    // 从数据中获取所有的消息
    private List<Message> load(){
    
    
        List<Message> messages = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
    
    
            // 1、和数据库建立连接
            connection = DButil.getConnection();
            //2、构造SQL语句
            String sql = "select * from message";
            statement = connection.prepareStatement(sql);
            //3、执行 SQL
            resultSet = statement.executeQuery();
            //4、遍历结果集
            while(resultSet.next()){
    
    
                Message message = new Message();
                message.from = resultSet.getString("from");
                message.to = resultSet.getString("to");
                message.message = resultSet.getString("message");
                messages.add(message);
            }

        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //5、释放资源
            DButil.close(connection,statement,resultSet);
        }
        return messages;
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        // 解析数据
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        // 保存数据
        save(message);
        //返回响应:主要是告诉我们的页面。信息是否存入成功。
        // 因为内容比较简单,我们就直接“输出”了。
        resp.setContentType("application/json; charset=utf8");// “指定”(告知浏览器/页面)返回数据的格式 与 读取方式
        // 注意!如果不加上 setContentType 这行代码,浏览器就会把下面返回的数据当做普通的字符串来读取!
        // 我们通过 setContentType 这行代码,告知浏览器数据是一个 json 形式的数据,jQuery的ajax就会将数据转换成json形式
        resp.getWriter().write("{ \"ok\": true }");
    }
    //处理:获取消息列表请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        // 获取整个消息列表中的元素,将其全部返回给客户端即可。
        List<Message> messages = load();
        String jsonString = objectMapper.writeValueAsString(messages);
        resp.setContentType("application/json; charset=utf8");//这行代码的用处,就多说了。
        // 将其转化的结果,返回给客户端
        resp.getWriter().write(jsonString);
    }
}

效果图

在这里插入图片描述


小结

开发一个表白墙(一个简单网站)基本步骤:

1、约定前后端交互的接口
请求是什么格式?响应是什么格式?

2、开发服务器代码
  2.1、先编写 Servlet 能够处理的前端请求
  2.2、编写数据库代码,来 存储 / 获取 关键数据

3、开发客户端代码
  3.1、基于 ajax 能够构造请求以及解析响应
  3.2、能够响应用户的操作。
比如:我们在点击按钮之后,触发给服务器发送请求的行为。

以后遇到像这种“网站”类的程序,实现过程都是类似的!
可以说,这就是一个模板。

说到这里,我们就需要拓展一下了。
MVC
M:Model(模式) - 操作数据存取的逻辑
V:View(视图)- 页面的显示
C:Controller(控制器) - 处理请求之后的关键逻辑

View 是和用户进行交互的,View 再和 Controller 进行交互。
相当于是 View 构造 请求,Controller 在进行处理。
Controller 再和 Model 进行交互,Model 开始操作数据在这里插入图片描述


Cookie 和 Session

Tomcat 和 HTTP协议博文中,介绍过。
但是!只是站在一个纯理论的基础上来理解的。

这里再把它拿出来讲。
是因为在 Servlet 中,对于 Cookie 和 Session 都有很好的支持!
所以,我们就可以基于 Servlet 里面提供的 Cookie 和 Session 的 API,来进行会话管理类似的操作。

回忆之前的例子:

  1. 到了医院先挂号. 挂号时候需要提供身份证, 同时得到了一张 “就诊卡”, 这个就诊卡就相当于患者的 “令牌”(相当于是 cookie,里面存储这用户的详细信息).
  2. 后续去各个科室进行检查, 诊断, 开药等操作, 都不必再出示身份证了, 只要凭就诊卡即可识别出当前患者的身份.
  3. 看完病了之后, 不想要就诊卡了, 就可以注销这个卡. 此时患者的身份和就诊卡的关联就销毁了. (类似于网站的注销操作)
  4. 又来看病, 可以办一张新的就诊卡, 此时就得到了一个新的 “令牌”
    在这里插入图片描述
    但其实在实际情况中,使用的是一个 session 的方式来保存的。
    毕竟一个人,他可能不止一张卡,而且卡可能会丢。
    因此将身份信息全部存储到卡上并不安全,所以,卡上只需要存储一个会话窗口编号(sessionId)。
    通过这个会话窗口编号,就可以打开对应的窗口,调取对应的信息。

    也就是说:
    每一张卡 相当于 开启了 一个会话窗口,都可以通过这个会话窗口来获取自身的详细信息。

    多余的,就不再讲了,前面博文中已经讲得很清楚了。

另外,这里我们补充一点:
Cookie 和 Session 的区别:

Cookie 是客户端的机制. Session 是服务器端的机制.
Cookie 和 Session 经常会在一起配合使用. 但是不是必须配合.


核心方法

HttpServletRequest 类中的相关方法

方法 描述
HttpSession getSession() 在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果为 false, 则当不存在会话时返回 null
Cookie[] getCookies() 返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把Cookie 中的格式解析成键值对.

getSession 方法,既能用于获取到服务器上的会话,也能用于创建会话。

它的具体行为,取决于参数。
如果参数为 true,会话不存在,则创建一个会话。
反之,如果参数为false,会话不存在,则返回一个null。

这就好比,我们来医院看病。
但是这个医院,我们是第一次来。
肯定是要先挂个号,那么人家肯定会问你有没有就诊卡?
如果你有,人家就不用给你办卡了。
反之,人家就会根据你的身份信息,给你办一张就诊卡。(工本费15元)

大家要明确:
在办卡之后,你拿到的不光是一张卡,还在该医院的服务器上存了一份档案。
这份档案就相当于是一个会话,卡上存储的就是 这个会话窗口的id号(sessionId)。

在实际情况中,调用 getSession 的时候具体做什么?
1、创建会话

首先先获取到请求中 cookie 里面的 sessionId 字段。
sessionId 就相当于是会话的身份标识。
然后我们去判定这个sessionId 是否在当前服务器上存在。
如果不存在,则进入创建会话 逻辑 / 操作。


创建会话:会创建一个 HttpSession 对象,并且生成一个 sessionId。
sessionId 是一个很长的数字,通常是用 十六进制来表示的。
这个数字能够保证唯一性:它这里面有一些列相关的算法,能够生成一个唯一的sessionId,然后这个Id 就作为我们当前会话的身份标识。

接下来,就把 sessionId 作为 key,把这个 HttpSession 对象,作为 value。
把这个键值对,给保存到 服务器内存 的一个“哈希表” 这样的结构中。
这里的“哈希表”:只是表示一个类似 哈希表的数据结构。
也就是说,一定是一个键值对结构,但是 是不是 哈希表 就不能保证了。

再然后,服务器就会返回一个 HTTP 响应,把 sessionId 通过 Set-Cookie 字段 返回给浏览器。
然后,浏览器就可以保存这个 sessionId 到 cookie中了。

2、获取会话

先获取到请求中的 cookie 里面的 sessionId 字段,也就是会话的身份标识。
判断这个 sessionId 是否在当前服务器上存在,也就是遍历“哈希表”中key,来判断这个 sessionId 是否包含在其中。
如果有,就直接调出 sessionId 对应的 HttpSession对象,并且通过返回值返回去。
会话,我们搞清楚了。
我们再来谈谈 HttpSession是什么?
HttpSession,其实就是 创建会话的时候,需要生成的一个关键对象。
这个对象本质上也是一个“键值对”的j结构。
注意! 由于 HttpSession 对象是一个键值对结构,所以允许程序员 对 HttpSession 对象进行套娃操作!
这和我前面讲的 表白墙 中获取所有信息记录的响应格式json是一样的。
一个 json 对象 可以让 另一个json 对象,作为自己keys中的一个value值。
HttpSession 也是如此,可以让另一个 HttpSession 对象作为 自己keys中的一个value值。
HttpSession键值对结构中,key必须是字符串类型,value 是一个 Object类型。
在这里插入图片描述
HttpSession 里面的每个键值对,称为 属性(Attribute)

还有一个方法:getCookies()
在这里插入图片描述

Cookie 类中的相关方法

每个 Cookie 对象就是一个键值对

方法 描述
String getName() 该方法返回 cookie 的名称。名称在创建后不能改变。(这个值是 SetCooke 字段设置给浏览器的)
String getValue() 该方法获取与 cookie 关联的值
void setValue(String newValue) 该方法设置与 cookie 关联的值。

HttpServletResponse 类中的相关方法

方法 描述
void addCookie(Cookie cookie) 把指定的 cookie 添加到响应中

响应中就可以根据 addCookie 这个方法,来添加一个 Cookie 信息(键值对)到 响应报文中 ,

这里添加进来的键值对,就会作为 HTTP 响应中的 Set-Cookie 字段来表示。

换个说法:把一个cookie键值对 转换成 字符串 格式,通过 Set-Cookie 来返回到 客户端(浏览器)这里。


HttpSession 类中的相关方法

一个 HttpSession 对象里面包含多个键值对. 我们可以往 HttpSession 中存任何我们需要的信息

方法 描述
Object getAttribute(String name) 该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null.
void setAttribute(String name, Object value) 该方法使用指定的名称绑定一个对象到该session 会话
boolean isNew() 判定当前是否是新创建出的会话

实战案例:网页登录

在这里插入图片描述

理解上述登录页面的逻辑之后,我们就可以开始尝试开发了。

在编写案例代码之前,我们需要先约定前后端交互接口。
根据上面的逻辑图,一共有两组交互:
1、登录
2、获取主页
在这里插入图片描述
大家要明白:
针对前后端交互接口,这里有很多种约定方式。
这么多约定方式,使用哪种都可以!
一定要确定出一种约定方式,
然后前后端代码的编写,就需要围绕着这一种约定的方式进行编写。
如果如果不按照这种方式去写,代码会出现很多问题!
比如:请求是GET,但是服务器中处理请求的方法是POST,那么就会触发405.


下面,我们就可以正式开始编写代码。

首先我们来写一个简易风格的登录页面

在这里插入图片描述


接下来,就是编写一个服务器程序,用于处理登录页面发送的请求。

在这里插入图片描述


编写服务器返回主页的逻辑

在这里插入图片描述


效果展示

先来看一下,登录成功的效果。
在这里插入图片描述
下面我们再来看一下登录失败的效果。
在这里插入图片描述
在这里,我再强调一句:会话里,是可以保存任意指定的用户信息的。
用户名,用户id,性别,账号等等… 都是可以存入会话中的!


拓展:在这个网页登录的基础上,添加一个简单的功能>>记录当前用户访问主页的次数

预期效果:当登录之后,首次访问主页,主页上就显示,当前访问了 1 次。
如果,我们后续再刷新页面,次数就会累加。
在这里插入图片描述
效果图
在这里插入图片描述


上传文件

上传文件也是日常开发中的一类常见需求. 在 Servlet 中也进行了支持.

核心方法

HttpServletRequest 类方法

方法 描述
Part getPart(String name) 获取请求中给定 name 的文件
Collection getParts() 获取所有的文件

其中最常使用的方法:getPart 方法

明确一点:
上传文件的时候,在前端需要用到 form 表单。
from 表单中需要使用特殊的类型:form-data

此时提交文件的时候,浏览器就会把文件内容以 form-data 的格式构造到 HTTP请求中。
然后,服务器就可以通过 getPart 来获取了。

注意!一个HTTP请求,可以一次性的提交多个文件的。
每个文件都称为是一个 Part。
每个Part 都有宇哥name(身份标识)
服务器代码中就可以根据 name 找到对应的 Part。
基于这个 Part 就可以进一步来获取到文件信息,并进行下一阶段操作。

Part 类方法

方法 描述
String getSubmittedFileName() 获取提交的文件名
String getContentType() 获取提交的文件类型
long getSize() 获取文件的大小
void write(String path) 把提交的文件数据写入磁盘文件

简单案例:上传一个图片文件

前端页面代码

在这里插入图片描述


后端:写一个 servlet 来处理 上传 请求

在这里插入图片描述


效果图

在这里插入图片描述

文章结束

猜你喜欢

转载自blog.csdn.net/DarkAndGrey/article/details/124904924
今日推荐