这篇博客,是基于上一篇博客对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;
    }
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