javaEE - 24( 23000 字 Servlet 入门 -2 )

一: Servlet API 详解

1.1 HttpServletResponse

Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到HttpServletResponse 对象中.

然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过Socket 写回给浏览器.

HttpServletResponse 核心方法:

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

注意:

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

1.2 代码示例: 设置状态码

实现一个程序, 用户在浏览器通过参数指定要返回响应的状态码.

  1. 创建 StatusServlet 类
@WebServlet("/statusServlet")  // 注解:将Servlet映射到URL路径,这里将Servlet映射到/statusServlet路径
public class StatusServlet extends HttpServlet {  // 定义一个名为StatusServlet的Java类,继承自HttpServlet类

  @Override  // 注解:表明下面的方法是覆盖父类的方法
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)  // 覆盖了父类的doGet方法,处理HTTP GET请求
throws ServletException, IOException {  // 方法可能会抛出ServletException和IOException异常
    String statusString = req.getParameter("status");  // 从请求参数中获取名为status的参数值,并存储在statusString变量中
    if (statusString != null) {  // 如果statusString不为空
      resp.setStatus(Integer.parseInt(statusString));  // 将HTTP响应状态设置为statusString表示的整数值
   }
    resp.getWriter().write("status: " + statusString);  // 向响应中写入内容,表示状态信息
 }
}
  1. 部署程序, 在浏览器中通过 URL http://127.0.0.1:8080/ServletHelloWorld/statusServlet?
    status=200 访问, 可以看到

在这里插入图片描述
3. 抓包结果:

HTTP/1.1 200
Content-Length: 11
Date: Mon, 21 Jun 2021 08:05:37 GMT
Keep-Alive: timeout=20
Connection: keep-alive

status: 200

变换不同的 status 的值, 就可以看到不同的响应结果

1.3 代码示例: 自动刷新

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

  1. 创建 AutoRefreshServlet 类
@WebServlet("/autoRefreshServlet")  // 注解:将Servlet映射到URL路径,这里将Servlet映射到/autoRefreshServlet路径
public class AutoRefreshServlet extends HttpServlet {  // 定义一个名为AutoRefreshServlet的Java类,继承自HttpServlet类

  @Override  // 注解:表明下面的方法是覆盖父类的方法
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)  // 覆盖了父类的doGet方法,处理HTTP GET请求
throws ServletException, IOException {  // 方法可能会抛出ServletException和IOException异常
    resp.setHeader("Refresh", "1");  // 设置HTTP响应头,指示浏览器每隔1秒自动刷新页面
    long timeStamp = new Date().getTime();  // 获取当前时间的时间戳
    resp.getWriter().write("timeStamp: " + timeStamp);  // 向响应中写入内容,表示时间戳信息
 }
}
  • 通过 HTTP 响应报头中的 Refresh 字段, 可以控制浏览器自动刷新的时机.
  • 通过 Date 类的 getTime 方法可以获取到当前时刻的毫秒级时间戳.
  1. 部署程序, 通过 URL http://127.0.0.1:8080/ServletHelloWorld/autoRefreshServlet 访问, 可以看到浏览器每秒钟自动刷新一次.

在这里插入图片描述

  1. 抓包结果
HTTP/1.1 200
Refresh: 1
Content-Length: 24
Date: Mon, 21 Jun 2021 08:14:29 GMT
Keep-Alive: timeout=20
Connection: keep-alive

timeStamp: 1624263269995

1.4 代码示例: 重定向

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

  1. 创建 RedirectServlet 类
@WebServlet("/redirectServlet")  // 注解:将Servlet映射到URL路径,这里将Servlet映射到/redirectServlet路径
public class RedirectServlet extends HttpServlet {  // 定义一个名为RedirectServlet的Java类,继承自HttpServlet类

