【Servlet】Servlet 详解(使用+原理)

1. Servlet 介绍

1.1 什么是 Servlet

  • Servlet(Server Applet 的缩写,全称 Java Servlet):用 Java 编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。狭义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指任何实现了这个 Servlet 接口的类,一般情况下,人们将 Servlet 理解为后者。

  • Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器

  • Servlet 是一种实现动态页面的技术,是一组由 Tomcat 提供给程序员的 API,帮助程序员简单高效的开发一个 web app

1.2 Servlet 的主要工作

  • 允许程序员注册一个类,在 Tomcat 收到的某个特定的 HTTP 请求的时候,执行这个类中的一些代码
  • 帮助程序员解析 HTTP 请求,把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
  • 帮助程序员构造 HTTP 响应,程序员只要给指定的 HttpResponse 对象填写一些属性字段,Servlet 就会自动的按照 HTTP 协议的方式构造出一个 HTTP 响应字符串,并通过 Socket 编写返回给客户端

2. Servlet 程序创建步骤

2.1 创建项目

以下使用 IDEA 带大家编写一个简单的 Servlet 程序,主要是让大家了解一个大致的流程

  • 首先使用 IDEA 创建一个 Maven 项目在这里插入图片描述

  • 创建好的项目如下

    在这里插入图片描述

  • 通过上图我们可以看到创建好的项目中有一些目录结构,这是 Maven 项目的标准结构,其中

    • src 用于存放源代码和测试代码的根目录
    • main 用于存放源代码的目录
    • test 用于存放测试代码的目录
    • java 用于存放 Java 代码的目录
    • resources 用于存放依赖的资源文件
    • pom.xml 是 Maven 项目的核心配置文件,关于这个 Maven 项目的相关属性,都是在这个 xml 中进行配置

2.2 引入依赖

Maven 项目创建完成后,会自动生成一个 pom.xml 文件,我们需要在这个文件中引入 Servlet API 依赖的 jar 包

  • 打开中央仓库,搜索 Servlet,点击 Java Servlet API

    在这里插入图片描述

  • 选择对应 Tomcat 版本的 Servlet(由于我当前使用的是 Tomcat 8 系列,所以选择 Servlet 3.1.0 即可)

    在这里插入图片描述

  • 将中央仓库提供的该版本的 xml 复制到项目的 pom.xml

    在这里插入图片描述

  • 修改后的 pom.xml 文件如下

    在这里插入图片描述

    一个项目中可以有多个依赖,每个依赖都是一个 <dependency> 标签。引入的依赖都要放在一个 <dependencies> 的标签中,该标签用于放置项目依赖的 jar 包,Maven 会自动下载该依赖到本地

  • 在拷贝的依赖中有几个参数,分别具有如下含义:

    • groupId 表示组织或者公司的 ID
    • artifactId 表示项目或者产品的 ID
    • version 表示版本号
    • scope 用于指定依赖的作用范围,包含所在项目的测试、编译、运行、打包等声明周期。
  • 如果你想找到刚刚 Maven 下载到本地的第三方库,路径如下

    在这里插入图片描述

2.3 创建目录

Web 项目对于目录结构还有自己的要求,只有 Maven 的标准目录是不够的,需要再创建以下目录并进行配置

  • 在 main 目录下,创建一个 webapp 目录

    webapp 目录就是用于部署到 Tomcat 中的一个重要目录,里面可以存放一些静态资源

  • 在 webapp 目录下,创建一个 WEB-INF 目录

  • 在 WEB-INF 目录下,创建一个 web.xml 文件

    Tomcat 通过找到这个 web.xml 文件才能够正确处理 webapp 中的动态资源

    在这里插入图片描述

  • 编写 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>
    

2.4 编写代码

以下编写一个让响应返回一个自定义字符换的简单代码

  • 创建一个 TestServlet 类,并且让它继承于 HttpServlet

    在这里插入图片描述

    HttpServlet 这个类来自于 pom.xml 中引入的 Servlet API 依赖的 jar 包

  • 在 TestServlet 类中重写 doGet 方法

    在这里插入图片描述

    doGet 是 HttpServlet 类中的方法,此处是在子类中重写了父类的 doGet

  • 为了了解 doGet 方法的作用,我们可以看看它的源码

    在这里插入图片描述

    • HttpServletRequest 表示 HTTP 请求,Tomcat 按照 HTTP 请求的的格式把字符串格式的请求转换成了一个 HttpServletRequest 对象,通过这个对象就可以获取请求中的信息
    • HttpServletResponse 表示 HTTP 响应,通过代码可以把响应的对象构造好,然后 Tomcat 将响应返回给浏览器
    • 通过 doGet 的源码我们可以大致了解,它的作用是根据收到的请求通过响应返回一个 405 或者 400,那么我们可以重写这个方法,根据收到的请求执行自己的业务逻辑,把结果构造成响应对象
  • 在 doGet 方法中,通过 HttpServletResponse 类的 getWriter() 方法往响应的 body 中写入文本格式数据

    在这里插入图片描述

    resp.getWriter() 会获取到一个流对象,通过这个流对象就可以写入一些数据,写入的数会被构造成一个 HTTP 响应的 body 部分,Tomcat 会把整个响应转成字符串,通过 Socket 写回给浏览器

  • 需要给 TestServlet 加上一个特定的注解 @WebServlet("/test")

    在这里插入图片描述

    上述助解表示 Tomcat 收到的请求中,URL 的 Servlet Path 路径为 /test 的请求才会调用 TestServlet 这个类的代码,注解中的字符串表示着 URL 的 Servlet Path

  • 到这里程序的编写已经完成了!但是你可能会疑惑上述代码不是通过 main 方法作为入口的,这是因为 main 方法已经被包含在 Tomcat 中了,我们写的程序并不能单独执行,而是需要搭配 Tomcat 才能执行起来(在 Tomcat 的伪代码中我们具体分析了这个问题)

2.5 打包程序

