Servlet的基本用法和示例

1. 认识Servlet

上一篇介绍了Tomcat的作用和使用意义

Tomcat提供http服务器,可以在把项目直接放到webapps目录下即可,但是这样得到的是一个静态页面,如果要实现网上聊天这样的交互式的动态页面,是非常困难的,于是sun公司就提供Servlet来帮助实现动态页面.

构建动态页面的技术,不同的语言提供不同的实现方式.

而Servlet就是由Java提供的一组API,它是运行在web服务器如Tomcat,可以响应http协议的请求,通过用户自己的实现逻辑,完成不同的响应,然后把结果返回给客户端(浏览器)

总而言之,Servlet把Socket,http协议,多线程并发等技术封装好了,我们不必关心这些,从而降低web app的开发门槛,从而提高开发效率

2. 第一个Servlet项目

下面就开始创建第一个Servlet项目啦

1) 创建项目

软件: IDEA

打开idea创建一个Maven项目,和创建Java项目相似

image-20221007151935189

image-20221007152023358

什么是Maven:

Maven是一种项目构建工具,我们之前写的代码都很简单,如果是面对复杂项目,会依赖很多外部第三方库,自身还有很多模块,模块之间也有依赖关系,此时的项目表示点击运行就可以了,而Maven可以将软件项目构建过程自动化,包括清理、编译、测试、报告、打包、部署项目。

2) 引入依赖

在Maven项目创建好后,会生成一个pom.xml文件:

image-20221007152713065

我们需要在该文件引入Servlet API依赖的jar包,可以手动下载+手动导入,但是Maven可以支持自动下载并导入依赖.

jar文件(Java归档 Java Archice): 该文件聚合大量的Java类文件,以zip格式创建,以.jar为文件拓展名,可以被编译器和JVM这要的工具直接使用

1)在中央仓库https://mvnrepository.com/ 中搜索servlet API

image-20221007153332103

2)选择版本

Tomacat和Servlet的版本要匹配

比如使用Tomcat8.5就需要使用Servlet3.1.0,可以在http://tomcat.apache.org/whichversion.html 查询版本对应关系

image-20221007153740066

下载3.1.0版本

image-20221007153919277

3)把中央仓库提供的xml复制到项目的pom.xml中

image-20221007154125725

修改后的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>org.example</groupId>
    <artifactId>helloServlet</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- 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>

    </dependencies>

</project>

标签内部放置项目依赖的 jar 包. maven 会自动下载依赖到本地

groupId: 表示组织名称

artifactId: 表示项目名称

version: 表示版本号

这三个就是在中央仓库确定唯一包的依赖

image-20221007155216146

从左到右,依次为groupId,artifactId,version

3) 创建目录

在项目创建好了,IDEA就已经自动为我们创建好了一些目录

image-20221007173016109

  • src: 源代码所在目录
  • main/java: 源代码的根目录,后续我们写的.java文件就放在这个目录中
  • main/resources: 项目的一些资源文件所在的目录
  • test/java: 测试代码的根目录

上面的目录还不能构建出一个项目,还需要创建一些目录/文件,具体有什么作用后面会解释

1**)创建 webapp 目录**

如图:image-20221007173604234

2)创建 web.xml

先在webapp目录下创建一个WEB-INF目录,然后再该目录下创建一个web.xml文件

image-20221007173920599

3)编辑 web.xml

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

wabapp在部署到Tomcat中的一个重要目录,可以放入一些静态资源,如CSS,HTML等等,

web.xml是为了使Tomcat能够正确处理webapp中的动态资源

4) 编写代码

在main/java下创建HelloServlet的.java文件,继承于HttpServlet类,并且重写其doGet方法

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;

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

解释说明:

  • HttpServlet类是javax.servlet.http包下采用HTTP通信协议的类
  • @WebServlet(“/hello”)注解,表示Tomcat收到的请求中,路径为/hello的请求才会调用HelloServlet类的代码
  • 重写 doGet 方法. doGet 的参数有两个, 分别表示收到的 HTTP 请求要构造的 HTTP 响应. 这个方法会在 Tomcat 收到 GET 请求时触发
  • HttpServletRequest 表示 HTTP 请求. Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body 等) 都是通过这个对象来获取
  • HttpServletResponse 表示 HTTP 响应. 代码中把响应对象构造好(构造响应的状态码, header, body 等)
  • resp.getWriter() 会获取到一个流对象, 通过这个流对象就可以写入一些数据, 写入的数据会被构造成一个 HTTP 响应的 body 部分, Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览器

注意:

上面的代码和平时写的代码有很大不同

  1. 当前代码不是通过main方法作为入口,main方法已经包含在Tomcat中,在合适的时机被Tomcat调用,所以当前的代码只是完整程序的一部分逻辑代码
  2. 对于可以被Tomcat调用的类所需要的条件主要有下面三个条件
    • 创建的类需要继承HttpServlet
    • 这个类需要使用@WebServlet注解来关联上一个HTTP路径
    • 这个类需要实现doXXX方法

5) 打包程序

使用Maven打包,一般在右侧可以看见,如果没有.可以按照菜单 -> View -> Tool Window -> Maven 打开)

image-20221007205331261

双击package,等待片刻后,就可以看见打包成功的信息

image-20221007205427426

如果打包失败,可以根据错误消息解决

打包成功后,可以看见在target目录下生成了一个jar包

image-20221007211930663

但是jar包不是我们需要的,Tomcat能够识别的是另一种war包格式

jar包和war包的区别:

  • jar是普通java程序打包的结果,包含一些.class文件,而war包是java web的程序,除了.class文件,还包含了HTML,CSS,图片,以及其他jar包,打包成war包格式才可以被Tomcat识别

helloServlet-1.0-SNAPSHOT.jar的由来

image-20221007212559171

