Servlet知识详解

x33g5p2x  于2022-04-26 转载在 其他  
字(31.9k)|赞(0)|评价(0)|浏览(363)

这篇博客,是基于上一篇博客对Servlet 知识的拓展。有兴趣的可以看一下。

一、安装 Smart Tomcat 插件

对于上篇博客讲到,将Tomcat和Servlet 中的代码联系起来,具体的步骤比较琐碎,但是并不复杂。需要创建目录、打包、部署程序到webapp 中 等操作,是比较麻烦的。

而引入了IDEA 中带有的 smart Tomcat 插件,对我们提高开发效率有一定的帮助。IDEA专业版自带有该插件,社区版就需要自行在IDEA中下载

在File栏中打开Setting:搜索smart Tomcat进行安装。

下载好smart Tomcat插件后,需要进行配置。

点击上图的按钮,会弹出一个界面,点击左上角的+ 号,选中smart Tomcat选项:

点击smart Tomcat选项后,还会有下图的界面,点击OK后,就会出现右上角那样的图标了:
需要注意的是:Context Path是上下文的意思,能够确定是哪个webapp,在访问Servlet代码的的时候会用到。

注:Toomcat不是 IDEA 的一部分,它们两个是完全互不相干的进程。
IDEA中一点击就能运行并且现显示 Tomcat的日志,这个过程其实是 IDEA 这个进程,调用了Tomcat 进程(进程创建+程序替换),IDEA 把Tomcat 的输出内容重定向到自己的终端窗口中。

我们写好了相关的代码后,运行就会自动地部署程序到Tomcat中,在IDEA 终端里面有个路径,可以打开去看一下该路径下有什么。

打开该路径的文件,我们可以发现,在我们的用户目录底下,它创建了一个.SmartTomcat 目录,里面存放的有很多子目录,都是包含着跟Servlet代码有关的文件。

实际上,Smart Tomcat这个插件的运行原理,并没有打包,而是只是给这个项目,创建了一个单独的目录,把当前正在运行的Tomcat 给临时复制了一个副本,来运行当前正在编辑的代码。
因此我们在webapp 中是看不到war 包的。

二、对于浏览器中的访问出错

代码:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("GET 请求");
    }
}

1.出现 404

最大的原因,就是路径写错了

a) 没有写上下文,即java100_Servlet ,这个在我们配置smart Tomcat的时候设置的。

b) 写了上下文没有写与代码中@WebServlet注释中写的路径,就没法将HT特定的HTTP 请求和代码相关联。

c) URL中写的最后的路径与代码中注释的路径不匹配,也会出现404 .

d) 如果 web.xml 配置错误,也会出现404 ,但是这个是复制上去的,一般不会错。

2.出现405

405的主要原因:请求的方法和代码中重写的方法对不上

a)在浏览器中访问Tomcat上的资源是GET 方法,而Servlet中的doPost方法是处理Post方法的。

b) 在重写doGet 方法时,编译器会自动地调用父类的doGet方法。父类中的doGet代码中,就是直接地返回405.

3.出现500

主要原因是:代码中抛出异常了,页面上或者Tomcat 日志中都会明确提示出异常的信息调用栈等详细信息

将代码改为:
字符串为空去求长度是不可以的,就会抛出异常。

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String str = null;
        int n = str.length();
        resp.getWriter().write("GET 请求");
    }
}

需要注意的是:实际开发中不要把错误信息直接显示在页面上。

4.出现空白页

出现空白页的原因是:在Servlet代码中什么都没有实现。即 如果把doGet方法内的唯一一条代码注释掉,就会出现空白页。

5.无法访问此网站

这个的主要原因是:Tomcat启动失败。
将注释变为hello,而不是/hello ,再去打包程序的时候,就会出错了。此时再去访问Tomcat的资源,就会显示无法访问。

页面错误:

部署时的错误提示,一般在终端有很多的信息时,错误的提示都在最上方。

三、Servlet运行原理

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

这个代码是基于在Tomcat 的基础上运行的。

上面的流程图,讲的是:Web Browser 是客户端,通过HTTP 协议发给HTTP 服务器,即Tomcat。Tomcat 拿到了HTTP 请求后,就会对请求进行解析,生成一个 HTTPServletRequest 对象。我们调用Servlet 类,来执行程序员写好的逻辑(Servlet Program),此时还有可能会连接到数据库。

