ARTICLE DETAIL

资讯详情

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

学生成绩管理系统V2实战:Servlet+JSP+MySQL+Bootstrap整合

学生成绩管理系统V2实战:Servlet+JSP+MySQL+Bootstrap整合 简介这是一套基于 Java 技术栈实现的学生成绩管理系统整合 Servlet、JSP、Bootstrap 与 MySQL适合 Java Web 初学者、毕业设计人员及需要快速搭建后台管理系统的开发者参考。系统覆盖学生信息管理、成绩录入、按姓名/课程/学期查询、统计分析和数据备份恢复等功能模块并采用 MVC 思想组织代码便于理解分层结构与后期扩展。资源共 575 个文件包体约 9.17MB包含 85 个 Java 源文件、74 个 JSP 页面、170 个 class 文件、122 个 JS、56 个 CSS 以及 jar 包、SQL 脚本和配置文件可覆盖从数据库设计、后端逻辑到前端页面的完整学习链路。已有 3118 人学习下载适合对照项目梳理 Servlet 请求处理、JSP 渲染、Bootstrap 响应式布局与 MySQL 操作的协作方式也可作为课程设计或入门实战参考。1. 学生成绩管理系统迭代到 V2Servlet JSP Bootstrap 组合到底解决了什么问题学生成绩管理系统在 Java 毕业设计里几乎年年上榜Servlet、JSP、Bootstrap、MySQL 这四个词一拼基本就暴露了这是经典业务型项目。标题末尾带-2我按这个系统的第二次迭代来理解第一版能加学生、录成绩跑是跑得起来但乱码靠运气、页面靠原生表格、改个查询条件要动三四个 JSP 文件。V2 要解决的就是把这套技术栈重新拆成能交付的样子——Servlet 只做路由和控制JSP 只做渲染Bootstrap 负责把界面撑起来MySQL 这边把表设计和连接参数理顺。这篇笔记按我落地这类项目的习惯顺序来写先定数据库再搭工程骨架然后写核心 CRUD最后集中讲那几个最隐蔽的坑。适合正在做课设、准备 Java 岗位面试或者要在一堆老 Servlet 项目上做二次开发的读者。2. 数据库与连接层设计先把学生、课程、成绩三张表的关系理顺很多同学写管理系统习惯“先写代码后建表”做到一半发现成绩表和课程表对不上再去改表结构牵一发动全身。V2 从一开始就按下述三张表设计后面所有 Servlet 和 JSP 都是围绕它们转。2.1 三张基础表学生表、课程表、成绩表的字段边界学生信息和成绩信息必须拆开不要让成绩表里塞一堆班级、学号、课程名这种冗余字段。冗余一时爽等你要统计“某课程平均分”或者“某班总排名”的时候冗余数据不一致就是灾难。成绩表只存student_id和course_id其他信息通过关联去查。成绩表要支持同一门课在不同学期、不同考试类型下重复出现所以“学生 课程 学期 考试类型”四个字段必须联合唯一防止重复录入同一份成绩。这是我第一版踩过的坑没有唯一键老师手抖点了两次提交同一条成绩录了两遍期末排名怎么算都多出来一个人。表名核心字段说明studentstudent_no、student_name、class_name学号做唯一键班级单独存coursecourse_name、credit课程名唯一学分保留一位小数scorestudent_id、course_id、semester、exam_type、score联合唯一键防重复外键关联前两张表2.2 初始化 SQLutf8mb4 字符集、联合唯一键、外键级联建库脚本是项目的地基我的习惯是直接写成init.sql每次重新部署都能一键还原。CREATE DATABASE IF NOT EXISTS student_scores DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE student_scores; CREATE TABLE student ( student_id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL COMMENT 学号, student_name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) DEFAULT 未分班 COMMENT 班级名称, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB COMMENT学生表; CREATE TABLE course ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(50) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分, UNIQUE KEY uk_course_name (course_name) ) ENGINEInnoDB COMMENT课程表; CREATE TABLE score ( score_id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL COMMENT 学期如 2024-2025-1, exam_type ENUM(midterm, final) DEFAULT final COMMENT 考试类型, score DECIMAL(5,2) NOT NULL COMMENT 成绩保留两位小数, exam_time DATE DEFAULT NULL, UNIQUE KEY uk_student_course_semester (student_id, course_id, semester, exam_type), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT, INDEX idx_semester (semester) ) ENGINEInnoDB COMMENT成绩表;这段 SQL 有几个参数值得展开说。utf8mb4而不是utf8是因为 MySQL 的utf8最多存三个字节一些特殊符号存不进去会直接报错或者存成乱码utf8mb4_unicode_ci是通用排序规则中文排序基本符合直觉。score字段用DECIMAL(5,2)范围是 0.00 到 999.99百分制成绩完全够用如果只是给学生存数据库可以让ENUM(midterm, final)无脑选择。外键的ON DELETE CASCADE和ON DELETE RESTRICT是两种相反的删法删除学生时成绩跟着删符合业务直觉删除课程时如果成绩表里已经引用了这门课直接拒绝删除避免历史成绩变成孤儿数据。如果以后要保留“误删”这种后悔药可以把 CASCADE 改成 RESTRICT在业务层做逻辑删除。2.3 JDBC 连接参数MySQL 8 的驱动和各种连接参数不管用不用框架JDBC 连接是绕不开的一层。V2 里没有引 HikariCP 之类的连接池核心原因是课设和内部小项目用DriverManager足够少一个依赖就少一个踩坑点。参数写在db.properties里方便换环境改配置。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_scores?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.passwordyour_password_here对应 Java 侧的工具类public class DbUtil { private static final Properties props new Properties(); static { try (InputStream in DbUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(in); Class.forName(props.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( props.getProperty(jdbc.url), props.getProperty(jdbc.username), props.getProperty(jdbc.password) ); } }characterEncodingutf8必须显式写上不写的话即使数据库和页面的字符集都对连接层也有可能在传输过程中把中文搞成问号。serverTimezoneAsia/Shanghai解决的是新版驱动读取数据库时间时的时区报错。allowPublicKeyRetrievaltrue是 MySQL 8 的caching_sha2_password认证插件相关主要解决启动时报“Public Key Retrieval is not allowed”。useSSLfalse则是因为本机开发环境没有证书不开 SSL 握手会更快。3. 搭建 Servlet JSP 骨架Maven 依赖、包结构、web.xml 配置数据库就绪之后下一步是工程骨架。我最怕看到一上来就建 JSP、把大量 Java 代码堆在页面里。V2 的工程骨架分三层controller放 Servletdao放 JDBC 数据访问model放实体类JSP 统一放到WEB-INF/jsp下面。3.1 Maven 的 pom.xmlServlet、JSTL、MySQL 驱动三个依赖够了Maven 项目的好处是依赖不用手动复制 jar 包。这里只加三个运行期需要的依赖其他都交给 Maven 传递。properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency /dependenciesjavax.servlet-api的 scope 只能是provided编译时需要运行时由 Tomcat 提供不加这个 scope 会导致版本冲突。jstl 1.2是 Tomcat 9 及之前版本配套的命名空间后面避坑章会专门讲 Tomcat 10 的兼容问题。mysql-connector-java 8.0.33对应 MySQL 8.x如果数据库是 5.7可以降到 5.1.49但新版驱动基本兼容旧版 MySQL问题不大。3.2 用 Maven 直接管理 Tomcat 插件省去手动部署手动把 war 包复制到 Tomcat 的 webapps 目录不算难但每次改代码都要重启 Tomcat很影响调试节奏。V2 的工程里加了tomcat7-maven-plugin名字虽然带 tomcat7但通过调整 Tomcat 版本可以实现对应的容器支持。build finalNamestudent-score-system/finalName plugins plugin groupIdorg.apache.tomcat.maven/groupId artifactIdtomcat7-maven-plugin/artifactId version2.2/version configuration port8080/port path/student-score-system/path uriEncodingUTF-8/uriEncoding /configuration /plugin /plugins /buildport是启动端口path是访问路径等于部署后的上下文路径比如访问首页就是http://localhost:8080/student-score-system/。uriEncodingUTF-8必须配至少 JSP 中文参数的中文乱码要靠它处理。执行mvn tomcat7:run就能在本地跑起来改 Java 代码重新编译后重启改 JSP 直接刷新页面。3.3 实体类与 DAO 的包边界model 层别写业务逻辑实体类是最容易写飞的一层有人把校验、格式化和拼接 SQL 都往里塞。实际上 model 层就是数据搬运工字段和表字段一一对应即可。public class Student { private Integer studentId; private String studentNo; private String studentName; private String className; public Student() { } public Student(Integer studentId, String studentNo, String studentName, String className) { this.studentId studentId; this.studentNo studentNo; this.studentName studentName; this.className className; } // getter/setter 省略实际代码里必须补全 }这里有两个约定俗成的点所有包装类字段用Integer而不是int因为数据库INT NULL在 Java 里映射成null时int会直接 NPE。所有字段名必须和表字段的驼峰映射一致比如student_no在 Java 里就叫studentNo这样后面从结果集填值的时候不容易手滑。我一般还会在 DAO 上方定义一个findAll、findById、insert、update、deleteById的接口这样 Servlet 调用起来一目了然。这就是最简单实用的 DAO 模式别把它上升到设计模式去理解。4. 写核心代码Servlet 收请求、DAO 查 MySQL、JSTL 渲染、Bootstrap 撑门面骨架搭完到了全篇最重的部分。我会按一个“查看学生列表 - 添加成绩 ”的完整链路来讲。最需要记住的分层原则Servlet 里不要写 JDBCJSP 里不要写 Java 代码所有页面取数据的活交给 DAO。4.1 Servlet 路由用 op 参数控制增删改查的入口V2 采用了统一入口的写法一个StudentServlet负责学生的增删改查通过op参数区分动作。这样 URL 不必每个动作都建一个 Servlet维护起来轻松一些。WebServlet(/student) public class StudentServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String op req.getParameter(op); if (op null) { op list; } switch (op) { case add: showAddPage(req, resp); break; case edit: showEditPage(req, resp); break; case delete: doDelete(req, resp); break; default: listPage(req, resp); break; } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String op req.getParameter(op); if (create.equals(op)) { doCreate(req, resp); } else { resp.sendRedirect(req.getContextPath() /student); } } private void listPage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListStudent students studentDao.findAll(); req.setAttribute(studentList, students); req.getRequestDispatcher(/WEB-INF/jsp/student_list.jsp) .forward(req, resp); } }这里需要注意的是 doGet 和 doPost 的分工。浏览器地址栏访问只发 GET所以查询和删除放在 doGet表单提交一般是 POST所以新增和修改放在 doPost。req.setCharacterEncoding(UTF-8)放在最前面它确保 POST 请求体里的中文在你读取参数之前就被正确解码漏掉这一行中文姓名大概率乱码。转发到WEB-INF/jsp下的 JSP浏览器无法直接访问这是安全上的一个习惯。4.2 DAO 层PreparedStatement 是底线杜绝字符串拼接 SQLDAO 是访问数据库的最后一层所有 SQL 必须用PreparedStatement原因很简单预编译能处理特殊字符还能防止基本的 SQL 注入。public class StudentDao { public ListStudent findAll() { String sql SELECT student_id, student_no, student_name, class_name FROM student ORDER BY student_no; ListStudent list new ArrayList(); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student s new Student(); s.setStudentId(rs.getInt(student_id)); s.setStudentNo(rs.getString(student_no)); s.setStudentName(rs.getString(student_name)); s.setClassName(rs.getString(class_name)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } public int insert(Student s) { String sql INSERT INTO student (student_no, student_name, class_name) VALUES (?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getStudentName()); ps.setString(3, s.getClassName()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }占位符?的赋值顺序从上到下从 1 开始数这是新手比较容易翻车的地方。setString的三个参数和 SQL 里的三个问号一一对应如果写反了记录就跑到错误字段去了。try-with-resources会自动关掉Connection、PreparedStatement、ResultSet这是避免连接泄漏最有效的写法比自己finally里手动关闭省心得多。4.3 把成绩写进数据库事务的核心价值成绩录入不能只往score表里插一行就完事如果以后要顺便更新学生平均分、课程平均分这类统计字段就必须保证两步操作同时成功或同时失败。用 MySQL 事务来做就是这种场景的标准解。public boolean saveScore(Score score) { String insertScore INSERT INTO score (student_id, course_id, semester, exam_type, score) VALUES (?, ?, ?, ?, ?); String updateStudentStat UPDATE student SET total_score total_score ? WHERE student_id ?; Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(insertScore)) { ps1.setInt(1, score.getStudentId()); ps1.setInt(2, score.getCourseId()); ps1.setString(3, score.getSemester()); ps1.setString(4, score.getExamType()); ps1.setBigDecimal(5, score.getScore()); ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(updateStudentStat)) { ps2.setBigDecimal(1, score.getScore()); ps2.setInt(2, score.getStudentId()); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }setAutoCommit(false)关闭自动提交后事务就开始了。两条 SQL 在同一个Connection上顺序执行都成功才commit任何一条抛异常都会进入rollback把已经执行的插回撤。这就像是“后悔药”避免数据库里出现“成绩加上了但总分没更新”这种半截数据。4.4 JSP 用 JSTL 和 EL 渲染列表页面里不要再写 Java 代码JSP 是视图层V2 的核心理念是不允许% %出现。用 EL 表达式\${}取值用 JSTL 的c:forEach做循环页面清爽很多多人接手也容易看懂。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 title学生列表/title link relstylesheet href${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css /head body div classcontainer mt-4 h3学生列表/h3 table classtable table-bordered table-hover thead tr th学号/th th姓名/th th班级/th th操作/th /tr /thead tbody c:forEach varitem items${studentList} tr td${item.studentNo}/td td${item.studentName}/td td${item.className}/td td a href${pageContext.request.contextPath}/student?opeditid${item.studentId} classbtn btn-sm btn-outline-primary编辑/a a href${pageContext.request.contextPath}/student?opdeleteid${item.studentId} classbtn btn-sm btn-outline-danger onclickreturn confirm(确认删除该学生吗);删除/a /td /tr /c:forEach /tbody /table /div /body /htmlitems${studentList}是 Servlet 里setAttribute(studentList, students)对应的名字必须完全一致。item.studentNo调用的就是实体类的getStudentNo()方法。每个链接都拼了${pageContext.request.contextPath}这个前缀保证了不管项目部署在哪个上下文路径下资源路径都不会 404。删除按钮上的onclick是浏览器内置的二次确认虽然它不能防住恶意请求但能防住手滑。4.5 Bootstrap 让页面真正能看栅格和表格样式后端写得再顺页面如果还是白底黑字表格演示效果会大打折扣。Bootstrap 的引入建议用本地静态资源而不是 CDN 网页链接课设答辩现场没网也不会掉链子。src/main/webapp/static/bootstrap/ ├── css/bootstrap.min.css └── js/bootstrap.bundle.min.js页面里用 Bootstrap 5 的栅格做表单布局表单一列放不下的时候可以把它改成两列div classrow div classcol-md-6 mb-3 label classform-label学号/label input typetext namestudentNo classform-control required /div div classcol-md-6 mb-3 label classform-label姓名/label input typetext namestudentName classform-control required /div /divcol-md-6就是栅格参数在中等宽度以上的屏幕上每个输入框占一半宽度mb-3控制上下间距。Bootstrap 的按钮、表格、导航栏细节比较多想要什么组件直接翻它的官方文档复制基础类名改成自己的参数就行没有必要一个个记。5. 排查与避坑从“能跑”到“不怕拷问”的五个高频翻车点这一章是踩坑记录每条都是真实项目里反复出现的问题。如果你照着上面的代码写遇到任何一个现象直接对照原因改就行。5.1 现象MySQL 8 启动报错 “Public Key Retrieval is not allowed”原因MySQL 8 默认认证插件是caching_sha2_passwordJDBC 连接时如果useSSLfalse驱动需要先向服务器获取公钥来加密密码传输但连接串里没有显式允许于是直接拒绝。 解决在jdbc.url末尾追加allowPublicKeyRetrievaltrue最简单的写法是jdbc.urljdbc:mysql://localhost:3306/student_scores?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue也可以把 MySQL 用户改回mysql_native_password插件但改动用户会影响所有连接不推荐。5.2 现象页面中文全部变成“?”或者“杩涘害”之类的乱码原因字符集在四个环节里至少有一处没对齐。最常见的是 JSP 页面本身是 UTF-8但 POST 参数解析时用了默认 ISO-8859-1也可能是数据库表用了utf8而连接参数没有characterEncodingutf8。 解决按链路统一口径。页面头部加上contentTypetext/html;charsetUTF-8Servlet 的doPost第一行写req.setCharacterEncoding(UTF-8)连接串里带上characterEncodingutf8建表语句里用utf8mb4。这四个地方都对齐基本不会乱码。5.3 现象Bootstrap 样式加载不出来浏览器控制台报 404原因JSP 里直接写了href/static/bootstrap/css/bootstrap.min.css。如果项目部署后上下文路径是/student-score-system浏览器就会去http://localhost:8080/static/...找资源根路径不对自然 404。 解决所有静态资源路径都加上上下文前缀link relstylesheet href${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css后端的转发路径不受影响但重定向sendRedirect也必须写成resp.sendRedirect(req.getContextPath() /student)否则会跳出当前上下文。5.4 现象JSP 列表能打开但没有数据直接在 MySQL 客户端执行 SQL 却有结果原因DAO 里打开连接后没有走到executeQuery最典型的是 SQL 查询条件接收的参数是null比如班级筛选框留空时直接拼接了LIKE %%之外的错误条件另一种情况是结果集映射错了列名rs.getString(student_no)手误写成驼峰studentNoMySQL 区分大小写时直接报列不存在。 解决第一步把 SQL 打印出来贴到客户端执行确认语句正确第二步检查rs取列名是否用的是 SQL 里的别名而非 Java 字段名。更好的做法是永远给查询列起别名让取数的名字跟实体字段完全一致SELECT student_no AS studentNo, student_name AS studentName FROM student5.5 现象Tomcat 启动报NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config原因项目的 JSTL 依赖缺失或者你装的是 Tomcat 10。Tomcat 10 把javax.*换成了jakarta.*而 JSP 页面里声明的 taglib 前缀是http://java.sun.com/jsp/jstl/core包名不匹配就会报类找不到。 解决如果坚持用当前代码就用 Tomcat 9 及以下版本并把 pom 里 JSTL 依赖加上如果想升级到 Tomcat 10JSP 里的 taglib 前缀和依赖包要全部换成jakarta.servlet.jsp.jstl.*系列。V2 这个项目的定位是老技术栈所以我一般直接锁 Tomcat 9。6. 让系统更好拿出手成绩排名 SQL 和最后一遍验收流程核心功能完成后真正拉开差距的是收尾技巧。成绩排名是老师最容易问到的“这功能你做了吗”的项目点也是学生成绩系统里最能体现 SQL 能力的地方。MySQL 8 及以上可以直接用窗口函数SELECT s.student_no, s.student_name, SUM(sc.score) AS total_score, RANK() OVER (ORDER BY SUM(sc.score) DESC) AS rank_no FROM score sc JOIN student s ON s.student_id sc.student_id GROUP BY s.student_id, s.student_no, s.student_name ORDER BY rank_no;RANK()在总分相同时会产生并列名次比如两个人都是 280 分就会同时是第 1 名而下一个是第 3 名。如果希望并列之后接着第 2 名就改用DENSE_RANK()。这个细节答出来面试官就知道你是真的在业务场景里调过排名逻辑而不只是背了窗口函数的参数。最后把我自己的验收习惯分享出来。动手部署前先把数据库重新初始化一遍跑init.sql确认从零能起来然后把页面里每一个按钮都点过去重点测试“删除有成绩的学生”这个级联操作确认不会把数据库干崩再打开浏览器控制台看有没有静态资源 404最后把 war 包放到另一个干净 Tomcat 里启动一次排除本机 IDE 环境问题。这套流程走完系统才算真正“能交付”。这几年带我踩得最快的一个教训就是自己觉得“应该没问题”的地方往往就是问题所在。希望这些细节能在你做这个方向时候帮你少走几步弯路。本文还有配套的精品资源点击获取
返回列表