likes
comments
collection
share

Servlet 开发学习

作者站长头像
站长
· 阅读数 18

一、HelloWorld - Servlet 代码示例

0. 创建 Maven 项目,引入 Servlet 依赖

在 maven 仓库的官网查找 servlet,进入第一个搜索结果,找到版本 3.1 ,将 xml 文件格式复制到maven项目中 pom.xml 文件里,<dependencies>标签下。

Servlet 开发学习

Servlet 开发学习

Servlet 开发学习

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

1. 创建目录

前两个是目录,最后一个是 xml 文件

Servlet 开发学习

然后在 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>

2. 编写代码

  1. 创建一个类,继承 HttpServlet
public class HelloServlet extends HttpServlet {
}
  1. 重写 doGet方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    super.doGet(req, resp);
}

注意:super.doGet(req, resp);在后面写代码的时候要删除掉,否则会有问题。

Servlet 开发学习

其中,doGet方法的参数 HttpServletRequestHttpServletResponse这两个对象,分别代表着 HTTP 请求HTTP 响应(HTTP 协议格式中包含的信息,都在这两个对象里面)

doGet方法不是我们手动调用的,而是 Tomcat自动调用的。Tomcat 回自动识别出合适的时机,来自动调用 doGet 方法。(其中合适的时机比较复杂,但是能明确的是,一定是通过 GET请求触发的!)

doGet做的工作,就是 Tomcat收到请求之后,到返回响应之前,中间的过程~

回顾下网络编程的过程:

  1. 读取请求并解析
  2. 根据请求计算响应
  3. 把响应返回给客户端

其中,步骤 1、3 是 Tomcat 自动完成的,步骤 2 是 doGet 完成的。

doGet参数中的 HttpServletRequest就是 Tomcat根据收到的 HTTP 请求,生成的对象。doGet里面根据当前的业务逻辑,依据 HttpServletRequest生成一个 HttpServletResponseTomcat再把 HttpServletResponse对象构造成 HTTP 响应报文,返回给客户端。

代码示例:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 把自动生成的调用父类的方法要删掉,否则会有问题
        //super.doGet(req, resp);
         
        // 当前写一个 hello world!
        // 这个打印是打印在服务器的控制台上
        System.out.println("hello world!");
        
        // 这个打印就是打印在响应报文中,显示到页面上
        // 把 "hello world!" 字符串作为响应报文的 body 了,浏览器就会把这个 body 显示到页面中
        resp.getWriter().write("hello world!");
    }
}

注意:要加上 @WebServlet注解

3. 打包

Servlet 开发学习

看到下面的字样,说明打包成功:

Servlet 开发学习

如果出错了,这里就会提示错误信息。

打包完成后,会生成一个 压缩包,在 target 目录下可以查看:

Servlet 开发学习

注意:生成的默认是 jar包,但是 Tomcat 能识别的是一个 war包。

所以此处需要修改 pom.xml,让生成工作生成 war包,在 <project>标签下添加如下:

<packaging>war</packaging>

通过 packaging标签来设置打包的格式~

Servlet 开发学习

也可以在 pom.xml通过 build标签中的 finalName标签,设置一下生成包的名字,把名字改简单点(可选):

<build>
    <finalName>hello_servlet</finalName>
</build>

Servlet 开发学习

通过上述操作,再进行一次打包:

Servlet 开发学习

4. 部署

上面的示例代码没有 main方法,不能单独执行。main 方法在 Tomcat 里面,上述代码需要部署到 Tomcat 中,由 Tomcat 进行调用~

将上一步得到的 war包,拷贝到 Tomcat 的 webapps目录中

Servlet 开发学习

5. 启动 Tomcat

进入 Tomcat 目录中的 bin目录,找到 startup.bat并双击运行:

Servlet 开发学习

看到下面的字样,说明 Tomcat 启动完毕

Servlet 开发学习

启动 Tomcat 的时候,能在 Tomcat 的日志中,看到一个提示:

Servlet 开发学习

意思为:Tomcat 发现了这个 war 包,就会对这个 war 包进行解压缩和加载。

在 Tomcat 中的 webapps 目录下就会发现解压好的 war 包

Servlet 开发学习

6. 验证

通过浏览器构造 HTTP 请求访问 Tomcat 服务器。

URL路径 = Context Path(war 包名字) + Servlet Path(注解里写的路径)

在浏览器中输入http://127.0.0.1:8080/hello_servlet/hello,得到结果:

Servlet 开发学习

我们对这个浏览器地址进行分析:

Servlet 开发学习

一旦你的请求的路径写错了(和服务器这里的情况不匹配)就会出现 404

Servlet 开发学习

二、更方便的部署 Tomcat

可以借助 IDEA 中的一个插件:Smart Tomcat,来简化 打包 和 部署。

1. 安装插件

在 IDEA 的 setting 中:

Servlet 开发学习

2. 使用 Smart Tomcat 插件

点击工具栏下方的 add configuration

Servlet 开发学习

点击 +,然后选择 Smart Tomcat

Servlet 开发学习

进入界面后:

Servlet 开发学习

创建完成后,点击绿色箭头运行即可:

Servlet 开发学习

在浏览器中打开下面的地址

Servlet 开发学习

并在后面加上 /hello即可看到效果:

Servlet 开发学习

可以在控制台上看到:

Servlet 开发学习

smart tomcat 的工作原理,其实和前面的手动拷贝部署不太一样。

此时你打开 tomcat 的 webapps 目录,看不到刚才的 war 包。

其实 tomcat 的运行方式有很多种,smart tomcat 是在运行 tomcat 的时候,通过其他手段,让 tomcat 直接加载了你代码中的 webapp 目录。这个时候其实跳过了打包+拷贝的过程,到那时也起到了部署的效果。

三、Servlet API

重点掌握 HttpServletHttpServletRequestHttpServletResponse

1. HttpServlet

核心方法

Servlet 开发学习

这些方法都是可以在创建子类的时候,进行重写的,上述方法的调用时机是不同的。

  • init:只会在该 Servlet 类第一次被使用的时候调用到,相当于是用来初始化的。
  • destroy:在 Servlet 对象被销毁的时候才会调用到,也只是调用一次,相当于收尾操作。
  • service:每次收到请求(无论是什么方法)都会调用 service。默认父类的 service 里面就会根据方法来调用 doGet/doPost....
  • doGet/doPost....:收到对应的 HTTP 方法的请求才会被调用。

前面已经通过浏览器里面输入 url 构造 get 请求了,post 请求咋办?

