Servlet 教程

Servlet 教程

Servlet 是 Java 服务端处理 HTTP 请求的核心技术,是 Java Web 开发的基础,运行在 Servlet 容器(如 Tomcat)中。

本教程包含

  • 基础篇:简介、环境设置、生命周期、实例
  • 请求响应篇:表单数据、HTTP 请求/响应、状态码
  • 进阶篇:过滤器、异常处理、Cookie、Session、数据库
  • 应用篇:文件上传、日期、重定向、计数器、国际化

为什么学习 Servlet

  • 理解 Java Web 底层原理
  • Spring MVC 等框架的基础
  • 面试高频考点
  • 维护老项目必备

运行环境

JDK 8+ + Tomcat 9/10 + Maven

Servlet 简介

Servlet 简介

Servlet 是运行在服务器上的 Java 类,用于处理客户端请求并返回响应。

工作原理

浏览器 → HTTP 请求 → Tomcat(Servlet 容器)
        → 匹配 Servlet → 调用 doGet/doPost
        → 生成响应 → HTTP 响应 → 浏览器

Servlet 是什么

  • Java 类,继承 HttpServlet
  • 由容器管理生命周期(加载、实例化、调用、销毁)
  • 处理 HTTP 请求的规范(Java EE 标准)

最小 Servlet

import javax.servlet.http.*;
import javax.servlet.*;
import java.io.*;

public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<h1>Hello Servlet!</h1>");
    }
}

与 JSP 的关系

JSP 本质是 Servlet(编译后生成 Servlet 类)
Servlet 偏逻辑,JSP 偏视图
现代开发用 Spring MVC 整合两者

优势与局限

优势: 标准化、跨平台、性能好
局限: 手写 HTML 输出繁琐 → 用 JSP/模板引擎替代

Servlet 环境设置

Servlet 环境设置

搭建 Servlet 开发环境:JDK + Tomcat + Maven + IDE。

1. 安装 JDK

java -version   # 需要 JDK 8+

2. 下载 Tomcat

https://tomcat.apache.org
# 解压到本地,如 D:/apache-tomcat-9.0.xx

3. 配置 IDE(以 IDEA 为例)

File → Settings → Build Tools → Tomcat
添加 Tomcat 安装目录

4. 创建 Maven Web 项目

mvn archetype:generate -DgroupId=com.example \
  -DartifactId=my-web -DarchetypeArtifactId=maven-archetype-webapp

5. 添加 Servlet 依赖

<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>4.0.1</version>
  <scope>provided</scope>   <!-- 容器提供 -->
</dependency>

6. 启动 Tomcat

# 命令行
catalina.sh run   # Linux/macOS
catalina.bat run  # Windows

# IDE 中配置 Tomcat 并启动(热部署)

验证

浏览器访问 http://localhost:8080/项目名/

Servlet 生命周期

Servlet 生命周期

Servlet 生命周期由容器管理,包含初始化、服务、销毁三个阶段。

生命周期方法

init()      初始化: 创建后调用一次
service()   服务: 每个请求调用,分发到 doGet/doPost
destroy()   销毁: 卸载前调用一次

完整流程

1. 加载类
2. 实例化(构造器)
3. init() 初始化(可重写做准备工作)
4. 处理请求(service → doGet/doPost)
5. destroy() 销毁
6. 卸载

示例

public class LifeServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("初始化: 加载配置、连接池...");
    }

    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws IOException {
        resp.getWriter().println("处理请求");
    }

    @Override
    public void destroy() {
        System.out.println("销毁: 释放资源...");
    }
}

加载时机

默认首次请求时加载
web.xml 配置 load-on-startup 可在启动时加载:
<load-on-startup>1</load-on-startup>

单例特性

Servlet 默认单例(一个实例服务所有请求)
多线程并发访问 → 注意共享变量线程安全

线程安全问题

不要用实例字段存请求相关数据
用局部变量或 ThreadLocal

Servlet 实例

Servlet 实例

从零创建一个完整可运行的 Servlet 实例。

1. 编写 Servlet