在程序编写好之后,就可以使用 Maven 进行打包

  • 首先修改 pom.xml,加入一些必要的配置(打包的类型和打包后的包名

    在这里插入图片描述

    • packaging 标签中用于设置打包的类型(如果不修改打包类型则默认为 jar 包,jar 包是普通 Java 程序打包的结果,里面包含了一些 .class 文件;而部署在 Tomcat 中的压缩包一般为 war 包,war 包里面是 Java Web 程序,里面除了 .class 文件之外,还包含 HTML、CSS、JavaScript、图片等等)
    • finalName 标签中用于设置打包后的名字(包名很重要,它对应着请求中 URL 的 Context Path
  • 执行打包操作(打开 Maven 窗口,展开 Lifecycle,双击 package 进行打包)

    在这里插入图片描述

  • 打包成功后,可以发现多了个 target 目录,该目录下有一个 testServlet.war 的压缩包

    在这里插入图片描述

2.6 部署程序

接下来我们就可以进行程序的部署

  • 首先将打好的 war 包拷贝到 Tomcat 的 webapps 目录下

    在这里插入图片描述

  • 启动 Tomcat(在 Tomcat 的 bin 目录中点击 startup.bat

    在这里插入图片描述

2.7 验证程序

此时通过浏览器访问 http://127.0.0.1:8080/testServlet/test 就可以看到程序实现的结果了

在这里插入图片描述

注意:URL 中的路径分成了两个部分 Context Path 和 Servlet Path

  • Context Path 这个路径表示一个 webapp,来源于打包的包名
  • Servlet Path 这个路径表示一个 webapp 中的一个页面,来源于对应的 Servlet 类 @WebServlet 注解中的内容

3. 使用 Smart Tomcat 进行部署

为了简化上述操作流程,其实是有一些更简单的方式

  • 对于创建项目、引入依赖、创建目录这三个步骤,其实可以使用项目模板来快速生成,但是由于项目模板加载速度很慢,因此这里并不推荐
  • 对于打包程序和部署程序这两个步骤,其实可以使用 Smart Tomcat 插件来快速实现,以下将介绍它的使用方式

3.1 安装 Smart Tomcat

  • 点击 File → Settings

    在这里插入图片描述

  • 点击 Plugins,在搜索栏搜索 Smart Tomcat,然后进行安装即可

    在这里插入图片描述

3.2 配置 Smart Tomcat

  • 点击 Add Configuration

    在这里插入图片描述

  • 点击左上角的+号,并选择 Smart Tomcat

    在这里插入图片描述

  • 主要修改这三个参数

    在这里插入图片描述

    • Name:这一栏其实可以随便填
    • Tomcat Server:表示 Tomcat 所在的目录
    • Deployment Directory:表示项目发布目录
    • Context Path:表示项目路径,默认值是项目名称
    • Servlet Port:表示服务端口
    • Admin Port:表示管理端口
    • VM options:表示 JVM 参数
  • 配置好 Smart Tomcat 之后,Add Configuration 就会显示成 Name 的名字,并且右边多了个三角形运行的符号

    在这里插入图片描述

3.3 使用 Smart Tomcat

  • 点击三角形运行 Smart Tomcat,出现如下信息表示程序启动成功

    在这里插入图片描述

  • 点击蓝色的连接,跳转到项目路径,再增加 Servlet Path 就可以显示出该程序的结果

    在这里插入图片描述

4. 访问出错解决方案

4.1 出现 404

出现 404 原因: 用户访问的资源不存在,大概率是 URL 的路径写的不正确

错误实例1: 少写了 Context Path 或者 Context Path 写错了在这里插入图片描述

错误实例2: 少写了 Servlet Path 或者 Servlet Path 写错了

在这里插入图片描述
错误实例3: web.xml 写错了(如清空 web.xml 中的内容)

在这里插入图片描述

4.2 出现 405

出现 405 原因: 访问的服务器不能支持请求中的方法或者不能使用该请求中的方法

错误实例1: 没有重写 doGet 方法

在这里插入图片描述

错误实例2: 重写了 doGet 方法,但是没有删除父类的 doGet 方法

在这里插入图片描述

4.3 出现 500

出现 500 原因: 服务器出现内部错误,往往是 Servlet 代码中抛出异常导致的

错误实例: 代码中出现空指针异常

在这里插入图片描述

4.4 出现“空白页面”

出现空白页原因: 响应的 body 中并没有内容

错误实例:resp.getWriter().write() 操作删除

在这里插入图片描述

4.5 出现“无法访问此网站”

出现“无法访问此网站”原因: 一般是不能正确访问到 Tomcat(可能是 Tomcat 没启动,也可能是 IP/端口号写错了)

错误实例: 注解 @WebServlet 中少写了 /在这里插入图片描述

4.6 出现中文乱码问题

响应出现中文乱码问题原因: 使用的编译器的编码方式(一般是 utf-8)和浏览器的编码方式不同,浏览器默认跟随系统编码方式,win10 系统默认是 GBK 编码

解决方式: 通过响应对象的 setContentType() 方法来修改浏览器对于响应正文的编码格式

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("吞吞吐吐大魔王");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKLlqalS-1650529905418)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220413163421106.png)]

5. Servlet 运行原理

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

5.1 Servlet 的架构

我们自己实现的 Servlet 是在 Tomcat 基础上运行的,下图显示了 Servlet 在 Web 应用程序中的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GGlWiBzk-1650529905419)(C:/Users/bbbbbge/Pictures/img/202203180021103.jpeg)]

当浏览器给服务器发送请求时,Tomcat 作为 HTTP 服务器,就可以接收到这个请求。Tomcat 的工作就是解析 HTTP 请求,并把请求交给 Servlet 的代码来进行进一步的处理。Servlet 的代码根据请求计算生成响应对象,Tomcat 再把这个响应对象构造成 HTTP 响应,返回给浏览器。并且 Servlet 的代码也经常会和数据库进行数据的传递。

5.2 Tomcat 的伪代码