由artifactId和version中的字符串拼接而成

1)把jar包转换成war包

在pom.xml中新增一个标签,表示打包成一个war包

<packaging>war</packaging>

2)修改生成war包的名称

<build>
	<finalName>ServletHelloWorld</finalName>
</build>

完整的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>org.example</groupId>
    <artifactId>helloServlet</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- 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>
    </dependencies>

    <packaging>war</packaging>
    <build>
        <finalName>ServletHelloWorld</finalName>
    </build>

</project>

重新使用Maven打包

image-20221007213304370

如果没有打包成功没有看见target目录生成,可能是没有勾选Show Excluded Files

image-20221007211814878

6) 部署程序

把war包copy到Tomcat/webapps下

image-20221007213557148

启动Tomcat它就会把war包解压

image-20221007213729286

image-20221007213735991

7) 验证程序

使用浏览器访问http://127.0.0.1:8080/ServletHelloWorld/hello

image-20221007214053907

说明程序运行成功

注意: URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path

3. 使用插件部署

先总结一下是如何创建一个Servlet项目

  1. 创建项目(利用Maven对项目创建,打包,部署等等)
  2. 引入依赖(引入Servlet所需的jar包 - 直接使用Maven引入,不必下载)
  3. 创建目录(创建webapp -> WEB-INF ->web.xml,让Tomcat正确处理)
  4. 编写代码(继承自HttpServlet类,重写doGet方法,并且加上@WebServlet注解)
  5. 打包程序(先修改pom.xml中的packing标签中的打包格式为war同时修改finalName来设置包名,最后使用Maven 打包)
  6. 部署程序(把war包拷贝到Tomcat的wabapps目录中,然后重启Tomcat服务器)
  7. 验证程序(使用浏览器输入URL构建HTTP请求访问Tomcat服务器, URL = Context Path(war包的名字) + Servlet Path(注解中的名字))

我们可以发现1~3步是每次创建项目后就不必重复操作了,而4 和7每次修改代码后都要修改,5,6就是可以简化的操作,这里的简化可以使用插件来完成.

上面把war包拷贝到webapps中太过于复杂,可以使用IDEA提供的插件Smart Tomcat(后面还可以使用SpringBoot)

插件:对程序的一些特定场景,做出一些特定功能的扩展

a. 安装Smart Tomcat插件

  1. File->setting

image-20221007215829850

  1. 点击Plugins 选择Marketplace搜索tomcat点击install

image-20221007220132104

b. 配置 Smart Tomcat 插件

  1. 找到’Add Configuration’点击打开,找到Smart Tomcat, 点击右上角的Create configuration创建配置

image-20221007220536495

  1. Name随便写, Tomcat server 是Tomcat的安装目录,其他的不用修改,这里的Context path默认填写的项目名称,会影响访问页面,这个不修改,点击ok

image-20221007220955937

  1. ok后,左上角出现咪咪了

image-20221007221245385

点击绿色三角形,IDEA就会自动进行编译, 部署, 启动 Tomcat 的过程

image-20221007221525269

此时在IDEA控制台中输出的Tomcat日志就没有乱码问题了

  1. 访问页面

在浏览器中输入http://127.0.0.1:8080/helloServlet/hello

image-20221007222004828

注意: URL 中的 PATH 也被分成两个部分, 和刚才不同的是, helloServlet 为 Context Path(在插件中配置的,默认为类名), hello 为 Servlet Path

在使用Smart Tomcat部署时并没有在Tomcat的webapps中拷贝war.

Smart Tomcat相当于在Tomcat启动时直接引用项目中的webapp和target目录

image-20221007222734647

c. 常见访问错误

1) 出现 404

404 表示用户访问的资源不存在. 可能是 URL 的路径写的不正确 或者注解内容不匹配

错误示例1: 通过 /hello 访问服务器

image-20221007223305193

错误示例2: 通过 /ServletHelloWorld 访问服务器

image-20221007223338621

错误示例3: 注解内容不匹配

image-20221007223455824

错误示例4: web.xml内容出错

image-20221007231106474

2) 出现405

405 表示对应的 HTTP 请求方法没有实现

错误实例: 没有实现 doGet 方法

image-20221007231314649

3) 出现500

往往是 Servlet 代码中抛出异常导致的

错误实例:

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

image-20221007231515109

错误信息会表示出来

4) 出现"空白页面"

不填写响应

image-20221007231714166

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

5) 出现"无法访问"

错误示例1 : 注解内容忘记打/

image-20221007232012121

image-20221007232025653

错误示例2 : 未启动插件

image-20221007232147101

d. 总结

在初学Servlet时,会遇见很多不同的问题,重点是学习排除错误和分析错误的思路

熟悉HTTP协议可以更加快速的寻错.

  • 4xx的状态码表示路径出错,通常需要检查URL, 以及代码中的Context path,Servlet Path是否一致
  • 5xx的状态码表示服务器出错,通常需要检查控制台的中打印的日志信息是否有报错
  • 出现无法连接,通常需要检查Tomcat是否启动,如果启动了,就看看日志是否报错
  • 空白页面需要使用抓包工具分析HTTP的请求响应过程

4. ServletAPI介绍

Servlet 提供了许多API,下面重点讲解HttpServlet,HttpServletRequset,HttpServletResponse

1) HttpServlet

核心方法

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

解释说明:

  • init() 方法只会在Servlet类第一次被使用时才会调用,用于初始化.
  • destory() 方法只会在Servlet对象被销毁时才会调用.
  • service() 每次收到请求都会调用,父类默认就会其调用请求方法
    • image-20221011205925598
  • HttpServlet中默认会根据当前请求调用对应方法:
    • image-20221011210234004

下面就演示如何使用这些方法,在使用之前要先构造请求,请求的构造有很多方法

  • 利用浏览器的地址栏,通过url构造
  • 利用前端的from表单构建(只能构建get和post)
  • 利用前端的ajax构造
  • 利用前端的src属性/herf属性构建

