JavaWeb(2-Servlet、Request 和 Response)

340 阅读1小时+

1、Servlet

定义:Servlet 是 Java 编程语言中用于构建动态 Web 应用程序的服务器端组件。
工作原理:Servlet 运行在支持 Java 的 Web 服务器或应用服务器(例如 Apache Tomcat)上,通过处理客户端(通常是浏览器)发送的请求并生成响应来实现动态内容。

屏幕截图 2024-08-18 095309.png

规范与接口:Servlet 是 JavaEE 规范中的一个标准接口,定义了 Web 应用程序如何处理请求。
实现与运行:开发者需要实现这个接口,或者继承一个已有的实现类(如 HttpServlet),然后由 Web 服务器来管理这些 Servlet 的运行。

1.1、创建Servletet("访问路径")

@WebServlet("/demo1")
    public class ServletDemo1 implements Servlet {
}

访问 Servlet

  • 确保项目的 Tomcat 服务器配置正确,并且项目已经被部署到 Tomcat 服务器上。
  • 启动 Tomcat 服务器。在浏览器中输入以下 URL 访问该 Servlet: http://localhost:8080/web-demo/demo1 其中 web-demo 是项目名,demo1 是在 @WebServlet 注解中配置的路径。

注意:

  • 定义Servlet的URL路径时,建议始终以斜杠(/)开头
  • 当在 IDEA 这个集成开发环境中手动输入注解 @WebServlet 时,IDEA会自动帮你导入这个注解类所在的包。
  • 访问后,打印结果在控制台的 Tomcat窗口 显示
  • 配置Tomcat服务器时,在 部署-应用程序上下文,可以编辑应用程序上下文(部分url)

1.2、执行流程

屏幕截图 2024-08-18 121645.png

步骤:

  1. 客户端发送 HTTP 请求
  2. Web 服务器接收请求
  3. Servlet 容器处理请求:
    1. 首次请求:加载并初始化 Servlet
    2. 每次请求:调用 service() 方法,根据请求类型调用 doGet()doPost() 等方法
  4. Servlet 处理请求并生成响应
  5. Web 服务器返回响应给客户端
  6. 在特定情况下销毁 Servlet

Web 服务器(或 Servlet 容器)与 Servlet 交互的机制:

  • Servlet 由 Web 服务器创建,Servlet 方法由 Web 服务器调用。这通过实现 Servlet 接口和使用服务器提供的反射机制实现。
  • 服务器知道 Servlet 中一定有 service() 方法,因为所有的 Servlet 类都必须实现 Servlet 接口,而这个接口规定了 service() 方法是必须实现的。

1.3、生命周期

对象的生命周期指一个对象从被创建到被销毁的整个过程

Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:

  1. 加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象

  2. 初始化:在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次

  3. 请求处理:每次请求Servlet时,Servlet容器都会调用Servlet的service()方法对请求进行处理。

  4. 服务终止:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的destroy()方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收

@WebServlet 注解中的 loadOnStartup 属性详解

  1. 定义loadOnStartup@WebServlet 注解中的一个可选属性,用于控制 Servlet 在 Web 容器启动时的加载和初始化行为。
    格式@WebServlet(urlPatterns = "访问路径", loadOnStartup = 整数)
  2. 属性值说明
  • 负整数(默认值为-1)
    初始化时机:当Servlet 第一次被访问
    适用场景:这种延迟加载适用于那些只在特定情况下才需要使用的 Servlet,减少服务器启动时的资源消耗。
  • 非负整数(0 或正整数)
    初始化时机:在 服务器启动
    适用场景:预加载(提前加载) Servlet 常用于需要在应用启动时就准备好的组件,如缓存、数据库连接池初始化、后台任务调度器等。
  1. 优先级控制
    1. 正整数(数字越小,优先级越高,最先加载)。
    2. 0(优先级低于正整数,但高于负整数)。
    3. 负整数(任何负整数都是只有在第一次访问时加载)。
  • 多个 Servlet 的优先级控制
    @WebServlet(urlPatterns = "/demo1", loadOnStartup = 2)
    public class DemoServlet1 extends HttpServlet {
        // This will be loaded after DemoServlet2
    }
    
    @WebServlet(urlPatterns = "/demo2", loadOnStartup = 1)
    public class DemoServlet2 extends HttpServlet {
        // This will be loaded before DemoServlet1
    }
    
  1. 注意事项
  • Servlet 初始化的副作用:在 init() 方法中进行复杂的初始化操作可能会延长服务器的启动时间,尤其是当 loadOnStartup 设置为非负整数时
  • loadOnStartup 也可以通过 web.xml 文件来配置

1.4、方法介绍

  1. 初始化方法:用于初始化 Servlet。
    void init(ServletConfig config)
    调用时机:
    如果 loadOnStartup 为非负整数,则该方法会在服务器启动时调用。
    如果 loadOnStartup 为负数(如 -1,默认值),则该方法会在第一次访问时调用。
    调用次数:整个生命周期中只调用一次

  2. 提供服务方法:用于处理客户端的请求。
    void service(ServletRequest req, ServletResponse res)
    调用时机:每次有客户端请求到达该 Servlet 时
    调用次数:每次客户端请求都会触发一次调用

  3. 销毁方法:用于清理资源。
    void destroy()
    调用时机:Servlet 容器决定卸载该 Servlet 或服务器关闭时
    调用次数:只调用一次。

  4. 获取 Servlet 配置对象:返回一个 ServletConfig 对象,该对象包含了 Servlet 的配置信息,如初始化参数、Servlet 名称等
    ServletConfig getServletConfig()
    调用时机:任何需要获取 Servlet 配置信息的时机都可以调用
    调用次数:多次调用

  5. 获取 Servlet 信息:返回一个字符串,通常用于提供关于 Servlet 的一些基本信息
    String getServletInfo()
    调用时机:任何时候
    调用次数:多次调用

1.5、体系结构——HttpServlet

屏幕截图 2024-08-18 145341.png

我们将来开发B/S架构的web项目,都是针对HTTP协议,所以我们自定义Servlet,会继承HttpServlet

HttpServlet 是一个抽象类,是 Java Servlet API 中的重要组成部分,用于处理基于 HTTP 协议的请求。它简化了开发者处理 HTTP 请求的过程,通过提供特定的 doGet()doPost() 等方法,使得开发者只需专注于具体的业务逻辑,而无需处理底层的协议细节。

1. HttpServlet 的原理

1.1. 继承和结构

  • HttpServlet 继承自 GenericServlet,而 GenericServlet 实现了 Servlet 接口。
  • HttpServlet 提供了对 HTTP 协议的特定支持,特别是通过 doGet()doPost()doPut()doDelete() 等方法来处理不同类型的 HTTP 请求。

1.2 service() 方法的作用

  • HttpServlet 的核心是 service() 方法。该方法负责接收客户端请求并根据请求方法(如 GET、POST)分发到相应的处理方法(如 doGet()doPost())。

  • 当一个请求到达时,Servlet 容器会调用 service() 方法,该方法根据请求的类型调用对应的 doXXX() 方法。

1.1.1 创建HttpServlet

  • 开发者需要创建一个 Java 类,并继承自 HttpServlet,然后根据需要重写 doGet()doPost() 等方法。(在类名旁补充 extends HttpServlet,手动输入IDEA会自动导入包中接口;手动输入doGet()doPost()可以根据提示自动补全方法)

其他步骤与创建Servlet类似

@WebServlet("/example")
public class ExampleServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 处理 GET 请求
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 处理 POST 请求
    }
}

1.6、urlPattern配置

URL 映射:URL 映射是指将特定的 URL 请求与对应的 Servlet 关联起来。这样,当客户端访问某个 URL 时,Servlet 容器可以根据 URL 找到相应的 Servlet 来处理请求。

urlPattern:urlPattern 属性定义了一个或多个 URL 模式 (即 访问路径),这些模式决定了当客户端请求某个 URL 时,应该由哪个 Servlet 来处理这个请求。这个属性可以接收单个字符串,也可以接收字符串数组。

Servlet 要想被访问,必须配置其访问路径(urlPattern)

  • 一个Servlet,可以配置一个 urlPattern
    @WebServlet(urlPatterns = "/demo")
  • 一个Servlet,可以配置多个 urlPattern
    @WebServlet(urlPatterns = {"/demo1","/demo2"})

当客户端请求匹配其中任何一个 URL 路径时,都会触发同一个 Servlet 来处理