可以使用 form表单(只支持 get 和 post)和 ajax。 下面通过 ajax 来演示: 创建 test.html 放到 webapp 目录,不能随便乱放! Servlet 开发学习

使用 ajax 需要 jquery 的引入

<!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>Document</title>
  </head>
  <body>
    <!-- src 放入jquery -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
    <script>
      $.ajax({
        type: 'post',
        url: 'hello', // 相对路径
        // url: '/Servlet/hello' 绝对路径
        success: function(body) {
          console.log(body);
        }
      });
    </script>
  </body>
</html>

注意1: 在 ajax 中属性 url,'hello' 是相对路径的写法,要访问的是 /Servlet/hello 这个路径,这个 ajax 的代码是处在 /Servlet/test.html 里面。如何判断相对路径是相对谁呢?看当前的执行 ajax 的文件是在哪个路径下。

注意2: /Servlet/hello 主要是 /开头,为绝对路径,就和当前文件所处位置无关了。但是 java 代码中的 @WebServlet注解,里面的路径必须带/,如果/忘记了,tomcat 就会认为是无效路径,不能启动/加载。

在带有 @WebServlet("/hello")的类中重写 doPost方法:

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    System.out.println("doPost");
    resp.getWriter().write("doPost");
}

这时候我们重启 Tomcat 服务

Servlet 开发学习

,然后在浏览器中输入地址:

Servlet 开发学习

把 html 放到 webapp 目录里的时候,此时该 html 也就相当于该 webapp 的一部分了。要访问这个页面,则必须要带上这里的 context path。

上面的页面是空的,此时我们 F12 打开控制台:

Servlet 开发学习

下面是简化过程:

Servlet 开发学习

在浏览器控制台上显示的 doPost,来自于 ajax 中的 success 方法(里面的打印),而 body 部分则来自服务器返回的数据(doPost 方法里面的)。


问题:浏览器输入url,返回的 body 是直接打印到界面上吗?

是的,直接通过浏览器输入 url 得到的响应的内容就是直接被浏览器渲染到界面上的。

但是 ajax 不是,拿到的响应数据,是由你的回调方法来处理,想让他显示就显示,不想显示就不显示。

比如,下面我们让他显示在浏览器上:

<!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>Document</title>
    <style>
        .one {
            font-size: 50px;
        }
    </style>
</head>
<body>
    <div class="one">

    </div>

    <!-- src 放入jquery -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
    <script>
        $.ajax({
            type: 'post',
            url: 'hello', // 相对路径
            // url: '/Servlet/hello' 绝对路径
            success: function(body) {
                // console.log(body);
                let div = document.querySelector('.one');
                div.innerHTML = body;
            }
        });
    </script>
</body>
</html>

重启tomcat,打开浏览器:

Servlet 开发学习


当前使用 ajax 构造出上述请求,但还是比较麻烦(要写代码),我们还可以通过第三方工具来构造 HTTP 请求,如 Postman。


面试题:谈谈 HttpServlet 的生命周期

这题主要考察的就是什么时机调用什么方法。

  1. 首次使用,先调用一次 init
  2. 每次收到请求,调用 service,在 service 内部通过方法来决定调用哪个 doXXX
  3. 销毁之前调用 destory

2. HttpServletRequest

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

Servlet 开发学习

这个类就提供了一组方法,让我们能够获取到 HTTP 请求中的这些信息。

核心方法

Servlet 开发学习

注意 里面有个方法叫做 getRequestURI。

URL:唯一资源定位符。URI:唯一资源标识符。

这两个术语表达的含义类似,两个概念很多时候是重合的。url 表示资源的位置,uri 表示资源的 id。

  • String getQueryString():得到的是完整的查询字符串,如:?a=10&b=20获取到其中的 a=10&b=20

下面的三个方法,相当于把 查询字符串 给解析成键值对:

  • Enumeration getParameterNames():得到所有的 key,以 Enum(枚举)的方式来表示。
  • String getParameter(String name):根据 key 得到 value。
  • String[] getParameterValues(String name):如果存在多个 key 相同的情况下,得到的value就是一个数组的形式。

下面的两个方法是获取请求报头(请求报头也是键值对):

  • Enumeration getHeaderNames():获取请求报头中所有的 key。
  • String getHeader(String name):根据 key 获取 value。
  • String getCharacterEncoding():获取到请求的字符编码是什么。(其实字符编码,就包含在 getContentType里面)
  • String getContentType():获取到整个 ContentType 的键值对,值里面可能包含 字符编码。
  • int getContentLength():获取到 body 的长度。
  • InputStream getInputStream():得到一个输入流对象。从这个对象中读取数据,其实就是读到了请求的body。请求body里面可能有些数据,可能会被这里的 getInputStream来获取。

注意:

关于 header,URL,这些方法,我们拿到的结果都是 String类型 的 数据。

至于有关 body 的方法,由于 body的长度可能会更长一些,我们并没有通过字符串方式来获取。

而是通过 流对象的方式,来获取。

下面通过代码示例来加深对上述方法的理解:

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

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.util.Enumeration;

@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 把 api 的执行结果,放到 StringBuilder 中
        StringBuilder stringBuilder = new StringBuilder();

        // 首行部分
        stringBuilder.append("<h3> 首行部分</h3>");
        stringBuilder.append(req.getProtocol()); // 获取 版本协议
        stringBuilder.append("<br>"); // 换行

        stringBuilder.append(req.getMethod()); // 获取 请求方法
        stringBuilder.append("<br>"); // 换行

        stringBuilder.append(req.getRequestURI()); // 获取 请求路径
        stringBuilder.append("<br>"); // 换行

        stringBuilder.append(req.getContextPath()); // 获取 请求的第一级路径
        stringBuilder.append("<br>"); // 换行

        stringBuilder.append(req.getQueryString()); // 获取 完整的查询字符串
        stringBuilder.append("<br>"); // 换行

        // header 部分
        stringBuilder.append("<h3> header 部分</h3>");
        Enumeration<String> headerNames = req.getHeaderNames(); // 获取报头中所有 key 值
        // 使用迭代器方式来遍历 获取到的 header 中 key 值
        while (headerNames.hasMoreElements()) {
            // 获取到 headerNames 中的一个 key 元素
            String headerName = headerNames.nextElement();
            // 通过 header 中 key 值,获取到对应的 value
            String headerValue = req.getHeader(headerName);

            // 将 key 和 value 组成键值对,放入 stringbuilder
            stringBuilder.append(headerName + ": " + headerValue + "<br>");
        }

        //在展示结果之前,需要指定浏览器的读取编码的方式,防止乱码
        resp.setContentType("text/html; charset=utf-8");
        resp.getWriter().write(stringBuilder.toString());
    }
}

