Java Web课程设计实战:手写JSP+Servlet+JDBC学生信息管理系统
简介:这份资源是一套基于JSP与Servlet实现的学生信息管理系统完整项目源码,面向正在学习Java Web开发、需要课程设计或练手案例的初学者与在校学生。项目采用MVC设计模式,将JSP页面作为视图、Servlet承担控制器职责、学生与班级数据模型配合JDBC完成增删改查,并附带数据库初始化脚本,覆盖请求响应流程、会话管理、前端页面构建与容器部署等环节。压缩包共576个文件,以html、css、png、js等前端页面与静态资源为主,另有20个java源文件及对应class、9个jar依赖、4个jsp页面和1个sql脚本,整体约2.75MB,结构完整可直接导入Tomcat运行。已有998人学习下载。读者可借此理解Servlet接收请求、调用DAO操作数据库、JSP渲染结果的完整链路,并参考其中的登录、学生与班级管理模块,作为课程设计或Java Web入门的实践范本。
1. 从一份“能跑但不敢交”的课设说起
每年做课程设计,总有人卡在同一个地方:功能明明跑通了,答辩时被问一句“你的请求是怎么从浏览器走到数据库的”,就答不上来。学生信息管理系统这个题目看着朴素,恰恰是检验 Java Web 基本功最狠的一类——它逼着你把 JSP、Servlet、JDBC、会话管理、表单校验这几块串成一条完整的链路,任何一环含糊,代码就会在演示现场翻车。这套方案面向两类人:一是要交课程设计或毕业设计、需要一份结构完整可参考实现的学生;二是刚接触 Java Web、想拿一个真实增删改查项目把 MVC 分层彻底搞明白的入门开发者。整套东西不依赖任何重型框架,纯 JSP + Servlet + JDBC 手写分层,好处是每一行请求处理逻辑都看得见,坏处是坑也得自己踩——下面就把需求、设计、实现和验收一条线讲透。
2. 需求与选型:为什么这个题目必须手写分层
2.1 先把功能边界划清楚
学生信息管理系统的核心需求其实就四块:学生信息的增删改查、按条件检索与分页、登录与权限区分、数据校验与异常提示。听起来简单,但真正决定项目能不能交的,是边界划得够不够清楚。我一般会先列一张功能清单,把“必须做”和“可以不做”分开,避免做到一半发现范围失控。
| 模块 | 必须实现 | 可选扩展 |
|---|---|---|
| 学生管理 | 新增、删除、修改、列表查询 | 批量导入导出 |
| 检索分页 | 按姓名/学号模糊查询、分页展示 | 多条件组合筛选 |
| 登录权限 | 管理员登录、未登录拦截 | 普通用户只读 |
| 数据校验 | 学号唯一、必填项校验、错误回显 | 前端实时校验 |
这张表的作用不是给你抄,而是让你在动手前就知道哪些功能是答辩必问的。比如“学号唯一”这条,很多人的实现是插入前查一次,结果并发下照样重复——正确做法是在数据库层加唯一索引,应用层捕获异常再提示,这一点后面避坑章节会细说。
2.2 技术选型:不用框架不是偷懒,是教学需要
选 JSP + Servlet + JDBC 而不是 Spring Boot,理由很实在:课程设计要展示的是你对 HTTP 请求生命周期、Servlet 容器、会话机制的理解,框架把这些都封装了,答辩时反而说不清。JSP 负责视图渲染,Servlet 负责接收请求和调度,JavaBean 承载数据,DAO 层封装数据库访问,这就是最经典的 MVC 落地形态。
选型上有几个具体决定值得说清楚。数据库用 MySQL,因为 JDBC 驱动成熟、课程环境普遍可用;连接管理不引入连接池也行,但我会建议至少写一个简单的工具类统一获取和关闭连接,否则每个 DAO 方法里重复写DriverManager.getConnection既难看又容易漏关。视图层用 JSP 而不是纯 HTML + AJAX,是因为 JSP 的 EL 表达式和 JSTL 标签能让列表渲染、条件判断写得很短,适合演示时快速讲清数据怎么从后端流到页面。
提示:如果课程环境允许,连接池用 Druid 或 HikariCP 都行,但要在报告里说明为什么引入——不是为了炫技,是为了避免每次请求都新建物理连接带来的性能损耗。
2.3 分层结构定下来,后面才不会乱
整个项目按四层组织:表现层(JSP 页面)、控制层(Servlet)、业务层(Service,简单项目可以合并进 Servlet,但我建议单独留出来)、数据访问层(DAO)。包结构大致是com.school.sms.controller、service、dao、entity、util。这样分的好处是,答辩时你可以指着包说“请求进 controller,调 service 做校验和业务组装,再交给 dao 落库,entity 在层间传递”,一句话就把架构讲明白了。
实体类 Student 的字段要和数据库表严格对应:学号、姓名、性别、班级、专业、联系电话、入学年份。字段别贪多,每多一个字段就多一处校验和一处页面绑定,课程设计阶段够用就好。
3. 总体设计:请求链路与数据库表怎么搭
3.1 一条请求的完整走向
理解这个项目最快的方式,是跟踪一次“查询学生列表”的请求。浏览器访问/student?action=list&page=1,Tomcat 把请求交给映射在/student上的 Servlet,Servlet 从request.getParameter取出 action 和页码,调用 Service 的查询方法,Service 调 DAO,DAO 用 JDBC 执行SELECT ... LIMIT ?,?,结果集封装成List<Student>返回,Servlet 把列表塞进request.setAttribute,再forward到list.jsp,JSP 用 JSTL 的forEach渲染成表格。整条链路没有任何魔法,每一跳都能在代码里指出来。
这种“一个 Servlet 管一个模块、用 action 参数分发”的写法,比每个操作写一个 Servlet 类更紧凑,也更适合课程设计的体量。代价是 Servlet 里的if-else分支会变多,所以分发逻辑要写得干净,每个分支只做参数提取和转发,业务判断全部下沉到 Service。
3.2 数据库表设计与索引考量
学生表的设计直接决定后面代码好不好写。主键用自增 id 还是学号?我倾向自增 id 做主键、学号加唯一索引,原因是学号可能被修改(虽然少见),而主键一旦被业务字段占用,关联和更新都会变麻烦。
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(2) DEFAULT '男' COMMENT '性别', class_name VARCHAR(50) COMMENT '班级', major VARCHAR(50) COMMENT '专业', phone VARCHAR(20) COMMENT '联系电话', enroll_year INT COMMENT '入学年份', UNIQUE KEY uk_sno (sno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表语句里有两个点值得展开。utf8mb4而不是utf8,是为了避免姓名里出现生僻字或特殊符号时乱码,这个坑在演示时特别致命——页面上一个方块字,评委立刻会问编码怎么处理的。UNIQUE KEY uk_sno是学号唯一的最后一道防线,应用层校验只是提前拦截,真正保证不重复的是数据库约束。另外phone用 VARCHAR 而不是数值类型,因为电话号码可能有前导零,用整型会丢。
用户表单独建一张,字段就用户名、密码、角色。密码存储不要明文,课程设计里至少做一次 MD5 加盐,虽然不算强安全,但能体现你有安全意识,答辩时是个加分点。
3.3 分页与检索的接口约定
分页参数统一用page(当前页)和pageSize(每页条数),Service 层负责把 page 换算成offset = (page - 1) * pageSize,DAO 层只认 offset 和 limit。检索条件用keyword一个参数,DAO 里拼WHERE name LIKE ? OR sno LIKE ?,注意用 PreparedStatement 的占位符而不是字符串拼接,这是防 SQL 注入的基本功,也是答辩高频问题。
接口约定清楚之后,Servlet 里就只剩参数解析和结果转发,代码量能压得很短。我一般会在 Service 里再包一层分页结果对象,把当前页、总页数、总记录数、数据列表一起返回,JSP 渲染分页条时直接取,不用在页面里算。
4. 核心模块实现:Servlet 分发、DAO 封装与页面渲染
4.1 Servlet 的 action 分发与登录拦截
控制层的核心是一个分发方法。下面这段是学生模块 Servlet 的骨架,重点看 action 怎么取、怎么分流,以及未登录时怎么拦。
@WebServlet("/student") public class StudentServlet extends HttpServlet { private final StudentService service = new StudentService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 登录拦截:会话里没有用户直接打回登录页 if (req.getSession().getAttribute("loginUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "list": list(req, resp); break; case "edit": edit(req, resp); break; case "delete": delete(req, resp); break; default: list(req, resp); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page = parseInt(req.getParameter("page"), 1); String keyword = req.getParameter("keyword"); PageResult<Student> result = service.query(page, 10, keyword); req.setAttribute("pageResult", result); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/WEB-INF/views/list.jsp").forward(req, resp); } // edit、delete 方法略,结构相同 }这段代码有几个设计决定要说明。登录拦截放在doGet最前面,而不是每个方法里重复写,保证任何入口都先过权限这一关;action为空时默认走列表,避免用户直接访问/student时报空指针;parseInt是自己写的工具方法,参数非法时回退到默认值 1,防止page=abc导致 500 错误。JSP 放在WEB-INF/views下,是为了禁止浏览器直接访问页面文件,所有视图必须经过 Servlet 转发,这也是 MVC 该有的样子。
doPost通常直接委托给doGet,或者单独处理新增和修改的表单提交。表单提交涉及中文参数,记得在读取参数前设置req.setCharacterEncoding("UTF-8"),否则姓名存进数据库就是乱码。
4.2 DAO 层的 JDBC 封装与资源关闭
DAO 层最容易出问题的地方不是 SQL 写错,而是连接和结果集没关。下面是一个查询方法的完整写法,注意 try-with-resources 的用法。
public List<Student> findByPage(int offset, int limit, String keyword) { List<Student> list = new ArrayList<>(); String sql = "SELECT * FROM student WHERE name LIKE ? OR sno LIKE ? " + "ORDER BY id DESC LIMIT ?, ?"; // try-with-resources 自动关闭 Connection/PreparedStatement/ResultSet try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + (keyword == null ? "" : keyword) + "%"; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, offset); ps.setInt(4, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 行到对象的映射单独抽方法 } } } catch (SQLException e) { throw new RuntimeException("查询学生列表失败", e); } return list; }try-with-resources是这段代码的关键,它保证无论正常返回还是抛异常,连接和语句对象都会被关闭。很多初学者的写法是在 finally 里手动 close,一旦漏掉一个或者 close 顺序写反,连接池很快就被耗尽,表现为“用着用着就卡住”。mapRow把 ResultSet 的一行转成 Student 对象,抽出来之后新增和修改的查询也能复用。SQL 里ORDER BY id DESC让最新录入的学生排前面,演示时新增一条立刻能在第一页看到,体验上更顺。
分页查询还需要一个统计总数的count(keyword)方法,SQL 是SELECT COUNT(*) FROM student WHERE name LIKE ? OR sno LIKE ?,条件要和列表查询完全一致,否则总页数会算错,分页条就会出现“点下一页空白”的经典 bug。
4.3 JSP 列表渲染与表单回显
视图层用 JSTL 渲染,避免在 JSP 里写 Java 脚本片段。列表页的核心是遍历和分页条。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach var="s" items="${pageResult.list}"> <tr> <td>${s.sno}</td> <td>${s.name}</td> <td>${s.className}</td> <td> <a href="student?action=edit&id=${s.id}">编辑</a> <a href="student?action=delete&id=${s.id}" onclick="return confirm('确认删除该学生?')">删除</a> </td> </tr> </c:forEach> </table>c:forEach的items直接取 Servlet 塞进 request 的pageResult.list,EL 表达式${s.name}会自动调用 getter,不用写脚本。删除链接加confirm是防止误点,这个细节答辩时经常被夸。编辑页的表单回显则是在 Servlet 里根据 id 查出学生对象放进 request,JSP 的 input 用value="${student.name}"填回去,新增和修改可以共用一个表单页,靠 id 是否为空区分。
分页条渲染时要注意边界:第一页不显示“上一页”链接,最后一页不显示“下一页”,当前页高亮。这些判断用c:if写,逻辑简单但漏了就会显得不专业。
5. 避坑与排查:五个最容易在演示现场翻车的点
5.1 中文乱码:现象是姓名变问号,根因在编码链
现象很典型:表单提交后数据库里存的是???,或者页面显示方块。原因通常有三处没统一——JSP 页面pageEncoding、请求体编码、数据库连接串编码。解决方法是三处都设 UTF-8:JSP 顶部<%@ page contentType="text/html;charset=UTF-8" %>,Servlet 读参数前req.setCharacterEncoding("UTF-8"),JDBC URL 加?useUnicode=true&characterEncoding=utf8。三处缺一处都可能出问题,我一般写个检查清单,部署前逐项过。
5.2 连接泄漏:现象是点几次就卡死,根因是资源没关
演示到一半页面转圈不动,重启 Tomcat 又好了,十有八九是连接没关。原因可能是 finally 里 close 写漏、异常路径没走到关闭逻辑,或者 ResultSet 没关。解决办法是统一用 try-with-resources,并且把获取连接的逻辑收进DBUtil,禁止在 DAO 里直接DriverManager.getConnection。排查时可以临时在获取和关闭处打日志,看打开数和关闭数是否对得上。
5.3 学号重复:现象是插入报错或出现两条相同学号,根因是校验和约束脱节
应用层查了一次说没重复,插入却成功了两次,这是典型的“检查后插入”竞态。原因是校验和插入之间没有原子性。解决办法是数据库加唯一索引兜底,DAO 捕获SQLIntegrityConstraintViolationException后返回友好提示“学号已存在”,而不是把异常堆栈甩到页面上。应用层校验保留,作为提前拦截,两者不冲突。
5.4 分页总数错位:现象是最后一页空白或数据重复,根因是 count 与 list 条件不一致
列表查询带了 keyword 条件,统计总数时忘了带,总页数就会偏大,翻到后面全是空页。解决办法是把查询条件抽成一个方法或参数对象,count 和 list 共用同一套条件拼装逻辑。另一个常见原因是 offset 算错,(page-1)*pageSize里 page 从 0 开始还是从 1 开始要统一,建议对外一律从 1 开始,内部换算时再减。
5.5 直接访问 JSP 绕过权限:现象是没登录也能看到数据页,根因是视图没放 WEB-INF
有人把 JSP 放在 webapp 根目录,浏览器直接输 URL 就能打开,登录拦截形同虚设。解决办法是把所有视图移到WEB-INF/views下,WEB-INF 目录浏览器无法直接访问,只能通过 Servlet forward 进入。这个改动很小,但安全意义上差别很大,答辩时能讲出“为什么放 WEB-INF”就是加分项。
6. 验收与答辩:怎么证明这套系统真的能交付
验收不是把功能点一遍就完事,而是要能回答“边界情况怎么处理”。我一般按三步走。第一步是功能验收,准备一张用例表,覆盖正常增删改查、空结果查询、非法页码、重复学号、超长输入、删除不存在记录这几类,逐条跑一遍并记录实际结果。第二步是异常验收,故意制造数据库断开、参数缺失、未登录访问,看系统是给出友好提示还是白屏报错——白屏是答辩大忌。第三步是代码走查,重点看三处:所有 JDBC 资源是否 try-with-resources、所有 SQL 是否用占位符、所有视图是否在 WEB-INF 下。
答辩时最容易被问的三个问题,提前准备好答案:请求从浏览器到数据库经过哪几层,每层职责是什么;学号唯一是怎么保证的,应用层和数据库层各起什么作用;分页的 offset 怎么算,count 和 list 条件为什么必须一致。能脱稿讲清这三条,基本就稳了。
| 验收项 | 通过标准 | 检查方式 |
|---|---|---|
| 增删改查 | 每项操作后列表数据正确刷新 | 手工操作 + 数据库核对 |
| 分页 | 首页/末页/中间页跳转正确,总数准确 | 构造 25 条以上数据测试 |
| 登录拦截 | 未登录访问任意业务 URL 均跳登录页 | 清空会话后直接输 URL |
| 异常提示 | 非法输入不出现 500 白屏 | 手工构造异常参数 |
| 编码 | 中文姓名存取无乱码 | 录入含生僻字姓名验证 |
最后说个血泪经验:我见过太多人功能全做完,答辩前一天才发现部署到另一台机器上数据库连不上,原因是连接串里写死了本机地址和密码。从那以后我每次交付前都强制走一遍“换环境部署”检查——把连接配置抽到 properties 文件,部署时只改配置不改代码,再完整跑一遍用例表。这个习惯帮我省过不止一次现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取