urlPattern 配置规则

  1. 精确匹配
  • 语法:@WebServlet("/访问路径")
  • 说明:与给定的路径完全匹配。只有完全符合此路径的请求才会被映射到该 Servlet。
  • 示例:
    • 请求 http://localhost:8080/YourAppContext/example 会被处理。
    • 请求 http://localhost:8080/YourAppContext/example/other 不会被处理。
  1. 目录匹配(路径匹配)
  • 语法:@WebServlet("/访问路径/*")
  • 说明:匹配指定路径下的所有子路径。
  • 示例:
    • 请求 http://localhost:8080/YourAppContext/example 会被处理。
    • 请求 http://localhost:8080/YourAppContext/example/other 也会被处理。
  1. 扩展名匹配
  • 语法:@WebServlet("*.扩展名")
  • 说明:匹配指定扩展名的所有请求路径。
  • 示例:
    • 请求 http://localhost:8080/YourAppContext/anything.do 会被处理。
  1. 默认(根路径)匹配
  • 语法:@WebServlet("/")@WebServlet("/*")
  • 说明:处理应用的所有请求,除非有更具体的路径匹配规则存在。
  • 示例:
    • 请求 http://localhost:8080/YourAppContext/anypath 会被处理。
  • //* 这两种 URL 模式的区别
    URL 模式 /
    • 匹配规则:
      当一个 Servlet 配置为 urlPatterns = "/" 时,它会匹配所有的请求,但不包括对静态资源(如 HTML、CSS、JS 文件)的请求以及 JSP 页面
    • 适用场景:
      通常用来作为应用程序的主控制器(如前端控制器模式中的 DispatcherServlet)。
      适用于希望处理所有动态请求,而静态资源仍由 DefaultServlet 处理的场景。 URL 模式 /*
    • 匹配规则:
      当一个 Servlet 配置为 urlPatterns = "/*" 时,它会匹配该应用下的所有请求路径,包括对静态资源和 JSP 页面等的请求
    • 适用场景:
      适用于希望捕获和处理所有类型的请求(包括静态资源和动态内容)的场景。
      这种配置常用于实现全局过滤器或统一处理逻辑的场景。
  • Tomcat 中的 DefaultServlet 处理
    • Tomcat 中的 DefaultServlet 是一个特殊的 Servlet,负责处理静态资源的请求,比如 .html.css.js.jpg 等文件。当其他的 URL 模式都匹配不上时,DefaultServlet 就会处理这些请求。
    • 如果你配置了 urlPatterns = "/"urlPatterns = "/*",你的 Servlet 会覆盖 DefaultServlet 的功能,导致所有的静态资源请求都被你的 Servlet 处理,而不是 DefaultServlet 处理。

优先级与匹配规则

  • 当一个请求到达时,Servlet 容器会根据 URL 映射规则找到最精确匹配的 urlPattern 并将请求分派到相应的 Servlet 进行处理。
  • 精确路径 > 目录路径 > 扩展名路径 > /* > /

1.7、XML 配置方式编写 Servlet

Servlet 从3.0版本后开始支持使用注解配置,3.0版本前只支持 XML 配置文件的配置方式

步骤:

  1. 编写 Servlet类
  2. 在 web.xml中配置该Servlet
<servlet>
    <servlet-name>exampleServlet</servlet-name>  //该Servlet 的名称
    <servlet-class>com.example.ExampleServlet</servlet-class>  //该 Servlet 的完整类名(包括包名)
</servlet>

<servlet-mapping>
    <servlet-name>exampleServlet</servlet-name>  //与URL路径关联的Servlet名称
    <url-pattern>/example</url-pattern>  //定义与该 Servlet 关联的URL路径
</servlet-mapping>

1.8、ServletContext

ServletContext 的概念

定义:ServletContext 是由 Web 容器(如 Tomcat)在启动时为每个 Web 应用程序创建的一个对象。它表示整个 Web 应用程序的上下文,通过它,Servlet 可以与 Web 容器进行交互,访问应用范围内的资源和信息。

作用范围:在同一个 Web 应用程序内,ServletContext 对象是全局共享的。

1.8.1、共享数据

这个示例展示了如何在不同的 Servlet 之间共享数据:通过ServletContext 对象,在一个 Servlet 中存储数据,并在另一个 Servlet 中读取这些数据。

示例

  1. HelloServlet 中的操作
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    // 重写 HttpServlet 的 doGet 方法,用于处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 获取当前 Servlet 所属的 ServletContext 对象
        ServletContext context = this.getServletContext();

        // 创建一个名为 "username" 的字符串变量,并赋值为 "秦疆"
        String username = "秦疆"; // 数据

        // 将 "username" 变量存储到 ServletContext 对象中,键为 "username",值为 "秦疆"
        // 这样在整个 Web 应用的其他 Servlet 中都可以通过 "username" 键来访问该值
        context.setAttribute("username", username);
    }
}
  • 获取 ServletContext 对象:通过 this.getServletContext() 获取当前应用的 ServletContext 对象。
  • 保存数据context.setAttribute("username", username)setAttribute() 方法用于存储数据,其中 "username" 是键,username 是值。
  1. GetServlet 中的操作
@WebServlet(urlPatterns = "/getc")
public class GetServlet extends HttpServlet {

    // 重写 HttpServlet 的 doGet 方法,用于处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 获取当前 Servlet 所属的 ServletContext 对象
        ServletContext context = this.getServletContext();

        // 从 ServletContext 中获取之前在 HelloServlet 中存储的数据
        // 这里使用 getAttribute 方法,传入键 "username" 来获取对应的值
        String username = (String) context.getAttribute("username");

        // 设置响应内容的 MIME 类型为 text/html,表示返回的是 HTML 格式的内容
        resp.setContentType("text/html");

        // 设置响应内容的字符编码为 UTF-8,确保中文字符能够正确显示
        resp.setCharacterEncoding("utf-8");

        // 通过响应对象的 writer 输出流,将获取到的 username 值以 HTML 格式输出到浏览器
        resp.getWriter().print("名字:" + username);
    }

    // 重写 HttpServlet 的 doPost 方法,用于处理 POST 请求
    // 这里调用 doGet 方法,以便 POST 请求也能执行与 GET 请求相同的逻辑
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
  • 获取数据:使用 context.getAttribute("username") 从 ServletContext 中获取之前在 HelloServlet 中保存的数据。
  • 输出数据:通过 resp.getWriter().print("名字:" + username) 将获取到的 username 值输出到浏览器。

流程理解

  1. 存储数据
  • 当访问 http://localhost:8080/YourAppContext/hello 时,HelloServlet 被调用。
  • HelloServlet 获取 ServletContext 对象,并通过 setAttribute() 方法将 "username" 键与 "秦疆" 值存储在 ServletContext 中。
  1. 读取数据
  • 当访问 http://localhost:8080/YourAppContext/getc 时,GetServlet 被调用。
  • GetServlet 获取 ServletContext 对象,并通过 getAttribute() 方法读取 "username" 键对应的值,然后将其输出到浏览器。

1.8.2、获取初始化参数

@WebServlet(
    urlPatterns = "/myServlet",  // 指定 Servlet 的 URL 映射
    initParams = {
        @WebInitParam(name = "url", value = "jdbc:mysql://localhost:3306/mybatis")  // 初始化参数
    }
)    //使用 @WebServlet 和 @WebInitParam 注解来配置 Servlet 的初始化参数
public class MyServlet extends HttpServlet {

    // 重写 HttpServlet 的 doGet 方法,用于处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取当前 Servlet 所属的 ServletContext 对象
        ServletContext context = this.getServletContext();

        // 使用 getInitParameter 方法从 ServletContext 中获取初始化参数 "url"
        String url = context.getInitParameter("url");

        // 将获取到的初始化参数 "url" 的值通过响应对象的 writer 输出流返回给客户端浏览器
        resp.getWriter().print(url);
    }
}

运行流程:Servlet 接收到请求后,获取初始化参数的值,并将该值作为响应返回给客户端。

1.8.3、请求转发

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    // 获取当前 Servlet 所属的 ServletContext 对象
    ServletContext context = this.getServletContext();

    // 输出调试信息到控制台,表明已经进入了当前的 Servlet (ServletDemo04) 的 doGet 方法
    System.out.println("进入了ServletDemo04");

    // 单独通过 ServletContext 对象获取 RequestDispatcher 对象,用于请求转发
    // 传入的参数 "/gp" 是目标 Servlet 的路径,这意味着请求将会转发到 "/gp" 这个路径上
    // RequestDispatcher requestDispatcher = context.getRequestDispatcher("/gp"); 
    // 使用 forward 方法将请求和响应对象转发到目标路径
    // requestDispatcher.forward(req, resp); 

    // 更简洁的方式:直接通过 ServletContext 的 getRequestDispatcher 方法获取 RequestDispatcher 对象并调用 forward 方法
    context.getRequestDispatcher("/gp").forward(req, resp);
}

1.8.4、读取资源文件

properties 配置文件

定义:Java 中常用的一种用于存储配置信息的文件格式。它通常以 .properties 为文件扩展名,并且以简单的键值对形式组织数据。

存储位置:properties 文件通常存储在 src/main/resources 目录下,也可以存储在 src/main/java 目录下,编译后它们都会被复制到 WEB-INF/classes 目录下。(在 Java Web 应用中,WEB-INF/classes/ 和 WEB-INF/lib/ 是默认的 classpath 目录,所有的类文件和资源文件都会被加载到 classpath 中。)

示例:展示如何在 Servlet 中通过 ServletContext 获取并读取 properties 文件,并将读取到的内容输出到客户端。

public class ServletDemo05 extends HttpServlet {

    // 重写 HttpServlet 的 doGet 方法,用于处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 通过 ServletContext 获取指定路径下的资源文件作为输入流
        // 这里的路径是相对于 WEB-INF 目录的,指向 classpath 下的 aa.properties 文件
        InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/com/kuang/servlet/aa.properties");

        // 创建一个 Properties 对象,用于加载和读取 properties 文件中的键值对
        Properties prop = new Properties();
        prop.load(is);  // 将输入流中的 properties 文件内容加载到 Properties 对象中

        // 从 Properties 对象中获取 "username" 和 "password" 属性的值
        String user = prop.getProperty("username");
        String pwd = prop.getProperty("password");

        // 将获取到的用户名和密码输出到客户端,格式为 "username:password"
        resp.getWriter().print(user + ":" + pwd);
    }

    // 重写 HttpServlet 的 doPost 方法,用于处理 POST 请求
    // 这里调用 doGet 方法,以便 POST 请求也能执行与 GET 请求相同的逻辑
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

详细解释

  1. 获取资源文件输入流
    InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/com/kuang/servlet/aa.properties");
  • 使用 ServletContextgetResourceAsStream 方法获取资源文件的输入流。资源文件路径是相对于 WEB-INF 目录的,在这里指向的是 classpath 中的 aa.properties 文件,读取 aa.properties 文件。
  1. 加载和读取 properties 文件
  • Properties prop = new Properties();:创建一个 Properties 对象,该对象用于处理 .properties 文件中的键值对。
  • prop.load(is);:使用 Properties 对象的 load 方法将输入流中的数据加载到 Properties 对象中,从而可以通过键名获取对应的值。
  1. 获取属性值
  • String user = prop.getProperty("username");:通过 getProperty 方法获取 "username" 属性对应的值。
  1. 输出属性值
  • resp.getWriter().print(user + ":" + pwd);
    • 使用 HttpServletResponsegetWriter 方法获取一个 PrintWriter 对象,用于将内容输出到 HTTP 响应中。
    • 将从 properties 文件中获取到的用户名和密码以 "username:password" 的格式输出到客户端浏览器。

2、Request和Response概述

Request对象代表客户端发送给服务器的请求信息。Response对象代表服务器返回给客户端的响应信息。

image.png

request 和 response 两个参数的作用与运行流程

request:获取请求数据

  1. 浏览器发送请求:当用户在浏览器中进行操作时,浏览器会发送一个 HTTP 请求到后台服务器(如 Tomcat)。
  2. 请求内容:这个 HTTP 请求中包含多个部分,如请求行、请求头和请求体。
  3. Tomcat 解析请求:后台服务器(Tomcat)会对接收到的 HTTP 请求进行解析,并将解析后的数据存入一个 HttpServletRequest 对象中。
  4. 获取请求参数:我们可以通过 request 对象获取客户端发送的请求数据,包括表单参数、请求头信息、URL 信息等。
  5. 后续处理:获取到请求数据后,服务器可以根据这些数据继续处理后续的业务逻辑,例如通过用户名和密码实现登录操作。

response:设置响应数据

  1. 业务处理完成:当服务器完成业务处理后,需要将处理结果返回给客户端(浏览器)。
  2. 封装响应数据:服务器将响应数据封装到 HttpServletResponse 对象中,包括状态码、响应头和响应体。
  3. Tomcat 解析响应:Tomcat 会解析 response 对象,将其转换为标准的 HTTP 响应格式,即由响应行、响应头和响应体组成的结构。
  4. 浏览器展示结果:浏览器接收到服务器的 HTTP 响应后,会解析响应内容,并将结果展示给用户,例如显示 HTML 页面或处理返回的数据。

总结:通过 requestresponse 对象,服务器能够接收客户端的请求数据,并将处理结果返回给客户端,完成整个请求-响应的流程。

示例:通过一个案例来初步体验下request和response对象的使用

@WebServlet("/demo3")
public class ServletDemo3 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 使用 request 对象获取客户端传递的请求参数 "name"
        // 例如,客户端发送的请求 URL 为 "/demo3?name=zhangsan",此时可以通过 getParameter("name") 获取参数值 "zhangsan"
        String name = request.getParameter("name");

        // 使用 response 对象设置响应头,指定内容类型为 "text/html",并设置字符编码为 "UTF-8",确保返回的 HTML 内容能够正确显示中文等多语言字符
        response.setHeader("content-type", "text/html;charset=utf-8");

        // 使用 response 对象的 Writer 输出流,将响应内容返回给客户端
        // 这里构建了一个 HTML 标题 `<h1>`,并包含从请求中获取的 "name" 参数
        // 如果 "name" 是 "zhangsan",那么返回的内容将是 "<h1>zhangsan,欢迎您!</h1>"
        response.getWriter().write("<h1>" + name + ",欢迎您!</h1>");
    }

    // 此方法可以处理通过表单或 AJAX 提交的 POST 请求
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 在控制台输出 "Post..." 以指示 POST 请求的处理
        System.out.println("Post...");

        // 也可以选择在 POST 请求中调用 doGet 方法,使得 GET 和 POST 请求的处理逻辑一致
        doGet(request, response);
    }
}

启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容: image.png

3、Request对象

3.1、Request继承体系

image.png

3.2、Request获取请求数据

3.2.1、获取请求行数据

请求行包含三块内容,分别是请求方式请求资源路径HTTP协议及版本

image.png

对于这三部分内容,request对象都提供了对应的API方法来获取,具体如下:

1. 获取请求方式

  • 方法:String getMethod()
  • 作用:获取请求的 HTTP 方法类型,如 GETPOSTPUTDELETE 等。
  • 示例:
    String method = request.getMethod(); // 返回 "GET" 或 "POST" 等
    

2. 获取虚拟目录(项目访问路径)

  • 方法:String getContextPath()
  • 作用:获取当前 Web 应用的上下文路径(虚拟目录)。上下文路径是用于区分多个 Web 应用的路径部分,通常是应用在服务器上的部署路径。
  • 示例:
    String contextPath = request.getContextPath(); // 返回 "/request-demo"
    
    说明:在 URL http://localhost:8080/request-demo/req1 中,/request-demo 就是虚拟目录(上下文路径)。

3. 获取完整的 URL(统一资源定位符)

  • 方法:StringBuffer getRequestURL()
  • 作用:获取客户端发出的完整请求 URL,包括协议、服务器地址、端口号和请求路径,但不包括查询字符串。
  • 示例:
    StringBuffer url = request.getRequestURL(); // 返回 "http://localhost:8080/request-demo/req1"
    
    说明:getRequestURL() 返回的是 StringBuffer 对象,因为 URL 可能是可变的。如果你需要一个 String 类型,可以使用 toString() 方法将其转换为 String

4. 获取 URI(统一资源标识符)

  • 方法:String getRequestURI()
  • 作用:获取请求的 URI,URI 是 URL 中的资源路径部分,不包括协议、主机名和端口号。
  • 示例:
    String uri = request.getRequestURI(); // 返回 "/request-demo/req1"
    
    说明:在 URL http://localhost:8080/request-demo/req1 中,/request-demo/req1 是 URI。

5. 获取查询字符串

  • 方法:String getQueryString()
  • 作用:获取请求 URL 中的查询字符串部分,查询字符串包含在 URL 的 ? 后面,用于传递参数。
  • 示例:
    String queryString = request.getQueryString(); // 返回 "username=zhangsan&password=123"
    
    说明:在 URL http://localhost:8080/request-demo/req1?username=zhangsan&password=123 中,username=zhangsan&password=123 是查询字符串。

3.2.2、获取请求头数据

请求头的数据,格式为 key: value

  • 方法: String getHeader(请求头名称)

  • 作用:获取指定名称的请求头的值

  • 示例:

    @WebServlet("/example")
    public class ExampleServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            // 获取 User-Agent 请求头的值
            String userAgent = request.getHeader("User-Agent");
        
            // 输出 User-Agent 请求头的值
            response.getWriter().println("User-Agent: " + userAgent);
        }
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
        }
    }
    

    将这个 Servlet 部署到你的 Servlet 容器(如 Tomcat)中。当你访问 http://localhost:8080/your-app/simple-header 时,Servlet 会输出 User-Agent 请求头的值:

    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
    

3.2.3、获取请求体数据

GET 请求通常用于从服务器请求数据,它不包含请求体;相反,POST 请求用于将数据发送到服务器,并且可以包含请求体。

对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:

  • 获取 字节输入流,如果请求体中的数据是以 字节形式 发送的,例如文件、二进制数据,或者不确定的数据格式
    ServletInputStream getInputStream()
  • 获取 字符输入流,如果请求体中的数据是 文本数据,例如 JSON、XML 或表单提交的文本数据
    BufferedReader getReader()

获取到请求体的具体实现步骤如下:

  1. 准备一个页面,在页面中添加form表单,用来发送post请求
  2. 在Servlet的doPost方法中获取请求体数据
  3. 在doPost方法中使用request的getReader()或者getInputStream()来获取
//在 Servlet 中读取请求体中的纯文本数据

@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 这个方法处理 GET 请求,但这里是空的,没有实现
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取 POST 请求体中的请求参数
        // 1. 获取字符输入流
        BufferedReader br = req.getReader();

        // 2. 读取数据
        String line = br.readLine();
        System.out.println(line);
    }
}

注意
BufferedReader 流是通过 Request 对象获取的,而 Request 是由 Servlet 容器管理的对象。在请求处理完毕后,Request 对象会被销毁,随之而来的还有与其关联的资源(如输入流 BufferedReader),这些资源会由容器自动处理和关闭,所以此处就不需要手动关闭流了。

  1. 启动服务器,通过浏览器访问 http://localhost:8080/request-demo/req.html
    访问后浏览器显示表单,点击表单提交按钮后,表单数据会通过 POST 请求发送到服务器,然后服务器会在控制台输出收到的请求数据。

3.2.4、获取请求参数的通用方式

请求参数:是客户端发送给服务器的数据,这些数据通常用于描述客户端的请求意图或传递特定信息以供服务器处理。请求参数可以以多种方式包含在 HTTP 请求中,具体方式取决于请求的类型(如 GET、POST 等)和数据的传递方式。

请求参数和请求数据的关系:

  • 请求参数 是请求数据的一个子集,专门用于传递结构化的信息,如查询条件、表单字段值、用户 ID 等。
  • 请求数据 是一个广泛的概念,包含了 请求参数、请求头、请求体等所有的 HTTP 请求相关信息。

GET与POST 请求数据组成

  • GET 请求:http://example.com/products?category=books&sort=price
    请求数据包含:请求头信息(如 User-Agent)、查询字符串(即请求参数 category=bookssort=price)。
  • POST 请求:提交一个表单或 JSON 数据。
    请求数据包含:请求头信息(如 Content-Type),请求体中的参数(如表单字段或 JSON 数据)。

获取请求参数的两种常用方法

GET 方式

  • 方法:String getQueryString()
  • 作用:获取 URL 中的查询字符串,即 ? 之后的部分。
  • 返回值:返回 ? 之后的整个查询字符串(String 类型)。如果没有查询字符串,返回 null

POST 方式

  • 方法:BufferedReader getReader()
  • 作用:获取 POST 请求体中的内容,通常在处理纯文本数据、JSON 数据或者 XML 数据时使用。
  • 返回值:返回 一个 BufferedReader 对象,可以用于逐行读取请求体的内容。

案例

@WebServlet("/req1") 
public class RequestDemo1 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 使用 getQueryString() 方法获取 GET 请求的查询字符串
        // 查询字符串是 URL 中 ? 之后的部分,例如 /req1?name=John&age=25
        String result = req.getQueryString();

        // 打印获取到的查询字符串到服务器控制台
        System.out.println(result);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 使用 getReader() 方法获取 POST 请求体中的数据
        // 该方法返回一个 BufferedReader 对象,用于读取请求体中的字符数据
        BufferedReader br = req.getReader();

        // 读取请求体中的第一行数据
        String result = br.readLine();

        // 打印读取到的请求体数据到服务器控制台
        System.out.println(result);
    }
}

思考

GET请求方式和POST请求方式区别主要在于获取请求参数的方式不一样,是否可以提供一种统一获取请求参数的方式,从而统一doGet和doPost方法内的代码

方案一:根据请求方式的不同分别获取请求参数值

使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值

@WebServlet("/req1") 
public class RequestDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取请求方式(GET 或 POST)
        String method = req.getMethod();

        // 定义一个字符串变量 params,并将其初始化为空字符串(即长度为 0 的字符串,没有任何字符,而不是空格字符串)用于存储请求参数
        String params = "";

        // 根据请求方式获取请求参数
        if("GET".equals(method)){
            // 如果请求方式是 GET,使用 getQueryString() 方法获取 URL 中的查询字符串部分
            params = req.getQueryString();
        } else if("POST".equals(method)){
            // 如果请求方式是 POST,使用 getReader() 方法获取请求体中的数据
            BufferedReader reader = req.getReader();
            // 读取请求体中的第一行数据并存储在 params 变量中
            params = reader.readLine();
        }

        // 将获取到的请求参数打印到服务器控制台
        System.out.println(params);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // POST 请求的处理逻辑委托给 doGet 方法,从而实现 GET 和 POST 请求处理的统一
        this.doGet(req, resp);
    }
}

详细解释

  • "GET".equals(method)
    equals() 方法
    作用:逐个比较字符串的每一个字符。如果所有字符都相同,并且它们的长度也相同,则返回 true;否则返回 false(区分大小写的,也可以与空字符串比较)
    格式:字符串a.equals(字符串b)
    // 比较 str1 和 str2,内容相同,返回 true 
    System.out.println(str1.equals(str2)); // 输出: true
    
  • params = reader.readLine();
    readLine() 方法
    作用:用于从输入流中读取一行文本并返回一个字符串(即当前行的内容),直到遇到行结束符(换行符 \n 或回车符 \r
    输出:
    • 行结束符:readLine() 方法读取到的字符串不包含行结束符(如 \n\r)。
    • 处理空行:如果行是空的,readLine() 返回一个空字符串 "",而不是 null
    • 返回 null:当到达流的末尾时,readLine() 返回 null,这通常用于终止读取循环。

这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用

方案二:调用request提供的方法获取请求参数

request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可

getParameter(String name)

作用:

  • 用于获取请求参数的单个值。包括通过 GET 请求的查询字符串,以及通过 POST 请求的请求体发送的数据。
  • 如果请求中有多个同名参数,它只返回第一个参数的值。

语法格式:

String paramValue = request.getParameter(String name);  //String name是请求参数的名称

返回值:

  • 返回与指定参数名称关联的参数值(String 类型)。
  • 如果请求中不包含该参数,返回 null

getParameterMap()

作用:

  • 用于获取所有请求参数及其对应的值
  • 适用于需要处理多个请求参数的场景,特别是在处理复选框、多选列表等情况下。

语法格式:

Map<String, String[]> paramMap = request.getParameterMap();
//String代表请求参数的名称;String[]代表请求参数的所有值

返回值:

  • 返回一个 Map<String, String[]> 形式的数组,其中键(String)是参数名称,值(String[])是该参数的所有值的数组。同名参数的所有值储存在一个 String[] 数组中。
  • 组成:一个参数名称对应一个String[] 数组,多个参数名称及其对应数组组成一个 Map<String, String[]> 形式的数组
  • 如果请求中没有参数,则返回的 Map 是空的。

示例:

// 从 HttpServletRequest 对象中获取所有请求参数及其值,并存储在一个 Map 中
// Map 的键是参数名称,值是该参数的所有值的数组
Map<String, String[]> paramMap = request.getParameterMap();

// 遍历 Map 中的每个参数项
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
    // 获取参数名称
    String paramName = entry.getKey();

    // 获取该参数对应的所有值(数组形式)
    String[] paramValues = entry.getValue();

    // 将参数名称及其对应的所有值输出到控制台
    // 使用 String.join() 将数组中的多个值连接成一个字符串,使用逗号分隔
    System.out.println("Parameter: " + paramName + ", Values: " + String.join(", ", paramValues));
}

详细解释

  • for (Map.Entry<String, String[]> entry : paramMap.entrySet())
    一个增强型 for 循环(也称为 "for-each" 循环),用于遍历一个 Map<String, String[]> 类型的集合中的每一个键值对,其中键 (String) 是请求参数的名称,值 (String[]) 是该参数对应的一个或多个值。

    • Map.Entry<String, String[]> 接口 是 java.util.Map 接口的一个嵌套接口,它表示 Map 中的一个键值对。Map.Entry 对象包含了 Map 中的一个键和值
    • entrySet() 方法
      定义:是 Map 接口中的一个方法,用于返回 Map 中所有键值对的集合 (Set<Map.Entry<K, V>>)。每个键值对都是 Map.Entry<K, V> 的实例。
      返回值类型:Set<Map.Entry<K, V>>,在这个例子中是 Set<Map.Entry<String, String[]>>
      作用:它提供了一种迭代 Map 中所有键值对的方式。
    • 增强型 for 循环
      用于遍历数组或集合。它的语法如下:
      for (Type variable : collection) {
          // 使用变量
      }
      
      在这个例子中,TypeMap.Entry<String, String[]>variableentrycollectionparamMap.entrySet()
  • String paramName = entry.getKey();
    获取该请求参数的名称
    getKey() 方法 是Map.Entry<K, V> 接口中的一个方法,用于返回当前 entry 对象的键值(key)

  • String[] paramValues = entry.getValue();
    获取该请求参数对应的所有值(数组形式)
    getValue() 方法 是Map.Entry<K, V> 接口中的一个方法,用于返回当前 entry 对象的值(value)

getParameterValues(String name)

作用:用于获取请求参数的所有值。适用于处理复选框、多选列表等场景,多个同名参数的值可以通过这个方法获取。

语法格式:

String[] paramValues = request.getParameterValues(String name);  //String name请求参数的名称

返回值:

  • 返回一个 String[] 数组,包含指定参数名称对应的所有值。
  • 如果该参数不存在,返回 null

注意事项:

  • 适用于获取复选框、多选列表或其他可能会有多个相同参数名的值的场景。
  • 需要对返回的数组进行 null 检查,以避免空指针异常。

示例:

// 从请求对象中获取名为 "hobbies" 的参数的所有值,并将其存储在字符串数组中
// request.getParameterValues("hobbies") 方法用于获取具有相同参数名的多个值
// 例如,如果 URL 是 "/demo?hobbies=reading&hobbies=traveling",那么该方法将返回 ["reading", "traveling"]
String[] hobbies = request.getParameterValues("hobbies");

// 检查 hobbies 数组是否为 null
// 如果用户提交的请求中没有包含 "hobbies" 参数,那么 getParameterValues("hobbies") 会返回 null
if (hobbies != null) {

    // 如果 hobbies 数组不为 null,遍历数组中的每个值
    for (String hobby : hobbies) {

        // 输出每个兴趣爱好的值到控制台
        // 例如,如果 hobbies 数组为 ["reading", "traveling"],将依次输出 "Hobby: reading" 和 "Hobby: traveling"
        System.out.println("Hobby: " + hobby);
    }
}

总结

  • getParameter(String name):用于获取单个请求参数的。适合在知道参数名且预期只有一个值的情况下使用。此方法使用的频率会比较高。
  • getParameterMap():用于获取所有请求参数及其值的映射的数组。适合需要遍历所有请求参数的场景。
  • getParameterValues(String name):用于获取具有相同名称的多个请求参数的。适合在表单中有多个同名参数(如复选框、多选列表)时使用。

这些方法提供了灵活的方式来处理 HTTP 请求中的参数,可以根据具体的需求选择适合的方法。

统一 doGetdoPost 方法的示例代码

通过使用 getParameter()getParameterMap()getParameterValues(),可以在 doGetdoPost 方法中统一处理请求参数,从而简化代码结构。以下是一个示例:

@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 将 GET 请求的处理委托给通用的处理方法 processRequest
        processRequest(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 将 POST 请求的处理委托给通用的处理方法 processRequest
        processRequest(req, resp);
    }

    // 通用的请求处理方法,用于处理 GET 和 POST 请求
    private void processRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取单个请求参数 "param1" 的值
        String param1 = req.getParameter("param1"); 
        
        // 获取所有请求参数的键值对,存储在 Map 中
        Map<String, String[]> paramMap = req.getParameterMap();  // 将请求中的所有参数及其对应的值存储到 paramMap 中
        for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {  // 遍历 Map 中的每个键值对
            String paramName = entry.getKey();  // 获取参数名称
            String[] paramValues = entry.getValue();// 获取参数对应的所有值(如果有多个值)
            System.out.println("Parameter Name: " + paramName + ", Value(s): " + String.join(", ", paramValues));  // 将参数名称和对应的值(以逗号分隔)打印到控制台
        }

        // 设置响应的内容类型为纯文本
        resp.setContentType("text/plain");

        // 向客户端返回处理结果
        resp.getWriter().println("Processed request with unified parameter handling.");
    }
}

可扩展性:如果将来需要在 doGetdoPost 之间有不同的处理逻辑,可以在这些方法中单独处理,然后调用 processRequest 方法完成共同的处理部分。

说明

  • processRequest() 方法:通过将 doGetdoPost 的处理逻辑提取到一个统一的方法中,实现代码的复用。这个方法处理 GETPOST 请求中的参数。
  • getParameter()getParameterMap() 的使用:这些方法由 HttpServletRequest 提供,统一了 GETPOST 请求参数的处理,无需开发者关心具体的请求方法。
  • 如果将来需要在 doGetdoPost 之间有不同的处理逻辑,可以在这些方法中单独处理,然后调用 processRequest 方法完成共同的处理部分。

3.3、IDEA快速创建模板

3.3.1、创建自定义的模板

创建自定义的 Live Templates 模板

  1. 在 IntelliJ IDEA 中,点击 File(文件) → Settings(设置)(在 Windows/Linux 上)
  2. 创建一个新的模板组(可选)
    如果要为模板创建一个新的分组,可以:
    1. 在设置窗口中,导航到 Editor(编辑器) → Live Templates(实时模板)
    2. 点击 “+”图标,在弹出的列表中选择 Template Group...(模板组) ,在弹出的窗口中,可以为模板组命名,点击 “OK” 创建新的模板组。
  3. 创建新的模板
    接下来,在刚才创建的组(或选择一个现有的组)中添加一个新的模板:
    1. 点击选中 新创建的模板组,再点击 “+”图标,在弹出的列表中选择 Live Template(实时模板)
    2. Abbreviation(缩写) 栏中输入一个简短的模板名称。这是在代码中触发模板的关键字
    3. Description(描述) 栏中输入描述
  4. Template text(模板文本) 中输入模板内容:
    • 在自定义的模板中,$...$ 是可替换的变量
    • $END$ 是 IDEA 的光标结束标记,表示代码生成后光标会停留在这里,方便你继续输入代码
  5. 添加变量和默认值
    1. 点击 Edit variables...(编辑变量) 按钮
    2. 对于 $...$ 变量,在 Expression(表达式) 列中可以设置各个变量的表达式(根据具体情况自动填充)、默认值(变量默认会被替换为该值)或 留空(将被提示输入这个变量的值)
  6. 设置适用范围
    选中要设置适用范围的模板,点击蓝色字“更改”,在弹出的列表中,可以勾选适用范围
  7. 点击 OK 保存模板。
  8. 使用模板
    1. 在项目中创建一个新的类,或者在现有的类中开始输入。
    2. 输入你定义的模板缩写,然后按 Tab 键,模板内容将自动展开并生成相应的代码。

创建自定义的 File and Code Templates 模板

  1. 在 IntelliJ IDEA 中,点击 File(文件) → Settings(设置)(在 Windows/Linux 上)
  2. 创建新的 Files(文件) 模板
    1. 在设置窗口中,导航到 Editor(编辑器) → File and Code Templates(文件与代码模板)
    2. File and Code Templates 设置窗口中,选择 Files 选项卡,表示你要创建一个文件级别的模板。
    3. 点击 + 按钮来添加一个新的模板。
  3. 设置新模板的文件名称、扩展名、文件名
  4. 编辑模板内容
    1. 选择刚刚创建的模板,在编辑区域中输入你想要的模板内容。在这里可以编写整个文件的结构和内容。
    2. 如果你想添加其他动态元素,可以使用 Velocity 模板语言的语法。
  5. 点击 ApplyOK 按钮保存模板。
  6. 使用模板
    1. 使用创建的自定义模板,右键点击项目中的 src 文件夹或目标包,选择 New → 命名的模板名称
    2. 在弹出的对话框中,输入新类的名称并点击 OK
    3. IntelliJ IDEA 会使用你定义的模板生成相应的文件,并替换模板中的占位符为实际值。
  7. (可选)创建新的 Include 模板
    Include 模板是一种可复用的模板片段,可以在其他 Files模板中被引用。
    1. File and Code Templates 设置窗口中,选择 Includes 选项卡。
    2. 点击 + 按钮,创建一个新的 Include 模板。
    3. 输入模板名称和内容,然后在其他 Files模板中使用 #parse("Include模板名称") 来引用这个 Include 模板。

3.3.2、修改已有的模板内容

修改 Live Template 模板内容
如果你已经使用 Live Templates 创建了一个自定义的 Servlet 模板,并且想修改它的内容,请按照以下步骤操作:

  1. 打开设置(Settings)
  2. 导航到 Live Templates
  3. 查找并选择要修改的模板。
  4. 修改模板内容
  • 在右侧的 Template text 区域中,修改模板的内容。你可以更改代码结构、添加新的变量或者修改已有变量的默认值。
  • 如果需要,还可以点击 Edit variables... 来修改变量的定义。
  1. 保存更改

修改 IntelliJ 默认的 Servlet 生成模板
如果你想修改 IntelliJ IDEA 默认提供的 Servlet 创建模板,请按照以下步骤操作:

  1. 打开设置(Settings)
  2. 导航到 File and Code Templates
  3. 查找 Servlet 模板
  • 在左侧的列表中,找到 Code 下的 Class 模板或其他相关模板(如 Servlet)。
  • 你可能需要展开 File and Code Templates 部分,然后查看 Code 下的各个模板。
  1. 修改模板内容
  • 选择要修改的模板(如 Servlet),然后在右侧的编辑器中修改模板内容。
  • 你可以在模板中使用预定义的变量(如 NAMEDATE 等),也可以添加自定义代码。
  1. 保存更改

3.3.3、File and Code Templates 和 Live Templates 区别

IntelliJ IDEA 中的 File and Code TemplatesLive Templates 都是用于自动生成代码的工具,但它们在用途和应用场景上有所不同。以下是它们的主要区别:

File and Code Templates中的 File模板:

  • 用途:用于生成整个文件或代码结构,通常在创建新文件时使用。
  • 应用场景;适用于生成类、接口、配置文件、HTML 文件等完整的文件内容。例如,当你创建一个新的 Java 类、Servlet、XML 文件时,可以使用 File and Code Templates 来定义这些文件的默认结构。
  • 触发方式:在创建新文件或类时,通过选择特定的模板来生成文件内容。通常通过 New FileNew Class 等操作触发。
  • 位置:模板被应用到整个文件的内容,而不是插入到已有的代码中。
  • 示例:创建一个新的 Java 类时,模板会自动生成类声明、包声明以及可能的注释等。

File and Code Templates中的 Include模板:

  • 用途:用于在其他 File and Code Templates 中的 File模板内容 中复用一段代码片段。它通常用于定义一些可以在多个 File模板中共享的通用代码块或结构。
  • 引用方式:Include 模板通常在其他 File and Code Templates 中的 File模板内容 中通过 #parse("Include模板名称") 来被引用,不能直接在代码编辑器中使用。

Live Templates模板:

  • 用途:用于在现有文件或代码中插入常用的代码片段,类似于代码片段的快捷方式。
  • 应用场景:适用于快速插入常用的代码模式、结构或语法块,例如 for 循环、try-catch 块、println 语句等。
  • 触发方式:在代码编辑器中通过输入特定的快捷符号或关键字,按下 Tab 键或其他触发键来生成代码片段。适用于在现有文件中快速插入代码。
  • 位置:模板内容被插入到光标所在位置,并可以嵌入到现有代码中。
  • 示例:你可以通过输入 模板缩写名(如 sout)然后按 Tab 键快速插入 System.out.println("");

这两者结合使用,可以大大提高编码效率,减少重复劳动。

3.4、请求参数中文乱码问题

POST 请求乱码

形成过程
服务器(如 Tomcat)默认使用 ISO-8859-1 编码来解码请求参数,因此当通过 request.getParameter()request.getReader() 获取 POST 请求中的 中文字符 参数时,就会导致解码错误,进而出现中文乱码问题。

解决方法

在处理请求之前设置正确的字符编码方式,通常是 UTF-8,在 doPost 方法中,添加以下代码来设置请求的编码方式:

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 设置请求体的字符编码为 UTF-8
    request.setCharacterEncoding("UTF-8");
    this.doGet(request, response);
}
  • setCharacterEncoding() 方法 用于设置请求或响应的字符编码,用于确保服务器能够正确地解析和处理请求或者响应

GET 请求乱码

形成过程

  1. 浏览器编码:当用户在浏览器的地址栏输入中文字符或通过表单提交包含中文的 GET 请求时,浏览器会对这些字符进行 URL 编码。浏览器通常使用 UTF-8 编码将这些中文字符转换为 URL 编码形式,并通过 GET 请求发送给服务器。
  2. 服务器解码:服务器(如 Tomcat)默认使用 ISO-8859-1 编码来解码 UTF-8 编码的参数,最终生成一串乱码。

解决方法

  1. 全局设置服务器的 URI 编码

在 Tomcat 的 server.xml 文件中,设置 ConnectorURIEncoding 属性为 UTF-8

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8"/>

这样,Tomcat 会在解码 URL 参数时使用 UTF-8 编码,从而正确处理中文字符。

  1. 手动解码

如果你无法修改服务器的配置,可以在代码中手动将 ISO-8859-1 解码后的字符串重新编码为 UTF-8。例如:

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 手动将 ISO-8859-1 编码的参数转换为 UTF-8
    String username = new String(request.getParameter("username").getBytes("ISO-8859-1"), "UTF-8");
    System.out.println(username);
}
  • getBytes("ISO-8859-1")
    getBytes(字符集名称) 方法:用于将指定将字符串(按照 字符集名称)编码成字节 数组,该字符串是用 字符集名称 解码的
  • new String(request.getParameter("username").getBytes("ISO-8859-1"), "UTF-8");
    new String(byte[] bytes, 字符集名称) 方法 用于将字节数组(bytes)按照指定的 字符集名称 解码成字符串 (前提:这个字节数组bytes 是按照指定 字符集名称 编码的字符数据)

解码是将文本字符串转换为二进制数据,而编码将二进制数据转换为文本字符串

  • 编码:文本字符串 → 二进制数据
  • 解码:二进制数据 → 文本字符串

Tomcat 8.0 及其之后的版本已经将 GET 请求的默认解码方式 设置为 UTF-8,因此在大多数情况下,使用这些版本的 Tomcat 不再需要手动设置 GET 请求的字符编码来避免乱码问题

3.5、Request请求转发

定义:Java Web 开发中的一种机制,允许在服务器端将请求从一个资源(如 Servlet 或 JSP)转发到另一个资源处理。

典型应用场景:包括在一个 Servlet 中处理部分请求逻辑,然后将请求转发给另一个 Servlet 或 JSP 来完成响应的生成。

流程示意 image.png

  1. 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
  2. 资源A处理完请求后将请求发给资源B
  3. 资源B处理完后将结果响应给浏览器
  4. 请求从资源A到资源B的过程就叫请求转发

请求转发的特点

  • 服务器端行为:请求转发完全发生在服务器端,客户端(浏览器)对转发行为是不可见的。客户端只知道它发送了一个请求并收到了一个响应,但它不会知道服务器在处理请求时进行了转发。
  • URL 不变:当请求被转发时,浏览器地址栏中的 URL 不会发生变化。这意味着从客户端的角度来看,它始终是在与同一个资源交互。
  • 共享同一个请求和响应对象:请求转发时,转发后的资源可以访问同一个 HttpServletRequest 对象,这意味着可以共享请求中的数据(如参数、属性)。
    整个处理链条中的所有 Servlet 和 JSP 都在操作相同的 HttpServletRequestHttpServletResponse 对象。任何在请求或响应对象上的更改都可以被链条中的其他资源看到和使用。
  • 不再返回客户端:转发操作后,控制权移交给了另一个资源,当前 Servlet 的代码不会再继续执行。因此,请求转发一般是 Servlet 的最后一个操作。
  • 只能转发到当前服务器的内部资源,不能从一个服务器通过转发访问另一台服务器

请求转发的实现方式

请求转发是通过 RequestDispatcher 对象完成的。以下是实现步骤:

  1. 获取 RequestDispatcher 对象
  • 使用 HttpServletRequest 对象的 getRequestDispatcher(String path) 方法获取 RequestDispatcher 对象。

    Re1questDispatcher dispatcher = request.getRequestDispatcher("/targetResource");
    

    其中 "/targetResource" 是相对于当前应用上下文的路径,可以是一个 Servlet 或 JSP 的路径。

  1. 调用 forward 方法
  • 使用 RequestDispatcher 对象的 forward(HttpServletRequest request, HttpServletResponse response) 方法进行请求转发。

    dispatcher.forward(request, response);
    

示例代码

  1. 在FirstDemo的doGet方法中进行请求转发
@WebServlet("/firstServlet")
public class FirstDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        // 输出调试信息到控制台,确认请求到达此Servlet
        System.out.println("firstdemo...");

        // 在请求对象中设置一个请求属性,键为 "message",值为 "Hello from the first servlet!"
        // 这个属性可以在请求转发到的目标Servlet中访问
        request.setAttribute("message", "Hello from the first servlet!");

        // 获取RequestDispatcher对象,将请求转发到另一个Servlet,路径为 "/afterServlet"。
        request.getRequestDispatcher("/afterServlet").forward(request, response);

        // 注意:forward之后的代码不会被执行,因为请求处理已经移交给了目标Servlet。
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

这个Servlet负责处理路径为 "/firstServlet" 的请求。它接收客户端的GET或POST请求,输出一个调试信息,设置一个请求属性 "message" 以便在下一个 Servlet(AfterDemo) 中使用,然后将请求转发到下一个 Servlet 进行进一步的处理。

  1. 接收请求的AfterDemo类
@WebServlet("/afterServlet")
public class AfterDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 输出调试信息到控制台,确认请求到达此Servlet
        System.out.println("afterdemo...");

        // 获取请求属性 "message"
        String message = (String) request.getAttribute("message");

        // 处理 "message" 属性:打印到控制台
        System.out.println("Received message: " + message);

        // 处理 "message" 属性:使用它生成响应
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("<html><body>");
        response.getWriter().write("<h1>Message from RequestDemo5: " + message + "</h1>");
        response.getWriter().write("</body></html>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}
  1. 启动测试

    访问http://localhost:8080/request-demo/firstServlet,就可以在控制台看到如下内容:

  • 控制台输出的内容:
    在 FirstDemo 中:

    firstdemo...
    

    在 AfterDemo 中:

    afterdemo...
    Received message: Hello from the first servlet!
    
  • 浏览器呈现的内容:
    Message from RequestDemo5: Hello from the first servlet!,以标题的形式(大号字体)显示在浏览器窗口

请求转发的两种写法:分步实现 与 链式调用

  • 分步实现:
    RequestDispatcher dispatcher = request.getRequestDispatcher("/afterServlet");
    dispatcher.forward(request, response);
    
    说明:在这种写法中,RequestDispatcher 对象首先被获取并赋值给变量 dispatcher,然后在下一行中调用 forward(request, response) 方法进行请求转发
    适用:适合在代码中需要对 dispatcher 变量进行进一步处理或调试时使用。例如,可能在获取 dispatcher 之后进行一些日志记录。
    优点
    • 更容易调试:可以在中间步骤中插入调试信息或进行断点调试。
    • 更清晰的逻辑:在更复杂的场景下,分步实现可能有助于清晰地表达代码的意图,特别是当需要对 RequestDispatcher 进行一些额外操作时。
  • 链式调用:
    request.getRequestDispatcher("/afterServlet").forward(request,response);
    
    说明:直接在一行中完成 RequestDispatcher 对象的获取和请求转发操作。
    适用:在简单的场景中使用,代码更紧凑。
    优点:更简洁、直观

使用场景

  • 逻辑处理和视图展示分离
    • 在 MVC 模式中,通常会有一个 Servlet 负责处理请求的业务逻辑,然后将结果通过请求转发的方式交给 JSP 页面进行展示。
  • 多步请求处理
    • 在某些复杂的请求处理中,可能需要多个 Servlet 或 JSP 协作完成工作。在这种情况下,可以通过请求转发将请求从一个资源传递到另一个资源。

3.6、request应用(可选)

image.png
  • 这个 index.jsp 页面是一个简单的登录表单,用户可以输入用户名、密码,并选择爱好。表单通过 POST 方法提交到 /login,由后端的 LoginServlet 处理。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<div style="text-align: center">
    <!-- 创建一个表单,使用 POST 方法提交到 /login -->
    <form action="${pageContext.request.contextPath}/login" method="post">
        用户名:<input type="text" name="username"/><br/> <!-- 输入用户名的文本框 -->
        密码:<input type="password" name="pwd"/><br/>    <!-- 输入密码的密码框 -->
        爱好:                                            <!-- 选择用户爱好的复选框 -->
        <input type="checkbox" name="hobbies" value="看电影"/>看电影
        <input type="checkbox" name="hobbies" value="阅读"/>阅读
        <input type="checkbox" name="hobbies" value="爬山"/>爬山
        <input type="checkbox" name="hobbies" value="摄影"/>摄影
        <br/>
        <input type="submit"/> <!-- 提交表单的按钮 -->
    </form>
</div>

</body>
</html>
  • 这个 success.jsp 页面是登录成功后展示给用户的简单页面,显示 "登录成功"。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>success</title> <!-- 页面标题为 "success" -->
</head>
<body>
<h1>登录成功</h1> <!-- 显示成功登录的消息 -->
</body>
</html>
  • 这个 LoginServlet 是一个处理用户登录请求的 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 java.io.IOException;
import java.util.Arrays;

// 使用 @WebServlet 注解来配置 Servlet 和 URL 映射
@WebServlet(name = "login", urlPatterns = "/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置请求和响应的字符编码为 UTF-8,以避免中文乱码问题
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");

        // 获取用户输入的用户名
        String username = req.getParameter("username");
        // 获取用户输入的密码
        String pwd = req.getParameter("pwd");
        // 获取用户选择的爱好,可以选择多个,所以用数组存储
        String[] hobbies = req.getParameterValues("hobbies");

        // 打印调试信息
        System.out.println("======================");
        System.out.println(username); // 打印用户名
        System.out.println(pwd); // 打印密码
        System.out.println(Arrays.toString(hobbies)); // 打印用户选择的爱好
        System.out.println("======================");

        // 将请求转发到 success.jsp 页面进行响应
        // 使用请求转发而不是重定向,可以保留请求中的数据
        req.getRequestDispatcher("/success.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // doPost 方法调用 doGet 方法,以处理 POST 请求
        doGet(req, resp);
    }
}

解释:

  • 通过 req.getParameter("username") 等方法获取用户提交的表单数据。
  • req.getParameterValues("hobbies") 获取用户选择的多个爱好,并将它们作为字符串数组处理。
  • 通过 System.out.println 打印调试信息,方便查看提交的用户名、密码和爱好。
  • 使用 req.getRequestDispatcher("/success.jsp").forward(req, resp); 将请求转发到 success.jsp 页面,显示登录成功信息。
  • doPost 方法调用 doGet,以便处理 POST 请求,使其逻辑与 GET 请求保持一致。

总结: 这段代码实现了一个简单的用户登录表单,用户提交表单后,后台通过 Servlet 处理请求,提取用户输入的信息并转发到成功页面。

4、Response对象

4.1、Response继承体系

image.png

4.2、Response设置响应数据

响应数据包含三块内容,分别是响应行响应头响应体

1. 响应行 image.png

设置响应状态码
方法:void setStatus(int sc);
说明:

  • 用于设置 HTTP 响应的状态码。状态码sc 是一个三位数字,它告诉客户端(如浏览器)服务器如何处理请求的结果。
  • 例如,200 表示请求成功,404 表示请求的资源未找到,500 表示服务器内部错误等。

2. 响应头 image.png

设置响应头键值对
方法:void setHeader(Stringname,Stringvalue);
说明:

  • 用于向 HTTP 响应中添加或覆盖一个特定的响应头。如果该头部已经存在,则会覆盖其原有的值。
  • name 表示要设置的 HTTP 响应头的名称,value 表示响应头对应的值。

3. 响应体
响应体通常包含 HTML、JSON、XML、文件、图片等。为了将这些数据发送到客户端,响应体数据有 两种主要的输出流:字符输出流(文本数据,如 HTML、纯文本、JSON、XML 等)和 字节输出流(二进制数据,如文件、图片、视频、PDF 等)。

获取字符输出流
方法:getWriter()
说明:

  • 用于获取一个字符输出流(PrintWriter对象),向客户端发送文本数据(如 HTML、JSON、XML、纯文本等)。
    PrintWriter out = response.getWriter();
    

获取字节输出流
方法:getOutputStream()
说明:

  • 用于获取一个字节输出流 (ServletOutputStream对象),用于向客户端发送二进制数据(如文件、图片、视频、音频等)。
    ServletOutputStream out = response.getOutputStream();
    

4.3、Respones请求重定向

定义:服务器在接收到客户端的请求后,不直接处理和返回响应,而是指示客户端向另一个URL发起新的请求。这种机制通常用于将用户引导到不同的页面或资源。

典型应用场景:在用户登录后重定向到主页,或在操作成功后重定向到确认页面。

流程示意 image.png

  1. 客户端请求:客户端发送一个HTTP请求到服务器。
  2. 服务器响应:服务器接收到请求后,调用sendRedirect()方法,并在HTTP响应中设置状态码302 Found(或有时用303 See Other),以及Location头来指定新URL。
  3. 客户端重新请求:客户端收到302响应后,自动向Location头中指定的URL发起新的HTTP请求。
  4. 新请求处理:服务器处理新的请求,并返回最终的响应。

重定向的特点

  • 客户端参与:重定向过程需要客户端的参与,即客户端会接收到重定向指令,并发起新的HTTP请求。
  • 请求数据丢失:由于这是一个新的请求,因此所有的原始请求数据(如参数、表单数据)不会自动传递到新的请求中,除非你将它们附加到新的URL中。
  • URL变化:客户端浏览器的地址栏会更新为重定向后的URL。这是与请求转发的主要区别。
  • 状态码302:重定向通常使用HTTP状态码302(临时重定向)。303状态码也可以用于指示客户端使用GET方法访问新的URL。如果想要永久重定向,可以使用301状态码,但这需要手动设置响应状态码。
  • 代码执行:重定向操作之后的代码仍然会继续执行,并不会终止当前的请求处理流程。

重定向的实现方式

方式一:

response.setHeader("Location", "/newPage");
response.setStatus(302);       // 必须手动设置重定向状态码

无论是先设置状态码再设置头部,还是先设置头部再设置状态码,浏览器都能够正确处理重定向。通常建议后者,可以让阅读代码的人更容易理解代码的意图。

方式二:

  • 方法:response.sendRedirect("/newPage");
  • 作用:用于发送一个重定向响应,自动设置响应状态码为 302(或 303/307)并将 Location 头设置为指定的URL/newPage,从而指示客户端(如浏览器)重定向到这个新的URL。
  • 内部机制sendRedirect 实际上是 setHeadersetStatus 的一个组合。它自动完成了重定向所需的操作,即设置 Location 头并调整状态码为 302

示例代码

  1. 在ResponseDemo1的doGet方法中给前端响应数据
@WebServlet("/resource1")
public class ResponseDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 输出调试信息到控制台,确认请求到达此Servlet
        System.out.println("resource1....");

        // 实现重定向:
        // 1. 设置响应状态码为 302,表示临时重定向
        response.setStatus(302);

        // 2. 设置响应头 Location,指定重定向的目标 URL
        // 客户端会自动向这个新的 URL (/request-demo/resource2) 发起请求
        response.setHeader("Location", "/request-demo/resource2");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 对 POST 请求的处理与 GET 请求相同
        this.doGet(request, response);
    }
}
  1. 创建ResponseDemo2类
@WebServlet("/resource2")
public class ResponseDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 输出调试信息到控制台,确认请求到达此Servlet
        System.out.println("resource2....");

        // 可以在这里生成并返回响应给客户端,例如输出 HTML 内容
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("<html><body><h1>You have been redirected to resource2</h1></body></html>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 对 POST 请求的处理与 GET 请求相同
        this.doGet(request, response);
    }
}
  1. 启动测试

访问http://localhost:8080/request-demo/resource1,就可以在控制台看到如下内容:

resource1....
resource2....
  • resource1.... 表示第一次请求成功到达 ResponseDemo1 Servlet。
  • resource2.... 表示经过重定向后,新的请求成功到达 ResponseDemo2 Servlet。

使用场景

  • 用户登录后的页面跳转
    • 登录表单处理可能需要提交 POST 请求,而重定向可以避免表单重复提交的问题,同时引导用户到一个新的 URL。
  • 页面重构或迁移
    • 当网站进行重构或迁移,某些页面的 URL 可能会改变。为了不影响用户访问旧的 URL,可以将旧的 URL 重定向到新的 URL。

4.4、转发与重定向中的路径处理

1. 转发与重定向的区别

转发

  • 路径解析:因为转发操作是在服务器内部完成的,它会基于当前请求的上下文路径来解析目标路径。因此,在转发路径中不需要加虚拟目录,服务器能够自动识别并补全路径。

  • 示例

    request.getRequestDispatcher("/resource2").forward(request, response);
    
    • 假设当前请求路径是 /request-demo/resource1,服务器会自动理解 /resource2 是相对于当前Web应用的路径 /request-demo 的,无需手动添加虚拟目录。

重定向

  • 路径解析:因为重定向是由客户端发起的新请求,浏览器只知道服务器的根路径,而不知道Web应用的虚拟目录。因此,在重定向时,路径中必须包含虚拟目录,以确保浏览器能够正确地解析并发起请求。
  • 示例
    response.sendRedirect("/request-demo/resource2");
    
    • 如果你不加虚拟目录 /request-demo,浏览器可能会认为 /resource2 是服务器根路径下的资源,而不是当前Web应用下的资源,从而导致请求错误。

2. 简单记忆规则

  • 浏览器(客户端)发起请求:需要加虚拟目录。(如重定向)
  • 服务器(服务端)发起请求:不需要加虚拟目录。(如转发)

案例

<a href='路劲'> 超链接,从浏览器发送,需要加
<form action='路径'> 表单,从浏览器发送,需要加
req.getRequestDispatcher("路径") 转发,是从服务器内部跳转,不需要加
resp.sendRedirect("路径") 重定向,是由浏览器进行跳转,需要加

3. 动态管理Web应用中的重定向路径

在Web开发中,重定向操作常常需要指定目标路径。在很多情况下,开发者可能会直接在代码中硬编码项目的虚拟目录(即项目访问路径)。然而,这种硬编码方式在项目部署后,如果虚拟目录需要更改,就会导致大量代码需要手动修改,增加了维护成本。

解决办法:动态获取上下文路径

通过 HttpServletRequest 对象获取当前Web应用的上下文路径,并在代码中动态构建重定向路径。

String contextPath = request.getContextPath();   //将上下文路径赋值给变量 contextPath
response.sendRedirect(contextPath + "/resp2");  //将客户端(浏览器)重定向到当前Web应用的 "/resp2"资源

4.5、Response响应字符数据

PrintWriter 是用于发送文本数据的输出流,适用于发送HTML、JSON、纯文本等字符数据。

步骤 1:设置响应的内容类型

在将字符数据写回浏览器之前,首先需要设置HTTP响应的内容类型(Content-Type),以便客户端知道如何处理和显示接收到的数据。

  • 方法:setContentType(String type)
  • 说明:用于设置响应内容类型与字符编码(可选),参数type表示响应的文件类型以及字符编码
    • text/html:表示返回的 文件类型 是HTML文档
    • text/plain:表示返回的 文件类型 是纯文本
    • application/json:表示返回的 文件类型 是JSON数据
    • charset=UTF-8:指定字符编码。UTF-8是通用字符编码,能够正确显示多语言字符
response.setContentType("text/html;charset=UTF-8");

步骤 2:获取字符输出流

通过 response.getWriter() 获取 PrintWriter 对象,这是向客户端输出文本数据的主要工具。

  • 方法:getWriter()
  • 说明:获取(返回)一个字符输出流 (PrintWriter 对象)
PrintWriter out = response.getWriter();

步骤 3:写入字符数据到响应体

使用 print()println() 方法将字符数据写入响应体。写入的内容会被发送到客户端并在浏览器中显示。

out.print("Hello, World!");  // 发送简单文本数据
out.println("<h1>Title</h1>");  // 发送HTML数据并换行

发送数据的两种写法:分步实现 与 链式调用

方式一:分步实现,使用变量 out

PrintWriter out = response.getWriter();
out.print("名字:" + username);
out.flush(); 
  • 如果需要多次输出,使用变量更方便
  • flush() 方法用于强制将缓冲区中的数据立即写入到输出流中,发送到客户端。

方式二:直链式调用 response.getWriter()

response.getWriter().print("名字:" + username);

直接使用 response.getWriter() 获取 PrintWriter,并立即调用 print() 方法

示例代码

response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("<html><body>");
out.print("<h1>Hello, World!</h1>");
out.print("</body></html>");

4.6、Response响应字节数据

ServletOutputStream 是用于发送二进制数据的输出流,适用于发送图片、文件、PDF等二进制内容。

步骤 1:设置响应的内容类型

在发送二进制数据之前,必须设置响应的 Content-Type,以便客户端正确处理接收到的数据。

response.setContentType("image/jpeg");
  • 二进制数据的类型:
    • application/pdf:表示返回的是PDF文件。
    • image/jpeg:表示返回的是JPEG图片。
    • application/octet-stream:表示通用的二进制文件下载。

步骤 2:获取字节输出流

通过 response.getOutputStream() 获取 ServletOutputStream 对象,这是向客户端输出二进制数据的工具。

  • 方法:getOutputStream()
  • 说明:
    用于获取一个字节输出流 (ServletOutputStream对象),用于向客户端发送二进制数据(如文件、图片、视频、音频等)。
    ServletOutputStream out = response.getOutputStream();
    

步骤 3:写入二进制数据到响应体

  • 方法:Paths.get(String first, String... more)
  • 说明:
    • 该方法用于将一个或多个路径字符串组合成一个完整的路径,并返回一个表示该路径的 Path 对象。Path 对象可以表示文件系统中的路径,既可以是相对路径,也可以是绝对路径。
    • first:路径的第一个部分,通常是根路径、文件夹路径,或者完整路径的起始部分。
    • more:可选的额外路径部分,可以包含多个路径片段。Paths.get() 方法会将这些路径片段依次拼接到 first 之后,形成一个完整的路径。
  • 示例:
    // 创建一个路径对象
    Path path = Paths.get("C:", "Users", "username", "Documents", "file.txt");
    //或者,Path path = Paths.get("C:/Users/Alice/Documents/file.txt")
    
    System.out.println(path.toString());  // 输出:C:\Users\username\Documents\file.txt
    
  • 两种写法
    • Paths.get("/path/to/your/image.jpg") :适用于你已经有完整路径字符串的情况,直接传入即可。
    • Paths.get("/path", "to", "your", "image.jpg") :适用于需要拼接多个路径片段的情况,并且更具有跨平台兼容性。

  • 方法:Files.readAllBytes(Path path)
  • 说明:
    • 该方法用于读取指定路径表示的文件的所有字节,并将这些字节作为字节数组返回。如果文件为空,则返回一个长度为 0 的数组。
    • 适用于需要将整个文件内容一次性加载到内存中的情况,常用于读取小型文件。
    • path:表示文件路径的 Path 对象。该路径可以是相对路径或绝对路径,指向要读取的文件。如果文件路径无效(例如文件不存在或路径指向的不是文件),将抛出相应的异常。
  • 异常:
    • IOException:如果发生 I/O 错误,例如文件不存在或无法读取。
    • SecurityException:如果存在读取文件的权限问题。
    • OutOfMemoryError:如果文件非常大,无法在内存中容纳整个文件的内容。
  • 示例:
    // 使用 Paths.get() 获取文件路径
    Path path = Paths.get("C:", "Users", "username", "Documents", "file.txt");
    
    try {
        // 读取文件的所有字节
        byte[] fileContent = Files.readAllBytes(path);
        System.out.println("File content length: " + fileContent.length);
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    在这个示例中,Files.readAllBytes() 读取指定路径文件的所有字节内容,并将其存储在一个字节数组中。如果读取成功,程序会输出文件内容的字节长度。
  • 使用场景:
    适用于一次性读取整个文件内容的情况,尤其当文件较小且可以安全地加载到内存中时。读取到的字节数组可以方便地转换为字符串或其他数据类型。在处理大文件时,谨慎使用此方法。

这两个方法常常一起使用,比如先用 Paths.get() 获取文件路径,然后用 Files.readAllBytes() 读取该文件的内容。如下“分步实现”。

示例代码

分步实现

response.setContentType("image/jpeg");
ServletOutputStream out = response.getOutputStream();
byte[] imageData = Files.readAllBytes(Paths.get("/path/to/your/image.jpg")); // 读取图片的二进制数据
out.write(imageData);  // 将图片数据写入响应体

或者

链式调用

response.setContentType("image/jpeg");
response.getOutputStream().write(Files.readAllBytes(Paths.get("/path/to/your/image.jpg"))); // 读取并发送图片数据

4.6.1、response下载文件

一、功能介绍

在 Web 应用中,实现文件下载的过程通常包括以下几个关键步骤:

  1. 获取下载文件的路径:确定服务器上需要下载的文件的物理路径。
  2. 获取下载的文件名:提取文件的名称,尤其是处理带有中文字符的文件名时,确保文件名不会出现乱码。
  3. 设置浏览器下载支持:设置响应头信息,确保浏览器能够识别并下载文件。
  4. 读取文件并输出:将文件内容通过输出流发送给客户端。

二、代码实现

以下是实现文件下载功能的完整 Servlet 代码示例:

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

        // 1. 获取下载文件的路径
        String realPath = "D:\\IdeaProjects\\javaweb-project-maven\\servlet-response\\src\\main\\resources\\哈哈.jpg";
        
        // 2. 获取下载的文件名
        String fileName = realPath.substring(realPath.lastIndexOf("\\") + 1);
        
        // 3. 设置响应头,确保浏览器能够处理下载请求,解决文件名中文乱码问题
        resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        
        // 4. 获取下载文件的输入流
        FileInputStream in = new FileInputStream(realPath);

        // 5. 创建缓冲区
        byte[] buffer = new byte[1024];
        int len;

        // 6. 获取 Servlet 的输出流对象 ServletOutputStream,用于将文件内容发送给客户端
        ServletOutputStream out = resp.getOutputStream();

        // 7. 将输入流的数据写入到缓冲区,然后通过输出流发送给客户端
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }

        // 8. 刷新并关闭流
        out.flush();
        out.close();
        in.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 将POST请求委托给doGet方法处理
        doGet(req, resp);
    }
}

详细解释

  • String realPath = "D:\\IdeaProjects\\javaweb-project-maven\\servlet-response\\src\\main\\resources\\哈哈.jpg";
    将一个表示文件路径的字符串赋值给变量 realPath,该路径指向 D: 盘上的一个项目目录下的 哈哈.jpg 文件。

    • String realPath 中的路径是一个硬编码的字符串路径,但由于路径分隔符不同,同一路径在其他操作系统上可能无法直接使用;不支持动态拼接
    • 通过 Paths.get() 方法可以在不同操作系统上工作;自动处理路径分隔符。
  • String fileName = realPath.substring(realPath.lastIndexOf("\\") + 1);
    从一个完整的文件路径 realPath 中提取出最后的文件名部分

    • 字符串路径的变量.lastIndexOf("字符(串)")( + n,可选) 方法 :返回指定子字符(串)在当前字符串(字符串路径的变量)中最后一次出现的索引位置(再在索引的基础上加 n 的位置,可选)
    • 字符串路径的变量.substring(int beginIndex) 方法 :用于截取字符串的一部分,从指定的 索引beginIndex 开始,一直到字符串的末尾。
  • resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
    设置 HTTP 响应头 Content-Disposition,指示浏览器以附件形式下载的方式处理响应内容,并且指定下载的文件名。

    • Content-Disposition 响应头 : 这是 HTTP 响应头的名称,指示浏览器如何处理响应的内容。它常用于控制内容是否以附件形式下载或直接显示。
      • 常用语法如下:
      Content-Dispositio: 内容的处理方式; filename = "下载时的默认文件名"
      //内容的处理方式、filename = "下载时的默认文件名" 都是字符串
      
    • attachment;filename=: 这是 Content-Disposition 头的值的一部分。attachment 表示内容应作为附件下载,而不是直接显示。filename= 后面的内容指定了下载时保存文件的名称。
    • + 是 Java 中的字符串连接操作符:用于将 "attachment;filename="URLEncoder.encode(fileName, "UTF-8") 的结果连接成一个完整的字符串 作为Content-Disposition 头的值。
    • URLEncoder.encode(String s, String enc) 方法:用于将字符串编码成 URL 编码格式,以便在 HTTP 头中安全传输。
      • 字符串 s:这是需要编码的字符串。通常,这个字符串包含普通文本或路径参数,可能包括需要编码的特殊字符。
      • 字符串 enc:这是字符编码方案的名称,例如 "UTF-8"。指定使用哪种字符集来编码字符串。
      • 返回值:返回一个 String,表示编码后的字符串。这个字符串适合在 URL 中使用
    • fileName: 这是要下载的文件的名称。在传输过程中,文件名可能包含特殊字符(如空格、中文等),这些字符在 URL 中可能导致问题,因此需要编码。
    • "UTF-8" : 指定编码类型为 UTF-8,这是最常用的编码方式之一,能够正确处理多种语言字符,包括中文。
  • FileInputStream in = new FileInputStream(realPath);
    这段代码创建了一个 FileInputStream 对象,其引用被 in 持有,并将其与指定路径 realPath 的文件相关联。

    • FileInputStream 类:是 Java 中的一个类,用于以字节流的方式从文件中读取数据。
    • realPath:这是一个字符串变量,表示文件的路径。它可以是绝对路径,也可以是相对路径。路径所指的文件将被 FileInputStream 打开,以便从中读取数据。
  • byte[] buffer = new byte[1024];
    声明并创建了一个大小为 1024 字节的字节数组 buffer作为缓冲区,用于在读取和写入时暂存数据。

  • while ((len = in.read(buffer)) > 0) {
    out.write(buffer, 0, len);
    }

    通过循环读取输入流的数据,将其写入到输出流中。每次读取 buffer 大小的数据,并写入到客户端,直到输入流中没有更多的数据可读为止。

    • InputStream.read() 方法:用于将输入流中的字节数据读取到提供的字节数组 b 中。返回实际读取的字节数,如果已到达流的末尾则返回 -1
    • OutputStream.write(byte[] b, int off, int len) 方法:用于将字节数组中从偏移量 off 开始的 len 个字节写入输出流。
  • out.flush() 方法 : 将缓冲区中的数据强制写入目标输出流,确保所有数据都被发送。

  • out.close() 方法 : 关闭输出流,释放相关资源,并确保数据已被写入目标位置。

  • in.close() 方法 : 关闭输入流,释放相关资源。

总结

本文通过一个完整的代码示例,详细介绍了在 Java Web 应用中实现文件下载的步骤。通过合理设置文件路径、响应头信息,以及正确处理输入输出流,我们可以轻松实现文件下载功能。同时,本文还讨论了如何处理中文文件名乱码的问题,并提供了 Servlet 的映射配置,确保该功能能够在 Web 应用中正常运行。

4.6.2、response 验证码实现

  • 前端实现
  • 后端实现,需要用到java图片类,生成一个图片
@WebServlet(name = "ImageServlet", urlPatterns = {"/image"}) 
public class ImageServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 设置响应头 'refresh' 的值为 3,表示浏览器在3秒后刷新当前页面
        resp.setHeader("refresh", "3");
        
        // 在内存中创建一个图片,宽80像素,高20像素,类型为RGB
        BufferedImage bufferedImage = new BufferedImage(80, 20, BufferedImage.TYPE_INT_RGB);
        
        // 获取图片的画笔对象
        Graphics2D bi = (Graphics2D) bufferedImage.getGraphics();
        
        // 设置背景颜色为白色
        bi.setColor(Color.WHITE);
        bi.fillRect(0, 0, 80, 20);
        
        // 在图片上写数据(绘制字符)
        bi.setColor(Color.BLUE); // 设置字体颜色为蓝色
        bi.setFont(new Font(null, Font.BOLD, 20)); // 设置字体,使用默认字体,加粗,大小为20
        bi.drawString(makeNum(), 0, 20); // 在图片上绘制字符串,位置为(0, 20)
        
        // 告诉浏览器以图片的方式打开
        resp.setContentType("image/jpeg");
        
        // 网站存在缓存,不让浏览器缓存
        resp.setDateHeader("Expires", 0); // 设置过期时间为0,表示立即过期
        resp.addHeader("Cache-Control", "no-cache"); // 设置缓存控制为不缓存
        resp.setHeader("Pragma", "no-cache"); // 设置Pragma头为no-cache

        // 将图片写入响应输出流中
        ImageIO.write(bufferedImage, "jpeg", resp.getOutputStream());
    }

    // 生成一个随机的6位数字字符串
    private String makeNum() {
        Random random = new Random(); // 创建一个随机数生成器对象
        String num = random.nextInt(999999) + ""; // 生成一个0到999999之间的随机整数并转换为字符串
        StringBuilder sb = new StringBuilder(); // 创建一个字符串构建器对象
        for (int i = 0; i < 6 - num.length(); i++) { // 如果生成的数字长度不足6位,前面补0
            sb.append("0");
        }
        return sb.toString() + num; // 返回补0后的6位数字字符串
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp); // 调用doGet方法处理POST请求
    }
}

详细解释

  • BufferedImage bufferedImage = new BufferedImage(80, 20, BufferedImage.TYPE_INT_RGB);
    使用 BufferedImage 类在内存中创建一个 80x20 像素、RGB 格式的图片
    • BufferedImage 类 用于在内存中表示图像的类。它允许你在内存中创建一个可编辑的图像
    • 8020:分别表示图像的宽度为 80 像素,高度为 20 像素
    • BufferedImage.TYPE_INT_RGB:指定图像的类型为 RGB 格式,即红、绿、蓝三原色模式。
  • Graphics2D bi = (Graphics2D) bufferedImage.getGraphics();
    获取一个 Graphics2D 对象,该对象用于对内存中的 BufferedImage 图像进行绘制操作
    • BufferedImage对象.getGraphics()方法:用于获取(返回)与此图像关联的 Graphics 对象。这个 Graphics 对象提供了一组用于绘制形状、文本、图像和其他图形内容的方法。
    • (Graphics2D):强制类型转换,将 Graphics 对象转换为更具体的 Graphics2D 类型,以便使用更多的图形处理方法(Graphics2DGraphics 的子类)。
  • bi.setColor(Color.WHITE);
    bi.fillRect(0, 0, 80, 20);
    设置绘图对象 bi 的当前颜色为白色 (Color.WHITE)。之后所有的绘制操作都会使用这种颜色,直到再次更改颜色。
    • Graphics2D对象.setColor(Color c) 方法:用于将当前绘图对象的颜色设置为指定的颜色。此后,所有绘制的形状、文本、图像等都会使用这个颜色,直到再次调用 setColor() 方法重新设置颜色
      • Color c:这是一个 Color 对象,表示要设置的颜色。Color 类包含预定义的颜色(如 Color.REDColor.BLUE),也可以通过 RGB、ARGB 或 HSB 值创建自定义颜色。
      • 如果不设置颜色,默认使用 Color.BLACK 进行绘制。
    • Graphics2D对象.fillRect(int x, int y, int width, int height) 方法:用于填充一个矩形区域。
      • 这个矩形的左上角坐标是 (x, y),宽度为 width,高度为 heigh
      • 填充颜色取决于当前 Graphics2D 对象的绘图颜色,即通过 setColor(Color c) 方法设置的颜色。
  • bi.setColor(Color.BLUE);
    bi.setFont(new Font(null, Font.BOLD, 20));
    bi.drawString(makeNum(), 0, 20);
    (0, 20) 位置以粗体、20像素大小的蓝色字体绘制 makeNum() 方法返回的字符串。
    • new Font(String name, int style, int size) 方法Font 类提供的构造方法,用于创建一个 Font 对象,允许开发者指定字体的名称、样式和大小
      • name:字体家族名称,如 "Arial"、"Times New Roman" 等。如果指定的字体不可用或 null,系统会使用默认字体。
      • style:字体样式,可以是 Font.PLAINFont.BOLDFont.ITALIC,或 Font.BOLD | Font.ITALIC 的组合。
      • size:字体的大小,单位是点(pt)。
    • Graphics2D对象.setFont(Font font) 方法 :用于设置用于绘制文本的字体
      • font:Font 对象,可以通过 Font 类的构造函数创建,用于指定字体的样式、大小和字体家族
    • Graphics2D对象.drawString(String str, int x, int y) 方法:用于在指定的位置绘制字符串 strxy 是文本起始点的坐标,通常基于左上角的坐标系
  • HttpServletResponse对象.setDateHeader(String name, long date) 方法
    用于设置 值为日期的HTTP 响应头的键值对
    • name:要设置的 HTTP 头的名称的字符串。
    • date:这是一个长整型值(long),表示从 UNIX 纪元(1970 年 1 月 1 日 00:00:00 GMT)以来的毫秒数。
  • HttpServletResponse对象.addHeader(String name, String value) 方法
    用于在 HTTP 响应中添加一个新的头字段。如果已经存在同名的头字段,addHeader 不会覆盖它们,而是会在响应中追加一个同名的头字段,这与setHeader 方法覆盖之前的值,确保响应中每个头字段名称只有一个值不同。
    适用场景: 当你需要为同一个头字段添加多个值时使用,比如设置 Set-CookieVary 等。
  • ImageIO.write(bufferedImage, "jpeg", resp.getOutputStream());
    将一个图像(在内存中表示为 BufferedImage 对象)以 JPEG 格式写入 HTTP 响应的输出流,从而将图像直接发送给客户端。
    • ImageIO.write(RenderedImage im, String formatName, OutputStream output) 方法
      用于将一个图像(在内存中表示为 BufferedImage 对象)以指定的格式写入指定的输出目标(例如文件、输出流等)。
      • ImageIOjavax.imageio.ImageIO 类,它提供了用于读取和写入图像的静态方法。
      • RenderedImage im:这是一个 RenderedImage 对象,表示要写入输出流的图像。通常,BufferedImageRenderedImage 的实现类之一,用于表示图像的内存模型。
      • String formatName:这是一个字符串,指定图像的文件格式。常见的格式包括 "png""jpg""bmp""gif" 等。
      • OutputStream output:这是一个 OutputStream 对象,表示图像的输出目标。它可以是文件输出流(如 FileOutputStream),也可以是网络输出流,或者其他任意类型的输出流。图像数据将写入这个流中。

整体流程

这行代码的作用是在服务器端生成一个图像,并将其作为HTTP响应的一部分发送给客户端。具体流程如下:

  1. 生成或获取图像bufferedImage 是在内存中已经生成或获取的图像对象。
  2. 设置响应类型:虽然在这行代码中没有明确设置,但通常会在这之前通过 resp.setContentType("image/jpeg"); 设置响应的内容类型为图像。
  3. 写入输出流ImageIO.write 方法将 bufferedImage 以JPEG格式编码,并通过 resp.getOutputStream() 将图像数据发送给客户端。

4.7、使用 IO 优化文件输出操作

在 Java Web 开发中,常常需要从服务器读取文件(如图片),并将其返回给客户端(如浏览器)。为了简化文件的读取与输出操作,我们可以借助 Apache Commons IO 库提供的工具方法。

一、项目配置:添加依赖

在使用 Apache Commons IO 库前,我们需要在项目的 pom.xml 文件中添加相应的 Maven 依赖:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

此依赖包含了 IOUtils 类,该类提供了一系列实用的 I/O 操作工具方法,能够显著简化流的处理工作。

二、Servlet 代码实现

接下来,我们将在 Servlet 中使用 IOUtils.copy() 方法来优化文件的输出操作。

  • 方法:IOUtils.copy(InputStream input, OutputStream output)
  • 说明:
    • 用于将数据从输入流复制到输出流。该方法在处理流的复制时非常高效。
    • input:输入流 (InputStream) 对象,表示要读取数据的来源。可以是文件输入流、网络输入流、内存中的字节流等。
    • output:输出流 (OutputStream) 对象,表示要将数据写入的目标。可以是文件输出流、网络输出流、内存中的字节流等。
    • 为了避免资源泄漏,调用者需要在复制操作后手动关闭流
  • 返回值:该方法没有返回值,但它会将输入流的数据完全复制到输出流中。
  • 异常处理:如果在复制过程中出现 I/O 错误(如读取或写入失败),该方法会抛出 IOException
  • 使用场景:
    • 文件复制:当你需要将一个文件的内容复制到另一个文件时,可以使用 IOUtils.copy() 简化代码。
    • 网络数据传输:从网络输入流中读取数据并直接发送到输出流(如响应流),在处理数据流的同时,减少了手动处理缓冲区的复杂性。
    • 内存流处理:将内存中的字节流快速复制到另一个流(如将字节数组输出到文件中)。
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 读取文件
        FileInputStream fis = new FileInputStream("d://a.jpg");

        // 2. 获取response字节输出流
        ServletOutputStream os = response.getOutputStream();

        // 3. 完成流的复制
        IOUtils.copy(fis, os);

        // 4. 关闭输入流
        fis.close();
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //将 POST 请求委托给 doGet() 方法处理,确保无论请求类型如何,均执行相同的逻辑。
        this.doGet(request, response);
    }
}

在上述代码中,IOUtils.copy() 方法将从 FileInputStream 读取的图片数据直接复制到 ServletOutputStream,将图片返回给客户端。使用 IOUtils.copy() 简化了流的复制操作,省去了手动循环读取和写入的代码。

5、用户注册登录案例

5.1、用户登录(通过 MyBatis 实现)

5.1.1、需求分析

屏幕截图 2024-11-17 140413.png
  1. 用户在登录页面输入用户名和密码,提交请求给 LoginServlet
  2. 在 LoginServlet 中接收请求和数据[用户名和密码]
  3. 在 LoginServlt 中通过 Mybatis 实现调用 UserMapper 来根据用户名和密码查询数据库表
  4. 将查询的结果封装到 User对象中进行返回
  5. 在 LoginServlet 中判断返回的 User对象是否为 null
  6. 如果为 nul,说明根据用户名和密码没有查询到用户,则登录失败,返回”登录失败”数据给前端
  7. 如果不为 null,则说明用户存在并且密码正确,则登录成功,返回”登录成功”数据给前端

5.1.2、环境准备

  1. 复制资料中的静态页面到项目的webapp目录下,效果如下: 屏幕截图 2024-11-17 165418.png

  2. 创建数据库,创建用户表,创建User实体类
    数据库、用户表

-- 创建数据库
CREATE DATABASE db1;

USE db1;

-- 创建用户表
CREATE TABLE tb_user(
	id INT PRIMARY KEY AUTO_INCREMENT,
	username VARCHAR(20) UNIQUE,
	PASSWORD VARCHAR(32)
);

-- 添加数据
INSERT INTO tb_user(username,PASSWORD) VALUES('zhangsan','123'),('lisi','234');

SELECT * FROM tb_user;

User实体类

package com.blog.pojo;  //声明该类所属的包

public class User {     //属性定义
    private Integer id;
    private String username;
    private String password;

    public User() {     //无参构造方法,用于创建空的 User 对象
    }

    public User(Integer id, String username, String password) {  //用于直接初始化 User 对象的属性值
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public Integer getId() {  //用于获取私有属性id的值
        return id;
    }

    public void setId(Integer id) {  //用于设置私有属性id的值
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {  //提供一个用户友好的字符串表示形式,便于调试和日志记录
        return "User{" +
                "id=" + id +
                ", username='" + username + ''' +
                ", password='" + password + ''' +
                '}';
    }
}  //返回的格式是:User{id=1, username='admin', password='123456'}

并且将User实体类拷贝到 com.itheima.pojo

屏幕截图 2024-11-17 173028.png
  1. 在项目的pom.xml导入Mybatis和Mysql驱动坐标
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.34</version>
</dependency>
  1. 创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMapper接口

将下面的 mybatis-config.xml核心配置文件 拷贝到resources目录下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:13306/db1?useSSL=false&amp;useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="PASSWORD."/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="com.blog.mapper"/>
    </mappers>
</configuration>

在下面的 com.itheima.mapper包下创建UserMapper接口

public interface UserMapper {
    
}

将下面的 UserMapper.xml拷贝到resources目录下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blog.mapper.UserMapper">
    
</mapper>
  • 注意:在resources下创建UserMapper.xml的目录时,要使用"/"分割 屏幕截图 2024-11-17 180725.png

5.1.3、代码实现

  1. 在UserMapper接口中提供一个根据用户名和密码查询用户对象的方法
//由于逻辑比较简单,所以这里用的注解
public interface UserMapper {

    /**
     * 根据用户名和密码查询是否有此用户
     * @param username 用户名
     * @param password 密码
     * @return 用户
     */
    @Select("select * from tb_user where username = #{username} and password = #{password};")
    User select(@Param("username") String username, @Param("password") String password);
}