演示结果:

Servlet 开发学习

注意到此处为 null:Servlet 开发学习,因为在目前构造的GET请求里是不包含查询字符串,所以结果为 null。接下来我们手动添加查询字符串:

Servlet 开发学习

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

创建一个新类 GetParameterServlet继承 HttpServlet 类

@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
}

重写 doGet方法,下面来演示获取到 GET 请求中的参数

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 我们手动从浏览器传来这样的一个请求:/getParameter/userId=1&classId=2
    // 这里的查询字符串 userId=1&classId=2 就是键值对的结构
    // 这里我们希望根据 key,拿到对应的 value
    String userId = req.getParameter("userId"); // key 值需要和请求中的相匹配,才能获取对应的 value 值
    String classId = req.getParameter("classId");
    
    // 打印数据
    resp.getWriter().write("userId: " + userId + " classId: " + classId);
}

重启 Tomcat,访问页面,得到如下结果:

Servlet 开发学习

如果我们不在 url 中添加 queryString,那么输出的值就是一个 null 值。

Servlet 开发学习

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

前端除了通过 queryString 来传参,还有其他传参方式,如通过 post 请求,通过 body 来传参到服务器这里。

回顾下,POST 请求的 body 的格式:

  1. application/x-www-form-urlencoded
  2. form-data
  3. application/json

接下来我们主要演示第一种和第三种格式:

1. 使用 x-www-form-urlencoded 请求格式

如果使用的是 x-www-form-urlencoded的请求格式,服务器该如何获取参数呢?

获取参数的方式,跟 GET 是一样的,也是使用 getParameter

如何在前端构造一个 x-www-form-urlencoded格式的请求呢?

有两种方式:

  1. 第三方工具 postman
  2. form 表单

创建一个类 PostGetParameterServlet继承 HttpServlet

@WebServlet("/postGetParameter")
public class PostGetParameterServlet extends HttpServlet {
    
}

重写 doPost方法处理 POST 请求

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 加入前端传过来的参数是 userId=1&classId=2
    // 此时服务器也是通过 req.getParameter 来获取参数的
    resp.setContentType("text/html; charset=utf-8"); // 指定浏览器读取方式
    String userId = req.getParameter("userId");
    String classId = req.getParameter("classId");
    resp.getWriter().write("userId = " + userId + " classId = " + classId);
}
postman 构造 x-www-form-urlencoded 请求格式

重启 Tomcat 服务,打开 postman 构造请求:

Servlet 开发学习

通过 fiddler 抓包可以看到,约定 body 使用 application/x-www-form-urlencoded 来传参,它的数据是在body里面的:

Servlet 开发学习

form 表单构造 x-www-form-urlencoded 请求格式

在 webapp 目录下创建 html 文件 :test1.html:

Servlet 开发学习

使用 vscode 来编写文件:

<!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>使用 form 表单提交post请求</title>
</head>
<body>
    <form action="postGetParameter" method="post">
        <input type="text" name="userId">
        <input type="text" name="classId">
        <input type="submit" value="提交">
    </form>
    
</body>
</html>

form表单里的action为相对路径Servlet 开发学习

重启 Tomcat,打开网页:

Servlet 开发学习

我们在里面写上数据,然后点击提交按钮:

Servlet 开发学习

通过 fiddler 抓包:

Servlet 开发学习

2. 使用 json 请求格式

json 格式示例:

{
  userId: 123,
  classId: 456
}

对于body为json的格式来说,手动解析并不容易,因为 json 里面的字段是可以嵌套的,如下情况:

{
  userId: 123,
  classId: {
    userId: 456
  }
}

因为手动解析 json 数据很麻烦,我们可以借助第三方库来处理 json 格式的数据。

在 java 的生态中,处理 json 的第三方库的种类有很多。

我们使用的是 Jackson(Spring 官方推荐的库),Spring 默认就是使用 jackson

来处理 json 格式的数据。

我们通过 Maven 中央仓库,引入 Jackson。

在 Maven 仓库里搜索 Jackson,然后选择第一个:

Servlet 开发学习

随便选择一个版本,复制其 xml 格式,此处我选择的是2.13.1:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.13.1</version>
</dependency>

引入 Jackson:

Servlet 开发学习

使用 Jackson 的方式非常简单,只需要掌握两个操作:

  1. 把 json 格式的字符串 转成 java对象
  2. 把 java 对象 转成 json 字符串

这两个操作对应到一个类 ObjectMapper(来自 Jackson),提供了两个方法:

  1. objectMapper.readValue():json 字符串转成 Java 对象。
  2. objectMapper.writeValue():Java 对象转成 json 字符串。

示例:

创建一个新类 PostJsonServlet继承 HttpServlet

@WebServlet("/postGetParameter2")
public class PostJsonServlet extends HttpServlet {
    
}

创建 Student 类:

class Student {
    public int userId;
    public int classId;
}

注意:

  1. 这里两个属性的名称,要和前端构造的 json 格式数据中的 key 要一致。否则下面进行 readValue 的时候,是无法转换成功的!
  2. 当前这个两个属性都设置成 public,如果设为 private,但是同时提供了 getter 和 setter 方法,效果是一样的。

重写 doPost方法

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 创建一个 Jackson 的核心对象
    ObjectMapper objectMapper = new ObjectMapper();

    // 从请求的 body 中进行读取,并解析
    // 使用 readValue 来吧 json 字符串转成 Java 对象
    // 第一个参数是一个 String 或者是一个 InputStream
    // 第二个参数是转换的结果对应的 java 类对象
    Student student = objectMapper.readValue(req.getInputStream(),Student.class);
    resp.getWriter().write(student.userId+", " + student.classId);
}

重启 Tomcat,使用Postman构造 json 请求:

Servlet 开发学习

Servlet 开发学习

问题:此时可能会出现一个疑惑:如何拿着 key 和 Student 类中的属性名作比较的呢?

Servlet 开发学习注意第二个参数,这是一个类对象,类对象里面会存储这个 Student 类的详细信息。

这就是反射(根据类对象获取里面关键信息的过程),类对象就是我们的 .class文件(字节码文件)

源代码是 .java编译成 二进制字节码文件.class,这个 .class文件被加载到内存中的时候,就成了 class 对象,此对象包含了源代码中的关键信息,自然也会包含属性名称。

总代码:

class Student {
    public int userId;
    public int classId;
}