a. 代码示例: doPost请求

  1. 在webapp下创建test.html文件

image-20221011222648421

<!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>
    <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
    <script>
        $.ajax({
            type: 'post',
            url: 'hello',
            // 在控制台中查看结果
            success: function(body) {
                console.log(body);
            }
        });
    </script>
</body>
</html>
  1. 在HelloServlet类中重写doPost方法
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("doPost");
    }
}
  1. 在浏览器中输入http://127.0.0.1:8080/helloServlet/test.html

image-20221011222852299

b. 代码示例: 事件触发请求

下面利用按键触发不同请求并把响应显示到页面上,同样利用ajax构建

  1. 利用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>
    <button id="doGet">get</button>
    <button id="doPost">post</button>
    <button id="doPut">put</button>


    <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>

    <script>
        let doGetBtn = document.querySelector('#doGet');
        doGetBtn.onclick = function() {
            $.ajax({
                type: 'get',
                url: 'hello',
                success: function(body) {
                    let div = document.querySelector('.one');
                    div.innerHTML = body;
                }
            })
        };
        let doPostBtn = document.querySelector('#doPost');
        doPostBtn.onclick = function() {
            $.ajax({
                type: 'post',
                url: 'hello',
                success: function(body) {
                    let div = document.querySelector('.one');
                    div.innerHTML = body;
                }
            })
        };
        let doPutBtn = document.querySelector('#doPut');
        doPutBtn.onclick = function() {
            $.ajax({
                type: 'put',
                url: 'hello',
                success: function(body) {
                    let div = document.querySelector('.one');
                    div.innerHTML = body;
                }
            })
        }
    </script>
</body>
</html>
  1. 重写对应方法
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doGet");
        resp.getWriter().write("doGet");
    }

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

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doPut");
        resp.getWriter().write("doPut");
    }
}
  1. 在浏览器中输入并验证

image-20221011224540076image-20221011224550287image-20221011224559499

c.利用PostMan构建请求

使用第三方工具PostMan构建请求(原本时chrome的插件,现在已经独立成一个程序了),

下载网站: Postman API Platform | Sign Up for Free 根据自己的电脑下载对应版本

image-20221011225001261

根据get请求

image-20221011225024688

d. 面试题: HttpServlet的生命周期

image-20221011225215153

根据上图来分析其生命周期

  1. 当服务器收到HTTP请求后,会转发给Servlet容器,如果没有创建Servlet对象就会实例化该Servlet对象,随后调用init(),该方法只会调用一次,直到对象销毁.之后接受的请求都会被service()处理.
  2. 每次收到请求,都会调用service()方法,在其内部分析当前请求类型并调用对应doXXX方法
  3. 在对象销毁之前调用destory()一次实现资源的清理.

2) HttpServletRequest

当Tomcat通过Scoket API 读取HTTP请求(字符串),然后把该请求按照HTTP协议的格式把字符串解析为一个HttpServletRequest对象.其对象中就包含了

  • 请求的方法
  • url
  • 版本号
  • body
  • header

下面介绍了如何通过该类提供的方法来获取这些内容.

核心方法

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

注意:

  • URL(统一资源标识符),是一种抽象的概念,即不管用什么方法只要能够唯一标记某个资源,就是URI;URL(统一资源定位符),是通过网络地址标记资源的符号,综上:URL是URI的子集
  • 请求对象是服务器接收到的信息,只能’读’,不能’写’.

a. 代码示例: 打印一个完整的请求信息

  1. 创建showRequset类
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 showRequest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置 Context 内容的格式,以及编码规范,防止出现乱码和浏览器不能识别html的标签(例如<br>标签)
        resp.setContentType("text/html; charset = utf-8");
        // 存放对象中读取的内容
        StringBuffer respBody = new StringBuffer();
        //返回请求协议的名称和版本
        respBody.append(req.getProtocol()).append("<br>");
        //返回请求的 HTTP 方法的名称
        respBody.append(req.getMethod()).append("<br>");
        //从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分
        respBody.append(req.getRequestURI()).append("<br>");
        //返回指示请求上下文的请求 URI 部分
        respBody.append(req.getContextPath()).append("<br>");
        //返回包含在路径后的请求 URL 中的查询字符串
        respBody.append(req.getQueryString()).append("<br>");


        respBody.append("<h3>headers: <h3>");
        // 迭代获取header中的内容
        Enumeration<String> headerNames = req.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            respBody.append(headerName + " ");
            respBody.append(req.getHeader(headerName));
            respBody.append("<br>");
        }
        resp.getWriter().write(respBody.toString());
    }
}

  1. 浏览器中输入127.0.0.1:8080/helloServlet/showRequest

image-20221012152345306

注意:

  • 一定要设置编码规范,否则会出现乱码的情况,这是因为浏览器默认和windows的编码规范一样都是GBK,而服务器是UTF-8,不一致会出现乱码.
  • header中的内容是以键值对的形式组织,所以可以根据键名,获取对应的值.

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

在URL中query string中可以传递参数,而在服务器可以调用getParameter这个API来获取参数

  1. 创建getParameter
@WebServlet("/getParameter")
public class getParameter 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("userId: " + userId + "classId: " + classId);
    }
}
  1. 在浏览器中输入不带参数的url.默认值为String的默认值null

image-20221012225952554

  1. 输入127.0.0.1:8080/helloServlet/getParameter?userId=3&classId=12获取参数信息

image-20221012230146606

注意:

  • getParameter的返回值默认为String,如果对参数类型有需要,可以利用对应包装类的装箱方法
  • 后端获取query string中的key是事先约定好的,不会存在不含有key的情况

c. 代码示例: 获取POST请求中的参数 - form表单