更详细的过程:

  1. 接收请求:
  • 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求.
  • 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机(这个过程也需要网络层和数据链路层参与).
  • 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理(根据端口号确定进程)
  • Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
  1. 根据请求计算响应
    在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等
  2. 返回响应:
  • 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把 HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去.
  • 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去.
  • 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机(这个过程也需要网络层和数据链路层参与).
  • 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理.
  • 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上

Servlet 的伪代码,可以自己去查看,只是了解Servlet运行的逻辑

四、Servlet API 详解

1. HttpServlet

我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法。

核心方法:

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

继承HTTPServlet 是为了重写该类里面的一些方法,重写方法的目的是为了能够把程序员定义的逻辑给插入到Tomcat 这个“框架”中,好让Tomcat 进行调用

类似于这样的操作,在前面也是见过的。
例如:Comparable、Comparable,它们是我们重写了里面的CompareTo方法和Compare方法,是根据我们自己的逻辑去执行代码,调用是该接口自己根据什么情况才去调用的。还有多线程中,类继承于Thread重写run方法,我们实际上也没有调用run。是利用多态的方式去实现的

实际上,在其它语言中,还有更简洁的做法:
如JS中的函数,对于一个操作:如:只需要赋值一个函数过去即可。

button:onlick=function() {
}

一个常见的面试题
说一下Servlet 的生命周期
答:
第一句话:Servlet在实例化之后调用一次init
第二句话:Servlet 每次收到请求,调用一次service
第三句话:Servlet 在销毁之前,调用一次 destroy

乱码问题:
当我们在body 中写入中文后,如下代码:

再在浏览器的控制台中去查看body时,发现出现了乱码的现象:

这个问题的原因是:IDEA 中与浏览器的编码方式是不一样的,在IDEA 中的编码方式是UTF-8,而在浏览器中的编码方式是挺复杂的,这个在浏览器中可以看到,因此浏览器就会默认按照该编码方式来解析响应,自然地,在控制台中看到的就是乱码了。

解决方案:
1.让服务器返回的数据就是 浏览器 的编码方式,和浏览器的编码方式一致。不推荐
2.让浏览器按照UTF-8进行解析,则只要在响应中的 header 里面加上Content-Type,在Context-Type 里面注明响应的编码是UTF-8 就可以了

先在webapp 中创建一个html文件,在html中设置Get请求的按钮和POST 请求的按钮,引入jQuery的第三方库来创建ajax表单,并且设置表单中的类型、发送路径、打印日志的方法。

前端代码:

<body>
    <button onclick="sendGet()">发送 Get 请求</button>
    <button onclick="sendPost()">发送 POST 请求</button>

    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script>
        function sendGet() {
            $.ajax({
                type:"get",
                url:"method",
                sucess:function(data,status) {
                    console.log(data);//打印的是body
                }
            })
        }
        function sendPost() {
            $.ajax({
                type:"post",
                url:"method",
                data:"request body",//post方法中有正文
                sucess:function(data,status) {
                    console.log(data);
                }
            })
        }
    </script>
</body>

界面如下:

在doxx方法中设置Type,如后面这段代码:resp.setContentType("text/html; charset=utf-8");
注意:设置Content-Type 的时候,是先设置后再去输出的,否则在控制台打印的仍然是GET ??? ,就没有起到编码的作用,因此要保证SetContent-Type 和 write 的先后顺序

单单设置这个还不够,还要设置该项目的编码方式。打开Setting,搜索encoding,有一个File encoding,将下图的两个选项选择utf-8即可。

对构建ajax表单的时候,url需要注意的点:构造请求的时候,路径的前面不要带 / ,否则 / 就是根目录了
(java100_servlet是上下文,method是代码中@WebServlet注释中的/method)

浏览器的编码方式可以在抓包的时候看到,在没有设置浏览器的编码方式的时候,去发送一个GET 请求,此时去抓包可以看到,里面的Content-Type 的编码方式是ISO-8859-1 .

2. HttpServletRequest

这个类就表示一个Http请求,理解这个类的前提就是要理解http 协议的格式。

回顾下Http请求的报文格式:
1.首行:方法类型,URL,版本号。URL 中进一步地分出来path,query string
2.header,一堆键值对,键值对的类型也很多
3.空行
4.body

在HttpServletRequest类中,有很多的方法,能够将Http报文格式里的内容给分离出来。由Tomcat把 字符串结构 的请求解析成一个结构化的数据 即 从字符串到结构化的数据,这样的过程称为“反序列化”

核心方法:

方法描述
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 对象