@WebServlet("/postGetParameter2")
public class PostJsonServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建一个 Jackson 的核心对象
        ObjectMapper objectMapper = new ObjectMapper();

        // 从请求的 body 中进行读取,并解析
        // 使用 readValue 来吧 json 字符串转成 Java 对象
        // 第一个参数是一个 String 或者是一个 InputStream
        // 第二个参数是转换的结果对应的 java 类对象
        Student student = objectMapper.readValue(req.getInputStream(),Student.class);
        resp.getWriter().write(student.userId+", " + student.classId);
    }
}

3. HttpServletResponse

核心方法

Servlet 开发学习

  • void sendRedirect(String location):返回一个重定向的响应,不是 set,而是 send,3xx 开头的响应。浏览器会自动的跳转到对应的新页面,String location就是你要跳转到的页面。
  • PrintWriter getWriter():得到字符流
  • OutputStream getOutputStream():得到字节流

得到流对象之后,下一步就是要 write。

代码示例1:设置状态码

创建一个新类 StatusCodeServlet继承 HttpServlet

@WebServlet("/status")
public class StatusCodeServlet extends HttpServlet {

}

这里我们重写 doGet方法

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setStatus(200); // 设置状态码
    resp.getWriter().write("hello"); // 将 hello 写入到响应的 body
}

重启 Tomcat,启动浏览器

Servlet 开发学习

使用 Fiddler 抓包,查看响应报文

Servlet 开发学习

我们再来演示下设置状态码为 404

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setStatus(404); // 设置状态码
    resp.getWriter().write("hello"); // 将 hello 写入到响应的 body
}

重启 Tomcat,打开浏览器

Servlet 开发学习

发现,返回的响应确实是 404,但是 hello 还是显示出来了。但是 404 不是表示 资源不存在/路径错误/路径缺失..的情况嘛,按理说是不显示任何数据的。

注意: 服务器返回的状态码,只是在告诉浏览器,当前的响应是一个什么状态,并不影响浏览器照常显示 body 中的内容。因此,不管响应的状态码是多少,都不会影响浏览器去显示 body 中的内容。

比如,我们打开 B站 一个不存在的页面:

Servlet 开发学习

显示了内容,通过抓包发现状态码是 404:

Servlet 开发学习

代码示例2:自动刷新

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

实现这个功能,只需要给 HTTP 响应中设置一个 header:Refresh 即可。

创建新类 AutoRefreshServlet继承 HttpServlet

@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
    
}

这里我们重写 doGet方法

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setHeader("Refresh","1"); // 每隔 1 s刷新一次响应内容
    resp.getWriter().write("timeStamp: " + System.currentTimeMillis());
}

重启 Tomcat,打开浏览器

Servlet 开发学习

第二次刷新

Servlet 开发学习

通过抓包可以看到,这个页面一直在刷新:

Servlet 开发学习

看到响应报文,发现响应中就会存在一个 Refresh 的键值对,其 value 值,就是我们设置的秒数

Servlet 开发学习Servlet 开发学习

也就是说,setHeader就是在 请求报头中新增一个键值对。如果你想要添加其他的键值对,也是一样的操作,直接填入 key 和 value 即可。

代码示例3:重定向

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

下面来构造一个 302,让浏览器触发页面跳转。

创建新类 RedirectServlet继承 HttpServlet

@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
    
}

重写 doGet方法

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setStatus(302);
    resp.setHeader("Location","https://cn.bing.com");
}

重启 Tomcat,打开浏览器,下面是原页面:

Servlet 开发学习

输入路径后回车,直接跳转

Servlet 开发学习

抓包结果:

Servlet 开发学习

跳转更为简洁的写法:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        resp.setStatus(302);
//        resp.setHeader("Location","https://cn.bing.com");

    // 更简洁的写法
    resp.sendRedirect("https://cn.bing.com");
}

效果是一样的。

四、实战示例

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

先导入前端代码:

<!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>告白墙</title>
    </head>
    <body>
    <!-- 通过内部样式style标签,引入CSS样式 -->
    <style>
    *{
    /* 首先先去除浏览器样式 */
    /* 将 内外边距设置为0,设置盒子模型为向内挤压 */
    margin: 0;
padding: 0;
box-sizing: border-box;
}

.containner{
    width: 100%;
}
h3{
    /* 文本居中 */
    text-align: center;
/* 上下边距为 20px,左右边距为0 */
padding: 20px 0;
font-size: 24px;
}

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

.row{
    width: 400px;
    height: 50px;
    /* 上下外边距为零,左右外边距自适应 */
    /* 就是元素水平居中操作 */
    margin: 0 auto;
    /* 弹性布局 */
    display: flex;
    /* 水平居中 */
    justify-content: center;
    /* 垂直居中 */
    align-items: center;
}

.row span{
    width: 60px;
    font-size: 17px;
}
.row input{
    width: 300px;
    height: 40px;
    line-height: 40px;
    font-size: 20px;
    text-indent: 0.5em;
    outline: none;
}

.row #submit{
    width: 360px;
    height: 40px;
    font-size: 20px;
    line-height: 40px;
    margin: 0 auto;
    color: white;
    background-color: orange;
    border: none;
    border-radius: 15px;
    outline: none;
}
/* 当鼠标点击按钮的时候,会改变按钮颜色 */
.row #submit:active{
    background-color: grey;
}
</style>
    <div class="container">
    <h3>表白墙</h3>
    <p>输入后点击提交,会将信息显示在表格中</p>
    <br>
    <div class="row">
    <span>谁: </span>
    <input type="text">
    </div>
    <div class="row">
    <span>对谁: </span>
    <input type="text">
    </div>
    <div class="row">
    <span>说什么: </span>
    <input type="text">
    </div>
    <div class="row">
    <button id="submit">提交</button>
    </div>
    </div>

    <script>
    let submitBtn = document.querySelector('#submit');
submitBtn.onclick = function(){
    // 1、获取 3个input 文本框中的数据
    let inputs = document.querySelectorAll('input');
    let from = inputs[0].value;
    let to = inputs[1].value;
    let say = inputs[2].value;
    if(from == ''|| to == '' || say == ''){
        // 用户填写的数据,并不完整。所以不提交。
        return;
    }
    // 2、生成一个新的 div,内容就是 三个 input 文本框的内容拼接
    // 再把这个 元素,挂在DOM树上。
    let newDiv = document.createElement('div');
    newDiv.innerHTML = from + "对" + to +"说:" + say;
    newDiv.className = 'row';
    // 将新建节点,挂在 container 这个节点下面
    let container = document.querySelector('.container');
    container.appendChild(newDiv);
}
	</script>