// src/main/java/com/example/WelcomeServlet.java
package com.example;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class WelcomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<html><body>");
        out.println("<h2>欢迎访问 Servlet 实例</h2>");
        out.println("<p>当前时间: " + new java.util.Date() + "</p>");
        out.println("<a href='welcome?name=老王'>带参数访问</a>");
        out.println("</body></html>");
    }
}

2. 配置 web.xml

<!-- webapp/WEB-INF/web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         version="4.0">
  <servlet>
    <servlet-name>welcome</servlet-name>
    <servlet-class>com.example.WelcomeServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>welcome</servlet-name>
    <url-pattern>/welcome</url-pattern>
  </servlet-mapping>
</web-app>

3. 部署运行

mvn package        # 生成 war
# 复制 war 到 Tomcat webapps 目录,或 IDE 部署

# 访问
http://localhost:8080/项目名/welcome
http://localhost:8080/项目名/welcome?name=老王

@WebServlet 注解方式(推荐)

@WebServlet("/welcome")
public class WelcomeServlet extends HttpServlet { ... }
// 无需 web.xml 配置

Servlet 表单数据

Servlet 表单数据

处理表单提交的数据是 Servlet 的核心功能之一。

表单页面

<form action="register" method="post">
  用户名: <input name="username"><br>
  密码: <input type="password" name="password"><br>
  爱好: <input type="checkbox" name="hobby" value="阅读">阅读
        <input type="checkbox" name="hobby" value="运动">运动<br>
  <button type="submit">注册</button>
</form>

Servlet 读取数据

@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req,
                          HttpServletResponse resp)
            throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");   // 中文编码

        // 单值
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        // 多值(复选框)
        String[] hobbies = req.getParameterValues("hobby");

        // 参数名列表
        java.util.Enumeration<String> names = req.getParameterNames();

        // 响应
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().println("注册成功: " + username);
    }
}

GET vs POST

GET:  参数在 URL,有长度限制,不安全(doGet)
POST: 参数在请求体,无长度限制,安全(doPost)

编码问题处理

// POST: 读取前设置
req.setCharacterEncoding("UTF-8");

// GET(Tomcat 8+ 默认 UTF-8,老版本需配置 URIEncoding)

参数处理技巧

req.getParameter("age")     // 永远返回 String
Integer.parseInt(req.getParameter("age"))   // 转数值
String v = req.getParameter("x"); if (v == null) v = "默认值";

Servlet 客户端 HTTP 请求

Servlet 客户端 HTTP 请求

HttpServletRequest 封装了客户端请求的全部信息。

读取请求信息

// 请求行
req.getMethod()            // GET / POST
req.getRequestURI()        // /app/welcome
req.getRequestURL()        // http://localhost:8080/app/welcome
req.getProtocol()          // HTTP/1.1
req.getQueryString()       // ?name=abc(查询串)

// 请求头
req.getHeader("User-Agent")    // 浏览器信息
req.getHeader("Referer")       // 来源页
req.getHeader("Cookie")
req.getHeaderNames()           // 所有头名

// 客户端信息
req.getRemoteAddr()        // 客户端 IP
req.getRemotePort()
req.getLocalAddr()

请求参数

req.getParameter("name")
req.getParameterValues("hobby")
req.getParameterMap()      // 全部参数 Map

请求属性(内部传递)

req.setAttribute("user", userObj);   // 转发给 JSP 用
Object u = req.getAttribute("user");
req.removeAttribute("user");

转发与包含

// 转发(服务器内部跳转,URL 不变)
RequestDispatcher rd = req.getRequestDispatcher("/result.jsp");
rd.forward(req, resp);

// 包含(把 JSP 输出包含到当前响应)
rd.include(req, resp);

会话获取

HttpSession session = req.getSession();
session.setAttribute("user", "Alice");

Servlet 服务器 HTTP 响应

Servlet 服务器 HTTP 响应

HttpServletResponse 用于设置响应状态、头和内容。

设置响应内容

// 内容类型与编码
resp.setContentType("text/html;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");

// 写内容
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h1>响应内容</h1>");
out.println("</body></html>");