下面通过 Tomcat 的伪代码的形式来描述 Tomcat 初始化和处理请求两部分核心逻辑

  • Tomcat 的初始化流程

    class Tomcat {
          
          
    
        // 用来存储所有的 Servlet 对象
        private List<Servlet> instanceList = new ArrayList<>();
        
        public void start() {
          
          
            // 根据约定,读取 WEB-INF/web.xml 配置文件
            // 并解析被 @WebServlet 注解修饰的类
            
            // 假定这个数组里就包含了我们解析到的所有被 @WebServlet 注解修饰的类.
            Class<Servlet>[] allServletClasses = ...;
            
            // 这里要做的的是实例化出所有的 Servlet 对象出来;
            for (Class<Servlet> cls : allServletClasses) {
          
          
                // 这里是利用 java 中的反射特性做的
                // 实际上还得涉及一个类的加载问题,因为我们的类字节码文件,是按照约定的
                // 方式全部在 WEB-INF/classes 文件夹下存放的,所以 tomcat 内部是
                // 实现了一个自定义的类加载器(ClassLoader),用来负责这部分工作。
                
                Servlet ins = cls.newInstance();
                instanceList.add(ins);
            }
            
            // 调用每个 Servlet 对象的 init() 方法,这个方法在对象的生命中只会被调用这一次
            for (Servlet ins : instanceList) {
          
          
                ins.init();
            }
            
            // 启动一个 HTTP 服务器,并用线程池的方式分别处理每一个 Request
            ServerSocket serverSocket = new ServerSocket(8080);
            // 实际上 tomcat 不是用的固定线程池,这里只是为了说明情况
            ExecuteService pool = Executors.newFixedThreadPool(100);
            
            while (true) {
          
          
                Socket socket = ServerSocket.accept();
                // 每个请求都是用一个线程独立支持,这里体现了 Servlet 是运行在多线程环境下的
                pool.execute(new Runnable() {
          
          
                    doHttpRequest(socket);
                });
            }
            // 调用每个 Servlet 对象的 destroy() 方法,这个方法在对象的生命中只会被调用这一次
            for (Servlet ins : instanceList) {
          
          
                ins.destroy();
            }
        }
        
        public static void main(String[] args) {
          
          
        	new Tomcat().start();
        }
    }
    
    • Tomcat 的代码内置了 main 方法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main 方法开始执行的
    • 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理
    • Tomcat 通过反射这样的语法机制来创建被 @WebServlet 注解修饰的类的实例
    • 这些实例被创建完之后,就会调用其中的 init 方法进行初始化
    • 这些实例被销毁之前,就会调用其中的 destory 方法进行收尾工作
    • Tomcat 内部也是通过 Socket API 进行网络通信
    • Tomcat 为了能够同时处理多个 HTTP 请求,采取了多线程的方式实现,因此 Servlet 是运行在多线程环境下的
  • Tomcat 处理请求流程

    class Tomcat {
          
          
        
        void doHttpRequest(Socket socket) {
          
          
            // 参照我们之前学习的 HTTP 服务器类似的原理,进行 HTTP 协议的请求解析和响应构建
            HttpServletRequest req = HttpServletRequest.parse(socket);
            HttpServletRequest resp = HttpServletRequest.build(socket);
            
            // 判断 URL 对应的文件是否可以直接在我们的根路径上找到对应的文件,如果找到,就是静态内容
                
            // 直接使用 IO 进行内容输出
            if (file.exists()) {
          
          
                // 返回静态内容
                return;
            }
            
            // 走到这里的逻辑都是动态内容了
            // 找到要处理本次请求的 Servlet 对象
            Servlet ins = findInstance(req.getURL());
            
            // 调用 Servlet 对象的 service 方法
            // 这里就会最终调用到我们自己写的 HttpServlet 的子类里的方法了
            try {
          
          
            	ins.service(req, resp);
            } catch (Exception e) {
          
          
            	// 返回 500 页面,表示服务器内部错误
            }
        }
    }
    
    • Tomcat 从 Socket 中读到的 HTTP 请求是一个字符串,然后 Tomcat 会按照 HTTP 协议的格式解析成一个 HttpServletRequest 对象
    • Tomcat 会根据 URL 中的 Path 判定这个请求是请求一个静态资源还是动态资源。如果是静态资源,直接找到对应的文件,把文件的内容通过 Socket 返回;如果是动态资源,才会执行到 Servlet 的相关逻辑
    • Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调用哪个 Servlet 实例的 service 方法
    • 通过 service 方法,就会进一步调用我们重写的 doGet 或者 doPost 方法等等
  • Servlet 的 service 方法的实现

    class Servlet {
          
          
        public void service(HttpServletRequest req, HttpServletResponse resp) {
          
          
            String method = req.getMethod();
            if (method.equals("GET")) {
          
          
            	doGet(req, resp);
            } else if (method.equals("POST")) {
          
          
            	doPost(req, resp);
            } else if (method.equals("PUT")) {
          
          
            	doPut(req, resp);
            } else if (method.equals("DELETE")) {
          
          
            	doDelete(req, resp);
            }
            ......
        }
    }
    
    • Servlet 的 service 方法内部会根据当前请求的方式,决定调用其中的某个 doXXX 方法
    • 在调用 doXXX 方法的时候,会触发多态机制,从而执行到我们自己写的子类的 doXXX 方法

6. Servlet API 详解

对于 Servlet 主要介绍三个类,分别是 HttpServlet、HttpServletRequest 和 HttpServletResponse。

其中 HttpServletRequest 和 HttpServletResponse 是 Servlet 规范中规定的两个接口,HttpServlet 中并没有实现这两个接口的成员变量,它们只是 HttpServlet 的 service 和 doXXX 等方法的参数。这两个接口类的实例化是在 Servlet 容器中实现的。

6.1 HttpServlet

核心方法

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