对于POST请求中的参数一般是通过body传递给服务器,而body中的数据格式有很多,下面是通过form表单的形式传递参数

  1. 创建类 PostParameter
@WebServlet("/getParameter")
public class getParameter 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("userId: " + userId + "classId: " + classId);
    }
}
  1. 在webapp目录下创建testPost.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>Post</title>
</head>
<body>
    <form action="postParameter" method="post">
        <input type="text" name="userId">
        <input type="text" name="classId">
        <input type="submit" value="提交">
    </form>
</body>
</html>
  1. 在浏览器中输入提交数据

image-20221012232529276

点击提交后跳转

image-20221012232939007

  1. 利用Fiddler抓包

image-20221012232630274

这里的参数类型为Content-Type: application/x-www-form-urlencoded

d. 获取POST请求中的参数(1) - JSON格式

利用postman构造POST请求

  1. 创建postParameterJson
@WebServlet("/postParameterJson")
public class postParameterJson extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json; charset=utf-8");
        String body = readBody(req);
        resp.getWriter().write(body);
    }

    private String readBody(HttpServletRequest req) throws IOException {
        int contentLength = req.getContentLength();
        byte[] buffer = new byte[contentLength];
        InputStream inputStream = req.getInputStream();
        inputStream.read(buffer);
        return new String(buffer, "utf-8");
    }
}
  1. 启动Tomcat后打开PostMan构造Json格式的body,按照下面的操作

image-20221013153734261

  1. 利用Fiddler查看请求的确切信息

image-20221013154049242

e. 获取POST请求中的参数 (2) - JSON格式

上面的JSON格式很简单,但是如果JSON格式变复杂了(例如嵌套JSON数据),此时如果由程序员自己写代码来解析并获取内部的键值对,就很艰难了.这里靠谱的解决方案是使用第三方库,本文讲解的是Jackson(同类的还有Gson, FastJson…).

引入Jackson步骤:

  1. 在中央仓库中搜索Jackson,选择 JackSon Databind

image-20221013155243098

  1. Jackson的版本有很多,随便选择,复制依赖配置到pom.xml中

image-20221013155646509

image-20221013155752862

  1. 修改postParameterJson
class JsonData {
    public int userId;
    public int classId;
}
@WebServlet("/postParameterJson")
public class postParameterJson extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json; charset=utf-8");
        String body = readBody(req);
        // 创建ObjectMapper 对象
        ObjectMapper objectMapper = new ObjectMapper();
        // 使用readValue方法把body字符串转换为JsonData对象
        JsonData jsonData = objectMapper.readValue(body, JsonData.class);
        resp.getWriter().write("userId: " + jsonData.userId + ", " + "classId: " + jsonData.classId);
        System.out.println("userId: " + jsonData.userId + ", " + "classId: " + jsonData.classId);
        System.out.println(objectMapper.writeValueAsString(jsonData));
    }

    private String readBody(HttpServletRequest req) throws IOException {
        int contentLength = req.getContentLength();
        byte[] buffer = new byte[contentLength];
        InputStream inputStream = req.getInputStream();
        inputStream.read(buffer);
        return new String(buffer, "utf-8");
    }
}
  1. 在postman中发送请求

image-20221013164714979

注意:

  1. 引入依赖后别忘记下载依赖,要不然没有ObjectMapper类,可以在C:\Users\a\.m2中查看,刚才下载的Jackson在

    • image-20221013170006649
  2. Jackson的使用其实就是把json格式的字符串转换为java对象,然后把java对象转换为json字符串,而Jackson提供ObjectMapper类帮助我们完成相互转换过程

  3. 创建的JsonData类中的属性的名字和类型要和body中字符串的key对应,访问权限是public,通过提供getter方法获取字段信息,

  4. Jackson 库的核心类为 ObjectMapper. 其中的 readValue 方法把一个 JSON 字符串转成 Java 对象. 其中的 writeValueAsString 方法把一个 Java 对象转成 JSON 格式字符串.

    • 控制台打印信息:image-20221013171500404
  5. readValue 的第二个参数为 JsonData 的 类对象. 通过这个类对象, 在 readValue 的内部就可以借助反射机制来构造出 JsonData 对象, 并且根据 JSON 中key 的名字, 把对应的 value 赋值给JsonData 的对应字段

  6. 如果json中key和类属性中名字不一致时,都会出现 500 状态码

    • json中的不一致image-20221013172346548
    • key中的不一致image-20221013172656091

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())之前进行的

a. 代码示例: 设置状态码

根据用户在浏览器中输入的参数指定要返回的状态码

  1. 创建 StatusServlet 类
@WebServlet("/statusServlet")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String status = req.getParameter("status");
        if (status != null) {
            resp.setStatus(Integer.parseInt(status));
        }
        resp.getWriter().write("status: " + status);
    }
}
  1. 通过PostMan构造参数并发送请求

image-20221013191546466

注意:

对于状态码404是默认服务器会显示:

image-20221013192219433

但是有些服务器会个性化定制404

  • 搜狗:image-20221013192321372
  • 百度:image-20221013192338354
  • Tomcat:image-20221013192407406
  • B站:image-20221013192443898

b. 代码示例: 自动刷新

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

  1. 创建 AutoRefreshServlet 类
@WebServlet("/autoRefreshServlet")
public class AutoRefreshServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("Refresh", "1");
        long time = System.currentTimeMillis();
        resp.getWriter().write("timeStamp: " + time);
    }
}
  1. 在浏览器中输入http://127.0.0.1:8080/helloServlet/autoRefreshServlet

image-20221013194904536

image-20221013194912363

  • 通过 HTTP 响应报头中的 Refresh 字段, 可以控制浏览器自动刷新的时机

应用场景: 文字直播(对现场直播转换为文字并对关键信息实时推送)

