ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JavaWeb学生成绩管理系统:MySQL建表到Jsp+Servlet部署全解析

2026/9/14 2:35:01 拓冰建站 浏览量
JavaWeb学生成绩管理系统:MySQL建表到Jsp+Servlet部署全解析 简介一份基于JavaWeb的学生成绩管理系统源码采用JSP MySQL实现附带完整数据库文件是大三期末大作业的98分项目。项目面向计算机专业学生与需要项目实战练习的开发者既可作为课程设计、期末大作业的参考范本也适合用来系统梳理JavaWeb分层开发流程。资源共353个文件压缩包9.85MB核心包括jsp页面、java源文件与class编译文件、css/js前端资源、jar依赖包以及sql数据库脚本和db数据库文件另有png截图与docx文档辅助理解项目结构。代码采用DAO、Service、Servlet分层设计包含基础数据访问实现、业务逻辑服务、控制器及Excel工具类覆盖学生、教师、成绩、考试等核心模块可支撑成绩数据的导入导出。目前已有197人学习下载适合需要快速上手并完成高质量JavaWeb作业的学习者。1. 为什么“学生成绩管理系统”能做成 98 分的 JavaWeb 项目很多 JavaWeb 课设选题最后都落在学生管理系统上但大多数人交上去的版本是一个 Jsp 页面拼 Servlet查询用 JDBC 裸写成绩单能显示删除一条记录要刷新三次数据库文件只有一张表。这种作品拿 65 分合理。而标题里这个 98 分版本区别不在页面多华丽而是把数据完整性、异常兜底、联表查询和部署交付做完整了——这些恰恰是答辩时老师最关注的四个评分点。这篇文章会顺着该项目的常见实现路径把 MySQL 表怎么建、Jsp 与 Servlet 怎么串、数据库文件怎么导入、部署常见的坑在哪以及最后如何用连接池和聚合 SQL 把代码从“能跑”提升到“能讲”完整拆开。适合正在做课设的学生也适合要接手别人 JavaWeb 项目做改造的开发者。2. 用 MySQL 表结构先立住数据层成绩表的关联与索引拿到项目源码的第一件事不是启动 Tomcat而是看数据库文件。学生成绩管理系统的数据模型不复杂核心关系只有“学生—课程—成绩”三张表。表结构设计得好后面的 Jsp 页面和 Servlet 代码都会轻松很多。这一章直接给出一套可复用的建表方案再讲数据库文件导入 MySQL 的两种常见路径。2.1 建三张核心表学生、课程、成绩的字段取舍先建立三张表student、course、score。下面是可以直接执行的 SQL 结构注意字段命名尽量用英文避免在 Jsp 和 Servlet 之间传递中文 key。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 学生ID, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 成绩ID, student_id INT NOT NULL COMMENT 学生ID, course_id INT NOT NULL COMMENT 课程ID, score DECIMAL(5,2) NOT NULL COMMENT 成绩分值, UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student (id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;上面的 SQL 在三张表之间建立了明确的外键关系同时给score设置了联合唯一键uk_student_course。这个设计有两个用途防止同一学生同一课程出现两条成绩让“按学生查全科成绩”和“按课程查所有成绩”都能命中索引。DECIMAL(5,2)用于成绩字段避免 FLOAT 或 DOUBLE 在比较时产生精度误差。如果项目在导入时报“外键约束失败”多半是导入顺序问题。先导student和course最后导score。也可以在源码的数据库文件中直接建立这三张表的关联关系很多 98 分项目的库文件会自带部分测试数据保留这些数据对后续联调 Jsp 页面很有帮助。2.2 数据库文件导入 MySQL 的两种路径拿到手的“含数据库文件”通常是.sql后缀。导入 MySQL 有两种主流方式命令行和 MySQL Workbench / Navicat。命令行方式适合快速重现核心命令如下mysql -u root -p -h 127.0.0.1 --default-character-setutf8mb4 score_manager.sql执行前先创建目标数据库避免 SQL 文件里的USE语句指向不存在的库名mysql -u root -p mysql CREATE DATABASE IF NOT EXISTS score_manager DEFAULT CHARACTER SET utf8mb4; mysql USE score_manager; mysql SOURCE /path/to/score_manager.sql;参数说明--default-character-setutf8mb4让中文数据在导入时使用完整的 UTF-8 字符集防止乱码。如果 SQL 文件中包含DROP TABLE IF EXISTS语句则多次导入不会报错但会重建表测试数据会被清空。使用 MySQL Workbench 时菜单栏选择 Server —— Data Import然后选择 Import from Self-Contained File指向.sql文件目标选score_manager库即可。导入如果卡住优先考虑字段编码不一致错误信息里出现Incorrect string value时替换方案是把库表和.sql文件都统一成utf8mb4后重导。下面给出高频错误对照报错关键字原因处理方式ERROR 1049 (42000): Unknown database目标数据库不存在先执行CREATE DATABASE score_managerERROR 1062: Duplicate entry唯一键与已有数据冲突清空目标表或改用INSERT IGNORE导入方式ERROR 1452: Cannot add or update a child row外键关联的父记录不存在先检查student表是否有对应 idIncorrect string value字符集不匹配建表和导入命令都加utf8mb4ERROR 2006: Server has gone awaySQL 文件过大调整max_allowed_packet参数后重新导入第 5 个错在导入较大的.sql文件时经常出现。在 MySQL 命令行执行SET GLOBAL max_allowed_packet 64 * 1024 * 1024;可将单次接收包上限调整到 64MB。2.3 用索引解决“按班级查成绩”的慢查询学生成绩管理系统最常被问到的功能是“按班级查平均分”或“按课程查不及格人数”。这类统计如果在student、score表里全表扫描数据量一旦到万级就会明显变慢。典型 SQL 写法如下SELECT s.class_name, AVG(sc.score) AS avg_score FROM student s JOIN score sc ON s.id sc.student_id JOIN course c ON sc.course_id c.id WHERE c.course_name JavaWeb GROUP BY s.class_name ORDER BY avg_score DESC;分析这条 SQL 的关联顺序先从course表定位课程 id然后通过score表的idx_course_id索引找到该课程所有成绩再通过student_id回表取student的班级字段。如果没有索引MySQL 会先做笛卡尔积后再过滤代价极大。Jsp 项目中很多“完全能跑但很卡”的问题不是代码问题而是联合查询的索引缺失。遇到这种场景给score表再补一个组合索引通常能解决ALTER TABLE score ADD INDEX idx_course_score (course_id, score);idx_course_score能让“某课程下按成绩排序或做聚合”直接走覆盖索引减少回表次数。建完之后用EXPLAIN验证key列出现idx_course_score就说明执行计划已经命中。3. 用 Jsp Servlet 实现成绩 CRUD 的完整链路数据层就绪后进入 JavaWeb 项目最核心的请求链路。一个成绩模块通常包含四个操作查询列表、录入、修改、删除。98 分项目和 70 分项目的最大区别在于过滤参数是否传递完整、事务是否覆盖写入和错误是否被统一捕获。3.1 Servlet 接收参数后如何交给 Service 层在 Jsp 页面表单提交后数据首先到达 Servlet 的doPost方法。常见做法是先在service层做一次参数校验再调用 DAO 层执行 SQL。下面给出一个降低耦合的代码结构WebServlet(/score/add) public class ScoreAddServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String studentId request.getParameter(studentId); String courseId request.getParameter(courseId); String score request.getParameter(score); // 参数校验避免空值直接进入数据库层 if (isInvalid(studentId) || isInvalid(courseId) || isInvalid(score)) { request.setAttribute(msg, 学生、课程和成绩不能为空); request.getRequestDispatcher(/score/add.jsp).forward(request, response); return; } // 异常统一处理防止 Servlet 堆栈直接暴露给页面 try { boolean success gradeService.addScore( Integer.parseInt(studentId), Integer.parseInt(courseId), new BigDecimal(score)); if (success) { response.sendRedirect(request.getContextPath() /score/list); } else { request.setAttribute(msg, 成绩添加失败可能该课程已录入); request.getRequestDispatcher(/score/add.jsp).forward(request, response); } } catch (NumberFormatException e) { request.setAttribute(msg, 成绩必须为数字); request.getRequestDispatcher(/score/add.jsp).forward(request, response); } } private boolean isInvalid(String value) { return value null || value.trim().isEmpty(); } }代码里有两个容易被忽略的参数request.setCharacterEncoding(UTF-8)必须在读取第一个参数之前调用否则表单提交的中文内容到 Servlet 内会乱码response.sendRedirect使用的是重定向表单重复刷新时不会重复提交这也是比forward更适合写操作的原因。3.2 用 PreparedStatement 写成绩插入参数顺序别写反Service 层校验通过后最终落到 DAO。下面是一段学生成绩管理系统里典型的插入方法使用PreparedStatement预编译 SQL避免字符串拼接带来的注入风险public boolean addScore(Score score) throws SQLException { String sql INSERT INTO score (student_id, course_id, score) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, score.getStudentId()); ps.setInt(2, score.getCourseId()); ps.setBigDecimal(3, score.getScore()); return ps.executeUpdate() 0; } }参数说明setInt(1, ...)对应 SQL 里第一个问号即student_idsetInt(2, ...)对应course_idsetBigDecimal(3, ...)对应score。这个顺序看起来简单但项目里最常见的运行时错误是把 course_id 和 score 写反——数据库端不会报错因为都是数值类型但成绩会被存成课程 id课程 id 被存成成绩检查时问题很大。所以写完 DAO 后建议先在数据库里手动执行一遍同一句 SQL确认字段顺序正确。用 try-with-resources 的好处是Connection、PreparedStatement都会自动关闭防止连接泄漏。如果项目里没有DBUtil类自己封装时至少要提供以下三个方法getConnection()、close(Connection, Statement, ResultSet)、以及getConnection里的驱动加载逻辑。很多旧项目把驱动加载放在静态代码块里这样类加载时就会触发Class.forName(com.mysql.cj.jdbc.Driver)不至于在每一次连接时重复加载。3.3 在 Jsp 页面用 EL JSTL 渲染成绩表格数据查询完成后要在 Jsp 页面展示成绩列表。以下是常见做法通过c:forEach循环渲染成绩列表并同步计算总分与平均分% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table border1 cellpadding6 tr th学号/th th姓名/th th课程/th th成绩/th /tr c:forEach items${scoreList} varsc tr td${sc.studentNo}/td td${sc.studentName}/td td${sc.courseName}/td tdfmt:formatNumber value${sc.score} pattern0.00//td /tr /c:forEach /tableEL 表达式${sc.studentNo}会调用Score对象的getStudentNo()方法。很多新手在 Jsp 页面上直接写sc.getStudentNo()这是错误的JSP 中不能直接调用get方法名。另外${scoreList}这个名字必须在 Servlet 中通过request.setAttribute(scoreList, list)设置过Jsp 才能在请求域中读到。fmt:formatNumber用pattern0.00统一保留两位小数。如果成绩是整数数据库返回后也会按两位小数展示符合成绩单的展示习惯。一个实用的小技巧在成绩后面跟上“及格 / 不及格”标记可以用 JSTL 的判断标签实现c:choose c:when test${sc.score 60} span stylecolor: green;及格/span /c:when c:otherwise span stylecolor: red;不及格/span /c:otherwise /c:choose这样在 Jsp 页内就完成了展示层逻辑。要避免在 Jsp 里写大量% %脚本片段尤其是嵌套循环和复杂判断那会让页面的可维护性大幅下降也会问答辩老师一句“为什么不遵循 MVC”。4. 部署与运行项目时最常见的 6 个环境问题拿到源码数据库导入成功开始配置 Tomcat 时会遇到几个很有共性的环境问题。这些问题每年都在不同的 JavaWeb 项目里重复出现。下面按排查顺序展开。4.1 Tomcat 部署后 ClassNotFound 的头号原因ClassNotFoundException 最多的情况出现在 mysql-connector 依赖没有被打包进 WEB-INF/lib。在 IDEA 里创建 JavaWeb 项目时需要把依赖 Jar 放到指定目录否则 Tomcat 运行时不会自动加载它们。常见做法是将 mysql-connector-java-x.x.x.jar 拷贝到WEB-INF/lib/目录下。如果项目使用 Maven在 pom.xml 中加入依赖后还需要确认 Artifact 是否把该库包含在部署包中。排查顺序如下先打开 IDEA 的 Project Structure —— Artifacts查看 Output Layout 里WEB-INF/lib下是否有该 Jar再检查 Tomcat 的部署目录中是否存在该文件最后看启动日志中报错的具体类名。如果日志显示java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver说明驱动类没有被类加载器找到优先检查 Jar 是否在WEB-INF/lib而非只存在于外部库目录。4.2 页面乱码与 SQL 乱码的排查顺序乱码问题能占到 JavaWeb 项目运行期问题的一半。排查顺序不能乱第一处是数据库连接 URL 的字符集参数第二处是 Jsp 页面顶部声明第三处才是数据库表的默认字符集。下面给出一个标准的连接串示例jdbc:mysql://127.0.0.1:3306/score_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai参数的说明useUnicodetrue通知驱动使用 Unicode 进行编码转换characterEncodingutf8指定客户端发送 SQL 和数据时所用的字符集serverTimezoneAsia/Shanghai解决 MySQL 8.0 中时区导致的时间类型偏差报错。如果连接串缺少characterEncodingutf8即使数据库表是utf8mb4Servlet 传入的数据到 MySQL 仍可能乱码。Jsp 页面最前面补上下面这行并且保证它位于所有 HTML 输出之前% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %如果数据库连接、页面编码都检查过了还乱码可以让项目在 Servlet 入口统一设置请求编码request.setCharacterEncoding(UTF-8)。注意该方法必须写在读取任何请求参数之前在 Filter 中处理更可靠。常见做法是写一个EncodingFilter把这一行放在chain.doFilter之前。4.3 MySQL 8.0 驱动名与 serverTimezone 参数的坑现在新安装的 MySQL 基本都是 8.0 以上但很多旧项目的源码里还写着com.mysql.jdbc.Driver。这个驱动类在 MySQL Connector/J 8.0 中被移除启动时会抛ClassNotFoundException。需要把驱动类改为Class.forName(com.mysql.cj.jdbc.Driver);同时检查连接串是否带serverTimezone。MySQL 8.0 的驱动在未指定时区时会抛出The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。报错信息里的乱码就是 JDBC 驱动在解析系统时区时出现的。将serverTimezoneAsia/Shanghai加入连接串即可解决。如果项目使用 Maven需要注意依赖版本号。MySQL 8.0 对应com.mysql:mysql-connector-j:8.x.x旧版mysql:mysql-connector-java:5.1.x在驱动名称和时区参数上和 8.x 版行为并不一致。下方表格列出两个版本的关键差异对比项5.1.x 驱动8.0.x 驱动驱动类名com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver连接串驱动标识jdbc:mysql://jdbc:mysql://serverTimezone可选不配置不报错必填否则连接时报错默认时区行为使用 JVM 时区使用数据库会话时区中文编码支持需要 characterEncodingutf8同样需要 characterEncodingutf8依赖坐标mysql:mysql-connector-javacom.mysql:mysql-connector-j如果项目用的是 5.1.x 且 MySQL 服务端是 8.0推荐把驱动升级到 8.0.x而不是反过来修改数据库端规则。驱动升级后旧代码里的Class.forName只改一行就行连接串补上时区参数99% 的情况都能跑通。还有 1% 的情况会报Public Key Retrieval is not allowed需要通过连接串加allowPublicKeyRetrievaltrue解决该参数与useSSLfalse搭配使用适合本地开发环境。5. 用连接池和聚合 SQL 把项目从“单机演示”升级到“可演示架构”最后一个阶段可以从两个维度提升项目质感把裸 JDBC 改成 Druid 连接池并把统计页面常见的多条查询合并成一条聚合 SQL。这两处改动短但答辩时值得多讲两分钟。5.1 用 Druid 连接池替换裸 JDBC 的五步改动传统DriverManager.getConnection在每次请求时创建一个物理连接资源开销明显。Druid 或 HikariCP 连接池会预先创建连接并在用完后回收。这一步不需要重写 DAO只需要改DBUtil类。创建druid.properties配置文件driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/score_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password123456 initialSize5 maxActive20 maxWait3000然后在DBUtil中加载配置public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }改动后的getConnection从连接池获取连接DAO 层代码完全不用变。initialSize5表示项目启动后预先建立 5 个连接maxActive20限制最大连接数防止并发过高拖垮数据库maxWait3000让获取连接的等待时间不超过 3 秒避免请求无限挂起。三个参数在线下项目里足够用改起来也没有副作用。5.2 一条聚合 SQL 算出及格率、优秀率与平均分原来的统计页面通常有三句 SQL分别查平均分、查最低分、查及格人数。实际上可以合并成一个查询部分 98 分项目的核心就在这里。下面这条 SQL 可在一次访问内完成全班统计SELECT c.course_name, COUNT(*) AS total_count, SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) AS pass_count, ROUND(AVG(sc.score), 2) AS avg_score, ROUND(MAX(sc.score), 2) AS max_score, ROUND(MIN(sc.score), 2) AS min_score, ROUND(SUM(CASE WHEN sc.score 90 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS excellent_rate FROM score sc JOIN course c ON sc.course_id c.id GROUP BY c.course_name ORDER BY avg_score DESC;SUM(CASE WHEN ...)相当于条件计数ROUND(..., 2)控制小数位excellent_rate字段直接用“90 分以上人数 / 总人数 * 100”算出优秀率。在 Jsp 页面里用c:forEach展示结果时的写法和第 3 章一致不需要特殊处理。把这段 SQL 放在 DAO 层返回ListMapString, Object即可。若需要给统计页加“按班级筛选”的功能可以通过以下方式动态拼接WHERE s.class_name ?占位符优先使用PreparedStatement传入不要直接拼字符串。统计结果展示在独立 Jsp 页面时建议配合一个简单的c:if判断容错例如total_count 0时才显示表格否则输出提示。这一层防护在数据为空时能明显降低页面的报错概率。5.3 给成绩列表加一个排序基线让每个页面可复现很多实现顺序是从 Jsp 到 Servlet 再到 DAO但项目验收的重点在数据一致性和可复现性。这一节做一个最后的补强定义成绩列表的统一排序基线。如果不显式指定ORDER BYMySQL 在LIMIT分页场景下可能返回不同顺序。在一个成绩管理系统里稳定输出顺序对核对成绩非常有帮助。推荐在成绩列表查询末尾固定追加以下排序条件ORDER BY sc.course_id ASC, sc.student_id ASC这样同一课程下的学生排名始终固定多次刷新不会出现顺序跳动。企业级的列表页通常还会在ORDER BY后追加主键以保证极端场景下的稳定输出。这组改动加上后项目在“数据层一致性”这一项上明显高出普通课设一档答辩时也可以用具体数据说明例如在 2000 条成绩数据下聚合 SQL 一次执行的耗时比三次往返少一半以上这个结论可以在本地用日志简单验证。到了这一步整个项目从建库、CRUD、部署到统计已经形成完整闭环技术点也都可以在答辩现场直接演示。本文还有配套的精品资源点击获取