Servlet 的生命周期: Servlet 的生命周期就是 Servlet 对象从创建到销毁的过程,下面来介绍其生命周期的过程

  • Servlet 对象是由 Tomcat 来进行实例化的,并且在实例化完毕之后调用 init 方法(只调用一次
  • Tomcat 对于收到的请求,都会通过对应的 Servlet 的 service 方法来进行处理(可以调用多次
  • Tomcat 在结束之前,会调用 Servlet 的 destory 方法来进行回收资源(最多调用一次

注意: init 和 service 能够保证在各自的合适时机被 Tomcat 调用,但是 destory 不一定,它是否能够被调用取决于 Tomcat 是如何结束的

  • 如果直接杀死进程,那么就来不及调用 destory
  • 如果通过 Tomcat 的管理端口(默认 8005)进行关闭,就能够调用 destory

处理 GET 请求示例:

直接通过浏览器 URL 发送一个 GET 方法的请求,来对这个请求进行处理

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLEwJSSm-1650529905419)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220414010557437.png)]

处理 POST 请求示例:

由于通过浏览器 URL 发送的请求是 GET 方法的请求,因此我们需要通过其它方式来发送一个 POST 请求然后用于处理。发送 POST 请求的方式有通过 Ajax、form 表单或者 socket api 进行构造,如果单纯的用于测试就比较麻烦,这里推荐使用软件 postman,这是一个很强大的 API 调试、Http 请求的工具。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FjHoxMyG-1650529905419)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220413162336658.png)]

6.2 HttpServletRequest

核心方法

方法 描述
String getProtocol() 返回协议的名称和版本号
String getMethod() 返回请求的 HTTP 方法的名称
String getRequestURL() 返回请求的 URL,不带查询字符串
String getRequestURI() 返回该请求的 URL 的一部分,不带协议名、端口号、查询字符串
String getContextPath() 返回指示请求 URL 中 Context Path 部分
String getServletPath() 返回指示请求 URL 中 ServletPath 部分
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() 用于读取请求的正文内容,返回一个 InputStream 对象

示例1: 通过上述方法返回一个页面是该请求的具体 HTTP 请求格式

@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
    
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        // 此处返回一个 HTML,在 HTML 中显示 HttpRequestServlet 类中的一些核心方法
        // 把这些 API 的返回结果通过 StringBuilder 进行拼接
        resp.setContentType("text/html;charset=utf-8");
        StringBuilder html = new StringBuilder();
        html.append(req.getMethod());
        html.append(" ");
        html.append(req.getRequestURL());
        html.append("?");
        html.append(req.getQueryString());
        html.append(" ");
        html.append(req.getProtocol());
        html.append("</br>");
        Enumeration<String> headerNames = req.getHeaderNames();
        while(headerNames.hasMoreElements()){
    
    
            String headName = headerNames.nextElement();
            String header = req.getHeader(headName);
            html.append(headName);
            html.append(": ");
            html.append(header);
            html.append("</br>");
        }
        html.append("</br>");
        //InputStream body = req.getInputStream();
        resp.getWriter().write(html.toString());
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfoyYGZK-1650529905419)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220414160226859.png)]

示例2: 处理 HTTP 请求的 body 中的数据格式

  • 如果 body 的内容格式是 x-www-form-urlencoded,使用 getParameter 进行处理

    • 此处是要获取 body 的数据,由于 GET 方法一般没有 body,这里使用 POST 方法演示
    • 约定 body 的数据格式为:x-www-form-urlencoded
    • 约定 body 的数据内容为:username=123&passwd=456
    @WebServlet("/postParameter")
    public class PostParameterServlet extends HttpServlet {
          
          
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            resp.setContentType("text/html;charset=utf-8");
            String username = req.getParameter("username");
            String passwd = req.getParameter("passwd");
            resp.getWriter().write("username=" + username + "</br>" +"passwd=" + passwd);
    
        }
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WZYFVi4-1650529905420)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220414165054978.png)]

  • 如果 body 的内容格式是 json,首先将整个 body 都读取出来,再借助第三方库的方法按照 json 的格式来进行解析,Java 标准库没有内置对于 json 解析的方法)

    • 此处是要获取 body 的数据,由于 GET 方法一般没有 body,这里使用 POST 方法演示
    • 约定 body 的数据格式为:json
    • 约定 body 的数据内容为:
      {
      username=123,
      passwd=456
      }
    • 此处使用 jackson 第三方库,使用之前需要去 Maven 的中央仓库将 jackson 的依赖引入 pom.xml 中[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTtJUM5R-1650529905420)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220414180437830.png)]
    • jackson 中的核心类是 ObjectMapper,通过这个类的 readValue(String content, Class<T> valueType) 方法,就可以将 json 字符串转化为一个类的对象(第一个参数是 json 字符串,第二个参数是类对象),ObjectMapper 会遍历定义的类中的每个成员的名称,去 json 字符串的 key 中查找,如果找到了就将对应的值返回给该成员
    // 自定义的将 json 字符串转化的类
    class UserInfo {
          
          
        public String username;
        public String passwd;
    }
    
    @WebServlet("/jsonParameter")
    public class JsonParameterServlet extends HttpServlet {
          
          
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            resp.setContentType("text/html;charset=utf-8");
            // 1. 先将整个 body 读取出来
            String body = readBody(req);
    
            // 2. 按照 json 格式进行解析
            ObjectMapper objectMapper = new ObjectMapper();
            UserInfo userInfo = objectMapper.readValue(body, UserInfo.class);
            resp.getWriter().write("username=" + userInfo.username + "</br>" + "passwd=" + userInfo.passwd);
        }
    
        // 定义一个方法来读取请求中的全部 body
        private String readBody(HttpServletRequest req) throws IOException {
          
          
            // 1. 先拿到 body 的长度,单位是字节
            int contentLength = req.getContentLength();
            // 2. 准备一个字节数组,来存放 body 内容
            byte[] buffer = new byte[contentLength];
            // 3. 获取到 InputStream 对象
            InputStream inputStream = req.getInputStream();
            // 4. 从 InputStream 对象中读取到数据,将数据放到字节数组中
            inputStream.read(buffer);
            // 5. 将存放 body 内容的字节数组转换成字符串
            return new String(buffer, "utf-8");
        }
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PxAWqBo-1650529905420)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220414181955002.png)]