写二进制数据(图片下载)

resp.setContentType("image/png");
ServletOutputStream sos = resp.getOutputStream();
byte[] data = Files.readAllBytes(Paths.get("img.png"));
sos.write(data);
sos.flush();

设置状态码

resp.setStatus(404);
resp.sendError(404, "页面不存在");
resp.sendError(500, "服务器错误");

重定向

// 302 重定向(浏览器重新发起请求,URL 变化)
resp.sendRedirect("/app/login.jsp");
resp.sendRedirect("https://www.example.com");

设置响应头

resp.setHeader("Content-Disposition",
    "attachment; filename=download.txt");  // 触发下载
resp.setHeader("Cache-Control", "no-cache");
resp.addHeader("Set-Cookie", "name=value");

转发 vs 重定向

转发(forward): 服务器内部,URL 不变,可带 request 属性
重定向(redirect): 浏览器新请求,URL 变,两次请求

Servlet HTTP 状态码

Servlet HTTP 状态码

HTTP 状态码表示请求处理结果,由三数字组成。

1xx 信息类

100 Continue    继续
101 Switching Protocols  切换协议

2xx 成功类

200 OK              成功(最常用)
201 Created         创建成功
204 No Content      无内容

3xx 重定向类

301 Moved Permanently  永久重定向
302 Found              临时重定向(sendRedirect 默认)
304 Not Modified       缓存未修改

4xx 客户端错误

400 Bad Request       请求错误
401 Unauthorized      未认证
403 Forbidden         禁止访问
404 Not Found         资源不存在(最常见)
405 Method Not Allowed 方法不允许

5xx 服务端错误

500 Internal Server Error  服务器内部错误
502 Bad Gateway            网关错误
503 Service Unavailable    服务不可用
504 Gateway Timeout        网关超时

Servlet 中设置

resp.setStatus(200);
resp.setStatus(404);
resp.sendError(500, "内部错误");

// 重定向自动产生 302
resp.sendRedirect("login.jsp");

常用判断

200-299  成功
300-399  重定向
400-499  客户端错误(检查请求)
500-599  服务端错误(检查日志)

Servlet 编写过滤器

Servlet 编写过滤器

过滤器(Filter)在请求到达 Servlet 之前或响应返回之后执行,用于通用处理:登录校验、编码、日志等。

过滤器接口

import javax.servlet.*;

public class EncodingFilter implements Filter {