说明
@Select 注解:

  • 用于直接嵌入 SQL 查询语句。
  • 这里定义了一条 SELECT 语句,用于从 tb_user 表中查询符合条件的用户

#{} 语法:

  • 是 MyBatis 的占位符,表示会动态从参数中取值。
  • #{username}#{password} 的值会分别被方法参数 usernamepassword 替换。
  1. 修改loign.html:创建一个登录表单,让用户输入用户名和密码,并提交到后端的 LoginServlet
<!DOCTYPE html>
<html lang="en">
<!-- 定义HTML文档类型为HTML5,并设置文档的语言为英语 -->

<head>
    <meta charset="UTF-8">
    <title>login</title>
    <link href="css/login.css" rel="stylesheet">
</head>

<body>

<!-- 定义一个div容器,用于包裹整个登录表单 -->
<div id="loginDiv">
    <!-- 表单开始,action指定提交的服务器端地址,method设置为POST方式提交 -->
    <form action="/request-demo/loginServlet" method="post" id="form">
        <h1 id="loginMsg">LOGIN IN</h1>
        
        <!-- 输入用户名的文本框,类型为文本输入框 -->
        <p>Username:<input id="username" name="username" type="text"></p>
        <!-- 输入密码的文本框,类型为密码输入框 -->
        <p>Password:<input id="password" name="password" type="password"></p>
        
        <!-- 定义一个div容器,用于包裹提交和重置按钮及注册链接 -->
        <div id="subDiv">
            <!-- 定义一个提交按钮,按钮显示文字为 "login up" -->
            <input type="submit" class="button" value="login up">
            <!-- 定义一个重置按钮,按钮显示文字为 "reset" -->
            <input type="reset" class="button" value="reset">
            <!-- 定义一个链接,指向 "register.html",文字显示为 "没有账号?点击注册" -->
            <a href="register.html">没有账号?点击注册</a>
        </div>
    </form>