通过这些方法可以获取到一个请求中的各个方面的信息,请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 “读” 方法, 而不是 “写” 方法

注意:
1.URL和URI 的含义是类似的,都是表示网络上的一个资源,L指的是Location(资源的位置),I指的是 identify (资源的标识符),
2.String getQueryString()方法返回的是完整的query string;如query string:a=10&b=20 ,返回的就是整个。
而下面这三个方法,都是对于query string 来操作的。第一个是返回的是所有query string 所有的key,以枚举的类型返回。第二个是返回key对应的value值。第三个是返回某个key 的所有value值,返回的是字符串数组类型,大多数用的是参数重复的时候(可能参数为a=10&a=20)。

3.下面这两个方法都是对header进行操作的。

假设报头为:

那么第一个方法返回的是header头中的key值,如:Content-Tpye、Content-Length等。第二个方法是返回header头中对应key 的value值。

4.下面这个方法是:先获取到InputStream对象,之后就可以从里面读到body的内容了。

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

要求:打印GET 方法(直接去访问服务器代码)中的请求信息。

思路:用StringBuilder 进行数据的拼接,将StringBuider的拼接后的数据以html 的形式显示到页面上。需要注意的是,获取到header中所有的key之后(枚举类型),需要进行类似迭代器的方式获取到对应的value值。

public class showRequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 把生成的响应的 body 给放到 respBody 中
        StringBuilder respBody = new StringBuilder();
        respBody.append(req.getProtocol());//获取协议类型
        respBody.append("<br>");
        respBody.append(req.getMethod());//获取方法类型
        respBody.append("<br>");
        respBody.append(req.getRequestURI());//获取URL
        respBody.append("<br>");
        respBody.append(req.getContextPath());//获取上下文
        respBody.append("<br>");
        respBody.append(req.getQueryString());//获取查询字符串
        respBody.append("<br>");

        respBody.append("<h3>headers:</h3>");
        Enumeration<String> headerNames = req.getHeaderNames();//获取header中的各种类型信息
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            respBody.append(headerName + ": ");
            respBody.append(req.getHeader(headerName));
            respBody.append("<br>");
        }
        resp.getWriter().write(respBody.toString());//以字符串的形式写入
    }
}

页面显示效果:

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

GET 请求中的参数一般都是通过 query string 传递给服务器的. 形如:

https://v.bitedu.vip/personInf/student?userId=1111&classId=100

此时浏览器通过 query string 给服务器传递了两个参数, userId 和 classId, 值分别是 1111 和 100,在服务器端就可以通过 getParameter 来获取到参数的值

因此我们可以约定,在客户端中的query string 中假设只有两个参数,userId和classId,当我们从客户端访问服务器的时候,让服务器能够返回客户端中输入的userId和classId 中的参数

创建GetParameterServlet类
因为约定的参数只有userId和classId,因此调用getParameter 的时候去查找名字相同的即可。

@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        String userId = req.getParameter("userId");
        String classId = req.getParameter("classId");
        // 很多时候需要对 query string 进行判定是否存在, 判定代码就得写成这种方式:
        if (userId == null || userId.equals("")) {
            // 参数不存在
            // 处理不存在的情况.....
        }
        resp.getWriter().write(String.format("userId: %s; classId: %s <br>",userId, classId));
    }
}

假设在客户端访问服务器的时候,没有带上参数(即127.0.0.1:8080/20220415/getParameter),那么页面的显示为:

因此,如果当前的query string 中的key不存在,那么得到的value 就是null 。

假设在客户端服务器服务器的时候带上这两个参数,如:127.0.0.1:8080/20220415/getParameter?userId=10&classId=1,则效果如下图显示:

再有,如果我们在客户端访问服务器的时候,只写了两个参数的key,没有填写值,如:127.0.0.1:8080/20220415/getParameter?userId=&classId= ,则页面显示如下图:此时getParameter得到的是一个空的字符串

因此有必要在代码中判断,在访问服务器的时候,是否带有两个参数,带有两个参数的时候是否又赋予了value

2.3 代码示例: 获取 POST 请求中的参数(1)

POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的形式,仍然可以通过 getParameter 获取参数的值。

我们知道Post请求的body 中的格式有三种:
1.application/x-www-form-urlencoded
如:a=10&b=20,跟query string 和类似。
2.mutipart/form-data
这种格式比较复杂,主要是用来传输文件的,生成一个分隔符。
3.application/json
如:

{
   a:10,
   b:20
}

