简介:这是一份面向有一定Java基础、正在学习JavaWeb开发的学生的实战项目资料包,以典型的学生成绩管理业务为主线,完整演示从登录鉴权、课程管理到学生与成绩维护的常见功能闭环。项目采用MVC设计模式与动态代理模式组织代码,开发环境基于Java和MySQL,适合用于课堂实训、课程设计或毕业设计前期的技术巩固。压缩包共271个文件,约23.41MB,以jar、java、class、jsp、xml等类型为主,可清晰看到分层架构中的控制层、业务层、数据访问层及前端页面实现;同时包含sql数据库脚本和doc文档,便于直接导入数据库并对照阅读设计与部署思路。目前已有6977人学习下载,对需要快速获得一套可运行、可改造的JavaWeb入门项目而言,这是性价比较高的参考素材。
1. 一份学生成绩管理系统源码的 .rar,拿来之后先做什么
很多人第一次接触这类压缩包时,以为里面是一个可以双击运行的完整成品,结果解压后只看到一堆 .java、.jsp、.sql 和 design.doc。学生成绩管理系统是 Java Web 课程设计里的常青树,它的价值不在架构有多强,而在于它把“页面 → Servlet → Service → DAO → MySQL”这条最基础的请求链路完整走了一遍。拿到手先别急着吐槽代码旧,而是把三样东西拆开看:数据库脚本定义了业务边界,源码结构决定了你改哪里,文档则告诉你设计者当时的思路。这篇内容适合两类人:一类是刚学完 Java 基础,想找一个能改、能跑、能应付答辩的练手项目;另一类是工作中偶尔接手老系统,需要快速从 .rar 里理出表结构、登录逻辑和部署步骤。我下面按一套可复现的流程来拆这个压缩包。
2. 先读数据库脚本:学生成绩系统最少需要几张表
拿到 .sql 文件,不要急着在 Navicat 里双击执行。数据库脚本是这套系统的地基,先读它。一个规范的学生成绩管理系统,核心不过学生、课程、成绩、用户四张表,但字段设计和约束写法能看出设计者的水平,也会直接影响你后面做统计查询。
2.1 打开 .sql 文件后的第一件事:先确认建库语句和字符集
脚本开头通常会有一段CREATE DATABASE,这里隐藏着第一个坑。不同版本的 MySQL 对字符集的处理不一样,MySQL 5.7 默认是utf8,MySQL 8.0 开始推荐utf8mb4。如果脚本里写的是DEFAULT CHARSET=utf8,而你本地是 MySQL 8.0,成绩表里存了表情符号就会报Incorrect string value。
先找到建库语句,确认以下信息:数据库名、字符集、排序规则。
CREATE DATABASE IF NOT EXISTS student_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_score;这条语句指定了数据库名student_score,字符集用utf8mb4,排序规则用utf8mb4_general_ci。IF NOT EXISTS的作用是防止重复执行脚本时报错。后面所有建表语句都基于这个库,所以脚本执行顺序必须是建库、选库、建表、插数据。
2.2 student、course、score、user 四张核心表与典型字段设计
课程设计级别的成绩管理系统,四张表就够。student存学生基本信息,course存课程,score存成绩,user存登录账号。下面这份是最简但完整的建表脚本,注意看外键和联合唯一索引的写法。
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID', student_no VARCHAR(20) NOT NULL COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', class_name VARCHAR(50) COMMENT '班级', gender CHAR(1) DEFAULT '男' COMMENT '性别', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', UNIQUE KEY uk_student_no (student_no) ) ENGINE=InnoDB COMMENT '学生表'; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID', course_no VARCHAR(20) NOT NULL COMMENT '课程编号', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', UNIQUE KEY uk_course_no (course_no) ) ENGINE=InnoDB COMMENT '课程表'; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT '成绩', exam_date DATE COMMENT '考试日期', UNIQUE KEY uk_stu_course (student_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) ) ENGINE=InnoDB COMMENT '成绩表';这段脚本里有几个值得注意的点。UNIQUE KEY uk_stu_course (student_id, course_id)是联合唯一索引,它的作用是同一门课同一个学生只能有一条成绩记录,从数据库层面防止重复录入。外键fk_score_student和fk_score_course保证成绩表里不能出现不存在的学生或课程。score DECIMAL(5,2)而不是FLOAT,是为了避免浮点数精度问题导致 89.5 变成 89.4999。
2.3 角色权限、初始账号与预置数据脚本的常见写法
四张表里user表比较特殊,它的设计决定登录逻辑怎么写。最常见的套路是加一个role字段,0 表示管理员,1 表示教师,2 表示学生,登录后根据角色跳转到不同页面。数据库脚本里通常会带一条初始管理员账号,方便第一次登录。
CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '密码', role TINYINT DEFAULT 2 COMMENT '0管理员 1教师 2学生', student_id INT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '用户表'; INSERT INTO user (username, password, role) VALUES ('admin', 'admin123', 0);初始密码用明文是课程设计项目的常态,但这个点在答辩时经常被老师追问。如果你打算把项目再做得体面一点,可以改成MD5或SHA-256加盐存储,但要注意登录判断那一段 Java 代码也要同步改。user表里预留了student_id字段,把登录账号和学生记录挂上钩,这样学生登录后只能查自己的成绩,这个设计比单独写一套权限判断要清爽很多。
3. 把源码结构拆开:Servlet 映射、JDBC 调用链和三处乱码源头
看完数据库脚本再打开源码,心里应该有了一张表关系图。Java Web 老项目的源码结构通常是 JSP + Servlet + DAO,里面没有 Spring 的@Autowired,也没有 MyBatis 的Mapper.xml,但只要能分清每个文件的职责,你就能定位到“改成绩查询”要动哪个文件。
3.1 先看 WEB-INF:web.xml 和 lib 目录决定技术坐标
解压后如果你的目录里有WEB-INF/web.xml,说明这是一个传统的 Servlet 项目,需要打成 war 包丢给 Tomcat。如果根目录有pom.xml,则说明这是 Maven 工程。最怕的是两种都有但互相矛盾,这种情况一般是文档没同步更新,以pom.xml为准。
web.xml是这个项目的总入口,主要看三处:<display-name>项目名、<welcome-file-list>默认首页、<servlet-mapping>的 URL 映射。下面是一段典型的web.xml片段。
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.student.controller.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping><servlet-class>写的是类的完整限定名,<url-pattern>定义浏览器访问路径。你访问http://localhost:8080/项目名/login时,Tomcat 会找到LoginServlet里的doPost方法。从这里可以顺着com.student.controller包名找到所有请求入口,比在 JSP 里满天飞找action要快得多。
3.2 一个 Servlet 映射读清请求入口:学生查询成绩的真实路径
以学生查询成绩为例,一般点是成绩查询菜单跳到一个 JSP 页面,页面提交表单后请求转发到QueryScoreServlet。我见过大量新手项目把这段代码直接写在 JSP 里用<% %>包裹,看起来能跑,但完全没法维护。
@WebServlet("/queryScore") public class QueryScoreServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String studentId = request.getParameter("studentId"); ScoreService service = new ScoreService(); List<Score> list = service.findByStudentId(Integer.parseInt(studentId)); request.setAttribute("scoreList", list); request.getRequestDispatcher("/score_list.jsp").forward(request, response); } }@WebServlet("/queryScore")是注解方式配置映射,比web.xml里写<servlet-mapping>少一步,但两者不能对同一个类重复配置,否则 Tomcat 启动时直接报Conflicting @WebServlet异常。request.setCharacterEncoding("UTF-8")必须放在读取任何参数之前。这段代码把参数接收、业务调用、页面跳转三件事串起来,是典型的“瘦 Servlet 胖 Service”写法。
3.3 查询链路拆解:从 JSP 表单到 JDBC 的 PreparedStatement 参数
很多人追代码时被三层结构绕晕,其实跟踪一个请求就全通了。JSP 页面里写<form action="${pageContext.request.contextPath}/queryScore" method="post">,表单里的studentId提交给QueryScoreServlet,Servlet 调ScoreService,Service 再调ScoreDAO,DAO 里执行的就是下面这条 SQL。
public List<Score> findByStudentId(int studentId) { String sql = "SELECT c.course_name, s.score, s.exam_date " + "FROM score s JOIN course c ON s.course_id = c.id " + "WHERE s.student_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); ResultSet rs = ps.executeQuery(); List<Score> list = new ArrayList<>(); while (rs.next()) { Score sc = new Score(); sc.setCourseName(rs.getString("course_name")); sc.setScore(rs.getBigDecimal("score")); sc.setExamDate(rs.getDate("exam_date")); list.add(sc); } return list; } catch (SQLException e) { throw new RuntimeException("查询成绩失败", e); } }写法上用了PreparedStatement的?占位符,统一用ps.setInt(1, studentId)传参,避免直接拼接字符串带来的 SQL 注入隐患。这种调用链在 Java 面试里会被当作最基础的八股文反复问,答清楚三层各自职责,比背十道面试题管用。JOIN关联查询把course_name直接带出来,程序里就不用再查一次课程表。
3.4 中文乱码的“三处位置”:编码问题都出在这
老项目最容易翻车的不是功能,而是乱码。如果你的学生姓名在页面上显示成问号,逐一定位这三个位置,顺序不要乱。
第一处是数据库连接字符串,必须带useUnicode=true&characterEncoding=utf8;第二处是 Servlet 里接收请求参数前调用request.setCharacterEncoding("UTF-8");第三处是 JSP 页面顶部的pageEncoding="UTF-8"。很多项目把前两处改了,第三处忘了改,结果页面标题正常,从数据库读出来的中文全部乱掉。
排错时有个规律:如果页面标题和数据都是乱码,问题几乎都在 JSP 的pageEncoding或contentType;如果只有从数据库查出来的数据乱码,问题在 JDBC URL 或表结构字符集;如果表单提交后数据库里存的就是乱码,那问题在request.setCharacterEncoding的执行顺序。用这个思路排查,比反复重启 Tomcat 高效得多。
4. 把数据库脚本导进 MySQL,在 IDEA+Tomcat 里跑起来
源码读完,接下来就是最考验耐心的环节:把项目跑起来。下面这套流程我在不同机器上重复过很多次,按顺序执行基本不会有偏差。这里面每一步都对应一个典型的失败场景,我会把参数列出来。
4.1 装环境前的版本匹配:JDK 8、Tomcat 9、MySQL 5.7/8.0 怎么搭配
拿到老项目先看 Java 文件里的语法:如果全是List、ArrayList、for循环,没有var和 lambda,那大概率是 JDK 8 时代写的。不要一上来装 JDK 21,老项目用新 JDK 编译容易遇到java.base模块问题。推荐组合是 JDK 8 + Tomcat 9 + MySQL 5.7 或 8.0。
| 版本项 | 推荐选择 | 说明 |
|---|---|---|
| JDK | 1.8(8u202 或更后) | 老项目编译兼容性最好 |
| Tomcat | 9.0.x | 支持 Servlet 4.0,兼容老代码 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动版本 |
| 项目结构 | war 或 exploded | IDEA 里两种方式都行 |
MySQL 8.0 要配套mysql-connector-java8.0.x 驱动,装在 Tomcat 的lib或项目的WEB-INF/lib下。版本不匹配时启动项目可以,但一执行 SQL 就报ClassNotFoundException: com.mysql.jdbc.Driver,换成com.mysql.cj.jdbc.Driver才行。
4.2 用 mysql 命令行执行数据库脚本,别依赖 GUI 导入
执行脚本这件事,GUI 工具有时候反而帮倒忙。Navicat 导入大文件时如果中途报错,不会明确告诉你哪行出了问题。用命令行最可靠,直接看到每条 SQL 的执行结果。先确认 MySQL 已经启动,然后把.rar里的.sql文件解压到磁盘上,进入命令行执行。
mysql -u root -p < D:/sql/student_score.sql如果.sql文件里已经有CREATE DATABASE和USE语句,上面这条命令就够了。执行成功后进入 MySQL 检查表是否建全。
mysql -u root -p SHOW DATABASES; USE student_score; SHOW TABLES; SELECT COUNT(*) FROM student;SHOW TABLES应该看到至少四张表,如果只有三张,说明脚本执行到中间中断了,检查中断位置附近的建表语句,多半是字段类型不兼容或者外键引用顺序不对。还有一种常见情况:source命令在 Windows 下路径分隔符要写成正斜杠,source D:/sql/student_score.sql;在mysql>提示符下同样可以执行。
4.3 改 JDBC 连接参数必须确认的 4 个值
脚本导成功,下一步就是改 Java 代码里的数据库连接配置。老项目一般有一个db.properties或者DBUtil.java,内容类似下面这样。
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_score?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456jdbc.url里有几个参数必须确认。localhost:3306是数据库地址和端口,如果 MySQL 没跑在默认端口要改。student_score是库名,必须和 2.1 节建库时一致。serverTimezone=Asia/Shanghai是给 MySQL 8.0 用的,老项目里通常没有这个参数,不加会报时间区异常。password改成你自己 MySQL 的 root 密码,硬编码在这里虽然不优雅,但课程设计项目都这样,跑通了再考虑用环境变量覆盖。
4.4 在 IDEA 里配置 Tomcat,部署 exploded artifact
环境变量配好、连接参数改好后,在 IDEA 里打开项目。如果项目没有pom.xml,IDEA 会识别成普通 Java Web 项目,需要手动导入WEB-INF/lib下的 jar 包。这一步要把所有mysql-connector、jstl、servlet-api的 jar 加入 Library,漏掉任何servlet-api的 jar,IDE 里能编译但启动时 Tomcat 会提示类冲突。
部署方式选 exploded 比 war 包方便。war 包相当于打包后整体发布,每次改 JSP 都要重新打包;exploded 相当于直接把webapp目录映射给 Tomcat,改完 JSP 刷新浏览器就生效。配置时在 IDEA 的Run/Debug Configurations里,选 Tomcat Server Local,Deployment标签下加Artifact...,选带 exploded 后缀的那一项。Application context默认写/项目名_war_exploded,这里建议改成/stu,访问 URL 短一点。
4.5 启动后先验证登录和成绩查询两条链路
Tomcat 启动成功不代表业务就通了,你还需要做两次验证。第一次验证登录,第二次验证数据能否正确展示。不要只打开首页看个样式就宣告成功。登录接口如果设计成表单提交,用下面的 curl 命令可以验证服务端是否正常响应。
curl -d "username=admin&password=admin123" \ http://localhost:8080/stu/login返回 302 跳转说明登录成功,返回 200 且页面里有报错信息,就看控制台里有没有SQLException或NullPointerException。SQL 异常大部分和 4.3 节的连接参数有关;空指针多半是request.getParameter的 Key 和前端的name属性没对上。验证成绩查询时,一定要选一个数据库里真实存在的学生 ID 来试,否则列表为空你无法判断是代码错了还是数据没插进去。查不出问题时,去 Tomcat 的logs/catalina.out里看完整堆栈,别只盯着 IDEA 控制台的最后两行。
5. 学生成绩管理系统的二次开发:加筛选条件、导出 Excel 报表
把项目跑通,只能说明你有环境配置能力。答辩或实际使用中,一定会遇到“能不能再加一个功能”的需求,最常被提的两个是组合条件筛选和成绩表导出。这两个需求改动范围可控,但能体现思路。
5.1 给成绩查询页面加“按班级筛选”JSP 下拉框与安全的 SQL 拼接
原始项目里成绩查询一般只能按学号或姓名查,现在要支持班级。修改思路:JSP 里加一个下拉框,查出班级列表,提交后 Servlet 把班级参数传给 Service,Service 再传到 DAO。SQL 部分不能直接拼WHERE class_name='加参数,要改成PreparedStatement动态拼接。
String sql = "SELECT s.student_no, s.name, s.class_name, c.course_name, sc.score " + "FROM score sc " + "JOIN student s ON sc.student_id = s.id " + "JOIN course c ON sc.course_id = c.id " + "WHERE 1=1"; if (className != null && !className.isEmpty()) { sql += " AND s.class_name = ?"; }WHERE 1=1是动态 SQL 的常用写法,它的作用不是没有条件,而是让后面的AND s.class_name = ?不用区分是不是第一个条件,减少 if-else 拼接。className为空时查询所有班级,有值时加上过滤条件,注意PreparedStatement的参数下标从 1 开始,如果前面还有别的条件,下标要依次递增,传参顺序不能乱。
5.2 用 Apache POI 导出 Excel:依赖、模板与数字格式的坑
成绩表要导出,最常见的做法是用 POI 写一个.xls。导出功能如果做在 Servlet 里,注意两个细节:第一,响应头要设置Content-Disposition,否则浏览器只会打开一个乱码页面;第二,成绩列必须设成数字格式,否则 89.50 会被显示成文本,Excel 里无法求和。
HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("期末成绩"); HSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("学号"); header.createCell(1).setCellValue("姓名"); header.createCell(2).setCellValue("课程"); header.createCell(3).setCellValue("成绩"); response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=scores.xls"); workbook.write(response.getOutputStream());response.getOutputStream()不能和PrintWriter混用,Servlet 里一处写了response.getWriter(),再写这段就会报IllegalStateException。.xls是 Excel 2003 格式,旧版本 POI 不支持.xlsx,如果导出后 Excel 提示文件格式与扩展名不匹配,直接改成.xls后缀最省事。数字格式建议在生成 cell 时调用setCellValue(Double.parseDouble(score)),让 Excel 自动识别为数值。
5.3 改造后的验证清单:回归测试要覆盖哪些入口
改完功能别急着关 IDE,先跑一遍验证清单。重点看三块:原有查询是否被新增条件影响,导出的文件能否正常打开,班级参数为空时是否返回全量数据。
| 验证场景 | 操作 | 预期结果 |
|---|---|---|
| 全量查询 | 不选班级直接查询 | 返回全部成绩记录 |
| 条件筛选 | 选择计算机2301班 | 只显示该班成绩 |
| 空结果处理 | 选一个没有学生的班级 | 页面提示“无数据” |
| 导出文件 | 点击导出按钮 | 下载 xls 文件且数字可计算 |
| 乱码回归 | 导出后打开 Excel | 中文姓名正常显示 |
导出文件出现中文乱码时,检查response.setCharacterEncoding("UTF-8")是否在write方法之前调用;导出文件打不开时,检查workbook.close()是否在write之后关闭流。到这里,这套学生成绩管理系统就具备了继续迭代的基础。
本文还有配套的精品资源,点击获取