</div>

</body>
</html>
  1. 编写LoginServlet:编写一个 Servlet,接收表单数据,调用 UserMapper 查询用户信息,并返回登录结果
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 设置响应内容类型为 HTML,字符编码为 UTF-8,防止出现中文乱码
        response.setContentType("text/html;charset=utf-8");
        
        // 1. 接收客户端传递的用户名和密码参数
        String password = request.getParameter("password"); // 获取请求中的密码参数
        String username = request.getParameter("username"); // 获取请求中的用户名参数

        // 2. 调用 MyBatis 完成数据库查询操作
        // 2.1 获取 SqlSessionFactory 对象,用于创建 SqlSession
        String resource = "mybatis-config.xml"; // MyBatis 配置文件路径
        InputStream inputStream = Resources.getResourceAsStream(resource); // 读取配置文件
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建 SqlSessionFactory 实例

        // 2.2 获取 SqlSession 对象,用于执行 SQL 语句
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 2.3 获取 Mapper 接口的代理对象,用于调用接口定义的方法
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        // 2.4 调用 Mapper 中的方法,查询数据库
        User user = mapper.select(username, password); // 根据用户名和密码查询用户信息

        // 2.5 释放资源,关闭 SqlSession
        sqlSession.close();

        // 3. 获取响应的输出流,返回结果给客户端
        PrintWriter writer = response.getWriter();

        // 判断查询结果是否为 null,返回相应的登录结果
        if (user != null) {
            writer.write("登陆成功"); // 如果用户存在,返回登录成功
        } else {
            writer.write("登陆失败"); // 如果用户不存在,返回登录失败
        }
    }
}


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}
  1. 启动服务器测试

    4.1 如果用户名和密码输入错误,则

