☰
Servlet+JSP手写新生报到系统:源码解析、部署避坑与事务优化
2026/10/9 12:48:06 网站建设 项目流程

简介:一套面向高校计算机专业学生与毕业设计者的Java Web完整项目,聚焦新生报到场景,涵盖信息录入、注册确认、宿舍分配、课程安排等核心流程,可帮助理解从需求分析到编码实现的完整思路。压缩包共161个文件,约2.36MB,包含29个JSP页面、6个Java源文件及对应class字节码、4个数据库文件(db/mdf/ldf)等,另有图片、CSS、JS辅助前端展示,结构清晰便于研读。目前已有158人学习,适合作为课程设计或毕业设计参考。通过源码可深入掌握Servlet与JSP的请求响应机制、SQL建表与增删改查、MVC分层思想,以及后端数据校验与基础安全防护等关键知识点,是快速搭建同类管理系统的高质量范本。

1. 一个老牌 Java 毕业设计长什么样:Servlet+JSP 手写的新生报到系统

如果你打算在毕设里展示「从数据库到前端页面的完整链路」,这套基于 Java+JSP+SQL 的新生报到系统是可以直接上手对照的源码。它不是 Spring Boot 全家桶项目,而是更老派但更容易看懂逻辑的 Servlet+JSP 手写 Web 应用:DBO 类管数据库连接,StudentsBean、CourseBean、KhBean 这类实体类撑起业务对象,StudentsServlet、AdminServlet、YdServlet 接收请求、调业务逻辑,JSP 页面负责把数据渲染成表格和表单。新生报到常见的流程——录入学生、确认报到、宿舍分配、课程信息维护——在这里都能找到对应模块。适合两类人:一是要交毕设、需要一套能讲清楚原理的参考代码的人;二是想把 JSP+Servlet+SQL 从「知道」变成「会用」的初学者。

2. 先拆架构再碰代码:这套系统的分层与四个核心类

2.1 Java Web 最经典的三层划分:JSP 管显示、Servlet 管控制、DBO 管数据库

这套系统没有引入 Spring、Struts 这类框架,走的是 Java Web 最原始也最直观的路子:浏览器发请求到 Tomcat,Tomcat 根据映射关系找到对应的 Servlet,Servlet 负责解析参数、调用业务方法、访问数据库,最后把结果塞进 request 作用域,转发给 JSP 渲染成 HTML 返回。整个过程你在课堂上学的 Servlet 生命周期、request 和 response 对象、转发与重定向,全都落到了实处。

我拆源码的习惯是先看web.xml或注解里的 URL 映射,再看 Servlet 里的 doGet/doPost 都处理了什么。这套系统里 YdServlet、StudentsServlet、AdminServlet 三个控制器把请求分了几大块:管理员登录走 AdminServlet,学生信息维护走 StudentsServlet,报到业务走 YdServlet。每一个 Servlet 做的事情都差不多——取参数、调 DBO 查数据库、把结果放到 request、转发给 JSP。把其中一个看懂了,剩下两个就是复制套路。

你会看到很多老项目喜欢把所有逻辑堆在 Servlet 里,这套系统相对克制,Bean 负责承载数据,Servlet 负责流程控制,DBO 负责数据库访问,职责边界比较清楚。读代码的时候按「请求进 Servlet → Servlet 操作 Bean → Bean 通过 DBO 落库」这条线走,比从 JSP 页面开始看要快得多。

2.2 类名拆解:Bean 是数据模型,Servlet 是控制入口

解压源码包之后,你会在 classes 目录下看到一堆 .class 文件,包名和类名直接暴露了作者的设计思路。我整理了一份职责对照表,照着这个表去读代码,十分钟就能把项目结构摸清楚:

类名类型职责推断
DBO工具类统一管理数据库连接与资源关闭,全项目共用一个入口
StudentsBeanJavaBean学生实体,字段对应学生表,含 getter/setter
StudentsServletServlet学生信息的增删改查入口
AdminBeanJavaBean管理员实体,承载登录账号密码
AdminServletServlet管理员登录校验、权限控制
CourseBeanJavaBean课程实体,对应课程表字段
KhBeanJavaBean考核/成绩相关实体,从命名看是「考核」拼音首字母
YdServletServlet「报到」拼音首字母,新生报到流程核心控制器
SmartUpload / SmartFile组件文件上传组件,处理录取通知书、证件照上传

