简介:基于Servlet+Jsp+MySQL搭建的学生成绩管理系统,是一套面向计算机相关专业毕业设计及Java实战练习的完整源码项目。系统区分为管理员、教师、学生三种角色,覆盖学生、教师、课程信息管理,教师开设课程与提交成绩,学生选课与查询成绩等核心功能;同时提供系统用户表、学生表、教师表、课程表及关联表的SQL数据库脚本,经过调试可直接运行,适合作为毕业设计或课程实践基础。资源包约1.53MB,共166个文件,主要包含69个JSP页面、25个Java源文件、25个class文件,以及SQL数据库脚本、XML配置、图片样式等辅助资源;代码中已实现完整的DAO层处理类,层次清晰,便于快速理解数据访问逻辑与二次开发。运行环境推荐Tomcat7以上、MySQL5以上,可使用Eclipse或IDEA开发调试;目前已有538人学习下载,适合需要完整系统参考或快速搭建成绩管理项目的开发者。
1. 基于 Servlet+MySQL+JSP 的学生成绩管理系统:老组合为什么还值得做
做学生成绩管理系统,第一反应往往是用 Spring Boot + Vue,但在实际课程设计、校内实训甚至小范围的教务辅助场景里,基于 Servlet+JSP+MySQL 的源码项目仍然是出现频率极高的一套。它没有微服务,没有前后端分离,却把 Java Web 最核心的能力全部过了一遍:HTTP 请求与响应、Servlet 生命周期、JDBC 操作、PreparedStatement 防注入、MySQL 事务与索引、JSP 标签库渲染。标题里的“源码+数据库”意味着解压后你拿到的是一整套可运行工程,导入 SQL、启动 Tomcat、改好数据库连接就能看到登录页。这篇笔记按我接手这类项目的习惯来写,从表结构讲到部署和排错,目标是让你拿到源码后能快速跑起来,也知道每一层为什么要这么设计。
2. 先把数据层定死:学生成绩管理系统的表结构设计与初始化 SQL
2.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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 2. 班级表 CREATE TABLE t_class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL, grade_year SMALLINT COMMENT '入学年份,如2024' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 成绩汇总视图与存储过程:统计逻辑放数据库更省心
业务层查询成绩时要关联很多表,如果每次都写三表 JOIN,DAO 层代码很啰嗦。常见做法是在数据库建一个视图 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. 用 Servlet+JSP 把成绩 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.properties:
jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_grade?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true jdbc.username=root jdbc.password=123456 # 连接池配置 initialSize=5 maxActive=20 maxWait=5000 minIdle=5然后再写 DBUtil:
package 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 里的useSSL=false是避免本地连 MySQL 8.0 时的 SSL 握手警告;serverTimezone=Asia/Shanghai解决驱动和数据库时区不一致导致的报错;characterEncoding=utf8是中文乱码的第一道防线。rewriteBatchedStatements=true是给批量插入优化用的,批量录成绩时能明显提升速度。Druid 的 maxWait 表示获取连接的最大等待毫秒数,超过就抛异常,防止线程池被占满后无限等待。
3.3 成绩查询 DAO:PreparedStatement 与 VO 映射
先给成绩实体 Score 设计字段,再写 ScoreDao。以“按关键字查询成绩列表”为例:
public List<ScoreVO> findScoreList(String keyword) throws SQLException { List<ScoreVO> 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 '1'='1来试你的登录框,参数化查询是最基本的防线。查询用到视图 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?action=list&pageNo=1。所有子类必须实现四个方法,这样 IDE 会强制提醒你每个操作都考虑到位。需要注意 switch 里的 default 分支不能省,用户手动改 URL 乱传 action 时,默认走列表总比报 404 友好。
3.5 成绩列表 JSP:JSTL+EL 渲染与排序展示
JSP 页面要遵循一个原则:能用 EL 和 JSTL 的,就不要写<% %>。下面是一个 score_list.jsp 的核心片段:
<%@ 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><th>考试日期</th><th>操作</th> </tr> <c:forEach items="${scoreList}" var="s"> <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 style="color:green">${s.score}</span></c:when> <c:when test="${s.score >= 60}"><span style="color:orange">${s.score}</span></c:when> <c:otherwise><span style="color:red">${s.score}</span></c:otherwise> </c:choose> </td> <td>${s.examDate}</td> <td><a href="scoreServlet?action=toEdit&scoreId=${s.scoreId}">修改</a></td> </tr> </c:forEach> </table> </body> </html>页面里一定要记得在顶部写 contentType 和 pageEncoding,否则中文必乱码。${s.score}这种写法依赖 ScoreVO 的 getter 方法,字段名不对或缺少 getter,这里直接空白。用<c:choose>做分数段颜色,比在 JSP 里嵌套 Java 三目运算清楚得多。很多人在 JSP 上纠结“图片如何对坐标定位”这类前端细节,其实成绩管理系统不靠特效,信息清晰最重要。把修改链接拼成scoreServlet?action=toEdit&scoreId=...,点击后由 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] basedir=D:/software/mysql-8.0 datadir=D:/software/mysql-8.0/data port=3306 character-set-server=utf8mb4 collation-server=utf8mb4_general_ci default-storage-engine=INNODB max_connections=100 [client] default-character-set=utf8mb4这里 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 再 source:
mysql -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.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_grade?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你自己改的密码如果 MySQL 是 5.7 版本,驱动 jar 还在用 mysql-connector-java-5.1.x,driverClassName 可以保留为 com.mysql.jdbc.Driver。MySQL 8.0 的官方驱动包把类名改成了 com.mysql.cj.jdbc.Driver,同时要求 URL 里带上 serverTimezone,否则会报The server time zone value '�й���ʱ��' is unrecognized。useSSL=false 是关闭 SSL 握手,避免开发环境因为证书问题报错。如果连接时出现Public Key Retrieval is not allowed,在 URL 后追加allowPublicKeyRetrieval=true即可,这是 MySQL 8 认证插件导致的常见问题。改完先写一个简单的DBUtil.getConnection()测试类跑一下,确认能拿到连接再启动 Tomcat,别等到页面报错才回头查。
4.4 部署到 Tomcat:war 包发布与端口冲突处理
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。启动 Tomcat:
catalina.bat run看到Server startup in xxx ms说明启动成功,然后访问:
http://localhost:8080/student_grade/如果页面始终打不开,检查浏览器访问的端口和 Tomcat 一致。默认是 8080,如果被占用或你改过端口,去 conf/server.xml 里找 Connector 节点:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />把 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 里加useSSL=false&allowPublicKeyRetrieval=true,同时把驱动 jar 升级到 8.x。如果不想换驱动,也可以把数据库用户改回 mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;注意,MySQL 8.4 已经移除了 mysql_native_password 默认支持,长期方案还是升级驱动并保持 useSSL=false。db.properties 里这行是最容易抄错的地方,等号两边不要留空格。
5.2 中文乱码的三层排查路线
现象:登录后学生姓名变成å¼ ä¸‰或???,页面上的中文全是乱码,但数据库直接查询正常。
原因:乱码可能发生在三个环节,浏览器没按 UTF-8 解析、JSP 响应没声明 UTF-8、JDBC 连接没指定 characterEncoding。如果只在 JSP 里写了 contentType,但忘了过滤器的 request 编码,表单提交的中文到 Servlet 仍然是 ISO-8859-1 的字节流。
解决:三层都设成 UTF-8。第一层,JSP 顶部写:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>第二层,在 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 后面必须带characterEncoding=utf8。MySQL 8.0 更推荐characterEncoding=UTF-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 页面第一次访问显示 500,Tomcat 日志里是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 走同一个连接。
排序不对的坑我踩过两次。第一次是成绩表字段用了 VARCHAR,ORDER 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?action=list"),而不是req.getRequestDispatcher("jsp/score_list.jsp").forward(...)。用户刷新 GET 请求,不会重新提交 POST。这是很多“成绩莫名重复”的元凶。
6. 上线前我会做的三件小事:分页、批量事务和密码盐值
成绩管理系统改到能跑只是第一步,真正交付前我还会补三个细节。
第一个是分页。成绩列表如果一页查出全年级几千条,页面会卡,浏览器也不愿意。写一个简单 PageUtil,SQL 末尾加LIMIT #{offset}, #{pageSize},页面用scoreServlet?action=list&pageNo=1控制页码。常见做法是查询总数 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)比对。
我自己的习惯是:拿到任何一份 Servlet+JSP 源码,第一件事不是去读业务代码,而是先把 SQL 脚本在干净库上跑一遍,再改 db.properties,最后按第四章那五步过一遍。这套流程跑顺了,后面加功能才谈得上改代码。遇到过太多“源码跑不起来”的问题,最后都落在驱动版本、字符集、未提交事务这些点上,而不是逻辑有多复杂。希望帮到你。
本文还有配套的精品资源,点击获取