6.3 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) 设置被发送到客户端的响应的字符编码,例如 utf-8
void sendRedirect(String location) 设置 Location 字段,实现重定向
PrintWriter getWriter() 用于往 body 中写入文本格式数据
OutputStream getOutputStream() 用于往 body 中写入二进制格式数据

示例1: 通过代码,构造出不同的响应状态码

@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        int status = 404;
        resp.setStatus(status);
        resp.getWriter().write("status=" + status);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-onfxNEdj-1650529905420)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220415004153808.png)]

示例2: 在响应报头设置一个 Refresh 字段,实现字段刷新程序

Refresh 的值表示每秒刷新的时间,当程序是毫秒级刷新的时候,可能存在误差

@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
    
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        // 给响应设置一个 Refresh 的 header,每隔 1s 钟刷新一次
        resp.setHeader("Refresh", "1");

        // 返回一个当前的时间,用来显示刷新的效果
        resp.getWriter().write("timestamp=" + System.currentTimeMillis());
    }
}

在这里插入图片描述

示例3: 实现重定向操作

  • 方法一:在响应报头设置状态码和 Location 来实现重定向

    @WebServlet("/redirect")
    public class RedirectServlet extends HttpServlet {
          
          
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            // 将状态码设置为 3XX
            resp.setStatus(302);
            // 设置一个 Location,重定向到 CSDN 博客主页
            resp.setHeader("Location", "https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343");
        }
    }
    
  • 方法二:直接使用 sendRedirect() 方法来实现重定向

    @WebServlet("/redirect")
    public class RedirectServlet extends HttpServlet {
          
          
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
    
            resp.sendRedirect("https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343");
        }
    }
    

    在这里插入图片描述

7. 实现服务器版表白墙程序

7.1 基本介绍

在之前的文章《【Web 三件套】 JavaScript WebAPI》中实现过了一个纯前端的表白墙代码,实现后的效果如下。这次将会结合上述的知识,实现一个服务器版的表白墙程序在这里插入图片描述

7.2 准备操作

  1. 创建好一个 Servlet 项目

  2. 将之前写好的纯前端的表白墙代码拷贝到 webapp 目录下

  3. 约定好前后端交互的接口,该程序只需约定两个接口

    • 从服务器获取全部留言

      • 约定请求:方法为 GET,请求路径为 /message

      • 约定响应:版本号为 HTTP/1.1,状态码为 200 OK,采用 JSON 数据格式

      • JSON 具体格式为:

        [{
                   
                   
        ​		from: "",
        ​		to: "",
        ​		message: ""}
        ]
        
    • 通过客户端给服务器新增一个留言

      • 约定请求:方法为 POST,请求路径为 /message
      • 约定响应:版本号为 HTTP/1.1,状态码为 200 OK,提交成功后响应页面显示“提交成功”
  4. 创建一个 MessageServlet 类,@WebServlet 注解为 /message,对应着约定的请求路径,通过上方的约定完成服务器段的代码

  5. 更改前端的代码

7.3 代码实现

后端代码实现:

import com.fasterxml.jackson.databind.ObjectMapper;

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.net.HttpRetryException;
import java.util.ArrayList;
import java.util.List;

// 这个类表示一条消息的详细情况
class Message{
    
    
    public String from;
    public String to;
    public String message;
}

@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    
    
    // 通过这个数组来表示所有的消息
    private List<Message> messages= new ArrayList<>();

    // 通过这个代码来完成获取服务器所有消息的操作
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        resp.setContentType("application/json;charset=utf-8");
        // 获取到消息列表
        // 此处要做的就是把当前的 messages 数组转成 json 格式返回给浏览器
        ObjectMapper objectMapper = new ObjectMapper();
        // 通过 ObjectMapper 的 writeValuesAsString() 方法就可以将一个对象转换成 json 字符串
        // 由于这里的 message 是一个 List,那么得到的结果是一个 json 数组
        String jsonString = objectMapper.writeValueAsString(messages);
        resp.getWriter().write(jsonString);
    }

    // 通过这个代码来完成新增消息的操作
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        resp.setContentType("text/html;charset=utf-8");
        ObjectMapper objectMapper = new ObjectMapper();
        Message message = objectMapper.readValue(req.getInputStream(), Message.class);
        messages.add(message);
        resp.getWriter().write("提交成功!");
    }
}

前端代码实现:

<!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;
            box-sizing: border-box;
        }
        .container {
      
      
            width: 600px;
            margin: 0 auto;
        }
        h1 {
      
      
            text-align: center;
            padding: 20px 0;
            color: pink;
        }
        p {
      
      
            text-align: center;
            font-size: 15px;
            color: grey;
            padding: 5px 0;
        }
        .row {
      
      
            display: flex;
            height: 40px;
            justify-content: center;
            align-items: center;
        }
        .row span {
      
      
            width: 80px;
        }
        .row .edit {
      
      
            width: 250px;
            height: 35px;
        }
        .row .submit {
      
      
            width: 330px;
            height: 40px;
            background-color: orange;
            color: #fff;
            border: none;
        }
        .row .submit:active {
      
      
            background-color: grey;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>表白墙</h1>
        <p>输入后点击提交,将会把消息显示在在墙上</p>
        <div class="row">
            <span>谁:</span>
            <input type="text" class="edit">
        </div>
        <div class="row">
            <span>对谁:</span> 
            <input type="text" class="edit">
        </div>
        <div class="row">
            <span>说什么:</span>
            <input type="text" class="edit">
        </div>
        <div class="row">
            <input type="button" value="提交"  class="submit">
        </div>
    </div>
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

    <script>
        let submitButton = document.querySelector('.submit');
        submitButton.onclick = function() {
      
      
            // 1. 获取到输入框里的内容
            let edits = document.querySelectorAll('.edit');
            let from = edits[0].value;
            let to = edits[1].value;
            let message = edits[2].value;
            // 2. 根据输入框的内容,构造 HTML 元素,添加到页面中
            if(from == '' || to == '' || message == '') {
      
      
                return;
            }
            let div = document.createElement('div');
            div.innerHTML = from + '对' + to + '说:' + message;
            div.className = 'row';
            let container = document.querySelector('.container');
            container.appendChild(div);
            // 3. 把上次输入的内容清空
            for(let i = 0; i < edits.length; i++){
      
      
                edits[i].value = '';
            }
            // 4. 把当前新增的消息发送给服务器
            let body = {
      
      
                from: from,
                to: to,
                message: message
            };
            $.ajax ({
      
      
                url: "message",
                method: "post",
                contentType: "application/json;charset=utf8",
                // 通过 JSON.stringify 将对象转成字符串
                data: JSON.stringify(body),
                success: function(data, status){
      
      
                    console.log(data);
                }
            })
        }


        // 服务器版本
        // 1. 在页面加载的时候,从服务器获取到消息列表,并显示在网页上
        function load() {
      
      
            $.ajax({
      
      
                method: "get",
                url: "message",
                success: function(data, status) {
      
      
                    // 此处得到的响应 data 其实已经被 jquery 转成了一个对象数组
                    // 但是这里的自动转换有个前提,服务器响应的 header 中 ContentType 是 json
                    let container = document.querySelector('.container');
                    for(let message of data){
      
      
                        // 遍历每个元素,针对每个元素拆功能键一个 div 标签
                        let div = document.createElement('div');
                        div.className = 'row';
                        div.innerHTML = message.from + "对" + message.to + " 说:" + message.message;
                        container.append(div);
                    }
                }
            })
        }
        load();

    </script>
</body>
</html>

7.4 持久化存储

通过上述修改,原本的纯前端代码就加上了服务器,只要服务器开启后,即使刷新网页,已经添加的数据也不会消失。但是如果重启服务器的话,原本的数据就会丢失,为了解决这个问题,就需要让数据能够持久化存储。

持久化存储: 是把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘),是一种将程序数据在持久状态和瞬时状态间转换的机制。

持久化存储机制包括: JDBC文件 IO

接下来将通过增加一个数据库来让上述表白墙程序可以持久化存储

  1. 先建库建表(可以先创建一个文件,将要建的数据库和表都写好)

    drop database if exits messagewall;
    create database messagewall;
    
    use messagewall;
    
    drop table if exits message;
    create table message (
        `from` varchar(50),
        `to` varchar(50),
        `message` varchar(1024)
    );
    
  2. 在 pom.xml 文件中引入 mysql 的 jar 包[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23JXKeIU-1650529905421)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220416172438996.png)]

  3. 连接数据库,创建一个 DBUtil 类,用于封装数据库的建立连接和资源释放操作

    import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    // 通过这个类来封装数据库的建立连接操作
    public class DBUtil {
          
          
        private static final String URL = "jdbc:mysql://127.0.0.1:3306/messagewall?characterEncoding=utf8&setSSL=false";
        private static final String USERNAME = "root";
        private static final String PASSWORD = "1234";
    
        private static DataSource dataSource = new MysqlDataSource();
    
        static {
          
          
            ((MysqlDataSource)dataSource).setURL(URL);
            ((MysqlDataSource)dataSource).setUser(USERNAME);
            ((MysqlDataSource)dataSource).setPassword(PASSWORD);
        }
    
        public static Connection getConnection() throws SQLException {
          
          
            return dataSource.getConnection();
        }
    
        public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){
          
          
            if(resultSet != null){
          
          
                try {
          
          
                    resultSet.close();
                } catch (SQLException throwables) {
          
          
                    throwables.printStackTrace();
                }
            }
            if(statement != null){
          
          
                try {
          
          
                    statement.close();
                } catch (SQLException throwables) {
          
          
                    throwables.printStackTrace();
                }
            }
            if(connection != null){
          
          
                try {
          
          
                    connection.close();
                } catch (SQLException throwables) {
          
          
                    throwables.printStackTrace();
                }
            }
        }
    }
    
  4. 修改 MessageWall 类的代码,主要修改的地方有两处,将原本的 messages 数组删除

    • 在获取消息时,可以增加一个 getMessages() 方法,用于拿到数据库中的所有消息

      // 从数据库获取到所有消息
      private List<Message> getMessages() {
              
              
          Connection connection = null;
          PreparedStatement statement = null;
          ResultSet resultSet = null;
          List<Message> messages = 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");
                  messages.add(message);
              }
          } catch (SQLException throwables) {
              
              
              throwables.printStackTrace();
          } finally {
              
              
              DBUtil.close(connection, statement, resultSet);
          }
          return messages;
      }
      
    • 在新增消息是,可以新增一个 addMessage() 方法,用于往数据库存储一条新消息

      // 往数据库新增一条消息
      private void addMessage(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
              statement.executeUpdate();
          } catch (SQLException throwables) {
              
              
              throwables.printStackTrace();
          } finally {
              
              
              DBUtil.close(connection, statement, null);
          }
      }
      

    到这里为止,一个完整的服务器表白程序就写好啦!在我自己撸上面的代码时,由于连接 MySQL 的 URL 中的端口号写错了,导致自己找了很久的 bug,所以大家如果尝试上述代码时遇到问题,一定要看清是不是自己哪个地方打错了!

8. Cookie 和 Session

8.1 Cookie 介绍

