【Java Web】Servlet 快速入门

Servlet(服务器小程序) 是运行在服务端(如 Tomcat)的 Java 小程序。它由 Sun 公司定义,用于处理动态资源。简单来说,Servlet 是一套技术标准(接口),在 Java 中只有符合这些标准的类才能有效响应客户端的请求。通过使用 Servlet,开发者可以构建动态的 Web 应用程序。Servlet 具有以下特点:

  • 处理请求:Servlet 是专门用于处理客户端请求并返回响应的一种技术
  • 运行环境:Servlet 必须在 Web 项目中开发,并在像 Tomcat 这样的服务容器中运行

动态资源 vs 静态资源

在介绍 Servlet 之前,首先了解一下静态资源和动态资源的区别。

  • 静态资源:在程序运行之前就已创建的文件,这些资源无需通过代码生成,常见的有 HTML、CSS、JavaScript、图片和视频等。

  • 动态资源:在程序运行时通过代码生成的内容,这些资源在运行前无法确定,通常由服务器根据请求实时生成,例子包括 Servlet 和 Thymeleaf。需要注意的是,动态资源并不包括页面动画或简单交互效果。

可以用去蛋糕店的例子来说明:静态资源就像你直接购买柜台上已经做好的蛋糕,而动态资源则是你告诉柜员你的要求,让他们现场制作蛋糕。

(1)静态资源请求流程

在请求静态资源时,客户端浏览器向服务器发送 HTTP 请求。例如,假设请求的是 a.png。服务器接收到请求后,Tomcat 会将 a.png 转换为响应报文,并将其发送回客户端。这个响应报文包含了请求的资源数据:

  • 响应行
  • 响应头(浏览器通过 Content-Type 获取数据类型,并执行对应解析)
  • 响应体(图片数据)

(2)动态资源请求流程

在请求动态资源时,客户端浏览器向服务器发送 HTTP 请求。服务器接收请求并提取参数,然后运行相应的 Java 代码生成响应数据。这些数据会通过 Tomcat 转换为响应报文返回给浏览器,响应报文的内容同样包括响应行、响应头、响应体信息。

每次动态资源请求都需要执行 Java 代码,而请求中的参数可能不同,因此生成的响应数据也会有所变化。在这个过程中,生成响应数据的 Java 代码必须遵循特定的技术标准和规范,这套标准其实就是 Servlet。

Servlet 运行流程

Servlet的运行流程是这样的:首先,客户端浏览器向服务器发送请求报文。Tomcat 接收到请求后,会将请求报文的信息转换为一个 HttpServletRequest 对象,该对象中包含了请求中的所有信息(请求行、请求头、请求体)。同时,Tomcat 会创建一个 HttpServletResponse 对象,该对象用于装载响应给客户端的信息,该对象将会被转换成响应的报文(响应行、响应头、响应体)。接着,Tomcat 根据请求中的资源路径找到对应的 Servlet(由后端定义并实现),将 Servlet 实例化,调用 service 方法,同时将 HttpServletRequest 和 HttpServletResponse 对象传入。待处理完成后,Tomcat 将 HttpServletResponse 对象转成响应报文。最后,服务器向客户端浏览器发送响应报文。

Servlet 开发流程

现在假设存在这样一个需求:校验用户注册时,用户名是否被占用。具体地,通过客户端向一个 Servlet 发送请求,携带 usename 信息,如果用户名存在(这里简化需求,假定存在用户名 IT),则向客户端响应 NO,否则响应 YES

(1)创建 Java Web 项目

创建一个名为 demo-servlet 项目,创建步骤在 《Tomcat 快速入门》这篇文章有提及。

(2)创建注册页面

在 web 文件夹下创建一个 index.html 文件(直接命名为 index,请求省略资源路径也能访问该页面),文件内容:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Register</title>
</head>
<body>

<!--
    http://127.0.0.1:8080/demo/userServlet
    请求体:username=it
-->

<form method="post" action="userServlet">
    用户名:<input type="text" name="username"> <br>
    <input type="submit" vlaue="校验">
</form>

</body>
</html>

(3)重写 service 方法,实现业务代码

在 src 文件夹下创建 com.it.servlet.UserServlet 类,实现 Servlet 接口中定义的方法:

java 复制代码
package com.it.servlet;
import jakarta.servlet.*;
import java.io.IOException;

public class UserServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {}

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {}

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {}
}

如果直接实现 Servlet 接口,需要实现上述多个方法,考虑到目前需求不需要实现这么多接口。因此,可以改成继承 HttpServlet 类,这个类间接实现了 Servlet 接口:

java 复制代码
public class UserServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 从 request 对象中获取请求信息(user 参数)
        String username = req.getParameter("username"); // 根据参数名获取参数

        // 2. 处理业务代码
        String res = "YES";
        if ("IT".equals(username)) {
            res = "NO";
        }

        // 3. 将响应数据存放入 response 对象中
        PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
        writer.write(res);
    }
}