14530084701066247200.png

4.2 如果用户名和密码输入正确,则 image.png

可能遇到的问题:java.lang.NoClassDefFoundError: org/apache/ibatis/io/Resources
去看看Tomcat的lib目录下有没有mybatis的jar包,如果没有则导入一个

5.2 用户注册

5.2.1、需求分析

image.png
  1. 用户在注册页面输入用户名和密码,提交请求给 RegisterServlet
  2. 在 RegisterServlet 中接收请求和数据[用户名和密码]
  3. 在 RegisterServlet 中通过 Mybatis 实现调用 UserMapper 来根据用户名查询数据库表
  4. 将查询的结果封装到 User对象 中进行返回
  5. 在 RegisterServlet 中判断返回的 User对象是否为 null
  6. 如果为 nul,说明根据用户名可用,则调用 UserMapper 来实现添加用户
  7. 如果不为 null,则说明用户不可以,返回"用户名已存在"数据给前端

5.2.2、代码编写

  1. 编写UserMapper提供根据用户名查询用户数据方法和添加用户方法
/**
* 根据用户名查询用户对象
* @param username
* @return
*/
@Select("select * from tb_user where username = #{username};")
User selectByUserName(@Param("username") String username);

/**
* 添加用户
* @param user
*/
@Insert("insert into tb_user(username, password)VALUES (#{username},#{password});")
void add(User user);
  1. 修改register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>欢迎注册</title>
    <!-- 引入外部样式表 register.css,用于设置页面的外观样式 -->
    <link href="css/register.css" rel="stylesheet">