假设此时是第一种格式,那么使用getParameter 方法来获取也是可以的,跟getQueryString 方法没区别。

代码:

@WebServlet("/postParameter")
public class PostParameterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        String userId = req.getParameter("userId");
        String classId = req.getParameter("classId");
        resp.getWriter().write(String.format("userId:%s classId:%s",userId,classId));
    }
}

为了构造POST 请求,我们需要写一个html 页面来验证服务器的程序。我们此处使用form表单的形式即可。

testPost.html:
注:input里面的name 与 代码中的getParameter里面的参数可以匹配的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>testPost</title>
</head>
<body>
    <form action="getParameter" method="post">
        <input type="text" name="userId">
        <input type="text" name="classId">
        <input type="submit" value="提交">
    </form>
</body>
</html>

testPost页面如下,此时都输入22:

点击提交后:

去抓包后,可以看到一个POST请求,并且可以看到body:

2.4 代码示例: 获取 POST 请求中的参数(2)

如果 POST 请求中的 body 是按照 JSON 的格式来传递, 那么获取参数的代码就要发生调整。
假设此时我们返回的数据是一个整体的JSON,并且是将整个JSON 格式的body作为响应进行返回,在客户端的控制台中打印出来

代码:

@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String body = readBody(req);
        resp.getWriter().write(body);//直接以Json整体作为字符串返回
    }

    private String readBody(HttpServletRequest req) throws IOException {
        InputStream inputStream = req.getInputStream();//req的getInputStream可以直接读取body中的数据
        int contentLength = req.getContentLength();
        byte[] buffer = new byte[contentLength];//body的长度的缓存字节数组
        inputStream.read(buffer);
        return new String(buffer,"utf-8");
    }
}

为了构造一个post请求,自己来设置一个post请求,并且传输的post请求以ajax的形式对Json进行传输。

testPost2.html:关键代码在script里面。

<button onclick="sendJson()">发送请求</button>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script>
        function sendJson() {
        let body = {
            userId:100,
            classId:1
        };

        $.ajax({
            type:'POST',
            url:'postParameterJson',
            contentType:"application/json;charset:utf-8",
            data:JSON.stringify(body),
            success:function(body,status) {
                console.log(body);
            }
        });
    }
    </script>

点击html 的页面发送请求按钮,在控制台中打印的是:

2.5 代码示例: 获取 POST 请求中的参数(3)

在上面,我们是把整个 body 视为一个整体进行返回了,但更多时候,是需要解析 json 格式的body,即获取到userId 和classId 里面具体的值。但是,json 的格式解析起来是比较复杂的,因为json里面可以再嵌套json,无限套娃。

那么它的格式这么复杂,我们要怎么去解析json 格式的数据呢?我们可以使用第三方库——Jackson Databind ,这个库也适用于Spring全家桶

注意:使用 jackson 解析json 的时候,需要先明确,要把这个 字符串 转成什么样的对象。可以参考下面的例子。

代码:
需要注意去体会ObjectMapper将Json对象转换成Java对象的过程

// 通过这个类来表示解析后的结果.
class JsonData {
    public int userId;
    public int classId;
}

@WebServlet("/postParameterJson")
public class PostParameterJsonServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 先把整个 body 读出来
        String body = readBody(req);
        // 使用 jackson 来解析
        // 先创建一个 jackson 的核心对象, ObjectMapper
        ObjectMapper objectMapper = new ObjectMapper();
        JsonData jsonData = objectMapper.readValue(body, JsonData.class);
        resp.getWriter().write(String.format("userId: %d; classId: %d <br>",
                jsonData.userId, jsonData.classId));
    }

    private String readBody(HttpServletRequest req) throws IOException {
        // 读取 body 需要根据 req getInputStream 得到一个流对象, 从这个流对象中读取
        InputStream inputStream = req.getInputStream();
        // 通过 contentLength 拿到请求中 body 的字节数
        int contentLength = req.getContentLength();
        byte[] buffer = new byte[contentLength];
        inputStream.read(buffer);
        return new String(buffer, "utf-8");
    }
}

此时输出的结果就不是整个body了:

对于将Json对象转换成Java对象的底层
1.先把Json格式的字符串转换成类似于 HashMap ,如:userId对应100,classId对应10。
2.根据类对象,获取到要转换结果的类,都有哪些属性,每个属性的名字等。此处就通过JsonData获取到,里面的属性有两个,名字分别是userId和classId(通过反射机制)。
3.拿着JsonData这里的每个属性的名字,去第一步构造的哈希表里面去查。如果查到了,就把查询到的值赋值到JsonData 对应的属性里面。