在之前的文章《HTTP 协议详解》中,就介绍过了 Cookie,可以结合本文的内容来搭配理解。

  • Cookie 是什么?

    Cookie 是浏览器提供的在客户端存储数据的一种机制(由于浏览器禁止了网页中的代码直接访问本地磁盘的文件,因此想要在网页中实现持久化存储,就可以通过 Cookie 这样的机制)

  • Cookie 里面存什么?

    Cookie 存储的数据都是程序员自定义的,存储的数据是一个字符串,是键值对结构的,键值对之间使用 ; 分割,键和值之间使用 = 分割

  • Cookie 从哪里来?

    服务器返回响应的时候,可以把要在客户端保存的数据以 Set-Cookie 这个 header 的方式返回给浏览器

  • Cookie 到哪里去?

    客户端下次访问服务器的时候,就会把之前保存好的 Cookie 再发送给服务器

  • Cookie 的典型应用场景:

    可以使用 Cookie 来保存用户的登录信息。比如我们登录过某个网站后,下次登录时就不需要重新输入用户和密码了

  • Cookie 的缺陷:

    每次请求都要把该域名下所有的 Cookie 通过 HTTP 请求传给服务器,因此 Cookie 的存储容量是有限的。

在了解 Cookie 以后,我们发现 Cookie 是不能够用于存储和用户相关的直接信息的,一是 Cookie 的存储容量有限,二是发送请求时占用带宽很多,三是不太安全。即这些数据不适合保存在客户端,保存在服务器是更合适的,通过会话(Session)的方式就能够保存这些数据。

8.2 Session 会话机制介绍

基本介绍:

在计算机中,尤其是在网络应用中,Session 称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中。注意会话状态仅在支持 Cookie 的浏览器中保留。

会话的本质:

  • 会话的本质就是一个哈希表,其中存储了一些键值对结构,key 叫做 sessionId,是一个不随机的、不重复的、唯一的字符串,value 就是要保存的身份信息,通过 HttpSession 对象来保存。key 和 value 都是 Servlet 自动创建的。
  • 每个用户登录都会生成一个会话,服务器会以哈希表的方式将这些会话管理起来
  • 一个会话的详细数据通过一个 HttpSession 对象来存储,并且 HttpSession 对象中存储的数据也是键值对结构,key 和 value 都是程序员自定义的

接着 Cooike 不适合用于存储用户相关的直接信息来讲,由于客户端不适合存储这些数据,服务器这边可以通过 Session 会话的方式来进行保存。下面将会以用户登录的流程来介绍 Session 会话机制

  • 当用户成功登录之后,服务器在 Session 中会生成一个新的记录,并把 sessionId 返回给客户端(例如 HTTP 响应中可以通过 Set-Cookie 字段返回,其中 Cookie 的 key 为 “JSESSION”,value 为服务器生成的 sessionId 的具体的值)
  • 然后客户端只需要保存这个 sessionId ,当后续再给服务器发送请求时,请求中就会带上 sessionId(例如 HTTP 请求中会带上 Cookie 字段用于传递 Session)
  • 服务器收到请求后,就会根据请求中的 sessionId 在 Session 中查询对应用户的身份信息,在进行后续操作

Session 会话机制的好处:

  • 使得客户端很轻量,不用保存太多数据
  • 客户端和服务器之间传输的数据量小,节省带宽
  • 数据都在服务器存储,即使客户端出现问题,数据也不会丢失

注意: Servlet 的 Session 默认是保存在内存中的,如果重启服务器 Session 数据将会丢失

8.3 Cookie 和 Session 的区别

  • Cookie 是客户端存储数据的一种机制,可以存储身份信息,也可以存储其它信息,是键值对结构的

  • Session 是服务器存储数据的一种机制,主要用于存储身份相关的信息,是键值对结构的

  • Cookie 和 Session 经常配合使用,但是不是必须的。

    • Cookie 也完全可以保存一些数据在客户端,这些数据不一定是用户身份信息,不一定是 sessionId
    • Session 中的 sessionId 也不需要非得通过 Cookie 和 Set-Cookie 来传递

8.4 Servlet 中 Cookie 和 Session 的核心方法

HttpServletRequest 类中的相关方法

方法 描述
HttpSession getSession(参数) 在服务器中获取会话,参数如果为 true,当不存在会话时会新建会话(包括生成一个新的 sessionId 和 HttpSession 对象),并通过 Set-Cookies 将 sessionId 返回给客户端;参数如果为 false,当不存在会话时会返回 null。如果存在 sessionId 且合法,就会根据这个 sessionId 找到对应的 HttpSession 对象并返回
Cookie[] getCookies() 返回一个数组,包含客户端发送请求时的所有 Cookie 对象,会自动把 Cookie 中的格式解析成键值对

HttpServletResponse 类中的相关方法

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

HttpSession 类中的相关方法

  • HttpSession是 Java平台对 session 机制的实现规范,因为它仅仅是个接口,具体实现为每个 web 应用服务器的提供商。
  • 服务器会为每一个用户创建一个独立的 HttpSession,表示为一个会话,并且一个 HttpSession 对象里包含了多个键值对,可以往 HttpSession 中存储需要的数据
方法 描述
Object getAttribute(String name) 该方法返回在 Session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null
void setAttribute(String name, Object value) 该方法使用指定的名称绑定一个对象到该 Session 会话中
boolean isNew() 判定当前的会话是否是新创建的

Cookie 类中的相关方法

  • 这个类描述了一个 Cookie,通过 Cookie 类创建的对象,每个对象就是一个键值对
  • HTTP 的 Cookie 字段中实际上存储的是多个键值对,每个键值对在 Servlet 中都对应一个 Cookie 对象
方法 描述
String getName() 该方法返回 cookie 的名称(这个值是 Set-Cookie 字段设置给浏览器的,创建之后不能改变)
String getValue() 该方法获取与 Cookie 关联的值
void setValue(String newValue) 该方法设置与 Cookie 关联的值

8.5 实现用户登录功能

接下来将使用上述的 Session 和 Cookie 的相关方法来实现一个用户登录功能,并且可以记录访问页面的次数

登录功能实现思路:

  1. 读取用户提交的用户和密码
  2. 对用户密码进行校验
  3. 判定是否登录成功
  4. 创建会话,保存自定义信息
  5. 重定向到指定页面