</body>
</html>

效果如下:

Servlet 开发学习

在写后端代码之前,我们首先与前端约定交互的接口,对于这个程序我们主要提供两个接口:

  1. 告诉服务器,当前留言了一条什么样的数据。
  2. 从服务器获取到当前的留言数据。

第一个接口的触发条件:

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

为了实现这个效果,客户端发送一个什么样的 HTTP 请求,服务器返回一个什么样的 HTTP 响应,我们做如下约定:

Servlet 开发学习

第二个接口的触发条件:

当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。

约定如下:

Servlet 开发学习

现在来编写后端代码:

  1. 创建新类 messageWall继承 HttpServlet
@WebServlet("/message")
public class messageWall extends HttpServlet {
    
}

这里面的@WebServlet("/message")路径不是乱写的,而是之前约定好的。Servlet 开发学习

  1. 定义一个类 Message用来读取 json 格式的数据:
class Message {
    public String from;
    public String to;
    public String message;
}

Servlet 开发学习

  1. 编写第一个触发条件:当用户点击 “提交”按钮的时候,就会发送一个 HTTP 请求,让服务器把这个数据存下来。重写 doPost方法:
// 用于读取请求并解析数据
private ObjectMapper objectMapper = new ObjectMapper();
// 用于存储信息
private List<Message> messages = new ArrayList<>();

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 解析数据
    Message message = objectMapper.readValue(req.getInputStream(),Message.class);
    // 保存数据
    messages.add(message);

    // 返回响应,主要告诉页面,信息是否存入成功
    // 因为内容比较简单,我就直接输出了
    resp.setContentType("application/json; charset=utf-8"); // 约定浏览器返回数据的格式与读取方式
    // 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
    // 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
    resp.getWriter().write("{"ok": True}");
}

首先第一步获取POST请求中的 body 信息:

Servlet 开发学习

这里引入服务器的目的:当刷新页面之后,以往发送的信息记录不至于会被清空(丢弃),这些数据可以持久化的存储在服务器上,我们希望保存在服务器的内存中:

Servlet 开发学习

  1. 编写第二个触发条件:当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。重写 doGet方法:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 获取整个消息列表中的元素,将其全部返回给客户端即可。
    // 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
    String jsonString = objectMapper.writeValueAsString(messages);
    resp.setContentType("application/json; charset=utf-8");
    resp.getWriter().write(jsonString);
}

注意:ObjectMapper既可以解析数据,又可以转换数据。readValue将数据形式转换为指定形式。writeValueAsString将参数中的数据转为字符串的格式。

Servlet 开发学习

后端总程序:

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

@WebServlet("/message")
public class messageWall extends HttpServlet {

    // 用于读取请求并解析数据
    private ObjectMapper objectMapper = new ObjectMapper();
    // 用于存储信息
    private List<Message> messages = new ArrayList<>();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取整个消息列表中的元素,将其全部返回给客户端即可。
        // 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
        String jsonString = objectMapper.writeValueAsString(messages);
        resp.setContentType("application/json; charset=utf-8");
        resp.getWriter().write(jsonString);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 解析数据
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        // 保存数据
        messages.add(message);

        // 返回响应,主要告诉页面,信息是否存入成功
        // 因为内容比较简单,我就直接输出了
        resp.setContentType("application/json; charset=utf-8"); // 约定浏览器返回数据的格式与读取方式
        // 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
        // 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
        resp.getWriter().write("{"ok": true}");
    }
}

注意: Servlet 开发学习这里的 true 要小写的,不能写成 True。因为要使用 json 格式的true,而不能用 java 格式的 True。


启动Tomcat服务,然后使用 Postman 来测试下程序:

  1. 首先使用 POST 来提交数据

Servlet 开发学习

  1. 使用 GET 来读取数据

Servlet 开发学习

此时,服务器端口的代码就写完了!

现在来编写前端代码的页面,基于上面发的前端代码进行修改:

将前端代码命名为 message.html放到 idea 里的 webapp 目录下:

Servlet 开发学习

  1. 首先引入 jquery
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>

Servlet 开发学习

  1. 实现第一个接口触发条件:当用户点击 “提交”按钮的时候,就会发送一个 HTTP 请求,让服务器把这个数据存下来。
// 4. 把当前获取到的输入框的内容,构造成一个 HTTP POST 请求
// 通过 ajax 发送给服务器
let body = {
  // 为了方便区分 key 与 value,我给 key 加上了双引号
  // 其实在 json 格式的数据中,key 一般都是要加上双引号的
  // 也可以省略双引号
  // 如果 key 带某些特殊字符,如空格,则必须带双引号
  "from":from,
  "to":to,
  "message":say
}
$.ajax({
  type:"post",
  url:"message",
  contentType:"application/json; charset=utf-8",
  data: JSON.stringify(body),
  success:function(body){
    alert("消息提交成功!");
  },
  error:function(){
    alert("消息提交失败!");
  }
});

Servlet 开发学习

  1. 实现第二个触发条件:当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
// 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
        function getMessage() {
            $.ajax({
                type: "get",
                url: "message",
                success: function(body) {
                    
                }
            })
        }

当前的 body 已经是一个 js 对象数组了。ajax 会根据响应的 content Type 来自动进行解析。如果服务器返回的 content Type 已经是 application/json 了,ajax 就会把 body 自动转换成 js 对象。如果客户端没有自动转,也可以通过 JSON.parse 函数来手动转换。

拓展:

对象 和 JSON 字符串之间的转换,有两组:

  1. JAVA

objectMapper.readValue把 json 字符串转成 java 对象

objectMapper.writeValueAsString把 java 对象转成 json 字符串

  1. JS

JSON.parse把 json 字符串转成 JS 对象

JSON.stringify把 JS 对象转成 json 字符串

body 是一个数组了,我们接下来遍历它:

// 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
function getMessage() {
  $.ajax({
    type: "get",
    url: "message",
    success: function(body) {
      let container = document.querySelector(".container");
      // 依次来获取数组中的每个元素
      for(let message of body) {
        // 创建一个 div 标签来存放一条记录(消息)
        let newDiv = document.createElement('div');
        // 将元素中的关键信息进行提取,然后构造成一个字符串
        newDiv.innerHTML = message.from + "对" + message.to
          + "说: " + message.message; // 这里不是 say 了,而是对象中的message了
        // 添加 css 样式
        newDiv.className = 'row';
        // 将新节点挂在 container 这个节点下面
      }
    }
  })
}
// 加上函数调用,一旦访问页面就回去访问服务器,就把里面的数据显示出来
getMessage();