  @Override  // 注解:表明下面的方法是覆盖父类的方法
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)  // 覆盖了父类的doGet方法,处理HTTP GET请求
throws ServletException, IOException {  // 方法可能会抛出ServletException和IOException异常
    resp.sendRedirect("http://www.sogou.com");  // 发送HTTP重定向响应到指定的URL,这里重定向到搜狗搜索主页
 }
}
  1. 部署程序, 通过 URL http://127.0.0.1:8080/ServletHelloWorld/redirectServlet 访问, 可以看到, 页面自动跳转到 搜狗主页 了.

  2. 抓包结果:

在这里插入图片描述

HTTP/1.1 302
Location: http://www.sogou.com
Content-Length: 0
Date: Mon, 21 Jun 2021 08:17:26 GMT
Keep-Alive: timeout=20
Connection: keep-alive

二: 表白墙服务器版本

结合上述 API, 我们可以把之前实现的表白墙程序修改成服务器版本. 这样即使页面关闭, 表白墙的内容也不会丢失.

2.1 准备工作

  1. 创建 maven 项目.
  2. 创建必要的目录 webapp, WEB-INF, 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>  // Web应用配置文件的根元素,包含整个Web应用的配置信息
  <display-name>Archetype Created Web Application</display-name>  // 设置Web应用的显示名称,显示在管理界面或日志中
</web-app>  // Web应用配置文件的结束标签
  1. 调整 pom.xml

引入依赖, 配置生成 war 包, 以及 war 包名字

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>表白墙服务器版</artifactId>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <!-- 加入 servlet 依赖 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
      <version>3.1.0</version>
      <!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
      <scope>provided</scope>
    </dependency>
    <!--
https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -
->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>
  </dependencies>
  <packaging>war</packaging>
  <build>
    <finalName>MessageWall</finalName>
  </build>
</project>
  1. 把之前实现的表白墙前端页面拷贝到 webapp 目录中.

在这里插入图片描述

2.2 约定前后端交互接口

所谓 “前后端交互接口” 是进行 Web 开发中的关键环节,具体来说, 就是允许页面给服务器发送哪些 HTTP 请求, 并且每种请求预期获取什么样的 HTTP 响应.

  1. 获取全部留言

请求:

GET /message

响应: JSON 格式

[
 {
    from: "黑猫",
    to: "白猫",
    message: "喵"
 },
 {
from: "黑狗",
    to: "白狗",
    message: "汪"
 },
  ......
]

我们期望浏览器给服务器发送一个 GET /message 这样的请求, 就能返回当前一共有哪些留言记录. 结果以 json 的格式返回过来.

  1. 发表新留言

请求: body 也为 JSON 格式.

POST /message
{
  from: "黑猫",
  to: "白猫",
  message: "喵"
}

响应: JSON 格式.

{
  ok: 1
}

我们期望浏览器给服务器发送一个 POST /message 这样的请求, 就能把当前的留言提交给服务器.

2.3 实现服务器端代码

  1. 创建 Message 类
public class Message {
  public String from;
  public String to;
  public String message;
}
  1. 创建 MessageServlet 类