</head>

<body>
<div class="form-div">
    <!-- 包含整个表单布局的容器 -->
    <div class="reg-content">
        <h1>欢迎注册</h1>
        <!-- 显示“已有帐号?”文字,提供一个指向登录页面的链接 -->
        <span>已有帐号?</span> <a href="login.html">登录</a>
    </div>
    <!-- 定义一个表单,action 指定提交目标,method 使用 POST 提交数据 -->
    <form id="reg-form" action="/request-demo/registerServlet" method="post">
        <!-- 使用表格来布局表单的输入字段和标签 -->
        <table>
            <!-- 表格单元格,显示“用户名”标签 -->
            <tr>
                <td>用户名</td>
                <!-- 定义输入框的容器单元格 -->
                <td class="inputs">
                    <!-- 定义文本输入框 -->
                    <input name="username" type="text" id="username">
                    <br>
                    <!-- 错误提示消息,初始状态为隐藏 -->
                    <span id="username_err" class="err_msg" style="display:none">用户名不太受欢迎</span>
                </td>
            </tr>
            <!-- 表格单元格,显示“密码”标签 -->
            <tr>
                <td>密码</td>
                <!-- 定义输入框的容器单元格 -->
                <td class="inputs">
                    <!-- 定义密码输入框 -->
                    <input name="password" type="password" id="password">
                    <br>
                    <!-- 错误提示消息,初始状态为隐藏 -->
                    <span id="password_err" class="err_msg" style="display:none">密码格式有误</span>
                </td>
            </tr>
        </table>
        <!-- 提交按钮的容器 -->
        <div class="buttons">
            <!-- 定义提交按钮 -->
            <input value="注 册" type="submit" id="reg_btn">
        </div>
        <!-- 清除浮动,确保布局正确 -->
        <br class="clear">
    </form>
