本文通过一个完整的"选课管理系统"登录注册项目,手把手带你学习 Java Servlet 的核心知识。适合没有任何 Servlet 基础、想快速上手的同学。


目录

  1. 课前准备:你需要装什么
  2. Servlet 是什么?一张图看懂
  3. 5 分钟写出你的第一个 Servlet
  4. Servlet 的一生:生命周期
  5. 用 @WebServlet 给 Servlet 分配网址
  6. web.xml:另一种配置方式
  7. 怎么拿到用户提交的数据?
  8. 登录成功了,怎么跳转页面?
  9. 直接往浏览器输出内容
  10. 让 Servlet 连上数据库
  11. 密码不能明文存——MD5 加密
  12. JSP:写 HTML 的页面模板
  13. 前端 JS 验证 + 后端 Servlet 验证,一个都不能少
  14. 把整个流程串起来
  15. 项目文件该放哪?标准目录结构
  16. 代码中的坑,我帮你踩过了
  17. 总结

1. 课前准备:你需要装什么

在开始写代码之前,先把这些工具准备好:

工具 干什么用的 本项目用的版本
JDK Java 运行环境 1.8
Tomcat Servlet 容器,运行 Servlet 的地方 8.5
MySQL 数据库 5.x / 8.x 都行
IntelliJ IDEA 写代码的 IDE 任意版本
mysql-connector-java JDBC 驱动,Java 连 MySQL 用的 5.1.47

重要概念: Servlet 不是那种 main 方法启动就能跑的程序。它必须放到一个叫 Servlet 容器 的东西里才能运行。Tomcat 就是最常用的 Servlet 容器。你把写好的 Servlet 部署到 Tomcat,Tomcat 负责接收浏览器请求,调你的 Servlet 来处理,然后把结果返回给浏览器。


2. Servlet 是什么?一张图看懂

一句话:Servlet 就是跑在服务器上、专门处理浏览器请求的 Java 类。

整个流程是这样的:

浏览器输入网址 → 请求到达 Tomcat → Tomcat 找到对应的 Servlet → Servlet 干活 → 返回页面给浏览器

Java 里 Servlet 的继承关系:

Servlet(接口,定规矩)
  └── GenericServlet(抽象类,实现通用功能)
       └── HttpServlet(抽象类,专门处理 HTTP 请求)← 你写 Servlet 就继承它

实际写代码的时候,你只需要做两件事:

public class MyServlet extends HttpServlet {
    // 处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) { ... }

    // 处理 POST 请求
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) { ... }
}

req 是请求对象——客户端发过来的所有信息都在里面。resp 是响应对象——你要返回给客户端的内容通过它来设置。


3. 5 分钟写出你的第一个 Servlet

下面就是这个项目的登录 Servlet 的完整代码。别被长度吓到,我们拆开一句一句讲:

package com.sleepcat;

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.sql.*;

