简介:一套基于Servlet+JSP实现的学生选课管理系统,面向计算机相关专业正在准备毕业设计的学生,以及需要Java Web项目实战经验的开发者。系统完整覆盖系统管理员、教师、学生三类角色:管理员可维护学生、教师、课程信息;教师能查看课程与学生、录入成绩;学生可浏览课程、选择课程、查询成绩,同时各角色均支持个人信息查看。开发环境基于JDK1.8与MySQL,可使用IDEA或Eclipse导入运行。资源包为ZIP格式,共112个文件,压缩后约2.39MB,包含22个Java源码、21个JSP页面、Servlet与DAO类,以及SQL数据库脚本和jar、css、js等前端与依赖文件,结构清晰,能直接还原项目并运行调试。目前已有554人学习下载。对于想快速掌握Servlet、JSP、MySQL三者整合开发,或需要一个可运行项目作为参考模板的读者,这套源码提供了完整的项目骨架、数据库建表脚本和分层设计思路,可以有效节省从零搭建环境、排查配置错误的时间。
1. 一个能直接跑通的 javaWeb 学生选课管理系统,到底值不值得用
如果你最近正在做课程设计,或者想找一份能改的毕设参考工程,大概率见过这样一个标题:javaWeb学生选课管理系统(完整源码+数据库)。这听起来像是一份下载下来就能直接跑通的成品,但根据我的经验,源码包真正花时间的地方往往不是选课业务本身,而是从解压到浏览器出现登录页的那段路。这个系统解决的是高校里最常见的选课场景:学生登录后能看到本学期课程、选课和退课,管理员端负责维护课程信息和查看选课记录。它基于 JavaWeb 的三层经典结构,用 Servlet 处理请求、JSP 渲染页面、MySQL 存数据,是一个很标准的“完整源码+数据库”教学案例。适合刚学完 JSP/Servlet 的读者、需要交课程设计的同学,以及想快速攒一套数据库增删改查演示项目的人。它最大的价值不在于功能有多花哨,而在于你可以在真实环境里把请求怎么走、事务怎么加、连接池怎么配彻底看明白。
2. 跑起来之前先想清楚:JavaWeb 选课系统的技术栈与 IDEA 运行配置
拿到任何一份“javaWeb学生选课管理系统 完整源码+数据库”,第一件事不是急着双击,而是打开压缩包看根目录有没有 pom.xml。有 pom.xml 说明是 Maven 工程,运行时 IDE 会去自动下载依赖;没有则是传统 Web 工程,直接靠 WEB-INF/lib 下的 jar 包支撑。两个版本的选课业务一样,但启动方式和排错思路完全不同。
2.1 选技术栈:Servlet+JSP 还是 SSM/Spring Boot
我一般直接建议用 JSP+Servlet+JDBC 这个版本,原因很实际。课程设计和毕设答辩时,老师更希望看到你自己实现 Servlet 里的请求分发、自己写 JDBC 的 PreparedStatement,而不是把一切都交给 Spring 的注解。同时这种老式工程的代码量小,一个选课操作对应的类往往只有几十行,新手改起来压力小。这里要注意,不少源码包里虽然也是 JavaWeb,但它用的框架是 Spring+SpringMVC+MyBatis,也就是人们常说的 SSM 版本。如果你看到的是 SSM 版本,那你的 IDEA 里至少要有 Maven 和 MyBatis 的相关插件,否则手写映射文件很容易出错。
关于连接数据库,这个阶段最常用的是数据库连接池。常见源码包会自带 c3p0 或 DBCP 配置,也有直接写死 DriverManager.getConnection 的。那种不用连接池的写法在入门阶段是好事,因为每一行代码都能看懂;但如果你要拿它做并发测试,还是改成连接池更稳。选型原则可以这样定:能直接看懂的底层 JDBC 代码优先,跑通后再谈性能优化。不要一上来就追求 Spring Boot,因为 Spring Boot 自动装配会把 Tomcat 和 JDBC 的细节藏起来,出了问题反而像个黑匣子。
这里要提一下运行环境。最常见的搭配是 JDK 8 + Tomcat 8.5 + MySQL 5.7。如果你用的是 JDK 17 和 Tomcat 10,旧源码会报 javax.servlet 包不存在,因为 Tomcat 10 把 API 改成了 jakarta.servlet。这个坑在环境阶段就会遇到,具体排查我会放在第4章。所以拿到源码后先确认 JDK 版本和 Tomcat 版本。很多源码包里有一个 README 或“环境要求.txt”,别忽略,里面往往写了作者当时使用的版本号。如果没写,就按 JDK8 和 Tomcat8.5 配,这是兼容性最稳的一组“后悔药”。
还有一个容易被忽略的细节:项目的字符集。旧工程常默认 ISO-8859-1,而数据库里写的是中文。如果你的 IDEA 没把项目编码改成 UTF-8,前端显示的全是问号。这个问题不是改一个地方就能好的,我会在第4章的报错清单里再把它拆开。
2.2 用 IDEA 打开源码并配置 Tomcat:最小步骤
以最常见的非 Maven 源码包为例,操作顺序是这样。先把压缩包解压到纯英文路径,比如 D:/workspace/student-course-system。路径里一旦出现中文,Tomcat 启动时常常报找不到类或加载不到资源,这就是典型的“玄学问题”,其实是文件编码或路径解析兼容性问题。然后打开 IDEA,选择 File > Open,直接选中解压后的源码目录。IDEA 会自动识别 Web Module 吗?不一定,很多老项目没有 .idea 和 .iml 文件,IDEA 会把它当成普通目录。
遇到这种情况,我一般这么做。右键项目根目录,选择 Add Framework Support,勾选 Web Application,这样 IDEA 才会生成 Web 目录结构。接着打开 File > Project Structure,在 Modules 里确认 Web 的路径指向 src/main/webapp 或 WebContent,并设置 web.xml 的路径。如果源码里有一个 classes 和 lib 目录,还要在 Artifacts 里把依赖的 jar 包加进 WEB-INF/lib。这一步忘掉的话,后面启动时马上给你一个 ClassNotFoundException。
下面这段代码是解压和预检的示意,以 Linux/macOS 为例:
# 解压源码包到工作目录,路径不要带中文和空格 unzip javaweb-student-course.zip -d ~/workspace cd ~/workspace/javaweb-student-course # 预检目录结构,确认是否有 pom.xml、WebContent 或 src/main/webapp find . -maxdepth 2 -type d | head -30这个命令的作用第一行是解压,第二行进入项目,第三行查看前两层目录结构。如果你在输出里看到 WebContent/WEB-INF,说明这是一个传统 Web 工程;如果看到 src/main/java 和 pom.xml,那它就是 Maven 工程,后面配置会略有不同。
接下来是配置 Tomcat。打开 Run/Debug Configurations,添加 Tomcat Server > Local。在 Server 标签页里选择你本地的 Tomcat 路径,如果没有,就去 Apache Tomcat 官网下载解压版,版本优先选 8.5。在 Deployment 标签页里点加号,选择 Artifact,把项目以 war exploded 方式部署。这里我特别说一句,war exploded 比 war 好用,因为它能直接把编译后的 classes 和资源目录挂在 Tomcat 上,改完代码刷新一下就能生效,不用每次重新打包。配置好 Application context 为 /student,然后启动。
如果你拿到的是 Maven 工程,步骤更简单。用 IDEA 的 Open 选择 pom.xml,导入后等待依赖下载,然后同样添加 Tomcat。但要注意 Maven 工程的依赖经常下载到一半失败,尤其是 mysql-connector 和 jstl,后面遇到可以手动在 Maven 仓库下载对应版本的 jar 放到 lib 下,这个不算作弊,是保命操作。
2.3 修改数据库连接参数:URL、驱动和密码
几乎所有源码都会有一个数据库连接文件,常见的是 db.properties、jdbc.properties 或者 DBUtil.java。无论它叫什么,都要把里面的连接信息改成你本地的。怎么找?全局搜索 3306,这是 MySQL 默认端口,能很快定位到连接串。然后搜索 username 和 password 关键字。
下面是一份常见的 db.properties 示例:
# 数据库连接配置,按本机环境改 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_course?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456解释一下这几个参数。driver 是 JDBC 驱动类名,MySQL 5.7 及之前用 com.mysql.jdbc.Driver;如果你本地是 MySQL 8.0,需要改成 com.mysql.cj.jdbc.Driver,而且 URL 后面最好加 serverTimezone=Asia/Shanghai,否则连接会报时区错误。数据库名 student_course 要和后面导入 SQL 时创建的库名完全一致,大小写也要对上,尤其是在 Linux 环境。
改完配置文件后,先不要启动 Tomcat,应该先用命令行验证 MySQL 能不能连通。这里用到的是最基础的 MySQL 常用命令:
mysql -u root -p student_course -e "select 1"如果返回 1,说明账号、密码、库名都没问题。如果你把驱动 jar 放错了位置,Tomcat 启动时会出现 ClassNotFoundException。所以我习惯先确认连接串,再确认 WEB-INF/lib 下的 mysql-connector-java.jar 是否存在,最后才去启动。这个顺序能帮你排除掉一大半“启动崩了”的情况。第4章里你会看到,很多翻车现场都是从“先启动再说”开始。
3. 数据库设计是这种系统的命根子:选课表的建表脚本与关键外键
数据库设计是整个系统的地基。学生选课管理系统的库,通常包含 4 张表:用户表、学生表、课程表、选课表。有些源码会把学生信息并进用户表,只留一个 user 表,但为了结构清晰,我更推荐拆开。因为选课业务需要学生表里的学院、班级信息,用户表只存账号密码和角色,拆开后权限设计才不会有耦合。
3.1 学生、课程、选课三张核心表的字段设计
下面是标准的三表加用户表建表脚本,是这类项目最常见的形态。如果你拿到的源码里表名不太一样,也大概率能对上这张结构。
-- 用户表,保存登录账号和角色,角色 1=管理员 2=学生 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 2 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生表,扩展学生个人资料 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, major VARCHAR(50), class_name VARCHAR(50), FOREIGN KEY (user_id) REFERENCES sys_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, teacher VARCHAR(30), credit DECIMAL(3,1), capacity INT NOT NULL DEFAULT 50, selected INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课表,联合主键防止重复选课 CREATE TABLE student_course ( student_id INT NOT NULL, course_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表脚本里的关键设计有三个。第一,所有表都用 InnoDB,并把外键显式写出来,这样数据库能保证不会出现选课记录指向一个不存在的学生。第二,student_course 用主键把 student_id 和 course_id 联合起来,数据库层面就杜绝了同一学生对同一门课选两次,比在 Java 代码里 select 判断再 insert 可靠得多。第三,course 表里存了 capacity 和 selected 两个字段,selected 表示已选人数,选课逻辑要在事务中做校验,否则并发时会出现超员。
还有一个细节是字符集。老代码里经常只写 DEFAULT CHARSET=utf8,这在 MySQL 5.7 里能用,但存 emoji 或特殊课程名字时有点风险。我一般直接写 utf8mb4,并且建库时也指定。如果你在源码包里看到的是 utf8,也不要急着改,因为可能存在表已经建立,改动需要 ALTER TABLE,后面再操作。
3.2 数据初始化:为什么要预置账号和课程数据
源码包里“完整数据库”最重要的一部分是初始化数据。没有初始数据,你跑起来看到的只有空表,连登录页都进不去。通常源码会提供一个 init.sql 或 student_course.sql,里面包含表结构和 INSERT 语句。很多下载包把这个文件命名为数据库文件夹,实际就是几个 .sql 文件。
初始化数据的选择有学问。学生账号往往以 stu001、stu002 这样的方式出现,密码常见的是 123456 或与账号同名。管理员账号一般是 admin。这些初始化数据对你来说非常重要,因为测试时你不需要自己手动插入一大段数据,直接拿现成的来跑。假设你拿到的 SQL 文件没有预置数据,就看下面这段参考,手动补一条管理员和一条学生:
-- 预置账号,密码这里用明文演示,真实环境务必改为哈希存储 INSERT INTO sys_user(username, password, role) VALUES ('admin', '123456', 1), ('stu001', '123456', 2); INSERT INTO student(user_id, student_no, name, major, class_name) VALUES (2, '20240001', '张三', '软件工程', '软工2401'); INSERT INTO course(course_no, course_name, teacher, credit, capacity, selected) VALUES ('C001', 'Java程序设计', '李老师', 3.5, 50, 0), ('C002', '数据库原理', '王老师', 2.5, 40, 0);注意,这里密码用明文只是为了让新手能一眼看懂登录过程。如果你要在真实环境部署,必须把密码改成 MD5 或 BCrypt 存储,并且登录逻辑里对应使用同样的加密函数。很多课程设计源码偷懒用明文,这也算是“完整源码”里最常见的问题之一。
除了账号数据,课程数据也要预置几门。选课时你总得先有课可选。我习惯把课程的编号、教师、学分、容量都写清楚,这样测试选课退课的时候能直观显示出来。初始化数据还有一个作用,就是验证你后面的 SQL 查询是否正确。比如,你可以用一条简单的 select 看看选课表是否为零,而不是在浏览器里乱点一气。
3.3 增删改查与事务:选课操作的SQL边界
选课系统的核心不是登录,而是“选课”这一条操作。很多人会以为选课逻辑就是插入一条选课记录,但实际上至少要做三件事:检查课程是否已满、检查是否重复选课、插入选课记录并更新已选人数。这三件事必须在同一个数据库事务里完成,否则一旦插入成功但更新失败,数据就不一致。
先看最基础的 SQL 增删改查。查询学生可选课程:
SELECT c.* FROM course c WHERE c.id NOT IN ( SELECT course_id FROM student_course WHERE student_id = ? );这是典型的“查一下还没选的课”。这里用了子查询,也可以用 LEFT JOIN 加 IS NULL 写法,效果一样但子查询更容易理解。接下来是选课操作的核心,我用 Java 代码配合事务来说明,因为大多数源码也是这么实现的:
// 选课核心逻辑:先更新,再插入,放在同一个事务里 Connection conn = dataSource.getConnection(); try { conn.setAutoCommit(false); // 原子更新:只有未满时才增加已选人数 PreparedStatement update = conn.prepareStatement( "UPDATE course SET selected = selected + 1 " + "WHERE id = ? AND selected < capacity" ); update.setInt(1, courseId); int affected = update.executeUpdate(); if (affected == 0) { throw new RuntimeException("课程已满或课程不存在"); } // 插入选课记录 PreparedStatement insert = conn.prepareStatement( "INSERT INTO student_course(student_id, course_id) VALUES(?, ?)" ); insert.setInt(1, studentId); insert.setInt(2, courseId); insert.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }注意这段代码里的关键点:UPDATE 语句中带了 WHERE selected < capacity。这一行是防止超员的唯一防线。在两个学生同时选同一门课的时候,如果只有一个名额,数据库的行锁会让后到的那个 UPDATE 影响行数为 0,从而触发回滚。如果你先 SELECT 判断容量再 UPDATE,两个请求同时读到还有 1 个名额,就会双双插入成功,造成超员。这种问题在课程设计演示时看不出来,但放到并发环境就出来了。
退课操作也要事务保护。退课不仅要删除选课记录,还要把 course.selected 回退 1。如果你只删了选课记录而不减已选人数,那系统运行几天后所有课程都会变成“满员”。很多源码原版确实没有做这一步,这也是一个可拿来加分的改进点。
4. 部署时最容易翻车的地方:运行流程、常见报错与排查
从源码到浏览器出现登录页,中间至少有六步。第一步,导入 SQL 文件,创建数据库。第二步,修改数据库配置文件。第三步,把项目导入 IDEA 并设置 Web Facet。第四步,配置 Tomcat 并设置 Deployment。第五步,启动 Tomcat,看控制台日志。第六步,浏览器访问项目路径。这六步每一步都可能出问题,但按照顺序来,你会少很多莫名其妙的“玄学失败”。
4.1 把一个完整Web工程跑通:从源码到点击运行
第一步导入 SQL。不要直接双击打开 SQL 文件,那只是预览。要把 SQL 文件通过 mysql 命令或 Navicat 导入。命令行导入这段代码非常实用:
# 先在 MySQL 里创建数据库,再导入 sql 文件 mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS student_course DEFAULT CHARSET utf8mb4;" mysql -u root -p student_course < /path/to/init.sql如果你用了 MySQL 8.0,数据库默认字符集可能和旧 SQL 里不一致,但上面的 CREATE DATABASE 已经指定了 utf8mb4,后面导入的表如果没有单独指定字符集,也会继承数据库的,这个不用担心。
接着回到 IDEA。如果你在第2章已经配置好了,这一步可以直接跳到部署。如果启动时没看到任何页面,先看 Tomcat 控制台的启动日志是不是有 “Deployed application at context path /student”。如果没有,说明 Deployment 里的 Artifact 没配置对。这一步骤最容易被忽略的是 Artifact 类型,要选 war exploded,不要选 war,否则项目改一次就要重新构建一次,很影响调试。
跑通运行后,还有一个默认入口问题。旧项目通常会在 web.xml 里配置 welcome-file 为 login.jsp 或 index.jsp,但有些源码包懒得写,Tomcat 会直接展示目录文件列表。遇到这种情况,访问 http://localhost:8080/student/ 时可能看到的是 404 或者一堆目录名。这不算部署失败,只是 welcome-file 缺项。你手动在 URL 后面加 /login.jsp 就能访问,但更优雅的做法是在 web.xml 里补一行。
这里是 web.xml 中 welcome-file 的常见写法:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>注意,这段配置写在 web-app 标签内部。如果没生效,看看 JSP 文件是不是放在 WebContent 或 webapp 的根目录,而不是放在 WEB-INF 内部。WEB-INF 下的 JSP 是禁止直接通过地址栏访问的,必须通过 Servlet 转发。很多源码为了保护页面会把登录页放在 WEB-INF,那么 welcome-file 就不该写 login.jsp,只能通过 Servlet 路径跳转。这个细节让你不至于配完还是 404。
4.2 五个高频报错与对应的排查方法
这一节我写五个我在帮人看这种项目时最常遇到的报错,每个都按“现象→原因→解决”来写。
第一个,Tomcat 启动时控制台报 ClassNotFoundException: com.mysql.jdbc.Driver。这种现象通常是驱动 jar 没有打包到 Artifact 里。原因很直接:你把 mysql-connector-java.jar 放在了项目的 lib 目录,但 IDEA 的 Artifact 输出里没有包含这个目录。解决方法是打开 Project Structure > Artifacts,找到 WEB-INF/lib 并双击添加依赖中的 jar,或者直接把 jar 文件复制到 Output Layout 的 WEB-INF/lib 下。这一步做完重新启动就可以了。
第二个,访问登录页时报告 404 或 500。404 通常是因为 Application context 配错了。你访问的路径是 /student/login.jsp,但 Deployment 里配的 Application context 是 /student-course。解决方法是把地址栏路径与 context 改成一致,或者在 Deployment 里直接改成 /。500 比较复杂,要去看 Tomcat localhost 日志的具体异常,常见的是 JSP 解析错误或 EL 表达式版本问题。这里没有统一解法,只能根据日志逐行看。
第三个,页面能显示,但是登录后提示数据库连接失败。日志里一般会有 Unknown database 或 Access denied for user。原因多数是 db.properties 里的库名和实际导入的数据库名不一致。比如你导入时把库名建成了 studentcourse,而配置里写的是 student_course。解决方法是先执行 SHOW DATABASES; 确认库名,再改配置文件。如果提示 Access denied,那就是用户名或密码错误,检查 root 账号和密码即可。
第四个,是中文乱码。现象很典型:页面上所有中文都变成问号或火星文,数据库里插进去的中文也是乱码。这里不是一个地方的问题,要同时检查 JSP 页面编码、连接串编码、数据库表字符集。常见的解决套路是先把 JSP 头部的 pageEncoding 设为 UTF-8,再把连接串加上 useUnicode=true&characterEncoding=utf8,最后确认表的 CHARSET 是 utf8mb4。三个地方都对齐了,乱码基本能消失。
第五个,选课操作后没有反应,但也不报错。这种现象很隐蔽,通常是点击选课按钮后,表单提交到 Servlet,Servlet 处理完后 redirect/forward 到请求列表页。如果选课页面是用 frameset 或 iframe 写的,跳转可能发生在子框架里,看起来像是没反应。另一种原因是当前学生的 student_id 不存在,导致插入外键失败,而 Servlet 又把异常吞掉了。解决方法是去看 Tomcat 日志,把 catch 里 e.printStackTrace() 打出来,不要只给一个空 return。很多源码包里习惯写 catch (Exception e) {},这个空 catch 会让你完全看不到异常,也成了这类项目里最大的黑匣子之一。
5. 验证学生选课系统没白折腾:一个最小回归用例和数据库核对技巧
项目跑起来只是开始,真正判断它“能用”,不是看登录页,而是走完一次完整选课流程。我现在拿到这类源码,不会急着去点每一个按钮,而是先跑一个最小回归用例:学生登录→浏览课程→选课→查看我的课程→退课。每一步都同时在数据库里做一次核对,确保前端页面和表数据对得上。
具体做法很简单。先用管理员账号登录后台,看课程列表里 C001 的 selected 是 0。然后切到学生账号 stu001,选这门课,选完立刻回到 MySQL 执行:
SELECT course_id, student_id FROM student_course WHERE course_id = 'C001'; SELECT id, selected, capacity FROM course WHERE course_no = 'C001';如果选课成功,student_course 里会多一条记录,同时 course 的 selected 从 0 变成 1。这里有个技巧:很多选课系统允许同一个学生重复选课,如果你在界面上没做限制,数据库的外键和联合主键会兜底报错。如果报错了,说明源码里的选课逻辑没抓住重复选择的排除条件,也就是第3章提到的 NOT IN 子查询没写对。
再验证退课。点退课后,再执行一次同样的查询,应该看到 student_course 记录消失,course 的 selected 又回到 0。这一步很关键,因为很多源码删了选课记录却不更新 selected,导致以后所有课程都报“已满”。如果你遇到这种情况,就找到了一个必须自己补上的坑。
等核心流程通了,我还会把 Tomcat 的控制台日志开着,再选一次课,看是否有异常打印。没有异常不代表没错,但有空 catch 的项目一定要格外注意。我的习惯是收到一个没见过的“完整源码+数据库”时,先花半小时把表结构和事务边界摸清,再跑页面。这套动作做下来,后面改代码才有底气。
希望这次梳理能帮到你,少走一段我当年走过的弯路。
本文还有配套的精品资源,点击获取