    @Override
    public void init(FilterConfig config) throws ServletException {
        // 初始化
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws java.io.IOException, ServletException {
        // 请求前: 设置编码
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        // 继续调用链
        chain.doFilter(request, response);

        // 响应后: 日志等
    }

    @Override
    public void destroy() {
        // 销毁
    }
}

配置过滤器(web.xml)

<filter>
  <filter-name>encoding</filter-name>
  <filter-class>com.example.EncodingFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>encoding</filter-name>
  <url-pattern>/*</url-pattern>   <!-- 所有请求 -->
</filter-mapping>

@WebFilter 注解

@WebFilter("/*")
public class EncodingFilter implements Filter { ... }

登录校验过滤器

@WebFilter("/admin/*")
public class AuthFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("user") == null) {
            response.sendRedirect("/login.jsp");
            return;   // 不放行
        }
        chain.doFilter(req, res);   // 放行
    }
}

过滤器链

多个过滤器按注册顺序执行
请求: F1 → F2 → Servlet
响应: Servlet → F2 → F1

Servlet 异常处理

Servlet 异常处理

统一处理 Servlet 异常,避免暴露堆栈给用户,提供友好错误页。

异常来源

代码异常(Exception)
请求错误(404 等状态码)
JSP 编译错误

web.xml 配置错误页

<error-page>
  <error-code>404</error-code>
  <location>/error404.jsp</location>
</error-page>

<error-page>
  <error-code>500</error-code>
  <location>/error500.jsp</location>
</error-page>

<!-- 捕获所有异常 -->
<error-page>
  <exception-type>java.lang.Exception</exception-type>
  <location>/error.jsp</location>
</error-page>

try-catch 处理

@WebServlet("/risk")
public class RiskServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws ServletException, IOException {
        try {
            int result = riskyOperation();
            resp.getWriter().println("结果: " + result);
        } catch (Exception e) {
            e.printStackTrace();          // 日志
            resp.sendError(500, "服务器开小差了");
        }
    }
}

错误页中获取信息

<%-- error.jsp 中 --%>
错误码: <%=request.getAttribute("javax.servlet.error.status_code")%>
异常: <%=request.getAttribute("javax.servlet.error.exception")%>
URI: <%=request.getAttribute("javax.servlet.error.request_uri")%>

最佳实践

1. 异常记日志(不打印到页面)
2. 统一错误页展示友好信息
3. 区分业务异常(提示)与系统异常(日志+500)

Servlet Cookie 处理

Servlet Cookie 处理

Cookie 是服务器保存在客户端的小数据,用于记住用户状态。

创建并发送 Cookie

Cookie cookie = new Cookie("username", "Alice");
cookie.setMaxAge(60 * 60 * 24 * 7);   // 有效期7天(秒)
cookie.setPath("/");                  // 作用路径
resp.addCookie(cookie);               // 发送到客户端

读取 Cookie

Cookie[] cookies = req.getCookies();
String username = null;
if (cookies != null) {
    for (Cookie c : cookies) {
        if ("username".equals(c.getName())) {
            username = c.getValue();
        }
    }
}

修改与删除 Cookie

// 修改: 重新添加同名 Cookie
Cookie c = new Cookie("username", "Bob");
resp.addCookie(c);

// 删除: 设置 MaxAge=0
Cookie c = new Cookie("username", "");
c.setMaxAge(0);
resp.addCookie(c);

Cookie 常用方法

getName()      名称
getValue()     值
setMaxAge(秒)  有效期(负数=会话结束删除,0=立即删除)
getMaxAge()    有效期
setPath()      路径
setHttpOnly(true)  防 JS 读取(安全)
setSecure(true)    仅 HTTPS 传输

应用场景

记住登录状态(记住我)
记录用户偏好(语言/主题)
购物车(少量数据)
跟踪浏览(统计)

限制

大小: 单个 ≤4KB,总数 ≤20 个左右
敏感数据不要放 Cookie(可被篡改)
大量数据用 Session/数据库

Servlet Session 跟踪

Servlet Session 跟踪

Session 是服务器端保存的会话数据,用于跨请求维持用户状态(登录态、购物车)。

获取 Session

HttpSession session = req.getSession();       // 有则取,无则创建
HttpSession session = req.getSession(false);  // 有则取,无则返回 null

Session 操作

// 存
session.setAttribute("user", "Alice");
session.setAttribute("cart", cartList);

// 取
String user = (String) session.getAttribute("user");

// 删
session.removeAttribute("user");

// 销毁整个会话(注销登录)
session.invalidate();

// 会话 ID
String id = session.getId();

Session 有效期

// web.xml 配置 30 分钟
<session-config>
  <session-timeout>30</session-timeout>
</session-config>

// 编程设置
session.setMaxInactiveInterval(30 * 60);   // 秒

实现原理

服务器为每个会话生成唯一 JSESSIONID
通过 Cookie 或 URL 重写传给客户端
客户端每次请求携带,服务器匹配 Session

登录态管理

登录成功:
  session.setAttribute("user", userObj);

需要登录的页面:
  if (session.getAttribute("user") == null) {
      resp.sendRedirect("/login.jsp");
      return;
  }

注销:
  session.invalidate();

Cookie vs Session

Cookie: 客户端存储,小,不安全
Session: 服务器存储,安全,可存任意对象
(集群部署时 Session 需共享方案:Redis)

Servlet 数据库访问

Servlet 数据库访问

Servlet 通过 JDBC 访问数据库,实现动态数据读写。

添加 MySQL 驱动

<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>8.0.33</version>
</dependency>

连接数据库

Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/shop?useSSL=false&characterEncoding=UTF-8";
Connection conn = DriverManager.getConnection(url, "root", "password");

查询数据

@WebServlet("/users")
public class UserListServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        try (Connection conn = getConnection();
             Statement stmt = conn.createStatement()) {

            ResultSet rs = stmt.executeQuery("SELECT id, name FROM users");
            out.println("<table border='1'>");
            while (rs.next()) {
                out.println("<tr><td>" + rs.getInt("id")
                    + "</td><td>" + rs.getString("name") + "</td></tr>");
            }
            out.println("</table>");
        } catch (Exception e) {
            e.printStackTrace();
            out.println("查询失败");
        }
    }
}

PreparedStatement(防注入)

String sql = "INSERT INTO users(name, age) VALUES(?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, "Alice");
    ps.setInt(2, 25);
    ps.executeUpdate();
}

最佳实践

1. 永远用 PreparedStatement(防 SQL 注入)
2. 使用连接池(如 HikariCP)替代 DriverManager
3. 用 DAO 层封装数据库操作
4. 现代项目直接用 MyBatis / Spring Data JPA

Servlet 文件上传

Servlet 文件上传

Servlet 3.0+ 原生支持文件上传,无需第三方库。

上传表单

<form action="upload" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <button type="submit">上传</button>
</form>

Servlet 处理上传

@WebServlet("/upload")
@MultipartConfig(                  // 关键注解
    maxFileSize = 10 * 1024 * 1024,    // 单文件 10MB
    maxRequestSize = 50 * 1024 * 1024, // 总请求 50MB
    fileSizeThreshold = 1024 * 1024    // 超过1MB写磁盘
)
public class UploadServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        Part filePart = req.getPart("file");   // 取文件
        String fileName = getFileName(filePart);

        // 保存路径
        String uploadPath = getServletContext()
            .getRealPath("/uploads");
        File dir = new File(uploadPath);
        if (!dir.exists()) dir.mkdirs();

        filePart.write(uploadPath + File.separator + fileName);

        out.println("上传成功: " + fileName);
    }

    private String getFileName(Part part) {
        String header = part.getHeader("content-disposition");
        for (String token : header.split(";")) {
            if (token.trim().startsWith("filename")) {
                return token.substring(token.indexOf("=") + 1)
                    .trim().replace(""", "");
            }
        }
        return "unknown";
    }
}

注意事项

1. 表单必须 enctype="multipart/form-data"
2. 文件命名防重(加时间戳/UUID)
3. 校验文件类型与大小
4. 上传目录勿放在可执行代码目录

Servlet 处理日期

Servlet 处理日期

在 Servlet 中处理日期,包括格式化、解析、时区。

获取当前时间

import java.util.Date;
import java.text.SimpleDateFormat;

Date now = new Date();
resp.getWriter().println(now);   // Sat Aug 18 14:30:00 CST 2026

格式化日期

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(new Date());
// 2026-08-18 14:30:00

// 模式说明
// yyyy 年  MM 月  dd 日
// HH 24小时  mm 分  ss 秒
// E 星期  a 上午/下午

解析日期字符串

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2026-08-18");
System.out.println(date);

处理请求中的日期参数

String dateStr = req.getParameter("birthday");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
    Date birthday = sdf.parse(dateStr);
    // 存入数据库等
} catch (ParseException e) {
    resp.getWriter().println("日期格式错误");
}

时区处理

// 指定时区格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));

// Java 8+ 推荐
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String s = now.format(fmt);

数据库日期交互

// 存入: Date → java.sql.Date
java.sql.Date sqlDate = new java.sql.Date(date.getTime());

// 读取: rs.getDate("created") → java.sql.Date → 格式化

Servlet 网页重定向

Servlet 网页重定向

重定向让浏览器跳转到另一个 URL,常用于登录后跳转、页面迁移。

重定向方法

resp.sendRedirect("login.jsp");
resp.sendRedirect("/app/dashboard");
resp.sendRedirect("https://www.example.com");

相对与绝对路径

// 相对路径(基于当前)
resp.sendRedirect("login.jsp");          // 同级
resp.sendRedirect("../index.jsp");       // 上级

// 根相对路径(推荐,带项目名)
resp.sendRedirect(req.getContextPath() + "/login.jsp");
// req.getContextPath() = /myapp
// 结果: /myapp/login.jsp

常见场景

// 登录成功后跳转
if (loginSuccess) {
    resp.sendRedirect("dashboard.jsp");
} else {
    resp.sendRedirect("login.jsp?error=1");
}

// 页面迁移(旧地址 → 新地址)
resp.sendRedirect("new-page.jsp");

// 防表单重复提交(PRG模式)
// POST 处理 → 重定向到 GET 页面

转发 vs 重定向对比

转发(forward):
  服务器内部,一次请求,URL 不变
  可传递 request 属性
  用于业务处理后的页面展示

重定向(sendRedirect):
  浏览器新请求(302),两次请求,URL 变化
  不能传 request 属性(用 Session/URL 参数)
  用于登录跳转、表单提交后

带参数重定向

resp.sendRedirect("result.jsp?msg=success&id=" + id);
// 目标页 req.getParameter("msg") 获取

Servlet 点击计数器

Servlet 点击计数器

实现网站访问计数,介绍两种方式:应用级计数器与持久化计数器。

1. 应用级计数器(内存)

@WebServlet("/count")
public class CountServlet extends HttpServlet {
    private int counter = 0;   // 注意: 单例,需同步

    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws IOException {
        synchronized (this) {
            counter++;
        }
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().println("访问次数: " + counter);
    }
}

2. 启动时初始化(统计总访问)

@WebServlet("/visit")
public class VisitCounter extends HttpServlet {
    private int totalVisits;

    @Override
    public void init() {
        // 从文件/数据库读取历史计数
        totalVisits = loadCount();
    }

    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp) throws IOException {
        totalVisits++;
        saveCount(totalVisits);   // 持久化
        resp.getWriter().println("总访问量: " + totalVisits);
    }
}

3. 按会话去重(独立访客)

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    HttpSession session = req.getSession();

    // 同一会话只计一次
    if (session.getAttribute("counted") == null) {
        totalVisits++;
        session.setAttribute("counted", true);
    }
    resp.getWriter().println("独立访客: " + totalVisits);
}

线程安全说明

Servlet 单例多线程 → 计数器需加锁或原子类
推荐: AtomicInteger

private AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

生产方案

简单站: 数据库记录 + 定时汇总
大数据: Redis 计数器(INCR 命令)

Servlet 自动刷新页面

Servlet 自动刷新页面

通过 Refresh 响应头或 META 标签让页面定时自动刷新,用于实时数据展示。

响应头方式

// 每 5 秒刷新当前页面
resp.setIntHeader("Refresh", 5);

// 5 秒后跳转到指定页面
resp.setHeader("Refresh", "5; url=dashboard.jsp");

// 立即跳转
resp.setHeader("Refresh", "0; url=newpage.jsp");

HTML META 方式

<!-- 在 JSP/HTML 的 head 中 -->
<meta http-equiv="refresh" content="5">
<meta http-equiv="refresh" content="5;url=dashboard.jsp">

实时时钟示例

@WebServlet("/clock")
public class ClockServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/html;charset=UTF-8");
        resp.setIntHeader("Refresh", 1);   // 每秒刷新

        PrintWriter out = resp.getWriter();
        out.println("<h1>服务器时间</h1>");
        out.println(new java.util.Date());
    }
}

监控面板示例

// 每 30 秒刷新,展示服务器状态
resp.setIntHeader("Refresh", 30);
// 输出 CPU/内存/在线人数等实时数据

注意事项

1. 刷新频率不要过高(一般 ≥5秒),避免服务器压力
2. 现代实时更新优先用 AJAX/WebSocket
3. 刷新会丢失页面状态(表单输入)

AJAX 替代方案

// 页面局部刷新,不丢失状态
fetch("/api/data")
  .then(r => r.json())
  .then(data => updateUI(data));

setInterval(refreshData, 5000);

Servlet 发送电子邮件

Servlet 发送电子邮件

Servlet 可以通过 JavaMail 发送邮件,用于注册验证、密码找回、通知。

添加依赖

<dependency>
  <groupId>com.sun.mail</groupId>
  <artifactId>jakarta.mail</artifactId>
  <version>1.6.7</version>
</dependency>

发送邮件工具类

import java.util.Properties;
import jakarta.mail.*;
import jakarta.mail.internet.*;

public class MailUtil {
    public static void send(String to, String subject, String content)
            throws MessagingException {
        // SMTP 配置
        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.qq.com");
        props.put("mail.smtp.port", "465");
        props.put("mail.smtp.ssl.enable", "true");
        props.put("mail.smtp.auth", "true");

        // 认证(发件人邮箱 + 授权码)
        Session session = Session.getInstance(props,
            new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication("sender@qq.com", "授权码");
                }
            });

        // 构建邮件
        MimeMessage msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress("sender@qq.com"));
        msg.setRecipients(Message.RecipientType.TO,
            InternetAddress.parse(to));
        msg.setSubject(subject, "UTF-8");
        msg.setText(content, "UTF-8", "html");

        Transport.send(msg);
    }
}

Servlet 中调用

@WebServlet("/sendmail")
public class MailServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String email = req.getParameter("email");
        String code = generateCode();   // 生成验证码

        try {
            MailUtil.send(email, "注册验证码",
                "<h2>您的验证码:</h2><b>" + code + "</b>");
            // 保存验证码到 Session
            req.getSession().setAttribute("code", code);
            resp.getWriter().println("邮件已发送");
        } catch (MessagingException e) {
            e.printStackTrace();
            resp.getWriter().println("发送失败");
        }
    }
}

注意事项

1. 邮箱需开启 SMTP 并获取授权码
2. 发送操作放后台线程,避免阻塞请求
3. 生产用消息队列异步发送
4. 发送频率限制,防垃圾邮件

Servlet 包

Servlet 包

了解 Servlet 相关包结构与常用类,是阅读文档和排查问题的基本功。

核心包

javax.servlet                Servlet 核心 API(Servlet 4)
javax.servlet.http           HTTP 相关(Servlet/Request/Response/Session)
javax.servlet.annotation     注解(@WebServlet 等)

// Tomcat 10+ / Jakarta EE 9+
jakarta.servlet
jakarta.servlet.http

javax.servlet 核心接口

Servlet            Servlet 接口
ServletRequest     请求抽象
ServletResponse    响应抽象
ServletContext     应用上下文(全局)
Filter             过滤器
FilterChain        过滤链
ServletConfig      配置对象

javax.servlet.http 核心类

HttpServlet        抽象类(继承实现)
HttpServletRequest  HTTP 请求
HttpServletResponse HTTP 响应
HttpSession        会话
Cookie             Cookie
HttpServletMapping  映射信息

常用类速查

ServletContext:
  getInitParameter("key")     全局参数
  getRealPath("/uploads")     真实路径
  getRequestDispatcher()      转发器

HttpServletRequest:
  getParameter / getAttribute / getSession
  getRequestURI / getHeader

HttpServletResponse:
  setContentType / sendRedirect / getWriter
  sendError / setHeader

版本对照

Servlet 4.0  javax.servlet   Tomcat 9
Servlet 5.0  jakarta.servlet  Tomcat 10
Servlet 6.0  jakarta.servlet  Tomcat 11

开发建议

注解开发优先(@WebServlet/@WebFilter)
用 HttpServlet 子类,实现 doGet/doPost
request/response 用完不需手动关闭(容器管理)

Servlet 调试

Servlet 调试

掌握 Servlet 调试技巧,快速定位问题。

1. 日志输出

// 简单打印(控制台)
System.out.println("请求参数: " + name);

// 专业日志 SLF4J + Logback
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger log = LoggerFactory.getLogger(MyServlet.class);
log.info("处理请求: {}", name);
log.error("出错了", e);

2. IDE 断点调试

IDEA 中以 Debug 模式启动 Tomcat
在 Servlet 代码设置断点
观察 request 对象、参数、变量值
F8 单步 / F9 继续

3. 检查请求与响应

// 打印请求信息
log.info("URI: {}, 方法: {}", req.getRequestURI(), req.getMethod());
log.info("参数: {}", req.getParameterMap());
log.info("Session ID: {}", req.getSession().getId());

// 响应状态
resp.getStatus();

4. 浏览器开发者工具

Network 标签:
  查看请求 URL、方法、状态码、响应头/体
  Payload: 请求参数
  Preview/Response: 服务器返回内容

Console: 前端 JS 错误

5. 常见问题排查

404: 检查 URL 与映射(@WebServlet路径)
405: 表单 method 与 doXxx 方法不匹配
500: 查看 Tomcat logs/catalina.out 堆栈
中文乱码: 编码设置(见前文)
ClassNotFound: 检查依赖与 lib 目录

6. Tomcat 日志

# Tomcat 日志目录
logs/catalina.out   启动与异常日志
logs/localhost.log  应用日志
logs/access_log     访问日志

Servlet 国际化

Servlet 国际化

国际化(i18n)让应用支持多种语言,根据用户区域显示对应语言内容。

准备资源文件

# messages_en.properties
welcome=Welcome to our website
login=Login

# messages_zh.properties
welcome=欢迎访问我们的网站
login=登录

Servlet 中使用

@WebServlet("/i18n")
public class I18nServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/html;charset=UTF-8");

        // 1. 根据浏览器语言选择 Locale
        Locale locale = req.getLocale();

        // 2. 加载资源
        ResourceBundle bundle = ResourceBundle.getBundle(
            "messages", locale);

        // 3. 输出翻译文本
        resp.getWriter().println(bundle.getString("welcome"));
        resp.getWriter().println(bundle.getString("login"));
    }
}

支持语言切换

// 通过参数或 Session 选择语言
String lang = req.getParameter("lang");
Locale locale;
if ("en".equals(lang)) locale = Locale.ENGLISH;
else if ("zh".equals(lang)) locale = Locale.CHINA;
else locale = req.getLocale();

// 记住选择
req.getSession().setAttribute("locale", locale);

带参数消息

# messages.properties
greeting=Hello {0}, welcome back!

// 格式化
MessageFormat.format(
    bundle.getString("greeting"),
    username);

日期与数字国际化

import java.text.*;

// 日期
DateFormat df = DateFormat.getDateInstance(
    DateFormat.MEDIUM, locale);
df.format(new Date());

// 数字
NumberFormat nf = NumberFormat.getNumberInstance(locale);
nf.format(12345.67);

最佳实践

1. 所有界面文案都放资源文件
2. 中文用 UTF-8 编码保存 properties
3. 前端国际化(i18next/Vue I18n)配合

Servlet 有用的资源

Servlet 有用的资源

Servlet 学习的参考资料、工具与进阶方向。

官方文档

Jakarta EE 规范: jakarta.ee
Servlet 规范 PDF(下载区)
Oracle Java EE Tutorial(英文)

学习教程

W3CSchool / 菜鸟教程 Servlet 部分
B站 Java Web 入门课程
《Head First Servlets & JSP》经典书籍
《Java Web 开发实战》

开发工具

Tomcat 9(经典) / Tomcat 10+(jakarta)
Maven(构建) / IntelliJ IDEA
Postman(接口测试)
MySQL Workbench(数据库)

常见框架(基于 Servlet)

Spring MVC    主流 Web 框架
Spring Boot   现代首选(内嵌 Tomcat)
Struts 2      老牌框架
JSF           组件化框架

进阶主题

过滤器与拦截器原理
Session 集群共享(Redis)
Web 安全(XSS/SQL注入/CSRF)
异步处理(Async Servlet)
WebSocket(实时通信)

学习路线建议

1. 手写 Servlet 理解 HTTP 交互
2. 结合 JSP/JSTL 做完整小项目
3. 学习 Filter/Listener 等组件
4. 进阶 Spring MVC → Spring Boot
5. 掌握后大部分项目直接 Spring Boot 开发

Servlet 虽被框架掩盖,但其原理(请求-响应-会话)是理解一切 Java Web 框架的地基,值得扎实掌握。

第 1 / 24 节