</div>
</body>
</html>

  1. 创建RegisterServlet类
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1. 接收用户数据
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        
        //封装用户对象
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);

        //2. 调用mapper 根据用户名查询用户对象
        //2.1 获取SqlSessionFactory对象
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //2.2 获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //2.3 获取Mapper
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        
        //2.4 调用方法
        User u = userMapper.selectByUsername(username);
        
        //3. 判断用户对象释放为null
        if( u == null){
            // 用户名不存在,添加用户
            userMapper.add(user);

            // 提交事务
            sqlSession.commit();
            // 释放资源
            sqlSession.close();
        }else {
            // 用户名存在,给出提示信息
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().write("用户名已存在");
        }

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}
  1. 启动服务器进行测试
  • 如果测试成功,则在数据库中就能查看到新注册的数据
  • 如果用户已经存在,则在页面上展示 用户名已存在 的提示信息

5.3、SqlSessionFactory工具类抽取

提出问题

上面两个功能(用户登录、用户注册)已经实现,但是在写Servlet的时候,因为需要使用Mybatis来进行数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码。

问题的本质

  1. 重复代码问题
    类似以下代码出现在多个地方:

    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    • 维护性问题:如果配置路径 mybatis-config.xml 发生变化,你需要手动修改每一处重复的代码。
    • 代码冗余:同样的逻辑多次重复,增加了代码量,降低了可读性。
  2. 资源浪费问题
    SqlSessionFactory 是一个线程安全的类,应该在应用程序启动时初始化一次,而不是每次需要数据库操作时重复创建。

    • 性能问题:频繁创建 SqlSessionFactory 会浪费系统资源和时间。