(4)在 web.xml 中配置请求映射路径

在 web.xml 文件中为 Servlet 类起一个别名,用于关联请求的映射路径,并且通过 servlet-class 项告知 Tomcat 对应要实例化的 Servlet 类:

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <!-- 
        配置 Servlet 类,并起一个别名
            servlet-name:用于关联请求的映射路径
            servlet-class:告知 Tomcat 对应要实例化的 Servlet 类
     -->
    <servlet>
        <servlet-name>userServlet</servlet-name>
        <servlet-class>com.it.servlet.UserServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>userServlet</servlet-name>
        <url-pattern>/userServlet</url-pattern>
    </servlet-mapping>
</web-app>

注意:servlet-mapping 中 url-pattern 项的内容需要有一个 / 作为开头。 / 后面的内容与 HTML 中 form 表单的 action 保持一致。

(5)启动项目,测试效果

运行前,点击运行键旁边的 Edit Configurations...,将当前 Tomcat 的 Deployment 换成当前的项目:

配置完成后,启动项目。可以看到浏览器直接显示了注册的 HTML 页面,原因是当请求路径 http://localhost:8080/demo/ 后没有接其他内容时,默认为 index.html。此时,用户名输入为 IT 时,页面返回 NO,输入其他值时,页面访问 YES

开发过程问题记录

(1)servlet-api.jar 导入问题

在开发过程中,我们自定义的 Servlet 需要继承 HttpServlet 类或实现 Servlet 接口。这些类和接口并不是 JDK 内置的,而是属于 Tomcat 中的 jar 包(servlet-api.jar)。因此,需要导入 servlet-api 这个 jar 包。正常导入 jar 包的流程是:在 WEB-INF 下创建一个 lib 目录,将 servlet-api.jar 复制到 lib 中。之后将当前 jar 包添加为项目依赖。右键 lib 目录,选择 Add as Library...,将 lib 设置为 Module Library

然而,我们在开发 Servlet 项目的过程中,并没有按照这种正常流程导入 jar 包,原因是:我们在 Project Structure 中导入了 Tomcat 依赖,而 Tomcat 依赖中已经包含了 servlet-api.jar。因此,不需要我们手动导入依赖。现在,我们还是解除一下手动导入的servlet-api.jar。在 Project Structure 中删除 Dependencies 中手动导入的依赖,并且删除 lib 目录下的对应 jar 包即可。

(2)Content-Type 响应头的问题

若希望客户端将数据当作特定类型的数据来解析,比如当作 html 代码解析,服务器端就需要设置对应的 Content-Type。方法是通过 HttpServletResponse 对象调用 setHeader 方法或者直接调用 setContentType 设置:

java 复制代码
public class UserServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 从 request 对象中获取请求信息(user 参数)
        String username = req.getParameter("username"); // 根据参数名获取参数

        // 2. 处理业务代码
        String res = "<h1>YES</h1>";
        if ("IT".equals(username)) {
            res = "<h1>NO</h1>";
        }

        // 3. 将响应数据存放入 response 对象中
        // 设置 Content-Type 响应头
        resp.setHeader("Content-Type", "text/html");
        // resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
        writer.write(res);
    }
}

其中,Content-Type 设置的值可以查看 Tomcat 目录下的 conf 中的 web.xml。比如,若希望客户端将数据当中 html 来处理,则可以在 web.xml 中查看 <mime-mapping> 中 html 对应的类型,结果为 text/html。因此,将 text/html 作为 setHeader 方法的第二个参数值。设置完成后,启动服务,可以在开发者模式中查看设置结果:

(3)url-pattern 的特殊写法问题

客户端浏览器输入的 URL 到具体 Servlet 方法的调用过程如下:当浏览器发送请求,例如 http://localhost:8080/demo/s1,它首先通过 URL 中的 IP 地址找到服务器,端口号则指向 Tomcat 服务。接着,demo 指定了运行的应用程序。最后,通过 WEB-INF/web.xml 中的路径映射,Tomcat 找到与 /s1 对应的 Servlet 实现类,并调用该类的 service 方法来处理请求。在配置路径映射的过程中,有几个具体细节需要注意:

首先,配置文件中的一个 servlet-name 是可以同时对应多个不同值的 url-pattern 的。比如下面的代码中,为 userServlet 配置了多个 url-pattern,通过 /userServlet/aa 都可以找到 UserServlet 实现类。

xml 复制代码
<servlet>
    <servlet-name>userServlet</servlet-name>
    <servlet-class>com.it.servlet.UserServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>userServlet</servlet-name>
    <url-pattern>/userServlet</url-pattern>
    <url-pattern>/aa</url-pattern>
</servlet-mapping>

其次,配置文件中的一个 servlet 标签可以同时对应多个 servlet-mapping 标签,这种写法实际上和为 userServlet 配置了多个 url-pattern 类似。

xml 复制代码
<servlet>
    <servlet-name>userServlet</servlet-name>
    <servlet-class>com.it.servlet.UserServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>userServlet</servlet-name>
    <url-pattern>/userServlet</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>userServlet</servlet-name>
    <url-pattern>/aa</url-pattern>