登录功能实现流程:

  1. 先实现一个登录页面

    <!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="password" name="password">
            <input type="submit" value="登录">
        </form>
    </body>
    </html>
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OvzuJIvC-1650529905422)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220420200000423.png)]

  2. 实现一个 Servlet 来处理上面的登录请求

    由于这里是通过 form 表单来构造的 post 请求,那么通过 HttpServletRequest 类中的 getParameter() 方法就能够获取请求正文中参数的值

    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 javax.servlet.http.HttpSession;
    import java.io.IOException;
    
    @WebServlet("/login")
    public class LoginServlet extends HttpServlet {
          
          
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            resp.setContentType("text/html;charset=utf8");
            // 1. 从请求中获取到用户名和密码
            String username = req.getParameter("username");
            String password = req.getParameter("password");
            // 2. 对用户密码进行校验
            if(username == null || "".equals(username) || password == null || "".equals(password)){
          
          
                resp.getWriter().write("<h3>账号或密码不能为空!</h3>");
                return;
            }
            // 3. 判断是否登录成功(假设用户名为 admin,密码为 1234。不过账号密码应该用数据库存储,这里只是用来测试)
            if(!username.equals("admin") || !password.equals("1234")){
          
          
                resp.getWriter().write("<h3>账号或密码错误!</h3>");
                return;
            }
            // 4. 登录成功,创建一个会话,用来记录当前用户的信息
            HttpSession session = req.getSession(true);
            //    通过这个操作,就给会话中新增了一个程序员自定义的信息,访问次数
            session.setAttribute("visitCount", 0);
            // 5. 把登录成功的结果反馈给客户端(这里的反馈不是简单的提示“登录成功”,而是直接跳转到指定页面)
            resp.sendRedirect("index");
        }
    }
    
  3. 通过实现一个 Servlet 来表示登录成功后重定向的页面

    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 javax.servlet.http.HttpSession;
    import java.io.IOException;
    
    @WebServlet("/index")
    public class IndexServlet extends HttpServlet {
          
          
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            resp.setContentType("text/html;charset=utf8");
            // 只有登录成功参数才能是 true,这里是拿参数,所以要填 false
            HttpSession session = req.getSession(false);
            // 判断当前用户是否登录
            if(session == null){
          
          
                // 可以提示未登录,也可以重定向到登录页面
                // resp.getWriter().write("<h3>登录为空!</h3>");
                resp.sendRedirect("login2.html");
                return;
            }
            // 表示用户登录过,获取会话中的访问次数
            Integer visitCount = (Integer) session.getAttribute("visitCount");
            visitCount += 1;
            session.setAttribute("visitCount", visitCount);
            resp.getWriter().write("<h3>visitCount = " + visitCount + "</h3>");
        }
    }
    
  4. 到这里为止,一个简单的用户登录功能就实现成功了。效果如下在这里插入图片描述

9. 上传文件操作

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

9.1 Servlet 中上传文件的核心方法

HttpServletRequest 类中的相关方法

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

Part 类中的相关方法

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

9.2 上传文件操作实现

  1. 先写一个前端页面,用于上传文件

    • 上传文件一般使用 post 请求的表单实现
    • 通过 form 表单构造上传文件,要加上一个 enctype 字段,它表示 body 中的数据格式,它的默认值为:x-www-form-urlencoded,这里要修改成:multipart/form-data,它是上传文件或者图片的数据格式
    • 第一个 input 中 type 的值为 file,它表示第一个输入框为文件选择框,name 的值与后端中通过 getPart 获取指定文件的操作有关
    <!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" value="上传">
        </form>
    </body>
    </html>
    
  2. 写一个 Servlet 用于处理上传的文件

    • 上传文件操作还需要给 Servlet 加上一个 @MultipartConfig 注解,否则服务器代码无法使用 getPart() 方法
    • getPart() 方法中的参数和 form 表单 input="file" 标签的 name 属性对应
    • 客户端一次可以提交多个文件,getPart() 方法根据 name 属性来获取不同的文件
    • 写入磁盘文件操作的路径之间可以使用两个反斜杠 \\ ,也可以使用一个正斜杠 /
    • 写入磁盘文件操作的路径最后为保存后的文件名,包括文件后缀
    import javax.servlet.ServletException;
    import javax.servlet.annotation.MultipartConfig;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.Part;
    import java.io.IOException;
    
    @MultipartConfig
    @WebServlet("/upload")
    public class UploadServlet extends HttpServlet {
          
          
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          
          
            resp.setContentType("text/html;charset=utf8");
            // 通过 getPart 方法获取到前端传来的文件
            Part part = req.getPart("MyFile");
            // 获取文件名
            String fileName = part.getSubmittedFileName();
            System.out.println("文件名为: " + fileName);
            // 获取提交的文件类型
            String fileType = part.getContentType();
            System.out.println("文件类型为: " + fileType);
            // 获取文件的大小
            long fileSize = part.getSize();
            System.out.println("文件大小为: " + fileSize);
    
    
            // 把文件数据写入磁盘文件
            part.write("C:\\Users\\bbbbbge\\Desktop\\upload.jpg");
    
            resp.getWriter().write("上传成功!");
        }
    }
    

    在这里插入图片描述

到这里为止,一个简单的文件上传操作就实现好了,我们可以通过抓包来观察下文件上传操作的请求是怎样的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agQADJIH-1650529905422)(C:/Users/bbbbbge/Pictures/%E6%8E%A5%E5%8D%95/image-20220421131825048.png)]

通过抓包操作我们会发现几点问题:

  • 正文的大小和我们上传文件的大小不同,正文的比上传的文件的字节数略大
  • 数据类型是 multipart/form-data 没有问题,但是后面多了一串 boundary=----WebKitFormBoundaryAl26z0nbP6JzAUGL,这个 boundary 在 body 中表示一个分隔线,第一条分割线下面是上传的文件的属性和文件的内容,当文件的内容结束时还有第二条分割线
  • 由于有这个分割线和文件的一些属性,因此使得请求中正文的大小比上传的文件的内容略大

猜你喜欢

转载自blog.csdn.net/weixin_51367845/article/details/124325311