简介:本资源是一套基于Java与MySQL开发的学生信息管理系统课程设计项目,面向计算机相关专业学生、Java Web初学者及需要完成课程设计或毕业设计的开发者。项目在IntelliJ IDEA 2020.3.3环境下开发,采用JavaBean、Servlet、DAO分层架构,通过Servlet处理请求并配置过滤器实现权限控制,数据持久化使用MySQL数据库。系统按角色划分功能:学生端支持成绩查询、课程平均成绩查询、班级开课查询与教学计划查询;教师端支持成绩录入、成绩查询及任课情况查询;管理员端覆盖学生、教师、课程、成绩、学院、专业与班级等信息的综合管理。压缩包共551个文件,约17.88MB,包含png与jpg界面截图、js与css前端资源、html与jsp页面、java源码、class编译文件、xml配置及jar依赖等,结构完整、便于二次开发与学习参考。目前已有568人学习下载,适合作为Java Web入门实战与课程设计参考案例。
1. 从一份课程设计说起:Java+MySQL 学生信息管理系统到底要解决什么
每年毕业季,总有一批同学被“学生信息管理系统”这个题目卡住。不是因为它有多难,而是因为网上能找到的代码要么跑不起来,要么缺表少字段,要么把增删改查写成了“能跑就行”的流水账。我带过几届课程设计,发现一个反直觉的现象:真正让人翻车的往往不是 Java 语法,而是 MySQL 连接、字符集、外键约束这些“看起来很简单”的地方。这套基于 Java+MySQL 的 Web 学生信息管理系统,核心就是三件事:用 JDBC 把 Java 和 MySQL 连起来,用 Servlet/JSP 或 Spring Boot 把页面请求接住,用一套合理的表结构把学生、班级、课程、成绩这些数据管清楚。它适合正在做课程设计的学生,也适合想从“只会写单文件 Java”过渡到“能跑一个完整 Web 项目”的初学者。下面我按实际落地的顺序,把选型、建表、连接、页面、排错和进阶技巧拆开讲,每一步都给出能直接抄的代码和参数。
2. 技术选型与数据库设计:为什么是 Java+MySQL 而不是别的
2.1 为什么这套系统绕不开 JDBC 和连接池
很多同学一上来就想用 Spring Boot + MyBatis,结果光配依赖就卡半天。我的建议是:课程设计阶段,先用原生 JDBC 把“连接—执行—取结果—关闭”这条链路走通,再考虑框架。原因很简单,JDBC 是 Java 操作 MySQL 的底层标准,任何框架都是对它封装。你只有亲手写过Connection、PreparedStatement、ResultSet,才能理解后面连接池为什么要配maxActive、maxIdle、validationQuery。
原生 JDBC 的典型写法如下,注意每个资源都要在 finally 里关闭,否则连接数一多,MySQL 直接报Too many connections。
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动未加载", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (ps != null) ps.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }这段代码里,URL后面的参数是血泪经验:useUnicode=true&characterEncoding=utf8解决中文乱码,serverTimezone=Asia/Shanghai解决时区报错,useSSL=false避免本地开发时的 SSL 警告。如果你用 MySQL 8.0 以上,驱动类名必须是com.mysql.cj.jdbc.Driver,写老版本的com.mysql.jdbc.Driver会提示驱动已废弃。
连接池方面,课程设计用 Druid 或 HikariCP 都行。我一般会配一个druid.properties,把初始连接数设成 5,最大活跃设成 20,并开启testWhileIdle。这样即使页面并发稍微高一点,也不会频繁创建连接。
2.2 学生信息管理系统的表结构怎么定才不返工
表结构没设计好,后面改字段能改到崩溃。我见过最典型的翻车是:学生表里直接存班级名称,结果班级改名,所有学生记录都要更新。正确做法是拆成class_info和student_info,用class_id关联。
下面是我常用的四张核心表,字段和类型都经过实际项目验证。
| 表名 | 关键字段 | 类型 | 说明 |
|---|---|---|---|
| student_info | student_id | VARCHAR(20) | 学号,主键 |
| student_info | student_name | VARCHAR(50) | 姓名,非空 |
| student_info | gender | CHAR(1) | 性别,M/F |
| student_info | class_id | INT | 外键,关联班级 |
| student_info | phone | VARCHAR(20) | 联系电话 |
| class_info | class_id | INT | 主键,自增 |
| class_info | class_name | VARCHAR(50) | 班级名称,唯一 |
| course_info | course_id | INT | 主键,自增 |
| course_info | course_name | VARCHAR(50) | 课程名称 |
| score_info | id | INT | 主键,自增 |
| score_info | student_id | VARCHAR(20) | 外键,关联学生 |
| score_info | course_id | INT | 外键,关联课程 |
| score_info | score | DECIMAL(5,2) | 成绩,0-100 |
建表时注意三点:第一,student_id用 VARCHAR 而不是 INT,因为学号可能带字母或前导零;第二,外键约束要加ON DELETE RESTRICT,防止误删班级导致学生数据悬空;第三,成绩字段用 DECIMAL(5,2),别用 FLOAT,否则 89.99 可能存成 89.989999。
CREATE TABLE class_info ( class_id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student_info ( student_id VARCHAR(20) PRIMARY KEY, student_name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT 'M', class_id INT, phone VARCHAR(20), FOREIGN KEY (class_id) REFERENCES class_info(class_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字符集统一用utf8mb4,不要用utf8,因为utf8在 MySQL 里是残缺的,存不了 emoji 和部分生僻字。这个坑我在一次录入学生姓名时踩过,名字里有个生僻字直接变成问号。
3. 从零跑通增删改查:Servlet+JSP 的最小可运行版本
3.1 用 Servlet 接收表单并写入 MySQL 的完整链路
页面能显示只是第一步,能把表单数据写进数据库才算跑通。我一般先写一个AddStudentServlet,用doPost接收参数,再用PreparedStatement插入。注意不要用Statement拼接 SQL,否则一个名字里带单引号就能让系统报错,甚至被注入。
@WebServlet("/addStudent") public class AddStudentServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String studentId = req.getParameter("studentId"); String studentName = req.getParameter("studentName"); String gender = req.getParameter("gender"); String classIdStr = req.getParameter("classId"); String phone = req.getParameter("phone"); String sql = "INSERT INTO student_info(student_id, student_name, gender, class_id, phone) VALUES(?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentId); ps.setString(2, studentName); ps.setString(3, gender); ps.setInt(4, Integer.parseInt(classIdStr)); ps.setString(5, phone); ps.executeUpdate(); resp.sendRedirect("studentList"); } catch (SQLException e) { req.setAttribute("error", "添加失败:" + e.getMessage()); req.getRequestDispatcher("add.jsp").forward(req, resp); } } }这里的关键参数是req.setCharacterEncoding("UTF-8"),必须放在获取参数之前,否则中文姓名会乱码。另外,classIdStr要做空值和数字格式判断,否则用户不选班级时Integer.parseInt直接抛异常。实际项目中我会加一层校验:学号不能为空、手机号格式用正则匹配、班级必须存在。
查询列表的 Servlet 类似,只是把executeUpdate换成executeQuery,然后遍历ResultSet封装成List<Student>,再转发到 JSP 渲染。分页的话,用LIMIT ?,?,页码从 0 开始算,每页 10 条。
3.2 JSP 页面里最容易出错的三个地方
JSP 现在虽然被模板引擎替代了不少,但课程设计里依然常见。我总结三个高频翻车点:第一,<%@ page contentType="text/html;charset=UTF-8" language="java" %>这行必须放在最上面,否则页面中文全是乱码;第二,用 JSTL 遍历列表时,<c:forEach items="${studentList}" var="stu">里的${stu.studentName}大小写必须和 JavaBean 的 getter 一致,写成${stu.studentname}取不到值;第三,表单提交的action路径不要以/开头,否则会跳到根路径,用相对路径addStudent更稳。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>学生列表</title></head> <body> <table border="1"> <tr><th>学号</th><th>姓名</th><th>性别</th><th>班级</th><th>操作</th></tr> <c:forEach items="${studentList}" var="stu"> <tr> <td>${stu.studentId}</td> <td>${stu.studentName}</td> <td>${stu.gender}</td> <td>${stu.className}</td> <td><a href="deleteStudent?id=${stu.studentId}">删除</a></td> </tr> </c:forEach> </table> </body> </html>如果列表里要显示班级名称而不是班级 ID,SQL 里用LEFT JOIN class_info c ON s.class_id = c.class_id,然后在 JavaBean 里加一个className字段。别在 JSP 里再查一次数据库,那样页面一多,连接池直接爆。
4. 避坑与排查:学生信息管理系统最常见的五个翻车现场
4.1 连接 MySQL 报错 Access denied 或 Public Key Retrieval
现象:启动项目后,控制台抛出java.sql.SQLException: Access denied for user 'root'@'localhost'或者Public Key Retrieval is not allowed。原因通常是密码不对,或者 MySQL 8.0 默认的caching_sha2_password认证插件和旧驱动不兼容。解决:先确认DBUtil里的用户名密码和 MySQL 实际一致;如果报 Public Key 错误,在 URL 后面加allowPublicKeyRetrieval=true,同时确保useSSL=false。另外,MySQL 8.0 安装时如果选了caching_sha2_password,可以用ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';改回旧插件,但更推荐升级驱动到 8.0.33 以上。
4.2 中文乱码:从数据库到页面全链路排查
现象:学生姓名在页面显示成??或者å¼ ä¸‰。原因可能出在三个环节:数据库字符集、连接 URL 字符集、JSP 页面字符集。解决:先执行SHOW VARIABLES LIKE 'character%';,确保character_set_server和character_set_database都是utf8mb4;然后检查 JDBC URL 是否带了characterEncoding=utf8;最后确认 JSP 的pageEncoding和contentType都是 UTF-8。如果还乱码,在 Servlet 里加resp.setContentType("text/html;charset=UTF-8")。我一般会写一个检查清单,三个地方全过一遍,基本能解决 99% 的乱码。
4.3 外键约束导致删除失败
现象:删除班级时,页面报Cannot delete or update a parent row: a foreign key constraint fails。原因是你建表时加了ON DELETE RESTRICT,而该班级下还有学生。解决:要么先删学生再删班级,要么把外键改成ON DELETE SET NULL,但后者会导致学生班级为空。我的做法是在删除班级前,先查一下SELECT COUNT(*) FROM student_info WHERE class_id=?,如果大于 0,提示用户“该班级下还有学生,不能删除”。这样既安全又友好。
4.4 连接池耗尽:Too many connections
现象:系统运行一段时间后,所有页面都卡住,MySQL 报Too many connections。原因通常是 JDBC 资源没关闭,或者连接池最大连接数设得太小。解决:先检查所有Connection、PreparedStatement、ResultSet是否在 finally 里关闭;如果用连接池,把maxActive调到 20 以上,并设置removeAbandoned="true"和removeAbandonedTimeout="180"。另外,validationQuery设成SELECT 1,保证空闲连接可用。这个坑我在一次演示时踩过,当时没关ResultSet,跑了十几分钟就崩了。
4.5 学号重复插入报主键冲突
现象:添加学生时,如果学号已存在,控制台报Duplicate entry '2023001' for key 'PRIMARY'。原因就是主键重复。解决:在插入前先查一次SELECT COUNT(*) FROM student_info WHERE student_id=?,如果存在就返回“学号已存在”的提示。更优雅的做法是用INSERT IGNORE或者ON DUPLICATE KEY UPDATE,但课程设计里手动判断更直观。注意不要用try-catch直接吞掉异常,否则用户不知道发生了什么。
5. 进阶技巧:用 JDBC 批量插入和事务保证成绩录入不丢数据
5.1 批量插入成绩:从 100 条 10 秒到 1 秒
成绩录入是学生信息管理系统里最耗时的操作。如果一条一条executeUpdate,100 条成绩可能要 10 秒以上。用addBatch和executeBatch能把这个时间压到 1 秒以内。核心代码如下:
String sql = "INSERT INTO score_info(student_id, course_id, score) VALUES(?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (ScoreDTO dto : scoreList) { ps.setString(1, dto.getStudentId()); ps.setInt(2, dto.getCourseId()); ps.setBigDecimal(3, dto.getScore()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException("成绩录入失败", e); }注意setAutoCommit(false)和commit()必须成对出现,否则批量插入不会生效。executeBatch返回一个 int 数组,表示每条语句影响的行数,可以用来判断是否全部成功。如果中途某条失败,整个事务回滚,保证成绩不会只录一半。
5.2 用事务保证“删除学生同时删除成绩”的一致性
删除学生时,如果只删student_info,score_info里会留下孤儿记录。正确做法是在一个事务里同时删两张表:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String sql1 = "DELETE FROM score_info WHERE student_id=?"; String sql2 = "DELETE FROM student_info WHERE student_id=?"; try (PreparedStatement ps1 = conn.prepareStatement(sql1); PreparedStatement ps2 = conn.prepareStatement(sql2)) { ps1.setString(1, studentId); ps1.executeUpdate(); ps2.setString(1, studentId); ps2.executeUpdate(); } conn.commit(); } catch (SQLException e) { if (conn != null) conn.rollback(); throw new RuntimeException("删除失败", e); } finally { DBUtil.close(conn, null, null); }这里的关键是conn.setAutoCommit(false)之后,两条 DELETE 要么都成功,要么都回滚。如果先删学生再删成绩,中间报错,成绩就删不掉了。所以顺序也很重要:先删子表,再删主表。
5.3 验证数据一致性的两个 SQL 检查
写完代码后,别急着点页面。先跑两个 SQL 确认数据没毛病。第一个检查孤儿成绩:SELECT COUNT(*) FROM score_info s LEFT JOIN student_info st ON s.student_id = st.student_id WHERE st.student_id IS NULL;如果结果大于 0,说明有成绩找不到对应学生。第二个检查班级人数:SELECT c.class_name, COUNT(s.student_id) FROM class_info c LEFT JOIN student_info s ON c.class_id = s.class_id GROUP BY c.class_id;看看有没有班级人数对不上。这两个查询我每次部署前都会跑一遍,比点页面靠谱得多。
最后说个我自己的习惯:每次改完数据库相关代码,先在本机用mysql -u root -p登录,手动执行一遍 SQL,确认语句本身没问题,再放到 Java 里跑。这样能把“SQL 写错”和“Java 代码写错”分开排查,省掉很多来回折腾的时间。希望帮到你。
本文还有配套的精品资源,点击获取