在创建JsonData 的时候,就需要先知道Json里面成员的名字,得和Jso字符串里的key 是匹配的。当然,如果不匹配的话,Jackson还提供了一些机制,来描述Json 字符串 的key 和构建出的结果类的字段之间的映射关系,但不必要这么麻烦。

小结:

3. HttpServletResponse

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

核心方法:

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

注意: 响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是 “写” 方法.
注意: 对于状态码/响应头的设置要放到 getWriter / getOutputStream 之前. 否则可能设置失效

3.1 代码示例: 设置状态码

@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 让用户传入一个请求.
        // 请求在 query string 中带一个参数. 就表示响应的状态码
        // 然后根据用户的输入, 返回不同的状态码的响应
        String statusString = req.getParameter("status");
        if (statusString == null || statusString.equals("")) {
            resp.getWriter().write("当前的请求参数 status 缺失");
            return;
        }
        resp.setStatus(Integer.parseInt(statusString));
        resp.getWriter().write("status: " + statusString);
    }
}

当我们在访问服务器的时候,带上参数status,则服务器那边会根据传入的参数status来设置响应的status,那么就会返回对应status 的状态码。
如:设置status为200

如:设置status为404

如:设置status为500

3.2 代码示例: 自动刷新

header中有个属性——Refresh,它的值就是隔多长时间刷新,单位是s 。

@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf8");
        resp.setHeader("Refresh", "1");
        // 这个方法可以获取到当前的毫秒级时间戳
        long timeStamp = System.currentTimeMillis();
        resp.getWriter().write("timestamp: " + timeStamp);
    }
}

访问服务器时的页面展示:每隔一秒时间戳都是在发生变化的,但是时间戳并不是经确到1s,因为网络传输之间是会有时差的。

抓某次刷新后的包可以看到:

3.3 代码示例: 重定向

重定向就是 “呼叫转移”,状态码是302 ,可以设置Location字段重定向。

代码:

@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(302);
        resp.setHeader("Location","https://www.baidu.com");
        //不再需要设置body了
    }
}

此时我们 客户端去访问服务器,一敲回车后,就可以看到页面是直接跳转到百度地址上的。

五、Postman 工具

下面对Servlet API的使用,会经常需要我们自己去构造请求,但是每次自己去构造请求会非常麻烦。要不就是form表单,要么就是ajax,对于可以用相同的请求,就不用每次去敲代码来实现。可以使用Postman 工具。

步骤1:点击+

步骤2:

最后点击sent就可以发送了。并且在下面的Response 栏中会显示出服务器返回的响应结果。

六、实现Web表白墙(了解后端即可)

1. 前后端分离实现表白墙(ajax)

约定:在客户端中提交的数据是Json格式的,HTTP请求的是POST 方法的话,POST的数据就在服务器中存储好,并且刷新页面数据不会丢失。如果是刷新页面的话,是GET方法,就会从服务器中读取之前存储的数据,保证上次在页面上的数据不丢失。

前端部分:放在body里面的关键样式:

<div class="container">
    <h1>表白墙</h1>
    <p>输入后点击提交, 会将信息显示在墙上</p>
    <div class="row">
        <span>谁</span>
        <input type="text" class="edit">
    </div>
    <div class="row">
        <span>对谁</span>
        <input type="text" class="edit">
    </div>
    <div class="row">
        <span>说什么</span>
        <input type="text" class="edit">
    </div>
    <div class="row">
        <input type="button" value="提 交" id="submit">
    </div>

    <!-- 每次点击 "提交" 都在下面新增一个 .row , 里面就是放置用户输入的话 -->

</div>

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