@WebServlet("/message")  // 注解:将Servlet映射到URL路径,这里将Servlet映射到/message路径
public class MessageServlet extends HttpServlet {  // 定义一个名为MessageServlet的Java类,继承自HttpServlet类
  // 用于保存所有的留言
  private List<Message> messages = new ArrayList<Message>();  // 声明并初始化一个ArrayList,用于存储Message对象的列表
  // 用于转换 JSON 字符串
  private ObjectMapper objectMapper = new ObjectMapper();  // 创建一个ObjectMapper对象,用于JSON与Java对象的转换
  // 获取所有留言
  @Override  // 注解:表明下面的方法是覆盖父类的方法
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)  // 覆盖了父类的doGet方法,处理HTTP GET请求
throws ServletException, IOException {  // 方法可能会抛出ServletException和IOException异常
    resp.setContentType("application/json;charset=utf-8");  // 设置响应的内容类型为JSON格式,编码为UTF-8
    String respString = objectMapper.writeValueAsString(messages);  // 将留言列表转换为JSON字符串
    resp.getWriter().write(respString);  // 将JSON字符串写入响应
 }
  // 新增留言
  @Override  // 注解:表明下面的方法是覆盖父类的方法
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)  // 覆盖了父类的doPost方法,处理HTTP POST请求
throws ServletException, IOException {  // 方法可能会抛出ServletException和IOException异常
    resp.setContentType("application/json;charset=utf-8");  // 设置响应的内容类型为JSON格式,编码为UTF-8
    Message message = objectMapper.readValue(req.getInputStream(), Message.class);  // 从请求中读取JSON,并转换为Message对象
    messages.add(message);  // 将新增的留言添加到留言列表中
    resp.getWriter().write("{ \"ok\": 1 }");  // 向响应中写入成功信息
 }
}
  • ObjectMapper 的 readValue 方法也能直接从一个 InputStream 对象读取数据.
  • ObjectMapper 的 writeValueAsString 方法也能把一个对象数组直接转成 JSON 格式的字符串.

2.4 调整前端页面代码

修改 “表白墙.html”

  1. 拷贝之前封装好的 ajax 函数
// 把之前封装的 ajax 函数拷贝过来
function ajax(args) {
// ...... 代码内容参考 HTTP 协议章节
}
  1. 新加 load 函数, 用于在页面加载的时候获取数据
