【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

    Servlet 中 web.xml 中的内容不能是空的,里面的写法是固定的(这里的写法专属于 Servlet),用到的时候可以直接拷贝下面的代码

    <!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 中表示一个分隔线,第一条分割线下面是上传的文件的属性和文件的内容,当文件的内容结束时还有第二条分割线
  • 由于有这个分割线和文件的一些属性,因此使得请求中正文的大小比上传的文件的内容略大
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吞吞吐吐大魔王

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

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

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

打赏作者

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

抵扣说明:

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

余额充值