<script>
        // 1. 在页面加载的时候, 访问服务器, 从服务器这边获取到消息列表, 并展示出来
        function load() {
            // 通过这个 load 函数, 从服务器上获取到消息, 并进行展示
            $.ajax({
                type: 'GET',
                url: 'message',
                success: function(data, status) {
                    // data 是响应的 body, 此时的响应可能只是一个字符串格式, 可以手动的进行一个解析, 按照 json 格式解析成对象
                    let container = document.querySelector('.container');
                    // let messages = JSON.parse(data);
                    let messages = data;
                    for (let message of messages) {
                        let row = document.createElement('div');
                        row.className = 'row';
                        row.innerHTML = message.from + '对' + message.to + '说: '+ message.message;
                        container.appendChild(row);
                    }
                }
            });
        }
        load();
        let submitButton = document.querySelector('#submit');
        submitButton.onclick = function() {
            // 1. 先获取到编辑框的内容
            let edits = document.querySelectorAll('.edit');
            let from = edits[0].value;
            let to = edits[1].value;
            let message = edits[2].value;
            console.log(from + ", " + to + ", " + message);
            if (from == '' || to == '' || message == '') {
                // 对用户输入做一个简单的校验. 验证一下当前是否是合法的提交.
                return;
            }
            // 2. 根据内容, 构造 HTML 元素. (.row 里面包含用户输入的话)
            let row = document.createElement('div');
            row.className = 'row';
            row.innerHTML = from + '对' + to + '说: ' + message;
            // 3. 把这个新的元素添加到 DOM 树上
            let container = document.querySelector('.container');
            container.appendChild(row);
            // 4. 清空原来的输入框
            for (let i = 0; i < edits.length; i++) {
                edits[i].value = '';
            }
            $.ajax({
                type:'POST',
                url:"message",
                data:JSON.stringify({from:from,to:to,message:message}),
                contentType:"application/json;charset=utf-8",
                success:function(data,status) {
                    if(data.ok==1) {
                        console.log("提交消息成功");
                    }else {
                        container.log("提交信息失败");
                    }
                }
            });
        }
    </script>

<style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        .container {
            width: 400px;
            margin: 0 auto;
        }

        h1 {
            text-align: center;
            padding: 20px 0;
        }

        p {
            text-align: center;
            color: #666;
            padding: 10px 0;
            font-size: 14px;
        }

        .row {
            height: 50px;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        span {
            width: 90px;
            font-size: 20px;
        }

        input {
            width: 310px;
            height: 40px;
        }

        #submit {
            width: 400px;
            color: white;
            background-color: orange;
            border: none;
            border-radius: 5px;
            font-size: 18px;
        }

        #submit:active {
            background-color: black;
        }

        .edit {
            font-size: 18px;
            padding-left: 5px;
        }
    </style>

后端部分:
注:在doGet方法中,因为是处理的get方法,因此就需要用一个数组将Json转变后的对象进行存储。只要客户端访问服务器,服务器就传给客户端一个Message的数组,给前端去进行处理。doPost方法只需要将客户端中输入的数据转为Json对象传给服务器,服务器再利用Jackson去进行Json对象的转化,保存到数组中

class Message {
    public String from;
    public String to;
    public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    public ObjectMapper objectMapper = new ObjectMapper();
    public List<Message> messageList = new ArrayList<>();

    @Override
    public void init() throws ServletException {//为了验证doGet方法是否正确
        Message message = new Message();
        message.from="黑猫";
        message.to="白猫";
        message.message="喵";
        messageList.add(message);
        Message message1 = new Message();
        message1.from="黑猫";
        message1.to="白猫";
        message1.message="喵";
        messageList.add(message1);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        objectMapper.writeValue(resp.getWriter(),messageList);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        messageList.add(message);
        resp.setContentType("application/json;charset:utf-8");
        resp.getWriter().write("{\"ok\": 1}");
    }
}

前端中需要注意的点:需要对doGet方法中传入的数组进行判断,因为此处是以json格式进行传输的,因此该数组被jQuery自动转成object/Array 了。要学会在客户端的源代码中打断点进行调试。

当前后端交互的过程出现问题,首先,前端和后端都有可能是问题的来源,因此就要定位是前端有问题还是后端,如果是前端,打开开发者工具,那么在控制台中就会显示具体的异常信息。找出异常的代码,借助chrome 调试器,主要就是看异常之前,代码中临时的数据是咋的。

上面的代码,即使是刷新页面,数据也会从服务器中上传到客户端。
但是,当前的服务器是把数据都保存到了 messageList 变量中,变量就是内存!一旦服务器重启,内存就会消失,随之之前保存的数据也会消失,这就会造成不可预知的后果。

如何让数据做到持久化?
有两种方式:
1.写入到文件中
2.写入到数据库中

2. 利用模板引擎实现表白墙(form表单)

前端代码:
鉴于表白墙的样式跟上面的一模一样,为了减少篇幅,我把CSS 的代码就去掉了。把form表单的前端关键代码显示出来就行。
在提交处利用form表单提交数据给服务器