例如冬奥会中的文字直播

image-20221013195105817

c. 代码示例: 重定向

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

  1. 创建 RedirectServlet 类
@WebServlet("/redirectServlet")
public class RedirectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.sendRedirect("http://www.sogou.com");
    }
}
  1. 在浏览器中输入http://127.0.0.1:8080/helloServlet/redirectServlet

image-20221013202554502

  1. 使用Fiddler抓包

image-20221013200359870

4) 综合案例

a. 服务器版表白墙

通过利用Servlet提供的API就可以把之前的表白墙修改为服务器版本,实现前后端分离.

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>
          <display-name>Archetype Created Web Application</display-name>
      </web-app>
      
  3. 引入依赖

    • <?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>
              <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
              <dependency>
                  <groupId>javax.servlet</groupId>
                  <artifactId>javax.servlet-api</artifactId>
                  <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.13.3</version>
              </dependency>
          </dependencies>
      </project>
      
  4. 把之前写好的表白墙前端代码放入webapp目录下

    • <!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>
              * {
                  margin: 0;
                  padding: 0;
              }
              .container {
                  width: 600px;
                  height: 600px;
                  margin: 0 auto;
                  background-color: pink;
                  border-radius: 20px;
              }
              h1 {
                  text-align: center;
                  padding: 20px 0;
              }
              p {
                  color: #666;
                  text-align: center;
                  font-size: 14px;
                  padding: 10px 0;
              }
              .row {
                  height: 40px;
                  display: flex;
                  justify-content: center;
                  align-items: center;
              }
              span {
                  width: 100px;
                  line-height: 40px;
              }
              .edit {
                  width: 200px;
                  height: 30px;
              }
              .submit {
                  margin-top: 15px;
                  width: 300px;
                  height: 40px;
                  color: #fff;
                  background-color: rgb(77, 163, 202);
                  border: none;
              }
              .submit:active {
                  background-color: gray;
              }
          </style>
      </head>
      <body>
      <div class="container">
          <h1>表白墙</h1>
          <p>输入信息后提交,将会出现在表白墙上哦</p>
          <div class="row">
              <span>谁:</span>
              <input class="edit" type="text">
          </div>
          <div class="row">
              <span>对谁:</span>
              <input class="edit" type="text">
          </div>
          <div class="row">
              <span>说什么:</span>
              <input class="edit" type="text">
          </div>
          <div class="row">
              <input type="button" value="提交" class="submit">
          </div>
      </div>
      <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
      <script>
          var cut = 1;
          var submit = document.querySelector('.submit');
          submit.onclick = function () {
              // 1.获取编辑框内容
              var editor = document.querySelectorAll('.edit');
              var from = editor[0].value;
              var to = editor[1].value;
              var message = editor[2].value;
              if (from == '' || to == '' || message == '') {
                  return;
              }
              // 2.构造子元素
              var row = document.createElement('div');
              row.className = 'row';
              row.innerHTML = '留言' + cut + ':  ' + from + ' 对 ' + to + ' 说 ' + message;
              row.style = 'font-size:10px font:宋体; height:35px'
              cut++;
      
              // 3.新增元素
              var container = document.querySelector('.container');
              container.appendChild(row);
              // 4.清除编辑框内容
              for (var i = 0; i < 3; i++) {
                  editor[i].value = '';
              }
          }
      </script>
      </body>
      </html>
      
2) 约定前后端交互接口

就是浏览器向服务器发送哪些HTTP请求,服务器处理这些请求并计算出响应,在这个过程中,约定传输和解析数据的格式,请求和响应的格式等等.

在第一次访问页面时应该获取之前存在的全部留言并显示到页面,当新增留言时就应该把留言内容传输给服务器并由它保存.在获取留言和新增留言的过程就涉及到浏览器发送请求,服务器返回响应的过程.

1)约定获取全部留言交互格式

请求: GET

GET /message

响应: JSON格式

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

2)新增新留言

请求: JSON格式

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

响应: JSON格式

{
	ok:1
}

ok为1说明新增成功,0说明新增失败

注意:

  • json格式传输数据时,采取的时键值对的格式, 这里的键应该是字符串类型的(这里为了省事,省略了),如果在PostMan中测试时请务必使用字符串.

  • 这里的"约定"其实就是之前讲的应用层协议之自定义协议,这里的约定也可以采取其他方式,只要能够到达预期的效果就可以了

3) 实现服务器端代码

1)创建Message类

class Message {
    public String from;
    public String to;
    public String message;
    
    @Override
    public String toString() {
        return "Message{" +
                "from='" + from + '\'' +
                ", to='" + to + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

Message类的成员变量就是传递过程中JSON中键值对中的值,所以在类外要能够访问,一般设置访问权限为public,如果降低访问权限就需要提供getter()方法,总之,要提供获取成员变量的方法.

2)创建MessageServlet类

这个类应该实现前后端的交互功能,即浏览器向服务器获取数据和浏览器提交数据到服务器.根据上面的约定,服务器应该分别使用doGet()方法和doPost()方法.

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    // 保存所有的留言
    private List<Message> messages = new ArrayList<>();
    // 用于转换JSON格式的字符串
    private ObjectMapper objectMapper = new ObjectMapper();

    // 获取所有的留言
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 这里就是约定的JSON格式
        resp.setContentType("application/json;charset=utf-8");
        // 把list中的Message对象转换为JSON格式并写入响应返回给服务器
        String respString = objectMapper.writeValueAsString(messages);
        resp.getWriter().write(respString);
    }
    //新增留言
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        Message message = objectMapper.readValue(req.getInputStream(), Message.class);
        messages.add(message);
        resp.getWriter().write("{ \"ok\": 1 }");
    }
}
4) 调整前端页面代码

添加下面的事件函数

