简介:本资源为基于Java+MySQL实现的学生信息管理系统课程设计项目,编号100010724,面向计算机相关专业学生及Java Web初学者,可用于课程设计、毕业设计参考或Servlet与DAO分层架构的实战练习。项目在IntelliJ IDEA 2020.3.3下开发,采用JavaBean、Servlet、DAO模式,由Servlet处理请求并配置过滤器进行权限控制,数据持久化使用MySQL。功能覆盖学生、教师、管理员三类角色:学生可查询成绩、课程平均分、班级开课与教学计划;教师可录入与查询成绩、查看任课情况;管理员负责学生、教师、课程、成绩、学院、专业与班级等信息管理。压缩包共551个文件,约17.88MB,包含png与jpg界面截图、js与css前端资源、html与jsp页面、java源码与class编译文件、xml配置及jar依赖等,结构完整便于二次开发。目前已有568人学习下载,适合需要完整分层架构案例与数据库设计参考的读者。
1. 从一份课程设计说起:Java+MySQL 学生信息管理系统到底要解决什么
每年毕业季和期末周,搜索框里「javaweb项目完整案例mysql」「java课程设计案例源码」这类词都会集中爆发。点进去看,绝大多数人卡的不是 Java 语法,也不是 MySQL 装不上,而是不知道一个能跑、能演示、能讲清楚的学生信息管理系统,边界到底划在哪。这套基于 Java + MySQL 的 Web 学生信息管理系统,本质是一个典型的 CRUD 后台:学生、班级、课程、成绩四类核心数据,配上登录鉴权和分页查询。它不追求高并发,也不碰分布式,但要求你真正把「浏览器发请求 → 后端处理 → 数据库落盘 → 页面回显」这条链路走通一遍。适合谁?正在做课程设计的学生、想补一个完整 Web 项目进简历的转行者,以及需要一套可扩展后台骨架的初级开发者。下面我按自己实际搭过几遍的顺序,把选型、建表、接口、页面和踩过的坑一次讲透。
2. 技术选型与工程骨架:为什么是 Servlet/JSP 而不是一上来就 Spring Boot
2.1 先想清楚:这套系统用原生 JavaWeb 还是 Spring Boot
很多人一上手就问「能不能直接 Spring Boot + MyBatis-Plus」,能,但要看你的目标。如果这是课程设计,答辩老师大概率会问你「请求怎么进的 Servlet」「JDBC 连接什么时候关」,这时候你用 Spring Boot 反而说不清底层。原生 Servlet + JSP + JDBC 的好处是链路透明,每一层你都能看到;坏处是样板代码多,但正好逼你把基础打牢。我的建议是:课程设计用 Servlet 3.1 + JSP + JDBC,简历项目再往上叠 Spring Boot。两者数据库层可以完全复用,迁移成本很低。
选型确定后,工程结构要提前定好,不然后面改包名能改到崩溃。常见做法是按分层建包:
src/ └─ main/ ├─ java/ │ └─ com/example/sims/ │ ├─ controller/ // Servlet,接收请求 │ ├─ service/ // 业务逻辑 │ ├─ dao/ // 数据库访问 │ ├─ entity/ // 实体类,对应表 │ └─ util/ // DBUtil、MD5 等工具 ├─ resources/ │ └─ db.properties // 数据库连接配置 └─ webapp/ ├─ WEB-INF/ │ ├─ web.xml │ └─ views/ // JSP 页面 └─ static/ // css、js这个结构不是官方标准,但它把「谁调用谁」写在了目录上。controller 只做参数校验和转发,service 写业务规则,dao 只碰 SQL。新手最容易犯的错是把 SQL 写在 Servlet 里,后面加一个字段要改五六个文件。
2.2 数据库连接:别把 DriverManager 写死在代码里
JDBC 连接是这套系统的命门。我见过太多人把 url、username、password 直接硬编码在 Java 文件里,换台机器就报Access denied。正确做法是抽到db.properties,用类加载器读取:
# src/main/resources/db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/sims?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=your_password注意serverTimezone这个参数,MySQL 8 之后不写它,连接时会直接抛时区异常,这是新手第一个翻车点。useSSL=false在本地开发可以关掉,生产环境要单独配证书,这里不展开。
读取配置和获取连接的代码:
// DBUtil.java public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 关闭资源,顺序不能反:ResultSet -> Statement -> Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明:静态代码块只在类第一次加载时执行一次,配置读一次就够。getConnection每次新建连接,适合课程设计这种低并发场景。参数说明:url里的characterEncoding=utf8保证中文不乱码,serverTimezone必须和数据库实际时区一致,否则存进去的时间会差 8 小时。关闭顺序必须是 ResultSet → Statement → Connection,反过来关会报错。
提示:如果后面要做连接池,把
getConnection换成从HikariDataSource取即可,上层代码不用动,这就是分层的好处。
3. 建表与实体映射:学生、班级、课程、成绩四张表怎么设计
3.1 表结构设计:主外键和字段类型的选择
数据库设计决定了后面 SQL 好不好写。核心四张表:student、clazz、course、score。学生属于班级,成绩关联学生和课程,这是典型的一对多和多对多。
CREATE DATABASE IF NOT EXISTS sims DEFAULT CHARSET utf8mb4; USE sims; CREATE TABLE clazz ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '班级名称', grade VARCHAR(20) COMMENT '年级' ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(30) NOT NULL, gender TINYINT DEFAULT 1 COMMENT '1男 0女', clazz_id INT, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (clazz_id) REFERENCES clazz(id) ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cname VARCHAR(50) NOT NULL, credit DECIMAL(3,1) DEFAULT 0 COMMENT '学分' ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT 0, UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );参数说明:utf8mb4而不是utf8,因为后者在 MySQL 里是残缺的三字节实现,存 emoji 或生僻字会报错。sno加 UNIQUE,学号重复是业务红线。score表用联合唯一键uk_stu_course,防止同一个学生同一门课录入两条成绩,这个约束比在 Java 里判断靠谱得多。gender用 TINYINT 存 0/1,比 VARCHAR 省空间,也避免「男/男性/Male」这种脏数据。
3.2 实体类与 DAO:把 ResultSet 映射成对象
实体类字段名和表字段对应,用驼峰命名。DAO 层负责把 ResultSet 转成对象,这里以学生查询为例:
// StudentDao.java public List<Student> findByPage(String keyword, int page, int size) { List<Student> list = new ArrayList<>(); String sql = "SELECT s.*, c.name AS clazzName FROM student s " + "LEFT JOIN clazz c ON s.clazz_id = c.id " + "WHERE s.name LIKE ? OR s.sno LIKE ? " + "ORDER BY s.id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, (page - 1) * size); ps.setInt(4, size); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setSno(rs.getString("sno")); s.setName(rs.getString("name")); s.setGender(rs.getInt("gender")); s.setClazzName(rs.getString("clazzName")); list.add(s); } } } catch (SQLException e) { throw new RuntimeException("查询学生失败", e); } return list; }逻辑说明:用PreparedStatement而不是拼接字符串,这是防 SQL 注入的底线,keyword里的单引号不会破坏语句结构。LEFT JOIN保证即使学生没分班也能查出来。分页用LIMIT offset, size,offset 从 0 开始,所以是(page-1)*size。参数说明:size一般设 10 或 15,太大页面卡,太小翻页烦。ORDER BY s.id DESC让新增的学生排前面,符合直觉。
注意:
LIKE的模糊查询在数据量大时会全表扫描,课程设计无所谓,真上生产要给name建索引或换全文检索。
4. 接口与页面打通:登录鉴权、分页查询和成绩录入
4.1 登录鉴权:Session 怎么用才不出安全漏洞
登录是这套系统唯一的安全入口。流程是:用户提交账号密码 → 后端查库比对 → 成功则把用户信息写进 Session → 后续请求用 Filter 拦截校验。
// LoginServlet.java 核心片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 密码做 MD5 后再比对,数据库存的是密文 String md5Pwd = MD5Util.encode(password); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPwd(username, md5Pwd); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/student/list"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(req, resp); } }逻辑说明:密码绝不能明文存库,MD5 虽然现在不够强,但课程设计够用,至少比明文强。登录成功后用sendRedirect而不是forward,避免刷新页面重复提交表单。参数说明:req.getContextPath()拿到项目部署路径,保证跳转地址正确,不写死/sims。
配套的登录拦截 Filter:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 登录页和静态资源放行 if (uri.endsWith("login.jsp") || uri.contains("/static/") || uri.endsWith("/login")) { chain.doFilter(request, response); return; } if (req.getSession().getAttribute("loginUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); }逻辑说明:白名单放行登录相关路径,其余一律检查 Session。参数说明:uri.contains("/static/")让 css、js 不被拦截,否则登录页样式全丢。这个 Filter 要在web.xml里配置拦截/*。
4.2 成绩录入与列表回显:一次请求的完整链路
成绩录入是业务最复杂的一环,要同时处理学生、课程两个下拉框和分数校验。前端 JSP 提交后,后端先校验分数范围,再入库。
// ScoreServlet.java 保存成绩 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int studentId = Integer.parseInt(req.getParameter("studentId")); int courseId = Integer.parseInt(req.getParameter("courseId")); double score = Double.parseDouble(req.getParameter("score")); if (score < 0 || score > 100) { req.setAttribute("msg", "分数必须在 0-100 之间"); req.getRequestDispatcher("/WEB-INF/views/score/add.jsp").forward(req, resp); return; } ScoreDao dao = new ScoreDao(); // 存在则更新,不存在则插入 dao.saveOrUpdate(studentId, courseId, score); resp.sendRedirect(req.getContextPath() + "/score/list"); }逻辑说明:先做业务校验再落库,避免脏数据。saveOrUpdate内部用INSERT ... ON DUPLICATE KEY UPDATE,依赖前面建的联合唯一键,一条 SQL 搞定新增和修改。参数说明:studentId、courseId从下拉框传过来是字符串,要转 int;score转 double 后校验范围。
对应的 SQL:
INSERT INTO score (student_id, course_id, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = VALUES(score);逻辑说明:MySQL 特有语法,遇到唯一键冲突就执行 UPDATE,省去先查后判的两次数据库交互。参数说明:VALUES(score)取的是本次要插入的值,MySQL 8.0.20 之后推荐用别名写法,但课程设计用这个兼容性更好。
提示:列表页回显成绩时,用 JOIN 把学生名和课程名一起查出来,别在 JSP 里循环查库,那是 N+1 查询的经典翻车现场。
5. 避坑与排查:这套系统最容易翻车的五个地方
5.1 中文乱码:现象是页面显示问号,原因是编码链路没统一
现象:提交表单后数据库里存的是???,或者页面显示乱码。原因:请求、响应、数据库三处编码不一致。解决:分三步统一。第一,JSP 页面头部写<%@ page contentType="text/html;charset=UTF-8" %>;第二,Servlet 里在取参数前设置req.setCharacterEncoding("UTF-8")和resp.setContentType("text/html;charset=UTF-8");第三,数据库连接 url 带characterEncoding=utf8,建库用utf8mb4。三处缺一处都会乱码,这是血泪经验。
5.2 连接泄漏:现象是跑一会儿就报 Too many connections
现象:系统用一段时间后所有请求卡死,日志报Too many connections。原因:Connection、Statement、ResultSet用完没关,连接池被占满。解决:所有数据库操作必须放在 try-with-resources 里,或者 finally 块中按 ResultSet → Statement → Connection 顺序关闭。排查方法:在 MySQL 里执行SHOW PROCESSLIST;,看是不是有一堆 Sleep 状态的连接堆着。
5.3 时区异常:现象是启动就报 The server time zone value is unrecognized
现象:MySQL 8 环境下连接直接失败,报时区无法识别。原因:MySQL 8 默认时区是 UTC,JDBC 驱动要求显式指定。解决:url 里加serverTimezone=Asia/Shanghai。如果还不行,检查 MySQL 服务端my.cnf里的default-time-zone配置。这个坑在「mysql安装配置教程」里很少有人提,但几乎人人都会遇到一次。
5.4 外键约束报错:现象是删班级时删不掉
现象:删除一个班级,报Cannot delete or update a parent row。原因:student表有外键指向clazz,班级下还有学生时不允许删。解决:要么先删或转移该班级下的学生,要么把外键改成ON DELETE SET NULL。我的做法是在 Service 层先查该班级有没有学生,有就提示用户,而不是直接抛数据库异常给前端。
5.5 分页总数算错:现象是最后一页多出空白或漏数据
现象:分页显示总页数不对,最后一页要么空白要么少几条。原因:总数查询和列表查询的 WHERE 条件不一致,或者LIMIT的 offset 算错。解决:总数 SQL 和列表 SQL 用完全相同的 WHERE 条件,offset 严格用(page-1)*size。排查时把两条 SQL 都打印出来,手动在 Navicat 里跑一遍对比结果,比盯着代码看快得多。
6. 进阶技巧:用一条 SQL 查出班级成绩统计,以及我踩过的验证习惯
系统能跑之后,真正拉开差距的是统计查询。答辩或演示时,老师最爱问「能不能按班级算平均分」。很多人第一反应是在 Java 里循环累加,数据一多就慢。正确做法是交给数据库:
SELECT c.name AS clazzName, COUNT(s.id) AS stuCount, ROUND(AVG(sc.score), 2) AS avgScore, MAX(sc.score) AS maxScore, MIN(sc.score) AS minScore FROM clazz c LEFT JOIN student s ON s.clazz_id = c.id LEFT JOIN score sc ON sc.student_id = s.id GROUP BY c.id, c.name ORDER BY avgScore DESC;逻辑说明:三层 LEFT JOIN 把班级、学生、成绩串起来,GROUP BY按班级聚合。用 LEFT JOIN 而不是 INNER JOIN,保证没有学生的班级也能出现在结果里,平均分显示 NULL 而不是整行消失。参数说明:ROUND(..., 2)保留两位小数,AVG会自动忽略 NULL 值,所以没成绩的学生不影响平均分计算。这条 SQL 直接映射到一个 VO 类,JSP 里遍历输出即可,比在 Java 里写循环优雅得多。
再补一个验证习惯:每次改完 DAO 层的 SQL,我都会先在 Navicat 或 MySQL 命令行里把参数替换成真实值跑一遍,确认结果对了再写进 Java。因为 Java 里调试 SQL 要重启 Tomcat,慢;数据库客户端里改一下就能看结果,快。这个习惯帮我省了无数时间。
还有一个容易被忽略的点:web.xml里的welcome-file-list要配好,否则访问根路径会 404。我一般把登录页设为欢迎页,用户一进来就是登录界面,体验顺。另外,JSP 页面尽量放WEB-INF/views/下,这样不能通过 URL 直接访问,必须走 Servlet 转发,安全性高一层。
最后说个我自己的教训:早期做这套系统时,我图省事把数据库密码写在了 DAO 的静态变量里,结果把代码发给同学参考,他那边数据库密码不一样,跑不起来还以为是代码有 bug,查了一下午。从那以后,凡是和环境相关的配置,一律抽到 properties 文件,代码里只留读取逻辑。这个习惯后来救了我很多次。希望帮到你。
本文还有配套的精品资源,点击获取