ARTICLE DETAIL

资讯详情

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

Servlet+MySQL+JSP 学生成绩管理系统:数据库建模到部署实战

Servlet+MySQL+JSP 学生成绩管理系统:数据库建模到部署实战 简介基于ServletJspMySQL搭建的学生成绩管理系统是一套面向计算机相关专业毕业设计及Java实战练习的完整源码项目。系统区分为管理员、教师、学生三种角色覆盖学生、教师、课程信息管理教师开设课程与提交成绩学生选课与查询成绩等核心功能同时提供系统用户表、学生表、教师表、课程表及关联表的SQL数据库脚本经过调试可直接运行适合作为毕业设计或课程实践基础。资源包约1.53MB共166个文件主要包含69个JSP页面、25个Java源文件、25个class文件以及SQL数据库脚本、XML配置、图片样式等辅助资源代码中已实现完整的DAO层处理类层次清晰便于快速理解数据访问逻辑与二次开发。运行环境推荐Tomcat7以上、MySQL5以上可使用Eclipse或IDEA开发调试目前已有538人学习下载适合需要完整系统参考或快速搭建成绩管理项目的开发者。1. 基于 ServletMySQLJSP 的学生成绩管理系统老组合为什么还值得做做学生成绩管理系统第一反应往往是用 Spring Boot Vue但在实际课程设计、校内实训甚至小范围的教务辅助场景里基于 ServletJSPMySQL 的源码项目仍然是出现频率极高的一套。它没有微服务没有前后端分离却把 Java Web 最核心的能力全部过了一遍HTTP 请求与响应、Servlet 生命周期、JDBC 操作、PreparedStatement 防注入、MySQL 事务与索引、JSP 标签库渲染。标题里的“源码数据库”意味着解压后你拿到的是一整套可运行工程导入 SQL、启动 Tomcat、改好数据库连接就能看到登录页。这篇笔记按我接手这类项目的习惯来写从表结构讲到部署和排错目标是让你拿到源码后能快速跑起来也知道每一层为什么要这么设计。2. 先把数据层定死学生成绩管理系统的表结构设计与初始化 SQL2.1 为什么成绩管理非 MySQL 不可并发、事务与索引成绩管理系统本质上是一个写多读也多的 OLTP 场景老师要录成绩、改成绩学生要查成绩管理员要统计及格率。若用普通文件或内存 Map 存数据单机演示可以多人同时操作时会出现脏写或丢失更新。MySQL 在这类项目里成为默认选择不是因为最新潮而是因为它把持久化、事务、索引和权限控制都做在了数据库内部。具体到成绩登记一条学生成绩的更新最少要保证它要么提交、要么回滚这就要靠 InnoDB 引擎提供的事务能力。MySQL 8.0 以后 InnoDB 是默认引擎行级锁在成绩表按学生课程更新时冲突很小。就算只是课设项目选 MySQL 也避免了你把数据写在程序内存里、一重启就清空的尴尬。另一个原因是整个 Java 生态对 MySQL 支持太成熟JDBC 驱动、Druid/HikariCP 连接池、Navicat 客户端任何环节出问题都能找到大量相似案例。很多人在“mysql安装教程”上花了大量时间这部分我在第四章单独展开。表结构设计阶段你只需要明确一点用 utf8mb4 而不是 utf8mb3否则中文和特殊字符比如姓名里的生僻字会显示不全。2.2 表结构设计学生、课程、成绩与用户四类核心表一个标准的学生成绩管理系统核心表不是越多越好而是围绕“谁登录、谁在哪个班、学什么课、考了多少分”来设计。我一般会拆成五张表t_sys_user 用户表保存系统登录账号角色分为管理员和教师密码不能存明文。t_class 班级表一个班级包含多个学生班级信息独立维护避免学生表里直接写一个字符串 class_name 导致数据冗余。t_student 学生表学号唯一外键关联班级 ID。t_course 课程表课程名称、学分、任课教师可选关联用户表。t_score 成绩表学生与课程的多对多关系落在这里一个学生一门课只能有一条成绩记录。成绩表是整张数据模型的核心。字段设计为 id 主键、student_id、course_id、score、exam_date、remark其中 student_id course_id 要加唯一索引。成绩字段用 DECIMAL(5,2)范围能覆盖 0.00-999.99正常考分够用避免浮点误差。考试日期用 DATE 类型备注用 VARCHAR(255)。如果后续要区分补考、重修可以再加一个 exam_type 字段但第一版不要过度设计。2.3 建库建表 SQL字符集、引擎、索引与初始化数据下面是配源码包时的建库脚本可以直接保存为 student_grade.sql 执行。我刻意把外键只放在成绩表上其余表之间用逻辑外键。物理外键在这类项目里看着严谨实际会给数据导入顺序、删除课程带来麻烦逻辑外键配合业务层校验更灵活。-- 创建数据库明确使用 utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS student_grade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_grade; -- 1. 用户表 CREATE TABLE t_sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(128) NOT NULL COMMENT 密码存MD5/SHA-256摘要, real_name VARCHAR(50) COMMENT 真实姓名, role ENUM(ADMIN,TEACHER) NOT NULL DEFAULT TEACHER, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; -- 2. 班级表 CREATE TABLE t_class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL, grade_year SMALLINT COMMENT 入学年份如2024 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT班级表; -- 3. 学生表 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(30) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender TINYINT COMMENT 1男 0女, birthday DATE, class_id INT, phone VARCHAR(20), INDEX idx_student_class (class_id), INDEX idx_student_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; -- 4. 课程表 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1), teacher_id INT, INDEX idx_course_name (course_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 5. 成绩表 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_date DATE, remark VARCHAR(255), UNIQUE KEY uk_stu_course (student_id, course_id), INDEX idx_score_course (course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student(id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;照这个脚本执行后所有表的默认引擎都是 InnoDB。为什么单独强调这一点因为 MyISAM 不支持事务成绩表一旦更新到一半数据库崩溃就会留下不一致数据。成绩表上保留两个外键因为成绩表作为子表学生或课程删除时级联删成绩是合理的不会出现“学生已删但成绩还在”的情况。UNIQUE KEY uk_stu_course 是成绩表最重要的唯一约束它从数据库层面堵住了重复录成绩。索引方面学生表按姓名索引成绩表按课程索引都是为了页面按课程筛选、按学生搜索时不会走全表扫描。再补一份初始化数据方便第一次部署就能登录看效果INSERT INTO t_sys_user (username, password, real_name, role) VALUES (admin, MD5(123456), 系统管理员, ADMIN), (teacher01, MD5(123456), 王老师, TEACHER); INSERT INTO t_class (class_name, grade_year) VALUES (2024级计算机1班, 2024), (2024级软件2班, 2024); INSERT INTO t_student (student_no, name, gender, birthday, class_id) VALUES (2024001, 张小明, 1, 2006-03-12, 1), (2024002, 李红, 0, 2006-07-21, 1), (2024003, 王强, 1, 2005-11-02, 2); INSERT INTO t_course (course_name, credit) VALUES (Java程序设计, 4.0), (数据库原理, 3.5), (数据结构, 4.0); INSERT INTO t_score (student_id, course_id, score, exam_date) VALUES (1, 1, 88.50, 2025-01-10), (1, 2, 76.00, 2025-01-11), (2, 1, 91.00, 2025-01-10), (3, 3, 68.00, 2025-01-12);密码字段用 MySQL 的 MD5 函数客户端传入 123456库里存的是 32 位摘要。课程设计这个强度够了但如果要真正上线建议换 SHA-256 并加盐我放到最后一章说。测试数据里故意没有把所有学生和课程都关联上这样你在 JSP 页面能看到成绩为空时的展示分支。学分字段 DECIMAL(3,1) 指最多 3 位有效数字、保留 1 位小数比如 4.0、3.5不会出现浮点误差。2.4 成绩汇总视图与存储过程统计逻辑放数据库更省心业务层查询成绩时要关联很多表如果每次都写三表 JOINDAO 层代码很啰嗦。常见做法是在数据库建一个视图 v_score_detail把学生姓名、学号、班级、课程名、成绩、考试日期拼好Java 只查视图。视图不提升查询性能但它让 SQL 变短减少 JSP 里拼接出错的可能。也可以再加一个视图统计每门课的平均分、最高分、最低分。CREATE VIEW v_score_detail AS SELECT sc.id AS score_id, st.student_no, st.name AS student_name, cls.class_name, co.course_name, sc.score, sc.exam_date FROM t_score sc LEFT JOIN t_student st ON sc.student_id st.id LEFT JOIN t_class cls ON st.class_id cls.id LEFT JOIN t_course co ON sc.course_id co.id; -- 每门课统计用于教师端报表 CREATE VIEW v_course_stats AS SELECT co.id AS course_id, co.course_name, COUNT(sc.id) AS exam_count, AVG(sc.score) AS avg_score, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score FROM t_course co LEFT JOIN t_score sc ON sc.course_id co.id GROUP BY co.id, co.course_name;视图之外有人会建议把成绩统计写成 MySQL 存储过程。我的看法是只读统计可以做但不建议把业务更新逻辑写进存储过程。存储过程有个明显缺点——版本管理困难数据库脚本和 Java 代码分两处维护换一个环境容易漏执行。二来调试不如 Java 代码方便。成绩管理系统里真正需要保证原子性的“批量录成绩”“修改成绩并重新算总分”这些操作放在 Java 的 Service 层用事务控制更直观。存储过程适合做一次性报表比如按学期统计各班平均分排名。3. 用 ServletJSP 把成绩 CRUD 串起来DAO、BaseServlet 与 JSTL 页面3.1 项目目录与分层约定别把 JDBC 写进 JSP拿到源码后第一件事是看目录结构。规范的学生成绩管理系统应该按三层模型组织WebContent ├── jsp │ ├── login.jsp │ ├── student_list.jsp │ ├── score_list.jsp │ └── ... ├── WEB-INF │ ├── web.xml │ └── lib └── index.jsp src ├── com/example/entity # 实体类Student, Course, Score, User ├── com/example/dao # 数据访问对象StudentDao, ScoreDao ├── com/example/service # 业务逻辑ScoreService ├── com/example/servlet # 控制器LoginServlet, ScoreServlet └── com/example/util # 连接池工具DBUtil为什么坚持分层因为 JSP 本身是视图如果在里面写 Class.forName、Connection、Statement页面会特别乱而且每次改数据库字段都要翻 JSP。Servlet 作为控制器只做三件事接收参数、调用 Service、转发或重定向。Service 层判断业务规则比如成绩不能小于 0、超过 100再调用 DAO。DAO 只负责 SQL 和结果集映射。这样的项目哪怕只有几个页面维护起来也不会想骂人。3.2 数据库连接池Druid 连接池与 db.properties 参数直接用 DriverManager.getConnection 每访问一次数据库就创建一次连接在本地课设能跑但并发上来后 TCP 握手开销大而且连接数一多 MySQL 会报Too many connections。我一般会在 util 包下放一个 Druid 连接池。先建一个 db.propertiesjdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_grade?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue jdbc.usernameroot jdbc.password123456 # 连接池配置 initialSize5 maxActive20 maxWait5000 minIdle5然后再写 DBUtilpackage com.example.util; import com.alibaba.druid.pool.DruidDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); dataSource new DruidDataSource(); dataSource.setDriverClassName(props.getProperty(jdbc.driverClassName)); dataSource.setUrl(props.getProperty(jdbc.url)); dataSource.setUsername(props.getProperty(jdbc.username)); dataSource.setPassword(props.getProperty(jdbc.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))); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这里有几个参数必须解释清楚。driverClassName在 MySQL 8.0 下要用 com.mysql.cj.jdbc.Driver如果你用的是 5.1 老驱动需要改回 com.mysql.jdbc.Driver否则报 ClassNotFoundException。URL 里的useSSLfalse是避免本地连 MySQL 8.0 时的 SSL 握手警告serverTimezoneAsia/Shanghai解决驱动和数据库时区不一致导致的报错characterEncodingutf8是中文乱码的第一道防线。rewriteBatchedStatementstrue是给批量插入优化用的批量录成绩时能明显提升速度。Druid 的 maxWait 表示获取连接的最大等待毫秒数超过就抛异常防止线程池被占满后无限等待。3.3 成绩查询 DAOPreparedStatement 与 VO 映射先给成绩实体 Score 设计字段再写 ScoreDao。以“按关键字查询成绩列表”为例public ListScoreVO findScoreList(String keyword) throws SQLException { ListScoreVO list new ArrayList(); String sql SELECT v.score_id, v.student_no, v.student_name, v.class_name, v.course_name, v.score, v.exam_date FROM v_score_detail v WHERE v.student_name LIKE ? OR v.course_name LIKE ? ORDER BY v.exam_date DESC, v.score DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { ScoreVO vo new ScoreVO(); vo.setStudentNo(rs.getString(student_no)); vo.setStudentName(rs.getString(student_name)); vo.setClassName(rs.getString(class_name)); vo.setCourseName(rs.getString(course_name)); vo.setScore(rs.getBigDecimal(score)); vo.setExamDate(rs.getDate(exam_date)); list.add(vo); } } } return list; }这里使用 PreparedStatement不要直接用 Statement 拼字符串。哪怕这是一个课设项目也会有人拿 OR 11来试你的登录框参数化查询是最基本的防线。查询用到视图 v_score_detail好处是 Java 端不用写三行 JOIN。排序写在 SQL 中成绩倒序、日期倒序页面拿到数据直接渲染不要在 JSP 里再排序。try-with-resources 写法保证 Connection、PreparedStatement、ResultSet 都能关闭不会把连接池的连接耗尽。注意 DAO 返回的是 List 而不是把 ResultSet 直接扔给 Servlet。3.4 BaseServlet 与 action 分发减少 Servlet 数量如果不做任何封装成绩模块至少需要 AddScoreServlet、EditScoreServlet、DeleteScoreServlet、ListScoreServlet 四个类web.xml 也要配四遍。我一般会写一个 BaseServlet把“根据 action 参数路由到方法”的公共逻辑抽出来。这样 ScoreServlet 只负责成绩相关的所有请求。public abstract class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.isEmpty()) { action list; } switch (action) { case add: doAdd(req, resp); break; case update: doUpdate(req, resp); break; case delete: doDelete(req, resp); break; case list: default: doList(req, resp); break; } } protected abstract void doAdd(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; protected abstract void doUpdate(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; protected abstract void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; protected abstract void doList(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; }使用 action 参数是 Servlet 时代非常常见的路由方式。好处是新增一个操作不用改 web.xml只需要在 servlet 里加一个 case。比如导出成绩单可以加case export。action 值由 JSP 页面里的 URL 参数决定例如scoreServlet?actionlistpageNo1。所有子类必须实现四个方法这样 IDE 会强制提醒你每个操作都考虑到位。需要注意 switch 里的 default 分支不能省用户手动改 URL 乱传 action 时默认走列表总比报 404 友好。3.5 成绩列表 JSPJSTLEL 渲染与排序展示JSP 页面要遵循一个原则能用 EL 和 JSTL 的就不要写% %。下面是一个 score_list.jsp 的核心片段% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle成绩列表/title/head body table border1 tr th学号/thth姓名/thth班级/thth课程/th th成绩/thth考试日期/thth操作/th /tr c:forEach items${scoreList} vars tr td${s.studentNo}/td td${s.studentName}/td td${s.className}/td td${s.courseName}/td td c:choose c:when test${s.score 85}span stylecolor:green${s.score}/span/c:when c:when test${s.score 60}span stylecolor:orange${s.score}/span/c:when c:otherwisespan stylecolor:red${s.score}/span/c:otherwise /c:choose /td td${s.examDate}/td tda hrefscoreServlet?actiontoEditscoreId${s.scoreId}修改/a/td /tr /c:forEach /table /body /html页面里一定要记得在顶部写 contentType 和 pageEncoding否则中文必乱码。${s.score}这种写法依赖 ScoreVO 的 getter 方法字段名不对或缺少 getter这里直接空白。用c:choose做分数段颜色比在 JSP 里嵌套 Java 三目运算清楚得多。很多人在 JSP 上纠结“图片如何对坐标定位”这类前端细节其实成绩管理系统不靠特效信息清晰最重要。把修改链接拼成scoreServlet?actiontoEditscoreId...点击后由 Servlet 调doEdit方法转发到编辑页不要在列表页里直接搞内联编辑。3.6 新增成绩与事务控制ON DUPLICATE KEY 的取舍新增成绩是针对 t_score 的 INSERT。由于表上有 uk_stu_course 唯一索引同一个学生同一门课再录一次会直接报Duplicate entry。与其让用户看到异常页面不如在 SQL 里用 ON DUPLICATE KEY UPDATE一次调用完成“存在就更新不存在就插入”。ScoreDao 的 addOrUpdateScore 方法可以这样写public int addOrUpdateScore(Score score) throws SQLException { String sql INSERT INTO t_score (student_id, course_id, score, exam_date, remark) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE score VALUES(score), exam_date VALUES(exam_date), remark VALUES(remark); 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()); ps.setDate(4, new java.sql.Date(score.getExamDate().getTime())); ps.setString(5, score.getRemark()); return ps.executeUpdate(); } }这个方案对单个学生录入非常合适一次数据库往返就完成 upsert。但注意如果批量模板里既有新增又有更新且希望“全部成功才提交”不能让每一条语句自动提交。常见做法是在 Service 层拿同一个 Connection先setAutoCommit(false)执行完所有 addOrUpdateScore 后再 commit任何一条失败就 rollback。因为成绩操作会涉及后续的平均分统计半途成功会造成数据不一致所以事务边界要清楚。MySQL 的 InnoDB 在 UPDATE 同一行时会加行锁多个老师同时给同一学生录成绩时这里不会互相覆盖。如果只是课程设计把事务写进 DAO 层也可以但不要一条 SQL 一个事务。4. 让源码跑起来MySQL 环境配置、数据库导入与 Tomcat 部署4.1 MySQL 安装与初始化zip 解压版和 my.ini 关键配置拿到源码后最常遇到的第一个拦路虎就是 MySQL。如果系统里已经有 MySQL可以跳过这节直接看 4.2。如果还是白纸环境我建议用 MySQL 8.0 的 zip 解压版而不是 exe 安装版一是干净二来卸载重装容易。解压后最关键的是 my.ini。Windows 下在 MySQL 根目录新建 my.ini至少包含这些配置[mysqld] basedirD:/software/mysql-8.0 datadirD:/software/mysql-8.0/data port3306 character-set-serverutf8mb4 collation-serverutf8mb4_general_ci default-storage-engineINNODB max_connections100 [client] default-character-setutf8mb4这里 basedir 和 datadir 必须和你的实际路径一致。character-set-server 用 utf8mb4否则后续建表即使写了 DEFAULT CHARSET连接层的默认字符集也可能不一致最终中文乱码。max_connections 对课设 100 足够。配置好后用管理员权限打开命令行执行mysqld --initialize-insecure net start mysql--initialize-insecure会创建一个 root 用户且密码为空适合本地开发之后自己再用ALTER USER设置密码。如果之前没初始化过 data 目录直接 net start mysql 会报“服务无法启动”这个报错我在第五章细讲。初始化完成后用mysql -u root -p登录执行第二章的建库脚本即可。如果你安装的是 Linux 服务器CentOS 下用 rpm 安装 mysql 后要记得启动 mysqld 服务和开机自启。本地的 Docker 安装方式核心配置不外乎端口、数据目录、字符集和 my.ini 思路一致。4.2 导入学生成绩数据库命令行 source 与 Navicat 方式源码包里通常会附带一个 .sql 文件可能叫 student_grade.sql也可能叫 db.sql。导入前先确认文件前面是否有 CREATE DATABASE如果没有你需要手动建库。命令行方式最通用mysql -u root -p student_grade.sql或者先登录 MySQL 再 sourcemysql -u root -p Enter password: mysql source D:/project/student_grade.sql;使用重定向时SQL 文件里的 CREATE DATABASE 和 USE 会被正确执行。用 source 方式时路径里的斜杠不要用反斜杠否则会转义错误。导入后执行SHOW TABLES; SELECT * FROM t_sys_user;看到五张表和初始化数据说明导入成功。如果用的是 Navicat右键数据库运行 SQL 文件也可以但要注意 Navicat 连接 MySQL 8 时可能需要关闭 SSL这个放到 4.3 一并说。4.3 修改 JDBC 配置driver、URL 与时区三个必改项源码自带的 db.properties 里的密码通常是作者本地的你拿到后第一步就是改它。三个必改项是 driver 类名、URL 参数、账号密码。jdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_grade?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你自己改的密码如果 MySQL 是 5.7 版本驱动 jar 还在用 mysql-connector-java-5.1.xdriverClassName 可以保留为 com.mysql.jdbc.Driver。MySQL 8.0 的官方驱动包把类名改成了 com.mysql.cj.jdbc.Driver同时要求 URL 里带上 serverTimezone否则会报The server time zone value й׼ʱ is unrecognized。useSSLfalse 是关闭 SSL 握手避免开发环境因为证书问题报错。如果连接时出现Public Key Retrieval is not allowed在 URL 后追加allowPublicKeyRetrievaltrue即可这是 MySQL 8 认证插件导致的常见问题。改完先写一个简单的DBUtil.getConnection()测试类跑一下确认能拿到连接再启动 Tomcat别等到页面报错才回头查。4.4 部署到 Tomcatwar 包发布与端口冲突处理Tomcat 运行时路径常见的有两种在 IDEA 内配置 Tomcat Server 启动或把项目打成 war 包扔进 webapps。课程设计阶段用 IDE 内置 Tomcat 最方便点 Debug 就能断点。但如果你想给别人演示war 包更可靠。在项目根目录用 Maven 执行mvn clean package如果项目不是 Maven 结构也可以直接把 WebContent 目录复制到 Tomcat 的 webapps 下改名为 student_grade。确保 WEB-INF/lib 下有 mysql-connector-java.jar 和 jstl.jar驱动 jar 不在 lib 里运行时会报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。启动 Tomcatcatalina.bat run看到Server startup in xxx ms说明启动成功然后访问http://localhost:8080/student_grade/如果页面始终打不开检查浏览器访问的端口和 Tomcat 一致。默认是 8080如果被占用或你改过端口去 conf/server.xml 里找 Connector 节点Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /把 8080 改成 8081重启即可。端口被占用时千万别用杀进程这种粗暴方式先netstat -aon | findstr :8080看是谁占用的再决定改端口还是停掉旧进程。4.5 启动后验证五步登录、录成绩、查列表、改数据、看日志部署完成不代表功能全通我会按下面的顺序过一遍访问首页看是否跳转到 login.jsp页面样式是否正常。用 admin/123456 登录注意密码在数据库里是 MD5 摘要登录逻辑是把输入值做 MD5 后比对。进入成绩列表页确认 v_score_detail 能查出测试数据并且中文不乱码。新增一条成绩故意用同一个学生课程再加一次验证 ON DUPLICATE KEY 更新逻辑。改掉成绩后刷新页面确认是重定向刷新而不是表单重复提交。这个验证过程 5 分钟就能完成但能覆盖数据库连接、字符集、事务、Servlet 路由四大高频故障点。最后别忘了看 Tomcat 的localhost.log很多异常页面没显示出来的信息都在里面。5. 常见问题与避坑排查手册从 MySQL 连接到 JSP 乱码5.1 MySQL 连接被拒SSL 与认证插件导致的连接失败现象首次跑项目点登录后浏览器白屏Tomcat 日志里出现Communications link failure后面跟着SSL connection error或Public Key Retrieval is not allowed。这是我在“mysql ssl连接错误”这个关键词下见过最多的问题。原因MySQL 8.0 默认开启 SSL同时默认认证插件是 caching_sha2_password。旧版本 mysql-connector-java 不认这种新插件而 URL 里没有显式告诉驱动不要用 SSL 时驱动会尝试 SSL 握手本地没有 valid 证书就报错。解决在 JDBC URL 里加useSSLfalseallowPublicKeyRetrievaltrue同时把驱动 jar 升级到 8.x。如果不想换驱动也可以把数据库用户改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;注意MySQL 8.4 已经移除了 mysql_native_password 默认支持长期方案还是升级驱动并保持 useSSLfalse。db.properties 里这行是最容易抄错的地方等号两边不要留空格。5.2 中文乱码的三层排查路线现象登录后学生姓名变成å¼ ä¸‰或???页面上的中文全是乱码但数据库直接查询正常。原因乱码可能发生在三个环节浏览器没按 UTF-8 解析、JSP 响应没声明 UTF-8、JDBC 连接没指定 characterEncoding。如果只在 JSP 里写了 contentType但忘了过滤器的 request 编码表单提交的中文到 Servlet 仍然是 ISO-8859-1 的字节流。解决三层都设成 UTF-8。第一层JSP 顶部写% page contentTypetext/html;charsetUTF-8 languagejava %第二层在 web.xml 里配置一个 CharacterEncodingFilter拦截所有请求WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }第三层JDBC URL 后面必须带characterEncodingutf8。MySQL 8.0 更推荐characterEncodingUTF-8两者在这里效果一样。建库建表时用到 utf8mb4这一步也不能少。如果你检查完三层还乱码去确认 MySQL 连接层语言设置执行SHOW VARIABLES LIKE character_set_server;如果是 latin1说明 my.ini 没生效或启动时用的不是这个配置文件。5.3 端口占用、找不到驱动类与 JSP 编译报错现象Tomcat 启动失败日志提示Port 8080 required by Tomcat v9.0 Server at localhost is already in use。原因很简单本机已经跑着另一个 Tomcat 或占用了 8080 的进程。解决在命令行执行netstat -aon | findstr :8080找到 PID 后看是不是自己的旧 Tomcat是就taskkill /PID xx /F不是就改 server.xml 里的 Connector 端口。很多次我都是因为 IDE 里没彻底停掉上一个 Tomcat而不是真的被别的进程占住。现象运行到数据库操作时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因mysql-connector-java.jar 不在 WEB-INF/lib 下。用 Maven 的同学看 pom.xml 里是否有依赖未用 Maven 的直接把 jar 复制到 lib。另一个隐藏原因是驱动版本和 URL 配置不匹配用 8.x 驱动配老类名不会报找不到但用 5.1.x 驱动配新类名 com.mysql.cj.jdbc.Driver 会报 ClassNotFoundException。现象JSP 页面第一次访问显示 500Tomcat 日志里是Unable to compile class for JSP。这通常不是 JSP 语法错而是 JDK 版本和 Tomcat 编译等级不匹配。解决确认 Tomcat 9 用 JDK 8 以上Tomcat 10 需要 JDK 11且 jakarta.servlet 的包名和旧版不一样。这个项目如果还是 javax.servlet不要用 Tomcat 10 直接跑会报找不到 HttpServlet。5.4 成绩更新“没生效”与排序不对的原因现象在页面修改成绩提示成功刷新后还是旧值也没有报错。大部分情况是事务没有提交。比如在 DAO 里手动conn.setAutoCommit(false)却忘了commit()连接池回收连接时把未提交事务回滚了。解决检查 DAO 或 Service 是否在所有写操作后调用了 commit。还有一种隐蔽情况代码用了 MyBatis 等框架但项目里混用了两套数据源更新走 A 事务查询走 B 事务读到了旧快照。这个问题在纯 Servlet 项目里少见但遇到时先确认所有 SQL 走同一个连接。排序不对的坑我踩过两次。第一次是成绩表字段用了 VARCHARORDER BY score DESC得到 9 排在 88 后面是字典序排序。解决成绩字段必须用 DECIMAL 或 INT。第二次是查询没有 NULL 值处理ORDER BY score DESC时 NULL 记录排在最前。解决排序前用ORDER BY score IS NULL, score DESC。页面端如果做了二次排序也可能覆盖 SQL 排序所以排序逻辑统一放 SQL 里。现象表单提交后按 F5浏览器弹窗提示是否重新发送导致重复录成绩。原因Servlet 处理完后用 forward 转发到 JSP而不是重定向。PRG 模式指 POST 后 Redirect 到 GET在 Servlet 最后用resp.sendRedirect(scoreServlet?actionlist)而不是req.getRequestDispatcher(jsp/score_list.jsp).forward(...)。用户刷新 GET 请求不会重新提交 POST。这是很多“成绩莫名重复”的元凶。6. 上线前我会做的三件小事分页、批量事务和密码盐值成绩管理系统改到能跑只是第一步真正交付前我还会补三个细节。第一个是分页。成绩列表如果一页查出全年级几千条页面会卡浏览器也不愿意。写一个简单 PageUtilSQL 末尾加LIMIT #{offset}, #{pageSize}页面用scoreServlet?actionlistpageNo1控制页码。常见做法是查询总数 count 再算总页数数据量不大时这个方案直接够用。int pageSize 10; int pageNo Integer.parseInt(req.getParameter(pageNo) null ? 1 : req.getParameter(pageNo)); int offset (pageNo - 1) * pageSize; String sql SELECT * FROM v_score_detail ORDER BY exam_date DESC LIMIT ?, ?;第二个是批量事务。当需要从 Excel 整批导入成绩时逐条自动提交一旦中间有一行不合格前面全部落库。我会在 Service 里这样控制Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); try { for (Score s : list) { scoreDao.insert(conn, s); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }这段代码把 Connection 从 DAO 层提升到 Service 层事务边界就是整批数据不再是一条 SQL。注释里强调 close 前还原 autoCommit是为了避免连接池复用时把脏状态留给下个请求。第三是密码盐值。不要直接用 MD5(123456) 存在数据库字典表能秒破。改造办法是存 salt 和 hash 两个字段登录时用DigestUtils.sha256Hex(salt password)比对。我自己的习惯是拿到任何一份 ServletJSP 源码第一件事不是去读业务代码而是先把 SQL 脚本在干净库上跑一遍再改 db.properties最后按第四章那五步过一遍。这套流程跑顺了后面加功能才谈得上改代码。遇到过太多“源码跑不起来”的问题最后都落在驱动版本、字符集、未提交事务这些点上而不是逻辑有多复杂。希望帮到你。本文还有配套的精品资源点击获取
返回列表