<!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>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        .container {
            width: 600px;
            height: 600px;
            margin: 0 auto;
            background-color: pink;
            border-radius: 20px;
        }
        h1 {
            text-align: center;
            padding: 20px 0;
        }
        p {
            color: #666;
            text-align: center;
            font-size: 14px;
            padding: 10px 0;
        }
        .row {
            height: 40px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        span {
            width: 100px;
            line-height: 40px;
        }
        .edit {
            width: 200px;
            height: 30px;
        }
        .submit {
            margin-top: 15px;
            width: 300px;
            height: 40px;
            color: #fff;
            background-color: rgb(77, 163, 202);
            border: none;
        }
        .submit:active {
            background-color: gray;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>表白墙</h1>
    <p>输入信息后提交,将会出现在表白墙上哦</p>
    <div class="row">
        <span>:</span>
        <input class="edit" type="text">
    </div>
    <div class="row">
        <span>对谁:</span>
        <input class="edit" type="text">
    </div>
    <div class="row">
        <span>说什么:</span>
        <input class="edit" type="text">
    </div>
    <div class="row">
        <input type="button" value="提交" class="submit">
    </div>
</div>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
    var cut = 1;
    var submit = document.querySelector('.submit');
    submit.onclick = function () {
        // 1.获取编辑框内容
        var editor = document.querySelectorAll('.edit');
        var from = editor[0].value;
        var to = editor[1].value;
        var message = editor[2].value;
        if (from == '' || to == '' || message == '') {
            return;
        }
        // 2.构造子元素
        var row = document.createElement('div');
        row.className = 'row';
        row.innerHTML = '留言' + cut + ':  ' + from + ' 对 ' + to + ' 说 ' + message;
        row.style = 'font-size:10px font:宋体; height:35px'
        cut++;

        // 3.新增元素
        var container = document.querySelector('.container');
        container.appendChild(row);
        // 4.清除编辑框内容
        for (var i = 0; i < 3; i++) {
            editor[i].value = '';
        }

        //[新增内容]5. 需要把输入的内容通过ajax把内容转换为json格式构造成POST方法,交给后端服务器
        let massageJson = {
            "from": from,
            "to": to,
            "message":message
        };
        $.ajax({
            type:'post',
            url:'message',
            contentType:'application/json;charset=utf-8',
            data:JSON.stringify(massageJson),
            success: function(body) {
                alert("提交成功!");
            },
            error: function() {
                // 会在服务器返回的状态码部署2XX时调用
                alert("提交失败!");
            }
        });
    }
    
    //[新增内容]6. 在页面加载时调用,从服务器中获取所有的留言信息并显示到页面上
    function load() {
        $.ajax({
            type:'get',
            url:'message',
            success: function(body) {
                // 从服务器得到的body已经是一个js对象的数组
                // 这是因为服务器本来返回的是JSON格式的字符串,但是ajax会根据Content-Type为application/json
                // 对body自动解析为js对象的数组,然后遍历数组并把其内容填充到页面上
                let container = document.querySelector('.container');
                for (let message of body) {
                    let newDiv = document.createElement('div');
                    newDiv.className = 'row';
                    newDiv.innerHTML = message.from + " 对 " + message.to + " 说 " + message.message;
                    container.appendChild(newDiv);
                }
            }
        })
    }
    // 在页面加载时自动调用
    load();
</script>
</body>
</html>

配置好Smart Tomcat

image-20221015214749312

再次理解Context Path和 Servlet Path

通常访问Servlet程序对应的路径是两级目录:

  • 第一级路径是Context Path代表当前的webapp(网站),再一个Tomcat上可以同时部署多个webapp(网站),webapps目录下的每个目录都是一个单独的webapp
    • 如果是通过startup.bat启动的Tomcat,那么webapps中对应的war包就是这个wabapp的context Path
    • 如果是通过smart Tomcat 启动Tomcat,那么参数的Context Path就是 上面配置中指定的目录
  • 第二级目录Servlet Path就是根据@WebServlet(“/message”)的内容确定,或者就是webapp下的静态文件.目录

实现效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OxjktIaY-1666595270341)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20221015215138436.png)]

下面使用Fiddler来查看其前后端交互过程

  1. 第一次进入页面

在第一次加载页面时就发生了两次HTTP请求

第一次:

image-20221015224628158

响应返回类型为text/html,是为了获取html页面内容

第二次:

image-20221015224810809

页面调用load()函数,从服务器中获取留言数据

由于开始没有数据返回的是一个空数组

image-20221015225104610

  1. 向服务器提交数据

请求类型为POST

image-20221015225253570

请求中需要新增的JSON数据

image-20221015225354290

服务器成功接受的响应内容

image-20221015225458435

  1. 刷新页面

刷新页面浏览器会向服务器获取全部留言信息

image-20221015230447790

image-20221015230631159

这是因为浏览器发起的load()函数中的ajax会触发GET请求调用doXXX方法,在服务器返回响应(getWriter().write()中的respString字符串)就会触发浏览器的代码(ajax的success回调函数->响应字符串作为实参传递给回调函数中的body形参),从而把服务器中的留言重新布置

所以load()函数回车打开页面/刷新页面时调用

但是上面的代码还存在一个问题,那就是数据不可以持久化存储,当服务器关闭时,存储的数据会被清空,那么如何解决?

解决方案:

  1. 存文件.所以IO流来操作文件
  2. 数据库: 使用MySQL和JDBC

主流使用的是数据库,下面先提供文件存放的代码

5) 数据存放在文件