</servlet-mapping>

最后,配置文件中,url-pattern 是支持模糊匹配的,使用 * 作为通配符,特别地:

  • / 表示匹配全部,不包含 jsp 文件
  • /* 匹配全部,包含 jsp 文件
  • /a/* 匹配前缀,后缀模糊
  • *.html 匹配后缀,前缀模糊

Servlet 注解开发

在基于配置的 Servlet 开发中,之前需要在 web.xml 中写很多配置。现在,可以通过注解简化这一过程,只需在 Servlet 类上添加 @WebServlet 并指定路径即可。需要注意的是,如果使用注解,就不需要再使用 XML 配置;同时,两个方式不能同时使用,否则会导致错误。

java 复制代码
@WebServlet({"/s1", "/s2", "/userServlet"})
// @WebServlet("/userServlet")
// @WebServlet(urlPatterns = {"/s1", "/s2", "/userServlet"})
public class UserServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 从 request 对象中获取请求信息(user 参数)
        String username = req.getParameter("username"); // 根据参数名获取参数

        // 2. 处理业务代码
        String res = "<h1>YES</h1>";
        if ("IT".equals(username)) {
            res = "<h1>NO</h1>";
        }

        // 3. 将响应数据存放入 response 对象中
        // 设置 Content-Type 响应头
        resp.setHeader("Content-Type", "text/html");
        // resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter(); // 返回一个响应体中打印字符串的打印流
        writer.write(res);
    }
}

Servlet 生命周期

每个 Servlet 类实例经历四个阶段:实例化、初始化、接收和处理请求,以及销毁。实例化通过构造器实现,初始化通过重写 init 方法,接收和处理请求通过重写 service 方法,销毁通过重写 destroy 方法。实例化、初始化和销毁只执行一次:实例化在第一次接收请求或服务器启动时触发,初始化在构造完成后触发,销毁在关闭服务前触发。接收和处理请求则在每次访问时执行。另外,Servlet 在 Tomcat 中采用单例模式,这意味着其成员变量在多个线程之间共享。因此,不建议在 service 方法中修改成员变量,以避免并发请求引发的线程安全问题。

java 复制代码
/**
 * 1. 实例化           构造器         第一次请求/服务器启动
 * 2. 初始化           init          构造完毕
 * 3. 接收和处理请求    service       每次请求
 * 4. 销毁             destroy       关闭服务
 */
@WebServlet("/servletLifeCycle")
public class ServletLifeCycle extends HttpServlet {
    public ServletLifeCycle() {
        System.out.println("构造器");
    }

    @Override
    public void init() throws ServletException {
        System.out.println("初始化");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("服务");
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
    }
}

前文提到,实例化在第一次接收请求或服务器启动时触发,这由 load-on-startup 参数决定。默认情况下,load-on-startup 的值为 -1,表示 Tomcat 启动时不会实例化该 Servlet,而是在第一次接收请求时才进行实例化。如果将该值设置为正整数,则表示 Tomcat 启动时按指定顺序实例化 Servlet(适用于多个 Servlet 的情况)。如果出现序号冲突,Tomcat 会自动协调启动顺序。基于 XML 配置和基于注解开发的参数设置如下:

xml 复制代码
<servlet>
    <servlet-name>userServlet</servlet-name>
    <servlet-class>com.it.servlet.UserServlet</servlet-class>
    <load-on-startup>6</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>userServlet</servlet-name>
    <url-pattern>/userServlet</url-pattern>
</servlet-mapping>
java 复制代码
@WebServlet(urlPatterns = "/servletLifeCycle", loadOnStartup = 6)

另外,DefaultServlet 是 Tomcat 定义的一个 Servlet 实现类。当请求静态资源时,比如 /a.png,该路径会与自定义的 Servlet 实现类实例进行匹配,若没有找到对应的 Servlet,Tomcat 会把请求交给 DefaultServlet 处理。

相关推荐
青岛少儿编程-王老师几秒前
CCF编程能力等级认证GESP—C++5级—20241207
java·开发语言·数据结构·c++·算法·青少年编程
跟着珅聪学java4 分钟前
maven 创建父子项目教程
java·数据库·maven
shangxianjiao6 分钟前
Javaweb web后端maven介绍作用安装
java·maven·idea
爪哇学长8 分钟前
Java应用性能优化:Redis与Memcached缓存
java·缓存·性能优化
DC_BLOG11 分钟前
数据结构绪论
java·数据结构·算法
461K.34 分钟前
scala 隐式对象
java·开发语言·ide·后端·scala·intellij-idea
北i1 小时前
MySQL数据库报“You are not owner of thread”错误的解决方法
后端·mysql
Wils0nEdwards1 小时前
Leetcode 加一
java·算法·leetcode
听风吟丶1 小时前
《深入理解 Java 中的 ImmutableList》
java·开发语言