<form action="message" method="POST">
        <div class="container">
            <h1>表白墙</h1>
            <p>输入后点击提交, 会将信息显示在墙上</p>
            <div class="row">
                <span>谁</span>
                <input type="text" class="edit" name="from">
            </div>
            <div class="row">
                <span>对谁</span>
                <input type="text" class="edit" name="to">
            </div>
            <div class="row">
                <span>说什么</span>
                <input type="text" class="edit" name="message">
            </div>
            <div class="row">
                <input type="submit" value="提 交" id="submit">
            </div>

            <!-- 每次点击 "提交" 都在下面新增一个 .row , 里面就是放置用户输入的话 -->

            <!-- 添加模板这里的变量, 每个 row 都是一个表白墙上的消息 -->
            <div class="row" th:each="message: ${messages}">
                <span th:text="${message.from}"></span>
                对
                <span th:text="${message.to}"></span>
                说: 
                <span th:text="${message.message}"></span>
            </div>
        </div>
    </form>

后端代码:
先创建ThymeleafConfig类来建立好Servlet共享的键值对

@WebListener
public class ThymeleafConfig implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        TemplateEngine engine = new TemplateEngine();
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(servletContext);
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        engine.setTemplateResolver(resolver);
        servletContext.setAttribute("engine",engine);
        System.out.println("engine 初始化完毕");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

再创建Message类和MessageServlet类

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

    public Message(String from, String to, String message) {
        this.from = from;
        this.to = to;
        this.message = message;
    }
}

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    private List<Message> messages = new ArrayList<>();

    @Override
    public void init() throws ServletException {
//        messages.add(new Message("黑猫", "白猫", "喵"));
//        messages.add(new Message("白猫", "黑猫", "喵呜"));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 读取 messages 列表, 根据列表的数据, 结合网页模板, 来构造出一个页面, 返回给浏览器.
        ServletContext context = getServletContext();
        TemplateEngine engine = (TemplateEngine) context.getAttribute("engine");
        WebContext webContext = new WebContext(req, resp, context);
        webContext.setVariable("messages", messages);
        String html = engine.process("messageWall", webContext);
        resp.getWriter().write(html);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理请求的时候, 需要给请求对象也设置一下字符集.
        req.setCharacterEncoding("utf-8");
        // resp.setCharacterEncoding("utf-8");
        // 处理请求的内容, 把读到的数据给解析, 得到 from, to, message, 构造出 Message 对象, 插入到 List 里面
        // 此处得到的请求的内容, 是 URL encode 的结果. Servlet 也不知道这个 encode 的结果是按照 UTF8 还是其他字符集进行编码的
        String from = req.getParameter("from");
        String to = req.getParameter("to");
        String msg = req.getParameter("message");
        Message message = new Message(from, to, msg);
        messages.add(message);
        // 直接来一个 重定向 操作, 重定向到 GET 版本的 /message , 就可以自动的重新获取到消息列表了
        resp.sendRedirect("message");
    }
}

实现效果跟用ajax是一样的,只是代码的实现方式不同。
大致效果:

3. 将数据写入到文件中来实现