// 从服务器加载数据, 显示在界面上
```javascript
function load() {  // 定义一个名为load的函数
  ajax({  // 调用ajax函数,发送HTTP请求
    url: 'message',  // 请求的URL路径为'message'
    method: 'GET',  // 请求方法为GET
    callback: function (data, status) {  // 定义回调函数,处理请求响应
      // 先把字符串格式的 body 转成 对象数组
      let messages = JSON.parse(data);  // 将接收到的JSON字符串解析为JavaScript对象数组
      // 把每个消息都构造一个 HTML 标签
      for (let message of messages) {  // 遍历消息数组
        var row = document.createElement('div');  // 创建一个<div>元素节点
        row.className = 'row';  // 设置<div>元素的类名为'row'
        row.innerHTML = message.from + '对' + message.to + '说: ' + message.message;  // 设置<div>元素的内容为消息内容
        // 3. 把构造好的元素添加进去
        var container = document.querySelector('.container');  // 获取类名为'container'的元素节点
        container.appendChild(row);  // 将构造好的<div>元素添加到'container'中
     }
   }
 });
}
// 调动 load 执行数据加载
load();  // 调用load函数,开始加载数据
  1. 修改原来的点击事件回调函数. 在点击按钮的时候同时给服务器发送消息.
// 给点击按钮注册点击事件
var submit = document.querySelector('.submit');  // 获取类名为'submit'的元素节点
submit.onclick = function () {  // 注册点击事件的回调函数
// ...... 前面的代码略, 参考 JavaScript(WebAPI) 章节.
  // 给服务器发送消息
  ajax({  // 调用ajax函数,发送HTTP请求
    method: "POST",  // 请求方法为POST
    url: "message",  // 请求的URL路径为'message'
    contentType: "application/json; charset=utf-8",  // 请求的内容类型为JSON格式,编码为UTF-8
    body: JSON.stringify({ from: from, to: to, message: message }),  // 将JavaScript对象转换为JSON字符串并作为请求体发送给服务器
    callback: function (data, status) {  // 定义回调函数,处理请求响应
      if (status == 200) {  // 如果响应状态码为200,表示成功
        console.log("提交消息成功!");  // 打印提交消息成功的信息到控制台
     } else {  // 如果响应状态码不为200,表示失败
        console.log("提交消息失败! " + status);  // 打印提交消息失败的信息到控制台
     }
   }
 });
}
  1. 此时在浏览器通过 URL http://127.0.0.1:8080/MessageServlet/表白墙.html 访问服务器, 即可看到

在这里插入图片描述
此时我们每次提交的数据都会发送给服务器. 每次打开页面的时候页面都会从服务器加载数据. 因此及时关闭页面, 数据也不会丢失.

但是数据此时是存储在服务器的内存中 ( private List messages = newArrayList(); ), 一旦服务器重启, 数据仍然会丢失.

2.5 数据存入文件

针对上面的问题, 如果把数据保存在文件中, 那么重启服务器也不会丢失数据了.

修改 MessageServlet 代码.

  • 删掉之前的 messages 成员.
  • 创建新的成员 String filePath, 表示要存储的文件的路径.
  • 新增 load 方法, 用来从文件中读取内容. (会在页面加载的时候调用 load)
  • 新增 save 方法, 用来往文件中写入内容. (会在提交留言的时候调用 save)
  • 文件格式按照 行文本 的方式存储. 每个记录占用一行, 每个记录的字段之间(from, to, message) 使用 \t 分隔.

文件格式形如:
在这里插入图片描述

代码如下:

@WebServlet("/message")  // 将Servlet映射到URL路径为"/message"
public class MessageServlet extends HttpServlet {  // 定义一个名为MessageServlet的Servlet类,继承自HttpServlet类
  // 用于保存所有的留言
  // private List<Message> messages = new ArrayList<Message>();  // 用于保存所有留言的列表
  // 用于转换 JSON 字符串
  private ObjectMapper objectMapper = new ObjectMapper();  // 创建一个ObjectMapper对象,用于JSON与Java对象的转换
  // 数据文件的路径
  private String filePath = "d:/messages.txt";  // 数据文件的路径
  public List<Message> load() {  // 加载留言数据的方法
    List<Message> messages = new ArrayList<>();  // 创建一个空的留言列表
    System.out.println("从文件读取数据");  // 打印消息,表示正在从文件读取数据
    try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {  // 使用BufferedReader读取文件数据
      while (true) {  // 循环读取文件内容
        String line = bufferedReader.readLine();  // 读取文件中的一行数据
        if (line == null) {  // 如果读取到文件末尾,跳出循环
          break;
       }
        String[] tokens = line.split("\t");  // 将读取到的行按制表符分割成字符串数组
        Message message = new Message();  // 创建一个Message对象
        message.from = tokens[0];  // 设置消息的发送者
        message.to = tokens[1];  // 设置消息的接收者
        message.message = tokens[2];  // 设置消息内容
        messages.add(message);  // 将消息添加到留言列表中
     }
   } catch (IOException e) {  // 捕获可能出现的IO异常
      // 首次运行的时候文件不存在, 可能会在这里触发异常.
      e.printStackTrace();  // 打印异常堆栈信息
   }
    System.out.println("共读取数据 " + messages.size() + " 条!");  // 打印消息,表示读取了多少条数据
    return messages;  // 返回读取到的留言列表
 }
  public void save(Message message) {  // 保存留言数据的方法
    System.out.println("向文件写入数据");  // 打印消息,表示正在向文件写入数据
    // 使用追加写的方式打开文件
    try (FileWriter fileWriter = new FileWriter(filePath, true)) {  // 使用FileWriter打开文件,追加写入数据
      fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");  // 将消息以制表符分隔写入文件
   } catch (IOException e) {  // 捕获可能出现的IO异常
      e.printStackTrace();  // 打印异常堆栈信息
   }
 }
  // 获取所有留言
  @Override  // 覆盖父类的doGet方法,处理HTTP GET请求
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    List<Message> messages = load();  // 加载留言数据
    resp.setContentType("application/json;charset=utf-8");  // 设置响应的内容类型为JSON格式,编码为UTF-8
    String respString = objectMapper.writeValueAsString(messages);  // 将留言列表转换为JSON字符串
    resp.getWriter().write(respString);  // 将JSON字符串写入响应
 }
  // 新增留言
  @Override  // 覆盖父类的doPost方法,处理HTTP POST请求
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setContentType("application/json;charset=utf-8");  // 设置响应的内容类型为JSON格式,编码为UTF-8
    Message message = objectMapper.readValue(req.getInputStream(), Message.class);  // 从请求中读取JSON,并转换为Message对象
    save(message);  // 保存新增的留言数据
    resp.getWriter().write("{ \"ok\": 1 }");  // 向响应中写入成功信息
 }
}

此时即使重启服务器, 留言数据也不会丢失了.

2.6 数据存入数据库

使用文件的方式存储留言固然可行, 但是并不优雅,我们还可以借助数据库完成存储工作.

  1. 在 pom.xml 中引入 mysql 的依赖
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.45</version>
</dependency>
  1. 创建数据库, 创建 messages 表
set character_set_database=utf8;  -- 设置数据库的字符集为UTF-8
set character_set_server=utf8;  -- 设置服务器的字符集为UTF-8
create database if not exists MessageWall;  -- 创建名为MessageWall的数据库,如果不存在的话
use MessageWall;  -- 使用MessageWall数据库
drop table if exists messages;  -- 如果存在名为messages的表,则删除该表
create table messages (`from` varchar(255), `to` varchar(255), `message` varchar(2048));  -- 创建名为messages的表,包含from、to和message三个字段,分别为VARCHAR类型,分别设置了长度限制
  1. 创建 DBUtil 类

DBUtil 类主要实现以下功能:

  • 创建 MysqlDataSource 实例, 设置 URL, username, password 等属性.
  • 提供 getConnection 方法, 和 MySQL 服务器建立连接.
  • 提供 close 方法, 用来释放必要的资源.
// 负责和数据库建立连接
public class DBUtil {
  // 数据库连接的URL
  private static final String URL = "jdbc:mysql://127.0.0.1:3306/MessageWall?characterEncoding=utf8&useSSL=false";
  // 数据库用户名
  private static final String USERNAME = "root";
  // 数据库密码
  private static final String PASSWORD = "";
  // 数据源对象
  private static DataSource dataSource = null;

  // 获取数据源对象
  private static DataSource getDataSource() {
    // 若数据源对象为空,则进行初始化
    if (dataSource == null) {
      // 使用同步块确保线程安全
      synchronized (DBUtil.class) {
        // 双重检查,防止多线程环境下重复初始化
        if (dataSource == null) {
          dataSource = new MysqlDataSource();  // 创建MySQL数据源对象
          // 设置数据源的URL、用户名和密码
          ((MysqlDataSource)dataSource).setUrl(URL);
          ((MysqlDataSource)dataSource).setUser(USERNAME);
          ((MysqlDataSource)dataSource).setPassword(PASSWORD);
        }
      }
    }
    return dataSource;  // 返回数据源对象
  }

  // 获取数据库连接对象
  public static Connection getConnection() {
    try {
      return getDataSource().getConnection();  // 获取数据库连接
    } catch (SQLException e) {
      e.printStackTrace();  // 捕获可能出现的SQL异常并打印异常堆栈信息
    }
    return null;  // 若出现异常,则返回空
  }

  // 关闭数据库连接、语句和结果集
  public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
    try {
      if (resultSet != null) {
        resultSet.close();  // 关闭结果集
      }
      if (statement != null) {
        statement.close();  // 关闭语句
      }
      if (connection != null) {
        connection.close();  // 关闭连接
      }
    } catch (SQLException e) {
      e.printStackTrace();  // 捕获可能出现的SQL异常并打印异常堆栈信息
    }
  }
}
  1. 修改 load 和 save 方法, 改成操作数据库
private List<Message> load() {
  List<Message> messages = new ArrayList<>();  // 创建一个空的留言列表
  // 1. 和数据库建立连接
  Connection connection = DBUtil.getConnection();  // 获取数据库连接
  PreparedStatement statement = null;  // 声明一个PreparedStatement对象,用于执行SQL语句
  ResultSet resultSet = null;  // 声明一个ResultSet对象,用于存储查询结果集
  try {
    // 2. 拼装 SQL
    String sql = "select * from messages";  // 拼装查询所有留言的SQL语句
    statement= connection.prepareStatement(sql);  // 创建PreparedStatement对象,并设置SQL语句
    // 3. 执行 SQL
    resultSet = statement.executeQuery();  // 执行SQL查询,并将结果存储到ResultSet对象中
    // 4. 遍历结果集合
    while (resultSet.next()) {  // 遍历结果集中的每一行数据
      Message message = new Message();  // 创建一个Message对象,用于存储每条留言的信息
      message.from = resultSet.getString("from");  // 从结果集中获取发送者信息,并赋值给Message对象的from属性
      message.to = resultSet.getString("to");  // 从结果集中获取接收者信息,并赋值给Message对象的to属性
      message.message = resultSet.getString("message");  // 从结果集中获取留言内容,并赋值给Message对象的message属性
      messages.add(message);  // 将当前留言对象添加到留言列表中
   }
 } catch (SQLException e) {  // 捕获可能出现的SQL异常
    e.printStackTrace();  // 打印异常堆栈信息
 } finally {
    // 5. 释放必要的资源
    DBUtil.close(connection, statement, resultSet);  // 调用DBUtil的close方法释放数据库连接、语句和结果集
 }
  return messages;  // 返回查询到的留言列表
}

private void save(Message message) {
  // 1. 和数据库建立连接
  Connection connection = DBUtil.getConnection();  // 获取数据库连接
  PreparedStatement statement = null;  // 声明一个PreparedStatement对象,用于执行SQL语句
  try {
    // 2. 拼装 SQL
    String sql = "insert into messages values(?, ?, ?)";  // 拼装插入留言的SQL语句
    statement = connection.prepareStatement(sql);  // 创建PreparedStatement对象,并设置SQL语句
    statement.setString(1, message.from);  // 设置SQL语句中第一个问号的值为留言的发送者
    statement.setString(2, message.to);  // 设置SQL语句中第二个问号的值为留言的接收者
    statement.setString(3, message.message);  // 设置SQL语句中第三个问号的值为留言的内容
    // 3. 执行 SQL
    statement.executeUpdate();  // 执行SQL插入操作
 } catch (SQLException e) {  // 捕获可能出现的SQL异常
    e.printStackTrace();  // 打印异常堆栈信息
 } finally {
    // 4. 释放必要的资源
    DBUtil.close(connection, statement, null);  // 调用DBUtil的close方法释放数据库连接和语句
 }
}

重新部署程序, 此时使用数据库之后也可以保证即使服务器重启, 数据也不丢失.

2.7 存入文件和存入数据库的区别

虽然都是把数据存储在磁盘上, 为什么我们说 “使用文件” 不优雅, “使用数据库” 更科学 呢?当前看起来, 明显是数据库操作的代码量要比文件操作的代码量更多呀.

但是实际上, 当前我们写的程序比较简单, 存储的数据比较少, 数据格式也不复杂. 这种情况下使用文件是比数据库代码更精简一些.

但是如果我们的程序更复杂, 数据更多并且数据格式也更复杂的时候, 单纯的文件操作就要比数据库操作更麻烦了.

因为数据库已经给我们提供了很多功能可以开箱即用. (例如数据类型的校验, 约束, 聚合查询, 联合查询, 子查询, 索引, 事务等等). 而如果基于文件来完成类似的功能, 就需要我们自己写很多代码来手动实现了.

三: Cookie 和 Session

HTTP 协议自身是属于 “无状态” 协议

“无状态” 的含义指的是:默认情况下 HTTP 协议的客户端和服务器之间的这次通信, 和下次通信之间没有直接的联系.

但是实际开发中, 我们很多时候是需要知道请求之间的关联关系的,例如登陆网站成功后, 第二次访问的时候服务器就能知道该请求是否是已经登陆过了

在这里插入图片描述

图中的 “令牌” 通常就存储在 Cookie 字段中.此时在服务器这边就需要记录令牌信息, 以及令牌对应的用户信息, 这个就是 Session 机制所做的工作.

3.1 理解会话机制 (Session)

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

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

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

sessionId 和 token 就可以理解成是同一个东西的不同叫法(不同视角的叫法).

在这里插入图片描述

  • 当用户登陆的时候, 服务器在 Session 中新增一个新记录, 并把 sessionId / token 返回给客户端.(例如通过 HTTP 响应中的 Set-Cookie 字段返回).
  • 客户端后续再给服务器发送请求的时候, 需要在请求中带上 sessionId/ token. (例如通过 HTTP 请求中的 Cookie字段带上).
  • 服务器收到请求之后, 根据请求中的 sessionId / token 在 Session 信息中获取到对应的用户信息,再进行后续操作.

Servlet 的 Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.

3.2 Cookie 和 Session 的区别

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

3.3 核心方法

  1. HttpServletRequest 类中的相关方法
方法描述
HttpSession getSession()在服务器中获取会话。参数为true时,如果会话不存在则新建会话;参数为false时,如果会话不存在则返回null。
Cookie[] getCookies()返回一个数组,包含客户端发送该请求的所有的Cookie对象。会自动将Cookie中的格式解析成键值对。
  1. HttpServletResponse 类中的相关方法
方法描述
void addCookie(Cookie cookie)把指定的 cookie 添加到响应中。
  1. HttpSession 类中的相关方法

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

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

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

方法描述
String getName()返回 cookie 的名称。名称在创建后不能改变。
String getValue()获取与 cookie 关联的值。
void setValue(String newValue)设置与 cookie 关联的值。
  • HTTP 的 Cooke 字段中存储的实际上是多组键值对. 每个键值对在 Servlet 中都对应了一个 Cookie对象.
  • 通过 HttpServletRequest.getCookies() 获取到请求中的一系列 Cookie 键值对.
  • 通过 HttpServletResponse.addCookie() 可以向响应中添加新的 Cookie 键值对.

五: 上传文件

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

5.1 核心方法

  1. HttpServletRequest 类方法
方法描述
Part getPart(String name)获取请求中给定 name 的文件
Collection < Part > getParts()获取所有的文件
  1. Part 类方法
方法名描述
String getSubmittedFileName()获取提交的文件名
String getContentType()获取提交的文件类型
long getSize()获取文件的大小
void write(String path)把提交的文件数据写入磁盘文件

5.2 代码示例

实现程序, 通过网页提交一个图片到服务器上.

  1. 创建 upload.html, 放到 webapp 目录中.
<form action="upload" enctype="multipart/form-data" method="POST">
  <input type="file" name="MyImage">
  <input type="submit" value="提交图片">
</form>
  • 上传文件一般通过 POST 请求的表单实现.
  • 在 form 中要加上 multipart/form-data 字段.
  1. 创建 UploadServlet 类
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    Part part = req.getPart("MyImage");  // 获取名为"MyImage"的上传文件部分
    System.out.println(part.getSubmittedFileName());  // 打印上传文件的原始文件名
    System.out.println(part.getContentType());  // 打印上传文件的内容类型
    System.out.println(part.getSize());  // 打印上传文件的大小
    part.write("d:/MyImage.jpg");  // 将上传文件写入指定路径
    resp.getWriter().write("upload ok");  // 返回上传成功的信息给客户端
  }
}

注意点:

  • 需要给 UploadServlet 加上 @MultipartConfig 注解. 否则服务器代码无法使用 getPart 方法
  • getPart 的 参数 需要和 form 中 input 标签的 name 属性对应.
  • 客户端一次可以提交多个文件. (使用多个 input 标签). 此时服务器可以通过 getParts 获取所有的Part 对象.
  1. 部署程序, 在浏览器中通过 URLhttp://127.0.0.1:8080/ServletHelloWorld/upload.html 访问,

在这里插入图片描述

  1. 选择文件后, 点击提交图片, 则页面跳转到 /upload 页面.

在这里插入图片描述

  1. 此时可以看到服务器端的打印日志
rose.jpg
image/jpeg
13058
  1. 同时在 d 盘中生成了 MyImage.jpg

在这里插入图片描述

  1. 上传图片请求的抓包结果为:
POST http://127.0.0.1:8080/ServletHelloWorld/upload HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 13243
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1:8080
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundaryTlrGjpjXbKJl4y5B
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,imag
e/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/ServletHelloWorld/upload.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=1CBA3519A24801120ADC3C00A70FF047

------WebKitFormBoundaryTlrGjpjXbKJl4y5B
Content-Disposition: form-data; name="MyImage"; filename="rose.jpg"
Content-Type: image/jpeg

  •JFIF •• • •   ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 95
 C ••••••••••••••••••••••••••••••••••• •• •••
•

可以看到Content-Type 为 multipart/form-data , 这样的请求中带有一个 boundary=----
WebKitFormBoundaryTlrGjpjXbKJl4y5B , 这个 boundary 在 body 这边作为一个 “分隔线”, 分隔线下面是上传的文件的属性和文件内容.

六: 附录: 代码片段

此处把一些常用代码片段罗列在这里. 后续我们写代码的时候可以在这个基础上拷贝过去直接修改.

6.1 目录结构

在这里插入图片描述

6.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.bit</groupId>
  <artifactId>test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <!-- 指定属性信息 -->
  <properties>
    <encoding>UTF-8</encoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <!-- 加入 servlet 依赖 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
      <version>3.1.0</version>
      <!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.45</version>
    </dependency>
    <!--
https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -
->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>
  </dependencies>
 
  <!-- 打包方式是 war 包,一种用于 web 应用的包,原理类似 jar 包 -->
  <packaging>war</packaging>
  <build>
    <!-- 指定最终 war 包的名称 -->
    <finalName>test</finalName>
  </build>
</project>

6.3 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>

6.4 hello world

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

6.5 读取请求报头

@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String contentType = req.getHeader("Content-Type");
    // 或者使用
    String contentType = req.getContentType();
 }
}

6.6 读取 GET 请求的 query string

@WebServlet("/getParameter")
public class GetParameter extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    String userId = req.getParameter("userId");
    String classId = req.getParameter("classId");
 }
}

6.7 读取 POST 请求的 body

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

6.8 设置状态码

@WebServlet("/statusServlet")
public class StatusServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    resp.setStatus(200);
 }
}

6.9 设置响应报头

@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    resp.setHeader("Refresh", "1");
 }
}

6.10 重定向

@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    resp.sendRedirect("http://www.sogou.com");
 }
}

6.11 创建新 Session

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    HttpSession session = req.getSession(true);
    session.setAttribute("username", "admin");
    session.setAttribute("loginCount", "0");
 }
}

6.12 获取已有 Session

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
  HttpSession session = req.getSession(false);
  if (session == null) {
    // 用户没有登陆, 重定向到 login.html
    resp.sendRedirect("login.html");
    return;
 }
  // 如果已经登陆, 则从 Session 中取出数据
  String userName = (String)session.getAttribute("username");
  String countString = (String)session.getAttribute("loginCount");
}

6.13 上传文件

@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
    Part part = req.getPart("MyImage");
    System.out.println(part.getSubmittedFileName());
    System.out.println(part.getContentType());
    System.out.println(part.getSize());
    part.write("d:/MyImage.jpg");
    resp.getWriter().write("upload ok");
 }
}
<form action="upload" enctype="multipart/form-data" method="POST">
  <input type="file" name="MyImage">
  <input type="submit" value="提交图片">
</form>
  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值