数据存放在文件中要指定数据的格式,我们约定以行文本的方式存储,索引一行数据代表一组数据,每个字段之间用\t来划分

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

    @Override
    public String toString() {
        return "Message{" +
                "from='" + from + '\'' +
                ", to='" + to + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    // 保存所有的留言
    //private List<Message> messages = new ArrayList<>();
    // 用于转换JSON格式的字符串
    private ObjectMapper objectMapper = new ObjectMapper();

    // 获取存放和提取的文件路径
    private String filePath = "d:/message.txt";

    // 获取所有的留言
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Message> messages = load();
        resp.setContentType("application/json;charset=utf-8");
        String respString = objectMapper.writeValueAsString(messages);
        resp.getWriter().write(respString);
    }
    // 新增留言
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        Message message = objectMapper.readValue(req.getInputStream(), Message.class);
        save(message);
        resp.getWriter().write("{\"ok\" : 1}");
    }
	// 把数据保存到文件中
    private void save(Message message) {
        System.out.println("向文件中写入数据");
        try (FileWriter fileWriter = new FileWriter(filePath, true)){
            fileWriter.write(message.from + "\t" + message.to + "\t" + message.message + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
	// 实时更新留言信息
    private List<Message> load() {
        List<Message> messages = new ArrayList<>();
        System.out.println("从文件中读取数据");
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))){
            while (true) {
                String line = bufferedReader.readLine();
                if (line == null) {
                    break;
                }
                String[] tokens = line.split("\t");
                Message message = new Message();
                message.from = tokens[0];
                message.to = tokens[1];
                message.message = tokens[2];
                messages.add(message);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("共读取" + messages.size() + "条记录");
        return messages;
    }
}

虽然文件存储是解决重启服务器数据丢失的问题,但是使用数据库能够提供更多查找/新增/修改数据的方法.并且如果数据量大,数据复杂就需要更多代码来组织这些数据了.相比之下,还是数据库香.

6) 数据存放在数据库

1)引入依赖

之前我们使用MySQL是下载jar包,现在只需要引入依赖,版本用这个很好,我突发奇使用8.0.12,结果出现版本不兼容,出现异常

所以还是使用根据自己的MySQL版本选择合适的依赖

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

2)创建数据库和message表

set character_set_database=utf8;
set character_set_server=utf8;

create database if not exists MessageWall;
use MessageWall;

drop table if exists messages;
create table messages(`from` varchar(100), `to` varchar(100), message varchar(100));

3)创建DBUtil类用于完成和数据库的连接过程

  • 创建 MysqlDataSource 实例, 设置 URL, username, password 等属性.
  • 提供 getConnection 方法, 和 MySQL 服务器建立连接.
  • 提供 close 方法, 用来释放必要的资源.
import com.mysql.cj.jdbc.MysqlDataSource;

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

// 此处建立连接需要使用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("123456");
        }
        return dataSource;
    }

    // 获取连接对象
    public static Connection getConnection() throws SQLException {
        return 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();
            }
        }
    }

}

4)修改MessageServlet

// 从数据库中查询数据
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 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 throwables) {
        throwables.printStackTrace();
    } finally {
        // 5. 释放资源
        DBUtil.close(connection, statement, resultSet);
    }
    return messageList;
}
// 把新增的数据存放到数据库中
private void save(Message message) {
    // 在外部创建对象方便关闭资源
    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);
        // 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);
    }
}

5) Cookie,Session和Token

1. 什么是Cookie

因为HTTP协议是无状态的协议

对事物处理没有记忆能力,每次客户端和服务器会话完成后,下一次会话和这次没有直接联系

但是在有些场景下需要去保存请求之间的联系,

比如用户登录场景, 在用户登录第一次登录后,下一次就不必再次登录,这时候就可以使用Cookie来保存这些信息

image-20221018162947721

图中的令牌就是存储在Cookie中.服务器就需要记录令牌信息和其对应的用户信息,这就是下面Session需要完成的

cookie特性:

  • Cookie是存放在浏览器上,为了安全考虑不会让页面直接去访问文件,而是去访问浏览器中的Cookie
  • **Cookie是不可跨域的,**每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,但是可以使用domain(Cookie属性)来访问一级域名和二级域名

Cookie属性:

属性说明
name=value按照键值对的方式来组织数据
domain指定当前cookie所在的域名,默认位当前域名
maxAgecookie 失效的时间(s),如果是正数,代表在maxAge后该cookie失效,如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie .如果为 0,表示删除该 cookie .默认为 -1。

2. 什么是Session(会话)

  • Session是一种记录服务器和浏览器之间会话状态的机制
  • Session是基于Cookie实现的,只不过Session是存储在服务器端,SessionId会被存储到浏览器的Cookie中

典型应用: 保存用户身份信息

利用Cookie和Session管理用户身份信息认证.

image-20221018162225700

说明:

  • 由服务器保存用户详细信息,在用户第一次登录由服务器生成唯一字段key,就是SessionID/Token,是按照键值对的形式存储,其value就可以根据需求存储用户信息,登录时间等等.
  • 在后续用户在给服务器发送请求时,在HTTP请求的Cookie字段中带上Session/token,用户就不必重复登录
  • 在服务器接受到HTTP请求后,根据请求中的Session/token在哈希表中查找用户对应信息.

3. 什么是Token

Token的引入:

Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生.

Token的定义:

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码

使用Token的目的:

Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮

如何使用Token:

  1. 用设备号/设备mac地址作为Token
  2. 用session值作为Token

通常情况下使用的是方式2,因为Session是服务器和客户端报错通信的唯一识别信息,在同一用户的多次请求中,Session始终都是同一个对象,而不是多个对象,因为可以对它加锁,所以在处理并发场景下用户多次请求时,就可以使用Token验证是否是相同的,如果不一致就可以认为是重复的请求(因为对Token加锁,不是锁所有者访问就会得到一个不同的Token,所以被认定为重复提交),将会被拒绝.

总结: 由于SessioId和Token使用方式的相似,我们可以理解为同一东西的不同叫法

4. Cookie和Session的区别

Cookie和Session是实现会话的技术手段