Servlet 开发学习

Servlet 开发学习

下面我们来看下,前后端代码结合的执行效果:

重启 Tomcat,先打开抓包工具 Fiddler,然后在浏览器中打开message.html文件:

Servlet 开发学习

抓包结果:

Servlet 开发学习

那么问题来了,之前再测试代码的时候,已经 send 三条数据了,为什么现在没了?

这是因为我们重启了 Tomcat 服务器,而我们是将消息存储在服务器的内存中的,因此,随着服务器的重启,内存中的数据也就丢失了。

上述的逻辑操作,只能保证页面刷新后,数据不丢失,而不能保证服务器重启后,数据不丢失。

下面来看下网页效果:

Servlet 开发学习

来看下抓包结果:

Servlet 开发学习

接着提交第二次数据,下方的记录仍然存在:

Servlet 开发学习

只要服务器不重启,数据就会一直在。服务器重启,数据就会被清空。

Servlet 开发学习

Servlet 开发学习

前后端交互流程:

Servlet 开发学习

前端总程序

<!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>告白墙</title>
</head>
<body>
<!-- 通过内部样式style标签,引入CSS样式 -->
<style>
    *{
        /* 首先先去除浏览器样式 */
        /* 将 内外边距设置为0,设置盒子模型为向内挤压 */
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    .containner{
        width: 100%;
    }
    h3{
        /* 文本居中 */
        text-align: center;
        /* 上下边距为 20px,左右边距为0 */
        padding: 20px 0;
        font-size: 24px;
    }

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

    .row{
        width: 400px;
        height: 50px;
        /* 上下外边距为零,左右外边距自适应 */
        /* 就是元素水平居中操作 */
        margin: 0 auto;
        /* 弹性布局 */
        display: flex;
        /* 水平居中 */
        justify-content: center;
        /* 垂直居中 */
        align-items: center;
    }

    .row span{
        width: 60px;
        font-size: 17px;
    }
    .row input{
        width: 300px;
        height: 40px;
        line-height: 40px;
        font-size: 20px;
        text-indent: 0.5em;
        outline: none;
    }

    .row #submit{
        width: 360px;
        height: 40px;
        font-size: 20px;
        line-height: 40px;
        margin: 0 auto;
        color: white;
        background-color: orange;
        border: none;
        border-radius: 15px;
        outline: none;
    }
    /* 当鼠标点击按钮的时候,会改变按钮颜色 */
    .row #submit:active{
        background-color: grey;
    }
</style>
<div class="container">
    <h3>表白墙</h3>
    <p>输入后点击提交,会将信息显示在表格中</p>
    <br>
    <div class="row">
        <span>谁: </span>
        <input type="text">
    </div>
    <div class="row">
        <span>对谁: </span>
        <input type="text">
    </div>
    <div class="row">
        <span>说什么: </span>
        <input type="text">
    </div>
    <div class="row">
        <button id="submit">提交</button>
    </div>
</div>

<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

<script>
    let submitBtn = document.querySelector('#submit');
    submitBtn.onclick = function(){
        // 1、获取 3个input 文本框中的数据
        let inputs = document.querySelectorAll('input');
        let from = inputs[0].value;
        let to = inputs[1].value;
        let say = inputs[2].value;
        if(from == ''|| to == '' || say == ''){
            // 用户填写的数据,并不完整。所以不提交。
            return;
        }
        // 2、生成一个新的 div,内容就是 三个 input 文本框的内容拼接
        // 再把这个 元素,挂在DOM树上。
        let newDiv = document.createElement('div');
        newDiv.innerHTML = from + "对" + to +"说:" + say;
        newDiv.className = 'row';
        // 将新建节点,挂在 container 这个节点下面
        let container = document.querySelector('.container');
        container.appendChild(newDiv);
        // 3.清空之前输入框的内容
        container.appendChild(newDiv);
        for(let i = 0; i < inputs.length;i++) {
            inputs[i].value = '';
        }

        // 4. 把当前获取到的输入框的内容,构造成一个 HTTP POST 请求
        // 通过 ajax 发送给服务器
        let body = {
            // 为了方便区分 key 与 value,我给 key 加上了双引号
            // 其实在 json 格式的数据中,key 一般都是要加上双引号的
            // 也可以省略双引号
            // 如果 key 带某些特殊字符,如空格,则必须带双引号
            "from":from,
            "to":to,
            "message":say
        }
        $.ajax({
            type:"post",
            url:"message",
            contentType:"application/json; charset=utf-8",
            data: JSON.stringify(body),
            success:function(body){
                alert("消息提交成功!");
            },
            error:function(a,b){
                console.log(a,b);
                alert("消息提交失败!");
            }
        });
    }
    // 当页面加载的时候,需要从从服务器获取到曾经存储的消息内容。
    function getMessage() {
            $.ajax({
                type: "get",
                url: "message",
                success: function(body) {
                    console.log(body);
                    let container = document.querySelector(".container");
                    // 依次来获取数组中的每个元素
                    for(let message of body) {
                        // 创建一个 div 标签来存放一条记录(消息)
                        let newDiv = document.createElement('div');
                        // 将元素中的关键信息进行提取,然后构造成一个字符串
                        newDiv.innerHTML = message.from + "对" + message.to
                                        + "说: " + message.message; // 这里不是 say 了,而是对象中的message了
                        // 添加 css 样式
                        newDiv.className = 'row';
                        // 将新节点挂在 container 这个节点下面
                        container.appendChild(newDiv);
                    }
                }
            })
    }
    
    // 加上函数调用,一旦访问页面就回去访问服务器,就把里面的数据显示出来
    getMessage();
</script>
</body>
</html>

表白墙进阶:持久化存储

在上面的程序中,当服务器重启的时候,List 里面的内容就会丢失!如何解决这个问题呢?关键是要把数据放在服务器的 硬盘 上存储!

有两种方法:

  1. 存文件。使用 IO 流来写文件/读文件。(麻烦,不同场景下可能不适用)
  2. 数据库。使用 MySQL + JDBC。

下面我们使用数据库的方法来改进代码:

  1. 引入 MySQL 依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.49</version>
</dependency>

Servlet 开发学习

  1. 创建数据库和数据表

在 idea 的 main 目录下创建 sql 文件db.sql

Servlet 开发学习

# 创建数据库
create database messageWall;

# 使用数据库
use messageWall;