客户端中的代码不改变,主要就是改变数据的存储方式。
doPost方法:
注:将message对象的from、to、message 属性以 \t 作为分隔符分割,保存在文件中即可。需要注意的是FileWriter fileWriter = new FileWriter(filePath, true),要设置true参数,是为追加写文件类型,即关闭文件后再打开,原来的数据还是存在的,若只是单纯地写文件,关闭文件再打开,前面的数据会丢失。

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        //messageList.add(message);
        save(message);
        resp.setContentType("application/json;charset:utf-8");
        resp.getWriter().write("{\"ok\": 1}");
    }

    private void save(Message message) {
                System.out.println("向文件中写入数据!");
        // FileWriter 的使用方法, 就和咱们前面介绍过的 PrintWriter 差不多. 里面都是有一个关键的方法叫做 write
        try (FileWriter fileWriter = new FileWriter(filePath, true)) {
            // 写入文件的格式也有很多方式. 可以直接写 json, 也可以使用行文本(每个记录占一行, 字段之间使用分隔符区分)
            fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

doGet方法:
注:因为此处读文件的时候要以行来读,每一行代表一个数据。又因为FileReader不能每行地读,因此可以给它封装成BufferedReader 来每行地读取数据

@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        List<Message> messageList = load();
        objectMapper.writeValue(resp.getWriter(),messageList);
    }

    private List<Message> load() {
        // 这个方法负责读文件, 把读到的数据获取到之后, 放到 List<Message> 中
        List<Message> messageList = new ArrayList<>();
        System.out.println("从文件加载!");
        // 此处我们需要按行读取. FileReader 本身不支持. 需要套上一层 BufferedReader
        // 这里使用 Scanner 也行
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
            while (true) {
                String line = bufferedReader.readLine();
                if (line == null) {
                    break;
                }
                // 如果读取到 line 的内容, 就把 line 解析成一个 Message 对象
                String[] tokens = line.split("\t");
                Message message = new Message();
                message.from = tokens[0];
                message.to = tokens[1];
                message.message = tokens[2];
                messageList.add(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return messageList;
    }

演示:原本什么都没有,因为此时文件中没有数据。

输入:

此时刷新页面,也没问题。此时关闭页面,重新打开,之前的数据还会有。此时关闭页面,重新打开,并且重启服务器,此时之前的数据还会有。

总代码:

class Message {
    public String from;
    public String to;
    public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    public ObjectMapper objectMapper = new ObjectMapper();
    private String filePath = "C:/java-language/java-language/20220413/messages.txt";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        List<Message> messageList = load();
        objectMapper.writeValue(resp.getWriter(),messageList);
    }

    private List<Message> load() {
        // 这个方法负责读文件, 把读到的数据获取到之后, 放到 List<Message> 中
        List<Message> messageList = new ArrayList<>();
        System.out.println("从文件加载!");
        // 此处我们需要按行读取. FileReader 本身不支持. 需要套上一层 BufferedReader
        // 这里使用 Scanner 也行
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
            while (true) {
                String line = bufferedReader.readLine();
                if (line == null) {
                    break;
                }
                // 如果读取到 line 的内容, 就把 line 解析成一个 Message 对象
                String[] tokens = line.split("\t");
                Message message = new Message();
                message.from = tokens[0];
                message.to = tokens[1];
                message.message = tokens[2];
                messageList.add(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return messageList;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        //messageList.add(message);
        save(message);
        resp.setContentType("application/json;charset:utf-8");
        resp.getWriter().write("{\"ok\": 1}");
    }

    private void save(Message message) {
                System.out.println("向文件中写入数据!");
        // FileWriter 的使用方法, 就和咱们前面介绍过的 PrintWriter 差不多. 里面都是有一个关键的方法叫做 write
        try (FileWriter fileWriter = new FileWriter(filePath, true)) {
            // 写入文件的格式也有很多方式. 可以直接写 json, 也可以使用行文本(每个记录占一行, 字段之间使用分隔符区分)
            fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 将数据写入到数据库来实现

涉及到数据库的代码编程,就会使用到JDBC,因此就要先引入 mysql Connector 的第三方库(5.1.47版本)。

首先要引入JDBC,就要实例化DataSource 类,会涉及到单例模式,并且涉及到资源的关闭,因此我们可以专门去创建一个类来进行这些操作。

DBUtil 类:

public class DBUtil {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/java100?useSSL=false&characterEncoding=utf8";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "111111";

    private static volatile DataSource dataSource = null;

    public static DataSource getDataSource() {
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    ((MysqlDataSource)dataSource).setURL(URL);
                    ((MysqlDataSource)dataSource).setUser(USERNAME);
                   ((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();
            }
        }
    }
}

doPost方法:
改变的是save,其余都不变。

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        //messageList.add(message);
        save(message);
        resp.setContentType("application/json;charset:utf-8");
        resp.getWriter().write("{\"ok\": 1}");
    }

    private void save(Message message) {
        System.out.println("写入数据到数据库");
        //1.先建立连接
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            //1.先与数据库建立连接
            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);
            int ret = statement.executeUpdate();
            if(ret==1) {
                System.out.println("插入成功");
            }else {
                System.out.println("插入失败");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,null);
        }
    }
}

此时我们验证是否能够插入成功,我们在客户端中输入,然后再去查看数据库。

说明插入是没有问题的。

doGet方法:能够从数据库中加载数据到客户端中。

@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        List<Message> messageList = load();
        objectMapper.writeValue(resp.getWriter(),messageList);
    }

    private List<Message> load() {
        List<Message> messageList = new ArrayList<>();
        System.out.println("从数据库读数据");
        Connection connection =null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from message";
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            while(resultSet.next()) {
                Message message = new Message();
                message.from=resultSet.getString("from");
                message.to=resultSet.getString("to");
                message.message=resultSet.getString("message");
                messageList.add(message);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,resultSet);
        }
        return messageList;
    }

相关文章