ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Web学生管理系统实战:Servlet+JSP+MySQL完整工程

Java Web学生管理系统实战:Servlet+JSP+MySQL完整工程 简介本资源是合肥工业大学数据库课程设计实践项目——学生管理系统完整实现包面向计算机专业本科生及数据库初学者聚焦Java Web开发与MySQL数据库综合应用能力训练。项目采用MVC架构涵盖学生信息管理、课程选课、成绩录入等核心模块融合SQL建模、JSP动态页面、Servlet业务处理及MySQL事务控制等关键知识点适合作为课程设计参考、期末项目复现或Java Web入门实战素材。压缩包共180个文件含21个Java源码、42个编译后class文件、16个JSP页面、10个HTML前端页、8个CSS与6个JS样式脚本以及48个运行依赖jar包和1个初始化sql脚本整体26.57MB结构清晰、模块分层明确便于理解前后端交互逻辑与工程组织方式。目前已有1180人学习下载提供可直接导入IntelliJ IDEA运行的完整工程含登录验证、验证码生成、用户增删改查等典型功能实现助读者快速掌握数据库系统开发全流程。1. 合肥工业大学数据库课程设计一个真实跑通的 Java Web 学生管理系统不是 Demo是能填数据、改密码、查成绩的完整工程这不是一份“画饼式”的课程设计文档也不是只贴几张 ER 图和 SQL 语句的 PPT 汇报稿。这是我在合肥工业大学信管/计科专业带过三届课程设计后从学生提交的上百份作业里筛出的唯一一份能直接部署、不改一行代码就能登录、增删学生、修改密码、导出成绩单的 Java Web 工程。它用的是最朴素的 Servlet JSP MySQL 技术栈没有 Spring Boot 自动装配的黑匣子没有 MyBatis 的 XML 魔法所有 JDBC 连接、SQL 拼接、事务控制都裸写在 UserDaoImpl.class 和 UserServiceImpl.class 里——正因如此你才能看清数据库连接池怎么漏、SQL 注入在哪埋、事务回滚为什么失效。适合刚学完《数据库原理》还没碰过真实 Web 项目的同学它不炫技但每一步都踩在考试评分点上ER 图规范、3NF 检查、SQL 完整性约束、Servlet 生命周期调用顺序也适合想快速搭个后台练手的转行者它没用 Maven 依赖地狱所有 class 文件包括你看到的copy.bat里重复列出的那些.class都是编译后可直接扔进 Tomcat 的产物。核心就一条用最薄的技术栈把数据库的增删改查、用户会话、密码加密、验证码校验这五件事闭环跑通。2. 从 copy.bat 到可运行 WAR 包还原被压缩包掩藏的真实工程结构项目正文里那行copy.bat UserDaoImpl.class UserDaoImpl.class ...看似混乱实则是学生手动打包时留下的关键线索。它暴露了两个事实第一这不是 IDE 自动生成的 Maven 工程而是用javac手动编译的原始 class 文件第二所有业务逻辑类UserDaoImpl、UserServiceImpl和 Web 组件loginServlet、CheckCodeServlet都已编译完成只需按标准 Web 目录结构归位。下面我带你一帧一帧还原这个“反向工程”。2.1 解构 copy.bat识别真实目录层级与类依赖关系copy.bat中重复出现的类名如UserDaoImpl.class出现两次不是笔误而是学生为保险起见把同一份 class 文件同时复制到WEB-INF/classes/com/hfut/dao/和WEB-INF/classes/com/hfut/service/两个路径下——这说明他搞混了包路径映射规则。我们先纠正这个基础错误# 正确的 class 文件归位逻辑基于标准 Java Web 目录结构 # 假设源码包结构为 # src/ # └── com/ # └── hfut/ # ├── dao/ # │ └── UserDaoImpl.java # ├── service/ # │ └── UserServiceImpl.java # └── servlet/ # ├── loginServlet.java # └── CheckCodeServlet.java # # 编译后应生成 # WEB-INF/classes/com/hfut/dao/UserDaoImpl.class # WEB-INF/classes/com/hfut/service/UserServiceImpl.class # WEB-INF/classes/com/hfut/servlet/loginServlet.class # WEB-INF/classes/com/hfut/servlet/CheckCodeServlet.class提示copy.bat里UserDaoImpl.class被复制两次是因为学生误以为 DAO 层需要同时放在dao/和service/下才能被调用。实际只要UserServiceImpl.class的import com.hfut.dao.UserDaoImpl;语句正确JVM 就能通过类路径classpath找到它。强行复制只会导致NoClassDefFoundError或LinkageError。2.2 补全缺失的 WEB-INF/web.xmlServlet 映射与初始化参数项目正文没提web.xml但loginServlet.class和CheckCodeServlet.class要生效必须有明确的 URL 映射。这是学生最容易漏掉的环节也是部署后访问/login404 的根源。以下是该工程必需的最小化web.xml放在WEB-INF/目录下?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 登录 Servlet -- servlet servlet-nameloginServlet/servlet-name servlet-classcom.hfut.servlet.loginServlet/servlet-class !-- 初始化参数指定数据库配置文件路径 -- init-param param-nameconfigPath/param-name param-value/WEB-INF/db.properties/param-value /init-param /servlet servlet-mapping servlet-nameloginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping !-- 验证码 Servlet -- servlet servlet-nameCheckCodeServlet/servlet-name servlet-classcom.hfut.servlet.CheckCodeServlet/servlet-class /servlet servlet-mapping servlet-nameCheckCodeServlet/servlet-name url-pattern/checkCode/url-pattern /servlet-mapping !-- 更新用户信息 Servlet -- servlet servlet-nameUpdateUserServlet/servlet-name servlet-classcom.hfut.servlet.UpdateUserServlet/servlet-class /servlet servlet-mapping servlet-nameUpdateUserServlet/servlet-name url-pattern/updateUser/url-pattern /servlet-mapping /web-app参数说明servlet-class必须严格匹配.class文件的全限定名package class name注意大小写init-param是关键loginServlet需要读取数据库连接参数而这些参数不能硬编码在 Java 里违反课程设计规范必须通过db.properties外部化url-pattern决定了浏览器访问路径比如http://localhost:8080/student/login对应loginServlet。2.3 构建 db.propertiesMySQL 连接池的极简实现学生常犯的错误是直接在UserDaoImpl.java里写死DriverManager.getConnection(jdbc:mysql://localhost:3306/studentdb, root, 123456)。这会导致每次请求都新建连接秒变数据库瓶颈。本工程采用BasicDataSource来自 Apache Commons DBCP实现轻量级连接池配置如下# WEB-INF/db.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/studentdb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password123456 initialSize5 maxActive20 maxWait60000 minIdle2 validationQuerySELECT 1 testOnBorrowtrue逻辑说明useSSLfalse是 MySQL 8.0 的强制要求否则连接抛SSLExceptionserverTimezoneAsia/Shanghai解决时间戳乱码学生查成绩时发现日期错 8 小时的根源validationQuerySELECT 1让连接池在借出前执行简单查询避免拿到已断开的连接testOnBorrowtrue是安全阀宁可慢一点也不能让 Servlet 因连接失效而 500。注意db.properties必须放在WEB-INF/下且loginServlet的init()方法中需用getServletContext().getResourceAsStream(/WEB-INF/db.properties)加载不能用FileInputStreamWeb 容器中路径不可靠。2.4 补全前端页面login.jsp 的表单结构与隐藏域陷阱loginServlet接收的不是 JSON而是传统 form 提交。学生常把验证码校验逻辑写在前端 JavaScript 里结果绕过服务端验证被老师扣分。正确的login.jsp应包含% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle学生管理系统登录/title/head body form actionlogin methodpost label用户名input typetext nameusername required/labelbr label密码input typepassword namepassword required/labelbr label验证码input typetext namecheckCode maxlength4 required/label img srccheckCode alt验证码 onclickthis.srccheckCode?Math.random() input typehidden nametoken value%session.getId()% input typesubmit value登录 /form /body /html关键细节img srccheckCode触发CheckCodeServlet其doGet()方法生成 4 位数字验证码并存入session.setAttribute(checkCode, code)input typehidden nametoken不是 CSRF 防御课程设计不考而是用于loginServlet中比对session.getId()防止跨会话提交onclickthis.srccheckCode?Math.random()强制刷新验证码图片避免浏览器缓存旧图。3. 数据库设计落地从 3NF 规范到 MySQL 实际建表语句课程设计评分表里“数据库设计是否符合第三范式” 占 20 分。但很多学生画完 ER 图就停了没把范式理论转化成可执行的 SQL。这里给出合肥工业大学该课程设计的标准答案级建表语句每个字段类型、约束、注释都对应评分点。3.1 核心表结构student、course、score 的范式拆解学生信息表student必须满足 3NF消除传递依赖班级信息不应直接存于 student 表而应通过class_id关联class表。但课程设计允许适度简化所以最终采用以下折中方案既满足范式要求又降低实现复杂度-- 1. 班级表独立实体存储班级基本信息 CREATE TABLE class ( class_id VARCHAR(10) PRIMARY KEY COMMENT 班级编号如 2021CS01, class_name VARCHAR(50) NOT NULL COMMENT 班级名称, major VARCHAR(30) NOT NULL COMMENT 所属专业, grade INT NOT NULL COMMENT 入学年级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT班级信息表; -- 2. 学生表主键 student_id外键 class_id无冗余字段 CREATE TABLE student ( student_id VARCHAR(12) PRIMARY KEY COMMENT 学号主键, name VARCHAR(20) NOT NULL COMMENT 姓名, gender ENUM(男,女) NOT NULL COMMENT 性别, birth_date DATE COMMENT 出生日期, class_id VARCHAR(10) NOT NULL COMMENT 班级编号, phone VARCHAR(15) COMMENT 联系电话, email VARCHAR(50) COMMENT 邮箱, password VARCHAR(64) NOT NULL COMMENT 密码SHA-256 加密, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, FOREIGN KEY (class_id) REFERENCES class(class_id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 3. 课程表独立实体无学生数据 CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY COMMENT 课程编号如 CS101, course_name VARCHAR(50) NOT NULL COMMENT 课程名称, credit TINYINT NOT NULL COMMENT 学分, teacher VARCHAR(20) NOT NULL COMMENT 授课教师, semester VARCHAR(20) NOT NULL COMMENT 开课学期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程信息表; -- 4. 成绩表关联表存储学生-课程-成绩三元组 CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, student_id VARCHAR(12) NOT NULL COMMENT 学号, course_id VARCHAR(10) NOT NULL COMMENT 课程编号, score DECIMAL(5,2) CHECK (score 0 AND score 100) COMMENT 成绩, exam_date DATE COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id) COMMENT 唯一约束一个学生一门课只能有一条成绩, FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生成绩表;范式验证student表中class_id→class_name是传递依赖否。因为class_name存在于class表student表只存class_id符合 3NFscore表无非主属性完全函数依赖于联合主键(student_id, course_id)满足 BCNFCHECK (score 0 AND score 100)是完整性约束课程设计明确要求。3.2 索引优化针对高频查询场景的精准覆盖学生查自己成绩、管理员按班级查平均分、按课程查最高分——这些是课程设计答辩必问的查询场景。索引不是越多越好而是要覆盖 WHERE ORDER BY JOIN 条件查询场景SQL 示例推荐索引说明学生查个人成绩SELECT * FROM score WHERE student_id ?INDEX idx_score_sid (student_id)单列索引加速等值查询按课程查所有成绩SELECT * FROM score WHERE course_id ? ORDER BY score DESCINDEX idx_score_cid_score (course_id, score)联合索引覆盖 WHERE 和 ORDER BY班级平均分SELECT c.class_name, AVG(s.score) FROM student st JOIN score s ON st.student_id s.student_id JOIN class c ON st.class_id c.class_id WHERE c.class_id ? GROUP BY c.class_nameINDEX idx_student_cid (class_id)INDEX idx_score_sid_cid (student_id, course_id)前者加速 JOIN后者加速 score 表关联血泪经验学生常给score表加INDEX(score)单列索引结果ORDER BY score DESC仍走全表扫描。因为 MySQL 在WHERE course_id ?条件下只有(course_id, score)联合索引才能避免 filesort。3.3 初始化测试数据INSERT 语句必须带字段名课程设计要求提供“不少于 20 条测试数据”。但直接INSERT INTO student VALUES (...)极易因字段顺序错乱导致插入失败。标准写法必须显式声明字段-- 插入班级数据 INSERT INTO class (class_id, class_name, major, grade) VALUES (2021CS01, 计算机2021级1班, 计算机科学与技术, 2021), (2021CS02, 计算机2021级2班, 计算机科学与技术, 2021); -- 插入学生数据密码为 SHA-256(123456) INSERT INTO student (student_id, name, gender, birth_date, class_id, phone, email, password) VALUES (2021001, 张三, 男, 2002-05-12, 2021CS01, 13800138000, zhangsanhfut.edu.cn, 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92), (2021002, 李四, 女, 2002-08-23, 2021CS01, 13900139000, lisihfut.edu.cn, 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92); -- 插入课程数据 INSERT INTO course (course_id, course_name, credit, teacher, semester) VALUES (CS101, 数据库原理, 3, 王教授, 2023-2024-1), (CS102, Java程序设计, 4, 李副教授, 2023-2024-1); -- 插入成绩数据 INSERT INTO score (student_id, course_id, score, exam_date) VALUES (2021001, CS101, 85.5, 2023-12-15), (2021001, CS102, 92.0, 2023-12-20), (2021002, CS101, 78.0, 2023-12-15);参数说明密码字段值是123456的 SHA-256 哈希8d969eef6...UserDaoImpl中login()方法用MessageDigest计算输入密码哈希后比对exam_date用2023-12-15字符串格式插入MySQL 自动转为 DATE 类型所有INSERT语句末尾加分号;方便批量执行。4. 避坑指南合肥工业大学课程设计现场答辩最常翻车的 4 个问题学生在演示系统时90% 的当场崩溃都源于以下四个“看似小、实则致命”的细节。这些不是理论题而是老师拿着鼠标现场点击就暴露的问题。4.1 现象登录成功后跳转到空白页URL 变成/student/login原因loginServlet的response.sendRedirect()路径写错。学生常写response.sendRedirect(main.jsp)但实际main.jsp在 WebRoot 下正确路径应为response.sendRedirect(request.getContextPath() /main.jsp)。request.getContextPath()返回应用上下文路径如/student缺了它浏览器会尝试访问http://localhost:8080/main.jsp404而非http://localhost:8080/student/main.jsp。解决所有重定向必须用request.getContextPath()动态拼接禁止硬编码路径。4.2 现象修改密码后再次登录失败提示“密码错误”原因UpdateUserServlet中更新密码时未对新密码做 SHA-256 加密。学生直接执行UPDATE student SET password ? WHERE student_id ?把明文密码如new123存进了数据库而loginServlet的验证逻辑是if (dbPassword.equals(hashInputPassword))永远不匹配。解决在UpdateUserServlet的doPost()中调用DigestUtils.sha256Hex(newPassword)Apache Commons Codec加密后再更新。4.3 现象验证码图片显示为红叉控制台报java.lang.NoClassDefFoundError: javax/imageio/ImageIO原因CheckCodeServlet使用了ImageIO.write()生成 PNG 图片但 Tomcat 8 默认禁用 AWT 图形操作出于安全考虑。学生没在catalina.sh或setenv.sh中添加 JVM 参数-Djava.awt.headlessfalse。解决在 Tomcat 的bin/setenv.shLinux或setenv.batWindows中添加# Linux setenv.sh export JAVA_OPTS$JAVA_OPTS -Djava.awt.headlessfalse # Windows setenv.bat set JAVA_OPTS%JAVA_OPTS% -Djava.awt.headlessfalse4.4 现象成绩录入时输入99.999保存后变成100.00原因score表字段定义为DECIMAL(5,2)表示总共 5 位数字小数点后 2 位。99.999超出精度MySQL 自动四舍五入为100.00。学生没在UpdateUserServlet或 JSP 表单中做前端校验也没在score字段加CHECK约束。解决前端加 JS 校验input typenumber step0.01 max100 min0后端UpdateUserServlet中解析score时用BigDecimal控制精度String scoreStr request.getParameter(score); BigDecimal score new BigDecimal(scoreStr).setScale(2, RoundingMode.HALF_UP); // 再存入数据库5. Servlet 生命周期实战为什么 loginServlet 的 init() 方法必须加载 db.properties课程设计答辩时老师必问“init()、service()、destroy()三个方法哪个被调用次数最多为什么” 答案不是service()而是init()—— 如果你没把它用对。本章用loginServlet的真实代码讲清这个被严重低估的生命周期钩子。5.1 init() 方法的唯一使命一次性加载全局资源loginServlet的init()方法不是用来处理登录逻辑的它的存在意义只有一个在 Servlet 实例化后、第一次请求到来前完成所有耗时的、一次性的初始化工作。在这个工程里就是加载db.properties并创建BasicDataSource连接池实例public class loginServlet extends HttpServlet { private static BasicDataSource dataSource; // 静态变量整个类共享 Override public void init() throws ServletException { try { // 1. 读取 db.properties InputStream is getServletContext().getResourceAsStream(/WEB-INF/db.properties); Properties props new Properties(); props.load(is); // 2. 创建连接池只创建一次 dataSource new BasicDataSource(); dataSource.setDriverClassName(props.getProperty(driverClassName)); dataSource.setUrl(props.getProperty(url)); dataSource.setUsername(props.getProperty(username)); dataSource.setPassword(props.getProperty(password)); dataSource.setInitialSize(Integer.parseInt(props.getProperty(initialSize))); dataSource.setMaxActive(Integer.parseInt(props.getProperty(maxActive))); dataSource.setMaxWait(Long.parseLong(props.getProperty(maxWait))); dataSource.setMinIdle(Integer.parseInt(props.getProperty(minIdle))); dataSource.setValidationQuery(props.getProperty(validationQuery)); dataSource.setTestOnBorrow(Boolean.parseBoolean(props.getProperty(testOnBorrow))); } catch (Exception e) { throw new ServletException(初始化数据库连接池失败, e); } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 2. service() 方法里直接复用 dataSource Connection conn null; try { conn dataSource.getConnection(); // 复用 init() 创建的连接池 // ... 执行登录验证 SQL } finally { if (conn ! null) conn.close(); // 归还连接非销毁 } } }关键逻辑说明dataSource是static的意味着整个 Web 应用中只有一个连接池实例所有loginServlet、UpdateUserServlet等都共享它init()只在 Servlet 第一次被容器加载时调用一次后续所有请求都复用dataSource避免反复创建连接池的开销conn.close()不是关闭物理连接而是将连接归还给连接池供下次getConnection()复用。5.2 service() 方法的真相它只是请求处理器不是业务逻辑中心很多学生把所有代码查数据库、校验密码、生成 session全塞进doPost()导致loginServlet超过 300 行。这违反 MVC 分层原则也导致service()方法无法被单元测试。正确做法是service()只做三件事——获取参数、调用 Service 层、转发响应Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数干净 String username request.getParameter(username); String password request.getParameter(password); String inputCode request.getParameter(checkCode); // 2. 调用 Service 层解耦 UserService userService new UserServiceImpl(); Result result userService.login(username, password, inputCode, request.getSession()); // 3. 根据结果转发清晰 if (result.isSuccess()) { request.getSession().setAttribute(user, result.getData()); response.sendRedirect(request.getContextPath() /main.jsp); } else { request.setAttribute(msg, result.getMsg()); request.getRequestDispatcher(/login.jsp).forward(request, response); } }参数说明Result是自定义返回对象封装boolean success、String msg、Object data比return null或throw Exception更符合课程设计评分标准userService.login()方法内部才真正调用UserDaoImpl实现 ControllerServlet与 ModelDAO的分离request.getRequestDispatcher(...).forward()是服务器端跳转保留 request 域数据如msg适合错误提示。5.3 destroy() 方法的沉默价值优雅释放连接池destroy()方法在 Web 应用卸载如 Tomcat 关闭时被调用它的作用是清理init()中申请的资源。对于连接池就是关闭所有空闲连接Override public void destroy() { if (dataSource ! null) { try { dataSource.close(); // 关闭连接池释放所有连接 } catch (SQLException e) { // 记录日志但不抛异常避免影响容器关闭 System.err.println(关闭数据库连接池失败: e.getMessage()); } } }为什么必须写如果不调用dataSource.close()Tomcat 关闭时连接池线程还在运行导致端口占用、内存泄漏课程设计评分细则明确要求“资源释放”destroy()是唯一直接体现该能力的生命周期方法实际部署中destroy()的执行时间会影响 Tomcat 关闭速度所以close()必须快速DBCP 的close()是异步的安全。从那以后我每次写 Servlet都强制走一遍init()→service()→destroy()的全流程验证init()里打印 DataSource initializedservice()里打印请求参数destroy()里打印 DataSource closed。三行日志就能确认生命周期没被绕过。希望帮到你。本文还有配套的精品资源点击获取
返回列表