drop table if exists message;
# 创建表,对于字段为sql关键字的话,需要给字段加上反引号``
create table message(`from` varchar(100), `to` varchar(100), message varchar(100));

Servlet 开发学习

  1. 调整后端代码
  • 删去 private List<Message> messages = new ArrayList<>();代码

Servlet 开发学习

在 方法doGet 和 doPost,相应的操作也要删除

Servlet 开发学习

  1. 和数据库建立连接

单独创建一个类 DBUtil,来实现与数据库的连接。

Servlet 开发学习

// 期望通过这个类来完成数据库的建立连接的过程
// 建立连接需要使用 DataSource,并且一个程序有一个 DataSource 实例即可。此处我们使用 单例模式 来实现。
public class DBUtil {

    private static DataSource dataSource = null;

    private static DataSource getDataSource() {
        if (dataSource == null) {
            dataSource = new MysqlDataSource();
            ((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/messageWall?characterEncoding=utf8&useSSL=false");
            ((MysqlDataSource)dataSource).setUser("root");
            ((MysqlDataSource)dataSource).setPassword("1234");
        }
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        return (Connection) getDataSource().getConnection();
    }

    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        // 此处建议分开写 try catch
        // 保证及时反馈一个地方的 close 异常,而不会影响到其他的 close 的执行

        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();
            }
        }
    }
}
  1. 封装数据库操作

messageWall类中添加函数 save()load()

把当前的信息存到数据库中save()

private void save(Message message) {
    Connection connection = null;
    PreparedStatement statement = null;
    try {
        // 1. 和数据库建立连接
        connection = DBUtil.getConnection();
        // 2. 构造 SQL 语句
        String sql = "insert into messagewall.message values(?,?,?)";
        statement = connection.prepareStatement(sql);
        statement.setString(1,message.from);
        statement.setString(2,message.to);
        statement.setString(3,message.message);
        // 3. 执行 sql 语句
        int ret = statement.executeUpdate();
        if (ret != 1) {
            System.out.println("插入失败!");
        } else {
            System.out.println("插入成功!");
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 4. 关闭连接
        DBUtil.close(connection,statement,null);
    }
}

从数据库查询到记录load()

private List<Message> load() {
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;
    List<Message> messageList = new ArrayList<>();

    try {
        // 1. 建立连接
        connection = DBUtil.getConnection();
        // 2. 构造 sql 语句
        String sql = "select * from messageWall.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");

            messageList.add(message);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 5. 释放资源
        DBUtil.close(connection,statement,resultSet);
    }

    return messageList;
}
  1. 修改相应的后端代码

Servlet 开发学习

Servlet 开发学习

效果

此时我们来查看效果,重启 Tomcat,进入浏览器:

一开始页面是空的,什么记录也没有,同时 sql 表中也没有记录:

Servlet 开发学习

Servlet 开发学习

现在我们插入数据:

Servlet 开发学习

查看数据表:

Servlet 开发学习

插入成功!

重启 Tomcat 打开浏览器(之前又提交了几次数据):

Servlet 开发学习

后端总代码:

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

@WebServlet("/message")
public class messageWall extends HttpServlet {

    // 用于读取请求并解析数据
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取整个消息列表中的元素,将其全部返回给客户端即可。
        // 此处我们使用 ObjectMapper 把 Java 对象,转换成 json 形式字符串
        List<Message> messageList = load();
        String jsonString = objectMapper.writeValueAsString(messageList);
        resp.setContentType("application/json; charset=utf-8");
        resp.getWriter().write(jsonString);
    }

    @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=utf-8"); // 约定浏览器返回数据的格式与读取方式
        // 注意:如果不加上 setContentType,浏览器就会把下面返回的数据当作普通字符串来读取
        // 我们通过 setContentType,告诉浏览器数据是一个 json 形式的数据,jQuery 的 ajax 就会将数据转换成 json 形式
        resp.getWriter().write("{"ok": true}");
    }

    // 把当前的信息存到数据库中
    private void save(Message message) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 1. 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2. 构造 SQL 语句
            String sql = "insert into messagewall.message values(?,?,?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1,message.from);
            statement.setString(2,message.to);
            statement.setString(3,message.message);
            // 3. 执行 sql 语句
            int ret = statement.executeUpdate();
            if (ret != 1) {
                System.out.println("插入失败!");
            } else {
                System.out.println("插入成功!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭连接
            DBUtil.close(connection,statement,null);
        }
    }

    // 从数据库查询到记录
    private List<Message> load() {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        List<Message> messageList = new ArrayList<>();

        try {
            // 1. 建立连接
            connection = DBUtil.getConnection();
            // 2. 构造 sql 语句
            String sql = "select * from messageWall.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");

                messageList.add(message);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5. 释放资源
            DBUtil.close(connection,statement,resultSet);
        }

        return messageList;
    }
}

五、Cookie 和 Session

关于 Cookie 的介绍可以看这篇文章的内容 www.yuque.com/justencount…

理解会话机制(Session)

服务器同一时刻收到的请求是很多的. 服务器需要区分清楚每个请求是从属于哪个用户, 就需要在服务器这边记录每个用户令牌以及用户的信息的对应关系。

会话的本质就是一个 "哈希表", 存储了一些键值对结构. key 就是令牌ID(token/sessionId), value 就是用户信息(用户信息可以根据需求灵活设计).

sessionId 是由服务器生成的一个“唯一性字符串”,从 session 机制的角度来看,这个唯一性字符串称为“sessionId”。但是站在整个登录流程中看待,也可以把这个唯一性字符串称为“token”。

Cookie 和 Session 的区别

  • Cookie 是客户端的机制,Session 是服务器端的机制。
  • Cookie 和 Session 经常会在一起配合使用,但是不是必须配合。
    • 完全可以用 Cookie 来保存一些数据在客户端,这些数据不一定是用户身份信息, 也不一定是 token / sessionId
    • Session 中的 token / sessionId 也不需要非得通过 Cookie / Set-Cookie 传递

核心方法

HttpServletRequest 类中相关方法

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

HttpServletResponse 类中相关方法

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

HttpSession 类中相关方法

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

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

Cookie 类中相关方法

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

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

实战案例:网页登录

Servlet 开发学习

登录界面

<!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>登录页面</title>
  </head>
  <body>
    <form action="login" method="post">
      <input type="text" name="username">
      <input type="text" name="password">
      <input type="submit" value="提交">
    </form>

  </body>
</html>

服务器程序:处理登录页面发送的请求

类名为 loginServlet,路径为/login