解决思路

  1. 抽取工具类

    • 将重复代码集中到一个工具类中,封装 SqlSessionFactory 的初始化过程。
  2. 使用静态代码块

    • 静态代码块会在类加载时执行,确保 SqlSessionFactory 只被初始化一次。

优化代码示例

以下是优化后的代码实现:

工具类 MyBatisUtil

import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class MyBatisUtil {
    // 静态变量,用于存储唯一的 SqlSessionFactory 实例
    private static SqlSessionFactory sqlSessionFactory;

    // 静态代码块:在类加载时初始化 SqlSessionFactory
    static {
        try {
            // 加载 MyBatis 配置文件
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 构建 SqlSessionFactory
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (Exception e) {
            // 捕获初始化失败异常
            e.printStackTrace();
            throw new RuntimeException("初始化 SqlSessionFactory 失败");
        }
    }

    // 提供静态方法获取 SqlSessionFactory
    public static SqlSessionFactory getSqlSessionFactory() {
        return sqlSessionFactory;
    }
}

优化后的使用方式

工具类抽取以后,对Mybatis的SqlSession进行数据库操作的时候,就可以直接调用工具类获取 SqlSessionFactory 实例:

SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();

示例:
获取 SqlSession

import org.apache.ibatis.session.SqlSession;

public class UserDao {
    public void someDatabaseOperation() {
        // 使用 MyBatisUtil 获取 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = MyBatisUtil.getSqlSessionFactory();
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 执行数据库操作
            // 示例:Mapper 操作
            UserMapper mapper = session.getMapper(UserMapper.class);
            mapper.someQuery();
        }
    }
}

其他

IDEA中输入sout,回车会自动生成 System.out.println()

断开或释放一个被占用的端口通常涉及停止使用该端口的进程。以下是如何在不同操作系统中查找并释放被占用端口的方法:

在 Windows 上释放端口

a. 查找占用端口的进程

  1. 使用 netstat 命令
    • 打开命令提示符(可以通过按 Win + R,输入 cmd,然后回车打开)。

    • 运行以下命令以查找占用指定端口的进程:

      netstat -ano | findstr :<端口号>
      
    • 例如,要查找占用端口 8080 的进程,可以运行:

      netstat -ano | findstr :8080
      
    • 输出将显示类似如下的信息:

      TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING       1234
      
    • 这里的 1234 是占用该端口的进程 ID (PID)。

b. 终止进程

  1. 使用 taskkill 命令

    • 获取到 PID 后,使用以下命令终止该进程:

      taskkill /PID 1234 /F
      
    • 替换 1234 为你之前查找到的进程 ID。

  2. 使用任务管理器

    • 打开任务管理器(Ctrl + Shift + Esc)。
    • 切换到 “进程” 选项卡或 “详细信息” 选项卡。
    • 找到 PID 对应的进程并右键点击它,选择 “结束任务”