如果让我从这几年看新人代码的经验里挑一个最容易被“我以为会了”糊弄过去的知识点,JSP 里的增删改绝对高票当选。很多同学照着教程敲,列表能出来,新增能入库,删除也能少一行,但一到“编辑”按钮就卡住——点进去表单是空的,或者姓名填在文本框里了,下拉框没选中、单选没勾上、多选更是乱成一团。这个问题的核心,就是标题里专门点出来的那两个字:回显。增删改查这四件事,回显不是单独的一步,而是编辑功能的骨架,也是判断一个人是真懂还是只会背代码的分水岭。这篇文章我就拿一个学生信息管理系统的完整案例,把 JSP 做增删改、以及编辑回显和校验失败回显的链路从头到尾捋一遍,适合正在做 JSP 课设、或者刚从 Servlet 学到 JSP 的初学者参考。
1. 先搞明白“回显”到底是哪一步的活
1.1 回显的两种典型场景
很多人一听到“回显”,第一反应就是“编辑页面把数据库的值填回去”。对,但只对了一半。实际开发里回显至少有两种场景,如果不区分,很容易写出看着能跑、一改就崩的代码。
第一种是编辑回显。用户点列表页的“编辑”按钮,系统根据这条记录的 id 去数据库查出完整数据,然后把这些数据填充到 edit.jsp 表单的各个控件里。下拉框要选中用户之前选的项,单选框要勾上用户之前的性别,复选框要勾上所有用户勾过的爱好。这是标题里说“回显”时默认指的场景。
第二种是校验失败回显。用户在新增页面提交数据时,后端校验发现“姓名为空”或者“年龄不是数字”,把请求打回 add.jsp,同时把用户已经填好的其他字段重新显示出来。这个场景如果处理不好,用户每次提交失败都要从头再填一遍,体验极差。
这两种回显虽然都叫回显,但数据来源完全不一样:一个是数据库查出来的,一个是上一次请求带过来的参数。很多教程只讲第一种,导致初学者在第二种场景里翻车。
1.2 一句话说清回显的数据流向
回显看起来是页面上的事,本质上是数据在“数据库 → Java对象 → request作用域 → JSP页面 → 表单控件”之间流动的过程。
数据库里的一行记录,先被 DAO 层查出来封装成 Student 对象,Servlet 拿到这个对象后调用 request.setAttribute("stu", student),然后通过请求转发把请求交给 JSP,JSP 里再通过 EL 表达式 ${stu.name} 把对象的属性取出来,最终输出到 HTML 控件的 value 属性或者选中状态里。
这个链条上任何一环断了,回显都会失败。最常见的断点有三个:DAO 没查到数据返回了 null、Servlet 用了重定向导致 request 里的数据丢了、JSP 里 EL 表达式属性名写错。后面我会逐个演示正确写法,你只要跟着把链路走通,再遇到回显问题就知道该往哪个环节排查。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭骨架:建表、JDBC 工具类、三层分包
2.1 学生表字段怎么设计
用什么业务做例子,直接决定回显的覆盖面。我选学生信息管理,是因为学生实体天然包含多种数据类型:性别是单选框、专业是下拉框、爱好是多选框、备注是文本域。把这些控件全部照顾到,你以后换成商品管理、订单管理,回显逻辑完全一样。
先建数据库表:
sql复制CREATE DATABASE student_ms DEFAULT CHARACTER SET utf8mb4;
USE student_ms;
CREATE TABLE student (
id INT AUTO_INCREMENT PRIMARY KEY,
stu_no VARCHAR(20) NOT NULL COMMENT '学号',
name VARCHAR(20) NOT NULL COMMENT '姓名',
gender VARCHAR(4) COMMENT '性别',
age INT COMMENT '年龄',
major VARCHAR(30) COMMENT '专业',
hobby VARCHAR(100) COMMENT '爱好,多个用逗号分隔',
remark VARCHAR(255) COMMENT '备注'
);
这里字段类型有几个细节值得注意。gender 用 VARCHAR 而不是 CHAR,是因为有些同学会传入空字符串,VARCHAR 处理起来更宽容。hobby 用逗号分隔存成一个字段,是刻意简化——如果做第三张关联表来表达多对多,会让课设代码量翻倍,初学者容易绕晕。真正生产环境我会拆表,但作为演示 JSP 回显,一个字符串字段配合 fn:contains 判断,反而能让回显逻辑更清晰。
2.2 DBUtil 工具类:把重复代码收起来
每次操作数据库都要经历“注册驱动、获取连接、预编译、执行、关闭资源”这五步,如果每个 DAO 方法里都写一遍,代码会膨胀到没法看。我习惯先抽一个 DBUtil:
java复制package com.example.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/student_ms"
+ "?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
private static final String USER = "root";
private static final String PASSWORD = "123456";
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static void close(ResultSet rs, PreparedStatement ps, Connection conn) {
if (rs != null) { try { rs.close(); } catch (SQLException e) { /* ignore */ } }
if (ps != null) { try { ps.close(); } catch (SQLException e) { /* ignore */ } }
if (conn != null) { try { conn.close(); } catch (SQLException e) { /* ignore */ } }
}
}
强调两点。第一,如果用 MySQL 8.x,驱动类名必须是 com.mysql.cj.jdbc.Driver,URL 里要带 serverTimezone,否则会报时区错误。第二,characterEncoding=utf8 这一项别省略,它和后端 request.setCharacterEncoding("UTF-8") 各管一段,少了哪个中文都可能乱码。
2.3 包结构:MVC 在 JSP 项目里怎么落位
这个案例的 package 结构我按最传统的三层来分:
text复制src/main/java
├── com.example.bean Student 实体类
├── com.example.dao StudentDao,所有 SQL 都在这
├── com.example.servlet ListServlet / AddServlet / DeleteServlet / EditServlet / UpdateServlet
└── com.example.util DBUtil
web
├── list.jsp
├── add.jsp
└── edit.jsp
Servlet 的角色是控制器:接收请求、调用 DAO、把数据放进 request、决定是转发还是重定向。JSP 的角色是视图:只负责展示数据和收集表单。Student 类就是纯 POJO,属性对应表字段,getter/setter 必不可少,因为 EL 表达式取值的底层依赖的就是 getter 方法,这里少写一个 getter,页面就多一个空值。
3. 列表页:增删改的起点,也是跳转的中枢
3.1 查询全部学生的 Servlet
列表页是用户操作的入口,没有列表页,新增了看不到、编辑了不知道去哪点。查询的 Servlet 非常短,但有两个细节值得说。
java复制package com.example.servlet;
import com.example.bean.Student;
import com.example.dao.StudentDao;
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.List;
@WebServlet("/list")
public class ListServlet extends HttpServlet {
private StudentDao studentDao = new StudentDao();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
List<Student> students = studentDao.findAll();
request.setAttribute("students", students);
request.getRequestDispatcher("list.jsp").forward(request, response);
}
}
第一个细节是 response.setContentType("text/html;charset=UTF-8"),这行决定浏览器用 UTF-8 解析响应内容,漏掉的话页面中文会乱码。第二个细节是 request 作用域,setAtt