// 处理登录页请求
@WebServlet("/login")
public class loginServlet extends HttpServlet {
    // 由于请求的 form 中的一个 POST 请求,因此这里使用 doPost 来处理
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf8");
        // 读取请求中的参数,判断当前用户的身份信息是否正确
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        if (username == null || username.equals("") || password == null || password.equals("")) {

            resp.getWriter().write("用户名或密码不完整!登录失败!");
            return;
        }

        // 验证用户名和密码是否正确,一般来说这些东西是存在数据库的
        if (!username.equals("zhangsan") || !password.equals("123")) {
            // 登录失败
            resp.getWriter().write("用户名或密码错误!登录失败!");
            return;
        }

        // 登录成功
        // 需要一个 会话(session)把用户信息存进去
        HttpSession session = req.getSession(true);
        session.setAttribute("username","zhangsan");
        session.setAttribute("password","123");
        // 第二个参数是 Object,虽然写的是0,但是自动装箱成 Integer了,所以取的时候也是Integer
        Integer visitCount = (Integer)session.getAttribute("visitCount");
        if (visitCount == null) {
            session.setAttribute("visitCount",0);
        }

        // 页面跳转到登录页面
        resp.sendRedirect("index");
    }
}

Servlet 开发学习

如果会话存在,直接获取(根据请求的 cookie 中的 sessionId 来查哈希表)

如果会话不存在,则会创建新的。

由于是新用户登录,因此这里的 cookie 中没有 sessionId,所以就查询不到,也就需要创建出新的会话对象。

同时 getSession 还会生成一个 sessionId 并且把这个 sessionId 作为 Set-Cookie 中的字段返回给浏览去保存。

问题:一个 sessionId 可以对应多个键值对吗?

一个 sessionId 对应到一个 HttpSession 对象,一个 HttpSession 对象里可以包含多个键值对。

Servlet 开发学习

每个登录的用户都有自己的会话,有多少个用户,就有多少个 session 对象。

服务器程序:返回主页信息

类名为 indexServlet,路径为 /index

// 用这个 servlet 返回主页信息
@WebServlet("/index")
public class indexServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 要做的工作,把当前用户信息展示到页面上
        HttpSession session = req.getSession(false);
        if (session == null) {
            // 用户未登录,则跳转到登录页面,要求用户重新登录
            resp.sendRedirect("login.html");
            return;
        }
        // 已经登录成功过,就会获取到会话中的数据
        // 由于 getAttribute 返回的是 Object,需要强转
        String username = (String)session.getAttribute("username");
        Integer visitCount = (Integer)session.getAttribute("visitCount");
        visitCount = visitCount+1;
        session.setAttribute("visitCount",visitCount);

        resp.setContentType("text/html; charset=utf8");
        resp.getWriter().write("当前用户为:" + username + " 访问次数:"+visitCount);
    }
}

效果展示

重启 Tomcat,打开浏览器:

Servlet 开发学习

代码基本流程

Servlet 开发学习

fiddler 抓包情况

第一次请求:获取到 login.html 页面

初始情况下,没有 cookie

Servlet 开发学习

初始情况的响应,也是没有Set-Cookie的

Servlet 开发学习

这些是未登录状态,不会涉及到任何用户身份信息。



第二次请求:输入用户名和密码,点击登录

Servlet 开发学习


这个Post请求中也没有 cookie

Servlet 开发学习

但是登录成功之后,响应报文则是由 set-cookie

Servlet 开发学习

key名字为 JSESSIONID(servlet自动生成的key的名字)

value为十六进制数字,这个就是浏览器生成的 sessionId

此时这个sessionId就会保存到浏览器中:

Servlet 开发学习

Servlet 开发学习

Servlet 开发学习

这个代码,实现了创建会话,生成 sessionId 并且把 sessionId 通过 Set-Cookie 返回这个操作

Servlet 开发学习

第三次请求:登录成功之后,自动重定向访问主页

Servlet 开发学习

可以看到此处有 cookie,这个 cookie 就是上次响应 set-cookie 中的内容。

后续再反复请求,请求中都会带上这个 cookie 了。

六、上传文件

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

核心方法

HttpServletRequest 类相关方法

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

Servlet 开发学习,这个 name不是上传文件的真实文件名,而是 html 里写的一个 name的属性值。

最常用的为 getPart方法:

上传文件的时候,在前端需要使用 form 表单。form 表单中需要使用特殊的类型 form-data

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

注意:一个 HTTP 请求,可以一次性提交多个文件的。

每个文件都称为一个 Part,每个 Part都有 name(身份标识)。服务器代码中就可以根据 name找到对应的 Part了。

基于这个 Part 就可以进一步来获取文件信息,进行下一步的操作了。

Part 类相关方法

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

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

前端代码

在 webapp 目录下创建 upload.html

<!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>上传文件</title>
  </head>
  <body>
    <form action="upload" method="post" enctype="multipart/form-data">
      <input type="file" name="MyFile">
      <input type="submit" name="提交">
    </form>
  </body>
</html>

注意:写 form 表单时,Servlet 开发学习要加上,这个语句单独给 “上传文件”提供的。

Servlet 开发学习中,type属性为 filename属性命名为 "MyFile"(此名字可以随便命名,用于和后端的交互)

前端效果:

Servlet 开发学习

后端:处理上传请求

创建 UploadServlet

@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Part part = req.getPart("MyFile");
        // 获取文件的真实名字
        System.out.println(part.getSubmittedFileName());
        // 获取文件大小
        System.out.println(part.getSize());
        // 获取文件类型
        System.out.println(part.getContentType());
        // 把文件写入到服务器这边的磁盘中
        part.write("e:/result.jpg");

        resp.getWriter().write("upload ok!");
    }
}

注意:

  1. Servlet 开发学习此处的 "MyFile" 正是前端代码命名的:Servlet 开发学习

  2. 要加注解 @MultipartConfig,如果不加这个注解,getPart方法是无法正常工作的,会在页面上抛出异常。这个注解是用来开启对上传文件的支持的。

效果

Servlet 开发学习

点击”提交“按钮后:

Servlet 开发学习

查看后端控制台:

Servlet 开发学习分别为文件名,文件大小,文件类型。

查看服务器磁盘:

Servlet 开发学习

注意:

我们通过代码Servlet 开发学习将图片写入了 E 盘,在本地保存了文件。由于当下这里的服务器和浏览器是在同一个主机上的,此时文件上传看起来没什么效果。如果服务器和浏览器在不同的主机上,上传效果就非常明显了。

fiddler 抓包

请求报文:

Servlet 开发学习响应报文:

Servlet 开发学习