@WebServlet(value = "/HtmlLoginServlet")
public class HtmlLoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String name = req.getParameter("name");
        String pass = req.getParameter("pass");

        try {
            Class.forName("com.mysql.jdbc.Driver");
            Connection con = DriverManager.getConnection(
                "jdbc:mysql:///homework", "root", "root");
            Statement stmt = con.createStatement();
            pass = MD5Encryption.encryptMD5(pass);
            String sql = "SELECT * FROM `login` where username='"
                         + name + "' and password='" + pass + "'";
            ResultSet rs = stmt.executeQuery(sql);
            if (rs.next()) {
                resp.sendRedirect("index.jsp");
            } else {
                resp.setContentType("text/html;charset=UTF-8");
                resp.getWriter().print("<script>\n" +
                    "    alert('用户名或密码错误!请重新登录');\n" +
                    "    window.location.href = 'login.jsp';\n" +
                    "</script>");
            }
        } catch (ClassNotFoundException | SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

逐行拆解:

  • @WebServlet(value = "/HtmlLoginServlet") — 告诉 Tomcat:“有人访问 /HtmlLoginServlet 这个地址的时候,用这个类来处理”。
  • extends HttpServlet — 继承 HttpServlet,你的类就变成了一个能处理 HTTP 请求的 Servlet。
  • doPost(...) — 这个方法专门处理 POST 请求。前端表单用 method="post" 提交,就会进这个方法。
  • req.getParameter("name") — 从请求里拿参数。参数名 "name" 对应前端 <input name="name">名字必须完全一致
  • resp.sendRedirect("index.jsp") — 让浏览器跳转到 index.jsp(重定向)。
  • resp.setContentType("text/html;charset=UTF-8") — 告诉浏览器"我返回的是 HTML,编码是 UTF-8"。这行必须在 getWriter() 之前写!
  • resp.getWriter().print(...) — 直接往浏览器输出内容。这里输出了一段 JavaScript,浏览器收到后会弹个 alert 框然后跳转。

4. Servlet 的一生:生命周期

Servlet 的生命由 Tomcat 全程管理,分三个阶段:

init()       →  诞生:第一次被访问时调用一次,做初始化工作
service()    →  工作:每次请求都调用,内部帮你分派到 doGet/doPost
destroy()    →  销毁:容器关闭或被卸载时调用一次,清理资源

三个你必须知道的事实:

  1. 一个 Servlet 类在内存中只有一个实例(单例),所有请求共享它。所以不要在 Servlet 里写会变的成员变量。
  2. init() 一生只调一次,适合放加载驱动这种一次性操作。
  3. service() 你一般不直接重写它,而是重写 doGet()doPost()service() 会根据请求类型自动分派。

GET 和 POST 的使用场景:

方法 什么时候用 特点
doGet() 点链接、直接访问 URL、表单 method=“get” 参数在 URL 里可见
doPost() 提交登录、注册等表单 参数在请求体里,URL 里看不到

5. 用 @WebServlet 给 Servlet 分配网址

Servlet 3.0 开始支持注解配置,一行代码搞定 URL 映射:

@WebServlet(value = "/HtmlLoginServlet")

当浏览器访问 http://localhost:8080/HtmlLoginServlet 时,Tomcat 就调用这个 Servlet。

@WebServlet 有哪些属性可以配:

属性 说明 例子
valueurlPatterns URL 路径,可以配多个 "/login"{"/a", "/b"}
name 给 Servlet 起个名字 "LoginServlet"
loadOnStartup 设为 1 表示 Tomcat 一启动就加载 1
initParams 初始化参数 @WebInitParam(name="db", value="mysql")

本项目两个 Servlet 的映射:

  • @WebServlet(value = "/HtmlLoginServlet") → 处理登录
  • @WebServlet(value = "/HtmlRegisterServlet") → 处理注册

前端表单 <form action="/HtmlLoginServlet" method="post"> 中的 action 必须和这个一致。


6. web.xml:另一种配置方式

在 Servlet 3.0 之前,只能通过 web.xml 来配置 Servlet。这个文件位于 WEB-INF/web.xml,是整个应用的配置中心。

本项目的 web.xml 长这样:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
</web-app>

因为本项目用 @WebServlet 注解配置了路由,所以 web.xml 是空的。

如果用 xml 方式配置,要这样写:

<!-- 声明一个 Servlet -->
<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.sleepcat.HtmlLoginServlet</servlet-class>
</servlet>

<!-- 把 Servlet 映射到一个 URL -->
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/HtmlLoginServlet</url-pattern>
</servlet-mapping>

注解 vs web.xml 选哪个?

方式 优点 缺点
@WebServlet 代码即配置,简洁直观 URL 路径写死在代码里
web.xml 集中管理,改 URL 不用重新编译 配置繁琐,xml 一长就不好维护

初学阶段用 @WebServlet 就够了,等你学了 Spring MVC,路由配置会更简单。


7. 怎么拿到用户提交的数据?

前端表单提交后,Servlet 里用 HttpServletRequest 来拿数据:

// 拿单个值(最常用)
String name = req.getParameter("name");

// 拿多选值(如 checkbox)
String[] hobbies = req.getParameterValues("hobby");

// 遍历所有参数名
Enumeration<String> names = req.getParameterNames();

// 拿到所有参数的 Map
Map<String, String[]> map = req.getParameterMap();

铁律: req.getParameter("xxx") 里填的字符串,必须和前端 <input name="xxx">name 属性的值一模一样

来看本项目中前后端是怎么对上的:

前端(login.jsp):

用户名:<input name="name" type="text"><br>
密码:<input name="pass" type="password"><br>

后端(HtmlLoginServlet):

String name = req.getParameter("name");   // 拿到用户名
String pass = req.getParameter("pass");   // 拿到密码

参数名 "name""pass" 前后完全一致,这就对上了。


8. 登录成功了,怎么跳转页面?

Servlet 处理完之后要跳转页面,有两种方式,区别很大:

方式一:重定向(sendRedirect)

resp.sendRedirect("index.jsp");
  • 浏览器地址栏 会变成 index.jsp
  • 实际发生了两次请求:服务器返回 302,浏览器再请求新地址
  • 可以跳转到外部网站(如 resp.sendRedirect("https://baidu.com")
  • 因为是两次不同的请求,不能携带 request 数据

方式二:请求转发(forward)

req.getRequestDispatcher("index.jsp").forward(req, resp);
  • 浏览器地址栏 不会变,还是原来的 URL
  • 一次请求,服务器内部转发
  • 只能在同一个应用内跳转
  • 可以携带 request 域数据(因为还是同一个 request)

本项目中的实际应用:

// 登录成功 → 重定向到首页
resp.sendRedirect("index.jsp");

// 登录失败 → 输一段 JS,在浏览器端弹窗并跳转
resp.getWriter().print("<script>\n" +
        "    alert('用户名或密码错误!请重新登录');\n" +
        "    window.location.href = 'login.jsp';\n" +
        "</script>");

失败时用了 JS 来做客户端弹窗再跳转,这是一个很实用的技巧——既给了用户提示,又控制了页面去向。


9. 直接往浏览器输出内容

有时候不需要跳转页面,直接输出内容就行。HttpServletResponse 提供了这个能力:

// 第一步:设置返回内容的类型和编码(必须在 getWriter() 之前!)
resp.setContentType("text/html;charset=UTF-8");

// 第二步:拿到输出流,开写
PrintWriter out = resp.getWriter();
out.print("<html><body>");
out.print("<h1>Hello World!</h1>");
out.print("</body></html>");

本项目中,登录失败的时候就是往浏览器输出了一个 <script> 标签:

resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().print("<script>\n" +
        "    alert('用户名或密码错误!请重新登录');\n" +
        "    window.location.href = 'login.jsp';\n" +
        "</script>");

浏览器收到这段 HTML 后,会执行里面的 JavaScript:弹窗 → 用户点确定 → 跳回登录页。一气呵成。


10. 让 Servlet 连上数据库

真实项目肯定要存数据。这个项目用的是最原始的 JDBC 来连 MySQL。虽然现在 Spring 项目都用 MyBatis 或 JPA 了,但 JDBC 是它们的老祖宗,搞懂它才能理解框架在干什么。

10.1 加载驱动 + 建立连接

// 加载 MySQL 驱动
Class.forName("com.mysql.jdbc.Driver");

// 建立连接
Connection con = DriverManager.getConnection(
    "jdbc:mysql:///homework",  // jdbc:mysql:///数据库名 = 连本机 3306 端口
    "root",                    // 用户名
    "root"                     // 密码
);

URL 速记: jdbc:mysql:///homework 省略了主机和端口,等价于 jdbc:mysql://localhost:3306/homework

10.2 查询(SELECT)

Statement stmt = con.createStatement();
String sql = "SELECT * FROM `login` WHERE username='" + name
           + "' AND password='" + pass + "'";
ResultSet rs = stmt.executeQuery(sql);

if (rs.next()) {
    // 查到了 → 登录成功
    resp.sendRedirect("index.jsp");
} else {
    // 没查到 → 登录失败
}

rs.next() 把光标移到下一行,有数据返回 true,没数据返回 false

10.3 插入(INSERT)

Statement stmt = con.createStatement();
String sql = "INSERT INTO homework.login (username, password) VALUES ('"
             + name + "', '" + pass + "')";
int count = stmt.executeUpdate(sql);

if (count > 0) {
    // count 是受影响的行数,>0 表示插入成功
    resp.sendRedirect("login.jsp");
}

10.4 JDBC 操作流程总结

Class.forName("驱动类名")        →  加载数据库驱动
DriverManager.getConnection()   →  获取数据库连接
connection.createStatement()    →  创建一个 Statement
stmt.executeQuery(sql)          →  执行查询,返回 ResultSet
stmt.executeUpdate(sql)         →  执行增删改,返回受影响行数
处理结果
关闭资源(rs → stmt → con,倒序关闭)

11. 密码不能明文存——MD5 加密

密码绝对不能明文存在数据库里。万一数据库泄露,所有用户的密码就全暴露了。

正确的做法是用哈希算法把密码算成一个固定长度的、不可逆的字符串再存。本项目用的是 MD5。

工具类 MD5Encryption.java:

package com.sleepcat;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Encryption {
    public static String encryptMD5(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(input.getBytes());           // 把字符串转成字节喂给 MD5
            byte[] messageDigest = digest.digest();    // 算出 16 字节的哈希值

            // 把 16 字节转换成 32 位的十六进制字符串
            StringBuilder hexString = new StringBuilder();
            for (byte b : messageDigest) {
                String h = Integer.toHexString(0xFF & b);
                while (h.length() < 2) h = "0" + h;   // 不满两位补 0
                hexString.append(h);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

使用方式:

pass = MD5Encryption.encryptMD5(pass);  // 先把密码加密,再和数据库比对

效果演示:

用户输入:123456
MD5 加密后:e10adc3949ba59abbe56e057f20f883e

数据库里存的是:e10adc3949ba59abbe56e057f20f883e
用户登录输 123456 → 加密 → 得到 e10adc... → 和数据库的比对 → 一致就放行

这样一来,即使有人看到数据库,也拿不到用户的真实密码,只能看到一堆哈希值。


12. JSP:写 HTML 的页面模板

JSP 本质就是 Servlet 的另一种写法——它会被 Tomcat 在运行时自动编译成一个 Servlet。所以 JSP = Servlet,只是写法不同。

12.1 JSP 的 page 指令

每个 JSP 文件开头都有这么一行:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

它告诉浏览器:“我这页面是 HTML,编码是 UTF-8”。

12.2 在 JSP 里写 Java 代码

<%@ page import="java.util.*" %>    <!-- 导包 -->
<%! int count = 0; %>               <!-- 声明成员变量 -->
<% count++; %>                      <!-- 写 Java 代码 -->
<%= count %>                        <!-- 输出到页面 -->

但实际开发中 JSP 里尽量少写 Java 代码,只做数据展示。本项目就做得很好——三个 JSP 页面基本就是纯 HTML + CSS。

12.3 登录页面完整代码(login.jsp)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>用户登录</title>
    <link href="css/login.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="login">
    <div class="message">选课管理系统</div>
    <form action="/HtmlLoginServlet" method="post">
        用户名:<input name="name" type="text"><br>
        密码:<input name="pass" type="password"><br>
        <input type="submit" value="登录">
    </form>
    <a href="register.jsp">用户注册</a>
</div>
</body>
</html>

关注这三个点:

  • action="/HtmlLoginServlet" → 表单提交到哪个 Servlet
  • method="post" → 用 POST 方式提交(密码不应该出现在 URL 里)
  • name="name"name="pass" → 前后端数据交互的桥梁

13. 前端 JS 验证 + 后端 Servlet 验证,一个都不能少

13.1 前端验证

注册页面(register.jsp)有完整的 JavaScript 验证逻辑:

function ver() {
    let name = document.getElementsByName("name")[0].value;
    let pass1 = document.getElementsByName("pass1")[0].value;
    let pass2 = document.getElementsByName("pass2")[0].value;

    if (name == '' || pass1 == '' || pass2 == '') {
        alert("注册输入项都不能为空!");
        return false;              // 返回 false,表单不提交
    } else if (pass1 != pass2) {
        alert("两次密码不一致,请重新输入!");
        return false;
    } else {
        return true;               // 验证通过,表单正常提交
    }
}

表单通过 onsubmit 绑定验证:

<form action="/HtmlRegisterServlet" method="post" onsubmit="return ver()">

onsubmit="return ver()" 这个写法很关键——当 ver() 返回 false 时,表单不会提交。

13.2 前端验证好还是后端验证好?答案是:都要

验证层 作用 能被绕过吗?
前端验证 减少无效请求,给用户即时反馈 (禁用 JS、f12 改 HTML 都能绕过)
后端验证 最后一道防线,保证数据安全 不能

本项目只做了前端验证,没做后端验证,这意味着安全性不够。 正确的做法是在 Servlet 里也加上:

if (name == null || name.trim().isEmpty() ||
    pass1 == null || pass1.trim().isEmpty()) {
    // 参数不合法,返回错误提示
    resp.getWriter().print("参数不能为空");
    return;
}

13.3 GET 和 POST 的区别

<!-- GET:参数跟在 URL 后面,适合搜索等不敏感的场景 -->
<form action="/search" method="get">

<!-- POST:参数在请求体里,适合传密码等敏感数据 -->
<form action="/login" method="post">

本项目登录和注册都正确用了 POST——这是正确的做法。


14. 把整个流程串起来

说了这么多,来看一次完整的请求是怎么走的。

14.1 整体架构

┌──────────────────────────────────────────────────────┐
│                    浏览器(客户端)                     │
│  login.jsp(登录页) ⇄ register.jsp(注册页) ⇄ index.jsp(首页) │
└──────────────────────┬───────────────────────────────┘
                       │ POST 请求
                       ▼
┌──────────────────────────────────────────────────────┐
│                   Tomcat(Servlet 容器)               │
│  HtmlLoginServlet(登录逻辑)    HtmlRegisterServlet(注册逻辑) │
│  ├─ 接收参数                                           │
│  ├─ MD5 加密密码                                       │
│  ├─ 查/写数据库                                         │
│  └─ 返回结果                                           │
└──────────────────────┬───────────────────────────────┘
                       │ JDBC
                       ▼
┌──────────────────────────────────────────────────────┐
│                    MySQL 数据库                        │
│  数据库:homework  表:login                           │
│  字段:username (VARCHAR), password (VARCHAR)          │
└──────────────────────────────────────────────────────┘

14.2 登录流程(一步一步走)

1. 浏览器访问 login.jsp → 看到登录表单

2. 用户填好用户名、密码 → 点"登录"
   → 浏览器 POST 请求 /HtmlLoginServlet
   → 请求体:name=zhangsan&pass=123456

3. HtmlLoginServlet.doPost() 干活:
   ① req.getParameter("name") → "zhangsan"
   ② req.getParameter("pass") → "123456"
   ③ MD5Encryption.encryptMD5("123456")
      → "e10adc3949ba59abbe56e057f20f883e"
   ④ 拼 SQL:
      SELECT * FROM login
      WHERE username='zhangsan'
        AND password='e10adc3949ba59abbe56e057f20f883e'
   ⑤ 执行查询

4. 判断:
   ├─ 查到了 → resp.sendRedirect("index.jsp") → 进首页
   └─ 没查到 → 输出 JS alert("用户名或密码错误") → 回登录页

14.3 注册流程

1. 点"用户注册"链接 → 进 register.jsp

2. 填信息,JS 先检查:
   ├─ 有空字段 → alert("不能为空") → 卡住
   ├─ 两次密码不一致 → alert("不一致") → 卡住
   └─ 都 OK → 表单 POST /HtmlRegisterServlet

3. HtmlRegisterServlet.doPost() 干活:
   ① 拿到 name、pass1
   ② MD5 加密密码
   ③ 拼 SQL:INSERT INTO homework.login (username, password) VALUES (...)
   ④ 执行插入

4. 判断:
   ├─ count > 0 → sendRedirect("login.jsp") → 去登录
   └─ count = 0 → JS alert("注册失败") → 回注册页

14.4 项目文件一览

文件 角色 说明
HtmlLoginServlet.java Servlet 处理登录请求
HtmlRegisterServlet.java Servlet 处理注册请求
MD5Encryption.java 工具类 MD5 密码加密
login.jsp JSP 登录页面
register.jsp JSP 注册页面
index.jsp JSP 登录成功后的首页
web.xml 配置文件 部署描述符
mysql-connector-java-5.1.47.jar 第三方库 MySQL JDBC 驱动
login.css 样式表 登录、注册页的样式

15. 项目文件该放哪?标准目录结构

servlet-usage-master/
├── src/                              ← 所有 Java 源码放这里
│   └── com/sleepcat/
│       ├── HtmlLoginServlet.java
│       ├── HtmlRegisterServlet.java
│       └── MD5Encryption.java
│
└── web/                              ← Web 资源根目录
    ├── index.jsp                     ← 用户可以直接访问的页面
    ├── login.jsp
    ├── register.jsp
    ├── css/                          ← 样式和样式相关的图片
    │   ├── login.css
    │   └── img/
    ├── images/                       ← 页面用的图片
    └── WEB-INF/                      ← 安全目录,用户无法通过 URL 访问!
        ├── web.xml                   ← 部署描述符
        └── lib/                      ← 第三方 jar 包,Tomcat 自动加载
            └── mysql-connector-java-5.1.47.jar

最重要的规则:WEB-INF/ 下的东西,浏览器无法直接访问。 所以配置文件、编译后的 class 文件、第三方 jar 包都放这里面,保证安全。

  • src/*.java → 编译后 → WEB-INF/classes/*.class(自动的)
  • WEB-INF/lib/*.jar → Tomcat 启动时自动加载

16. 代码中的坑,我帮你踩过了

学习代码不等于生产代码。这个项目有几处明显的问题,你现在知道了以后就能避开。

坑一:SQL 注入——最严重的安全漏洞

项目中的写法:

String sql = "SELECT * FROM `login` WHERE username='"
             + name + "' AND password='" + pass + "'";

直接把用户输入拼进 SQL 字符串。如果用户在用户名框里输入:

' OR '1'='1' --

拼出来的 SQL 就变成了:

SELECT * FROM `login` WHERE username='' OR '1'='1' --' AND password='xxx'

'1'='1' 永远成立,-- 后面全被注释掉了。结果就是不用密码直接登录。

怎么修?用 PreparedStatement:

String sql = "SELECT * FROM login WHERE username = ? AND password = ?";
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, name);                    // 第一个 ? 用 name 的值
pstmt.setString(2, encryptMD5(pass));        // 第二个 ? 用加密后的密码
ResultSet rs = pstmt.executeQuery();

PreparedStatement 会把参数和 SQL 结构分离,用户输入的内容永远被当作"值"而不是 SQL 的一部分来执行,从根本上杜绝了注入。

坑二:数据库资源不关闭

Connection con = DriverManager.getConnection(...);
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 用完了,rs、stmt、con 都没关!

每次连数据库不关,连接池很快就被占满,新的请求就连不上数据库了。

怎么修?用 try-with-resources:

String sql = "SELECT * FROM login WHERE username = ? AND password = ?";
try (Connection con = DriverManager.getConnection(url, user, pwd);
     PreparedStatement pstmt = con.prepareStatement(sql)) {
    pstmt.setString(1, name);
    pstmt.setString(2, pass);
    try (ResultSet rs = pstmt.executeQuery()) {
        if (rs.next()) {
            resp.sendRedirect("index.jsp");
        }
    }
} catch (SQLException e) {
    throw new RuntimeException(e);
}
// try 块结束,con、pstmt、rs 自动关闭,不用你手动处理

括号里的资源只要实现了 AutoCloseable 接口,就会在 try 块结束时自动关闭。

坑三:只有前端验证,没有后端验证

前面第 13 节已经讲过了。注册页只靠 JS 做非空和一致性检查,但 JS 可以被绕过去。正确的做法是前后端都验证:

// Servlet 里必须加这段
if (name == null || name.trim().isEmpty()) {
    // 返回错误,不往下走
    return;
}

坑四:数据库密码硬编码

DriverManager.getConnection("jdbc:mysql:///homework", "root", "root");

密码直接写在代码里,想改密码就要重新编译。而且一旦代码上 git,密码就公开了。

改进方式: 放在配置文件(如 db.properties)里,代码运行时读取。

# db.properties
jdbc.url=jdbc:mysql:///homework
jdbc.username=root
jdbc.password=root

坑五:MD5 不适合生产环境

MD5 已经被证明有碰撞漏洞,而且计算速度太快,攻击者可以用彩虹表秒破常见密码。生产环境应该用 bcryptscrypt——它们内置了盐值,而且计算速度故意做得慢,增加暴力破解的成本。

但在学习阶段,用 MD5 理解密码哈希的原理完全没问题,学到这个知识点就够了。


17. 总结

通过这个项目,你应该已经掌握了:

知识点 核心内容
Servlet 基础 继承 HttpServlet,重写 doGet()/doPost()
URL 映射 @WebServlet 注解,web.xml 配置
获取参数 req.getParameter("name"),名字要和前端一致
跳转页面 sendRedirect() 重定向 vs forward() 转发
输出响应 resp.setContentType() + resp.getWriter().print()
JDBC 操作 Class.forName()getConnection()executeQuery()/executeUpdate()
密码加密 MessageDigest 做 MD5 哈希
JSP page 指令,表单,和 Servlet 的交互
前后端协作 前端 JS 验证 + 后端 Servlet 验证,表单 POST 提交
项目结构 WEB-INF/ 安全目录,lib/ 放 jar 包,标准布局
安全意识 SQL 注入、资源泄露、硬编码、输入校验

Servlet 是 Java Web 开发的基石。Spring MVC 的 DispatcherServlet,本质上也是一个 Servlet。把 Servlet 搞懂了,再学 Spring Boot 你会发现那些"自动配置"不再神秘——你知道下面是怎么回事。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