这套命名风格很有年代感,但好处是看一眼类名就大概知道它管哪块业务。KhBean 这种命名对英文不好的同学反而友好,它就是「考核」的拼音缩写,对应学生成绩或报到考核记录。SmartUpload 是老牌上传组件,在 Servlet 3.0 之前是做文件上传的标配,这套系统里多半用于上传学生照片或证明材料。

读这种老项目有个诀窍:先读两个 Servlet,再读 Bean 的字段,最后回到 JSP 页面看表单提交了哪些参数。因为 JSP 是给用户看的,字段名和表单 name 属性一一对应,从页面反推数据库表结构,比直接看 SQL 脚本更快。

2.3 从 JSP 页面看请求流向:一个表单对应一个 Servlet 方法

随便打开一个报到相关的 JSP 页面,你会看到表单的 action 指向某个 Servlet,例如form action="ydservlet?action=register" method="post"。这种「一个 Servlet 多个 action 参数」的设计在老项目中非常常见,对应 Servlet 里就是一大串 if/else 或 switch 判断。YdServlet 里通常会有 register、confirm、assignDorm 这几个分支,分别处理新生登记、报到确认、宿舍分配。

看懂了这一点,你就明白为什么毕设答辩时老师喜欢问「你的事务是怎么控制的」——因为这种手写 Servlet 的项目里,事务往往是被忽略的。插入学生信息和更新宿舍占用数这两步操作,如果中间抛异常,数据就会不一致。这也是后面改造时最值得动手的点。

3. 数据库设计与 DBO 封装:新生表、课程表该怎么建

3.1 五张核心表把报到流程串起来

新生报到系统的数据模型不复杂,核心就是围绕「学生」这张主表展开。按报到流程拆,至少需要五张表:学生表存基本信息,管理员表存登录账号,宿舍表管分配,课程表提供可选课程,选课表做学生和课程的多对多关联。下面这套建表脚本是我从这类项目里归纳出的最常见结构,字段命名风格贴合老项目的习惯,你拿到源码后对照着改就行:

CREATE DATABASE IF NOT EXISTS freshman_report DEFAULT CHARSET utf8; USE freshman_report; -- 学生表:核心主表 CREATE TABLE students ( student_id VARCHAR(20) PRIMARY KEY COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(2) COMMENT '性别', id_card VARCHAR(18) UNIQUE COMMENT '身份证号', phone VARCHAR(11) COMMENT '手机号', major VARCHAR(100) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', dorm_id VARCHAR(10) COMMENT '宿舍编号', status TINYINT DEFAULT 0 COMMENT '0-未报到 1-已报到 2-已分配宿舍', create_time DATETIME COMMENT '录入时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 管理员表 CREATE TABLE admins ( admin_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 宿舍表 CREATE TABLE dormitories ( dorm_id VARCHAR(10) PRIMARY KEY COMMENT '宿舍编号', building VARCHAR(20) COMMENT '楼栋', room_no VARCHAR(10) COMMENT '房间号', capacity INT DEFAULT 4 COMMENT '可住人数', used INT DEFAULT 0 COMMENT '已住人数' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 课程表 CREATE TABLE courses ( course_id VARCHAR(10) PRIMARY KEY, course_name VARCHAR(50) NOT NULL, credit DECIMAL(2,1) COMMENT '学分', teacher VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 选课表:学生与课程多对多 CREATE TABLE stu_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20), course_id VARCHAR(10), FOREIGN KEY (student_id) REFERENCES students(student_id), FOREIGN KEY (course_id) REFERENCES courses(course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

字段说明里有几个细节值得注意。status字段是这个系统的状态机核心,我用 TINYINT 而不是 VARCHAR 存状态,就是为了后面用整数比较,省去字符串匹配的麻烦。id_card加了 UNIQUE 约束,防止同一身份证号被重复录入,这是报到系统里非常关键的一道防线。宿舍表的used字段是冗余设计的典型例子——它本可以通过 COUNT 查出来,但为了在列表页快速显示「已住/剩余」,作者选择直接存一个数字,代价就是分配宿舍时必须手动维护这个字段。

选课表用id做自增主键而不是联合主键,是故意留的余地。同一个学生可以分不同学期选同一门课,如果拿(student_id, course_id)做联合主键,后面想扩展学期字段就得改表结构。这种设计在你答辩时可以说「为后续扩展留了空间」,是很加分的点。

3.2 DBO 连接封装:全项目共用一个 Connection 入口

老项目的 DBO 类通常长这样——静态方法返回连接,另一个静态方法关资源。代码不多,但所有 Servlet 都依赖它,属于那种「看着不起眼、改坏了全崩」的类:

package com.freshman.db; import java.sql.*; public class DBO { // 老项目用 com.mysql.jdbc.Driver,MySQL 8 以上要换成 com.mysql.cj.jdbc.Driver private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/freshman_report" + "?useUnicode=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; // 获取连接,所有 Servlet 统一走这里 public static Connection getConn() { Connection conn = null; try { Class.forName(DRIVER); conn = DriverManager.getConnection(URL, USER, PASSWORD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return conn; } // 统一关闭资源,ResultSet -> Statement -> Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

这段代码里最容易踩坑的是 URL 末尾的characterEncoding=utf8。如果少了这个参数,JSP 页面传来的中文姓名存进 MySQL 就会变成乱码,而且这个问题在本地开发时经常被忽略,因为有些人本机数据库默认字符集恰好是 utf8,换一台机器部署就翻车。Class.forName(DRIVER)这行在 JDBC 4.0 之后其实可以省略,但老项目保留它是为了兼容旧版本驱动,我建议你别删,删了反而可能在某些 Tomcat 版本下报错。

getConn()每次调用都新建连接,没有连接池,这是老项目的通病。毕设管理系统、课设系统这种并发量不超过几十的场景完全够用,但你要在答辩时主动提一句「这里可以用连接池优化」,会显得你清楚它的瓶颈在哪。DBO 的 close 方法也值得学,它是标准的关闭顺序:先关 ResultSet,再关 Statement,最后关 Connection,顺序反了可能在 MySQL 端产生资源泄漏警告。

4. YdServlet 报到流程实现:从表单到入库的完整链路

4.1 报到状态流转:从录入到入住的三个节点

新生报到系统本质上是在管理一条状态链。学生信息刚录入时是「未报到」,到校确认后变成「已报到」,分配宿舍后变成「已分配宿舍」。这套系统的 StudentsBean 里大概率有一个 status 字段,YdServlet 的不同 action 就是推动这条状态链往下走的引擎。

我建议你读代码时把这条链画在纸上:register动作插入学生记录(status=0),confirm动作把 status 改成 1,assignDorm动作更新 dorm_id 并把宿舍 used 字段加 1,同时把 status 改成 2。每一步对应一个 UPDATE 语句,而「宿舍 used + 1」和「学生 dorm_id 更新」这两步是最容易出现数据不一致的地方——如果先更新宿舍再更新学生,中间抛异常,宿舍就多算了一个人。

4.2 YdServlet 的 doPost 分发逻辑:一个入口处理三类请求

老项目最常见的写法是一个 Servlet 对应多个 action,YdServlet 的核心结构大概是这样的。注意看参数校验和转发路径的处理:

package com.freshman.servlet; import com.freshman.bean.StudentsBean; import com.freshman.db.DBO; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.*; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @WebServlet("/ydservlet") public class YdServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 必须先设置,否则中文参数变乱码 String action = req.getParameter("action"); if ("register".equals(action)) { registerStudent(req, resp); } else if ("confirm".equals(action)) { confirmStudent(req, resp); } else if ("assignDorm".equals(action)) { assignDorm(req, resp); } else { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "unknown action"); } } private void registerStudent(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String studentId = req.getParameter("studentId"); String name = req.getParameter("name"); String idCard = req.getParameter("idCard"); String phone = req.getParameter("phone"); String major = req.getParameter("major"); // 后端二次校验,前端校验不可信 if (studentId == null || studentId.trim().isEmpty() || name == null || name.trim().isEmpty()) { req.setAttribute("msg", "学号和姓名不能为空"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } Connection conn = null; PreparedStatement ps = null; try { conn = DBO.getConn(); String sql = "INSERT INTO students(student_id, name, id_card, phone, major, status)" + " VALUES(?, ?, ?, ?, ?, 0)"; ps = conn.prepareStatement(sql); ps.setString(1, studentId); ps.setString(2, name); ps.setString(3, idCard); ps.setString(4, phone); ps.setString(5, major); int rows = ps.executeUpdate(); if (rows > 0) { resp.sendRedirect("list.jsp"); // 成功后重定向,避免刷新重复提交 } else { req.setAttribute("msg", "数据库写入失败"); req.getRequestDispatcher("/register.jsp").forward(req, resp); } } catch (SQLException e) { e.printStackTrace(); req.setAttribute("msg", "系统异常:" + e.getMessage()); req.getRequestDispatcher("/register.jsp").forward(req, resp); } finally { DBO.close(conn, ps, null); } } }

这个 Servlet 里有三个值得学习的习惯。第一行req.setCharacterEncoding("UTF-8")必须放在读取任何参数之前,否则 POST 请求的中文参数会乱码,这是老项目中排名第一的坑。第二个细节是后端校验——前端 JSP 可能用onsubmit做了必填校验,但那是防君子不防小人,直接 curl 提交一个空学号照样能打到你后端,所以 Servlet 里必须有trim().isEmpty()这种判断。第三个细节是成功后用sendRedirect而不是forward,这样用户按 F5 刷新时不会把同一条记录插入两次。

看到PreparedStatement而不是Statement拼接 SQL,说明作者有防注入的意识。?占位符配合setString传参,MySQL 驱动会对参数做转义,单引号、反斜杠这类字符不会破坏 SQL 结构。你答辨时被问到「怎么防 SQL 注入」,直接拿这段代码说事,比背概念有说服力得多。

这里补充一句环境上的坑:老项目用javax.servlet包名,如果你用的是 Tomcat 10 以上版本,必须改成jakarta.servlet,否则编译直接报错。我自己第一次迁这种项目到新环境时,光改 import 就改了半个多小时,建议你在动手前先确认 Tomcat 版本。

4.3 宿舍分配:两个 UPDATE 的事务边界

assignDorm方法是这套系统里事务意识最薄弱、但也最值得改造的地方。标准流程是两步:更新学生的dorm_id和 status,同时把宿舍的used加 1。如果这两步之间抛出异常,就会出现学生有宿舍但宿舍人数统计不对的情况。

老项目的写法往往是连续执行两条 executeUpdate,没有事务包裹:

// 常见错误写法:两步操作之间没有事务 try { conn.setAutoCommit(false); // 开启事务 ps1.executeUpdate(); // UPDATE students SET dorm_id = ? WHERE student_id = ? ps2.executeUpdate(); // UPDATE dormitories SET used = used + 1 WHERE dorm_id = ? conn.commit(); // 提交 } catch (SQLException e) { conn.rollback(); // 回滚 }

正确的做法是conn.setAutoCommit(false)关掉自动提交,两步执行成功后commit(),任何一步失败就rollback()。这个知识点几乎是毕设答辩必问的——老师看到你手写 JDBC 又没有事务,一定会追问。你提前把这段代码准备好,主动说出来,等于把送分题握在手里。

5. 部署与测试常见问题排查:从 404 到中文乱码的五个坑

5.1 现象:war 包部署后启动报ClassNotFoundException: com.mysql.jdbc.Driver

原因:MySQL 驱动 jar 包没有打进 WEB-INF/lib 目录。用 IDEA 导出 war 包时,如果依赖库的 scope 是 provided,或者直接用jar命令手工打包,很容易漏掉 lib 下的驱动文件。Tomcat 启动时加载 Servlet,Servlet 里调用Class.forName("com.mysql.jdbc.Driver")找不到类,启动日志就会报这个错。

解决:解压 war 包检查WEB-INF/lib/下是否有mysql-connector-java-x.x.x.jar。没有就把驱动 jar 和 SmartUpload 的 jar 一起拷进去,重新打包。另外注意,MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8.x 要换com.mysql.cj.jdbc.Driver,两个包名不一样,中途升级数据库一定要同步改 DBO 类。

5.2 现象:新生姓名存进 MySQL 再查出来全是???或中文乱码

原因:乱码是三个环节叠加的结果。JSP 页面本身不是 UTF-8 编码,Servlet 没设req.setCharacterEncoding,JDBC URL 缺characterEncoding=utf8。任何一个环节断了,中文就保不住。三个人排查这个问题最容易互相甩锅:前端说页面编码没问题,后端说数据库字符集是 utf8,最后发现是 JDBC URL 没带参数。

解决:三个地方一次性全改掉。JSP 头部加<%@ page contentType="text/html;charset=UTF-8" %>,文件本身用 UTF-8 保存;Servlet 或 Filter 里统一setCharacterEncoding("UTF-8");JDBC URL 末尾加?useUnicode=true&characterEncoding=utf8。改完之后清浏览器缓存再测,不然旧页面还会用 . 旧编码提交表单。

5.3 现象:SmartUpload 上传录取通知书照片,文件名中文变乱码

原因:SmartUpload 组件内部按 ISO-8859-1 解析上传的文件名,它和 UTF-8 不兼容。这是老组件的老毛病,当年做文件上传没有更好的选择,现在看它处理中文文件名非常吃力。

解决:上传后拿到文件名手动转编码。常见做法是new String(filename.getBytes("ISO-8859-1"), "UTF-8")再存入数据库。如果 SmartUpload 版本支持setCharset("UTF-8")方法,优先用这个方法,底层会统一处理。要是项目里还没引入这个组件,我建议直接用 Servlet 3.0 的Part接口替代,代码量差不多,中文兼容性好一个量级。

5.4 现象:SQL 语句在 Navicat 里能执行,程序里报Table 'xxx' doesn't exist

原因:多半不是表真的不存在,而是连接的库不对。JDBC URL 里写的库名是freshman_report,但建表脚本执行时用的是另一个库,或者项目里有多个application.properties配置了不同的数据库地址。还有一种情况是表名带反引号或前后有空格,程序里拼接 SQL 时没对齐。

解决:先在 MySQL 里执行SHOW TABLES确认表名,再核对 DBO 类里 URL 的库名。如果建表脚本里有USE database_name,确认脚本执行时确实切换过去了。表名建议统一用小写加下划线,Linux 下 MySQL 表名大小写敏感,Windows 下不敏感,这个差异经常让跨平台开发的人翻车。

5.5 现象:访问 Servlet 地址直接 404,但 JSP 页面能打开

原因:三个常见原因。一是web.xml里没配 Servlet 映射,但代码里用的是@WebServlet注解,Tomcat 版本低于 7 不支持注解扫描;二是访问路径写错了,@WebServlet("/ydservlet")定义在注解里,但 JSP 表单的 action 写的是servlet/YdServlet,路径对不上;三是请求路径没带项目上下文,http://localhost:8080/ydservlet和http://localhost:8080/freshman/ ydservlet差一个项目名。

解决:先看 Tomcat 版本,7.0 以上支持注解,以下必须在web.xml手写servlet和servlet-mapping节点。再看浏览器地址栏,确认是带项目名的完整路径。最后打开 Tomcat 的localhost.log,里面会记录每个 Servlet 的映射路径,照着日志改就没错。

6. 从能跑到能上线:打包、验证与下一步改造点

这套系统在本地能跑通只是第一步,真要拿去演示或部署到服务器,我一般按下面这个清单走一遍,每一步都能省掉一次无意义的返工。

先处理打包问题。老项目没有 Maven 结构,直接在WEB-INF/lib里塞 jar 包。用 IDEA 导出 war 时,一定要勾选Include in project build,把mysql-connector-java.jar、SmartUpload.jar以及jstl.jar都带进去。打包完先看 war 包大小,如果只有几十 KB,多半是 jar 没进来,白打了。验证方法是把 war 丢到tomcat/webapps/下,启动后访问http://localhost:8080/project_name/,首页能打开再继续。

然后是环境版本对齐。JDK 8 配 Tomcat 8 是这套系统最稳的组合,JDK 11 配 Tomcat 9 也能跑,但要注意javax.servlet和jakarta.servlet的包名差异。MySQL 如果用的 8.x,驱动必须换com.mysql.cj.jdbc.Driver,同时确认serverTimezone=Asia/Shanghai参数加上了,不然时间字段会报错。

最后说改造方向。我会优先把 DBO 换成 Druid 连接池,代码改动很小,只动一个工具类,就能消除「每次请求新建连接」的隐患。第二步是把 YdServlet 里的事务边界补上,宿舍分配和选课操作都包上事务。第三步是给密码加盐哈希——老项目大概率是明文存储,这个在答辩时被问到就尴尬了。如果想把系统往 Spring Boot 迁,表结构和业务逻辑可以原样保留,把 Servlet 改成 Controller,JSP 挪到resources/templates下,工作量没有想象中大。

说到底,这套源码真正的价值不在「能跑」,而在于它把 Java Web 的每个零件都拆开了摆在你面前——DBO、Bean、Servlet、JSP、SQL 脚本、上传组件,每一个都能单独拎出来讲清楚原理,这在 Spring Boot 黑匣子里是做不到的。我去年帮一个学弟调类似的课设,他拿 Spring Boot 写了半天,连请求是怎么进 Controller 的都说不清;换了这套 JSP+Servlet 的代码,他对着 YdServlet 的 import 列表,反而把整个请求链路理顺了。从那以后,我每次给别人讲 Java Web 入门,都先让他拆一遍老项目的 Servlet 再碰框架。希望你拿到这套源码之后,也别急着跑起来,先把 DBO 类和 YdServlet 读一遍,再动手改,收获会大不一样。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询