会话: 浏览器第一次向服务器发送请求时,会话建立,直到有一方断开连接为止.在这次会话中可以有多次请求和响应

区别:

  • Cookie是客户端的机制,Session是服务器的机制
  • 存储数据大小不同: Cookie保存的数据不能大于4kb(对同一个域名下的总Cookie数量限制为20),虽然Session没有大小限制,但是占用过多会提高服务器的负担
  • 安全性不同: Session比Cookie安全
  • 生命周期不同: Cookie可设置为长时间保存,比如默认登录功能;Session一般会在客户端关闭或者Session超时都会失效

5. 核心方法

HttpServletRequest 类中的相关方法

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

HttpServletResponse 类中的相关方法

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

HttpSession 类中的相关方法

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

Cookie 类中的相关方法

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

注:

  • HTTP 的 Cookie 字段中存储的实际上是多组键值对. 每个键值对在 Servlet 中都对应了一个 Cookie对象.
  • 通过 HttpServletRequest.getCookies() 获取到请求中的一系列 Cookie 键值对.
  • 通过 HttpServletResponse.addCookie() 向响应中添加新的 Cookie 键值对.

代码示例: 实现用户登录

利用form表单提交用户信息,实现用户登录的监测登录次数

1)创建IndexServlet 类

用于登录成功后显示用户信息在页面上

@WebServlet("/index")
public class IndexServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        // 1.判断用户是否已经登录过
        HttpSession session = req.getSession(false);
        // 如果用户没有登录,则要求用户重新登录
        if (session == null) {
            // 跳转到登录页面
            resp.sendRedirect("login.html");
            return;
        }
        // 2. 登录成功,获取 会话 中的数据
        String username = (String)session.getAttribute("username");
        Integer loginCount = (Integer)session.getAttribute("loginCount");
        // 访问次数加一
        loginCount += 1;
        // 更新访问次数
        session.setAttribute("loginCount",loginCount);

        // 3. 返回数据给响应,并显示到页面
        resp.getWriter().write("当前用户为: " + username + " 访问次数: " + loginCount);
    }
}
  • HttpSession类已经把哈希表封装起来了,使用getSession 操作内部提取到请求中的 Cookie 里的 sessionId, 然后查找哈希表, 获取到对应的 HttpSession 对象. 所以一个SessionId就对应一个HttpSession对象(二者是按照键值对的形式组织的),然后HttpSession对象内部是由许多键值对构成的
    • image-20221018194058184

2)创建 login.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>Document</title>
</head>
<body>
    <form action="login" method="POST">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" value="提交">
    </form>
</body>
</html>
  • input中的name属性是按照post的格式,把用户名和密码通过body键值对的形式传入服务器

3)创建 LoginServlet 类

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        // 1. 获取用户名和密码
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        // 2. 验证用户信息是否合法
        if (username == null || username.equals("") || password == null || password.equals("")) {
            // 返回一个提示.
            resp.getWriter().write("用户名或者密码不完整! 登录失败!");
            return;
        }
        if (!username.equals("admin") || !password.equals("123")){
            resp.getWriter().write("用户名或者密码错误! 登录失败!");
            return;
        }
        // 3. 登录成功,建立一个会话,然后把用户信息写入会话中
        HttpSession session = req.getSession(true);
        session.setAttribute("username", "admin");
        Integer visitCount = (Integer)session.getAttribute("loginCount");
        if (visitCount == null) {
            session.setAttribute("loginCount", 0);
        } else {

        }
        resp.sendRedirect("index");
    }
}
  • 此处的 getSession 参数为 true, 表示查找不到 HttpSession 时会创建新的 HttpSession 对象, 并生成一个 sessionId, 插入到 哈希表 中, 并且把 sessionId 通过 Set-Cookie 返回给浏览器

4)部署程序

5)流程详解

  1. 第一次进入(从未登录)

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

image-20221018195612712

不存在Cookie

第一次响应:

image-20221018195928465

也没有Set-cookie属性

  1. 第一次登录

第二次请求验证用户名和密码是否正确

image-20221018200811064

image-20221018200824887

第二次响应set-cookie属性已经含有

image-20221018200940875

这里的JSESSIONID就是Servlet自动生成的key,value就是8A6EA685A2786F306ECD62ED0D21129F,也是服务器生成的SessionId

  1. 第三次请求

第三次请求携带刚才的Cookie访问服务器

image-20221018201524232

image-20221018201531499

在浏览器中也可以看见Cookie和Session

image-20221018201632711

Cookie中存放一些信息

image-20221018201738405

第三次响应中包含用户登录次数的信息

image-20221018202052907

  1. 后续刷新页面,用户访问次数增加

后续的请求都会带着这个Cookie去访问服务器

image-20221018202258441

image-20221018202317466

代码示例: 上传文件

核心方法:

HttpServletRequest 类方法

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

Part 类方法

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

1)创建 upload.html, 放到 webapp 目录中

<!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>
    <form action="upload" enctype="multipart/form-data" method="post">
        <input type="file" name="MyFile">
        <input type="submit" value="提交文件">
    </form>
</body>
</html>
  • 一般是利用form表单的POST请求实现上传文件
  • form表单中的enctype="multipart/form-data"指定上传的是文件
  • input标签中的name属性值对应的就是Part getPart(String name) 中的name

2)创建 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.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 对象

3)部署程序,

image-20221018204154126

image-20221018204231063

image-20221018204250836

点击上传文件时的post请求:

image-20221018204506701

数据格式:

image-20221018204552272

开头的和form表单中的对应,后面的决定上传文件的起始和结束边界,可以通过查看body部分观察到

文件开始标志

image-20221018204735826

携带了文件的相关属性

文件结束标志

image-20221018204826926

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zzt.opkk

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

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

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

打赏作者

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

抵扣说明:

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

余额充值