
简介这是一套基于JavaWeb的学生选课系统完整源码与SQL数据库资源面向计算机、通信、人工智能、自动化等相关专业的在校学生、教师及从业者可用于期末课程设计、课程大作业或毕业设计参考。项目为个人毕设成果答辩评审分达95分代码经过调试测试可稳定运行既适合初学者学习JavaWeb开发流程也便于基础较好者在此基础上修改扩展功能。资源包共198个文件约10.41MB以35个Java源文件、29个JSP页面、23个JAR依赖包、23个JavaScript脚本、15个XML配置及12个CSS样式文件为主另含SQL数据库脚本、Gradle构建文件与图片资源覆盖后端逻辑、前端交互与数据持久化各层。目前已有437人学习下载读者可借此掌握选课系统的模块划分、数据库表结构设计、前后端数据交互与项目部署思路是一份结构完整、可直接运行的学习范本。1. 从一份课程作业压缩包说起JavaWeb 学生选课系统到底包含什么很多计算机专业的学生在课程设计阶段都会拿到类似「基于JavaWeb的学生选课系统源码sql数据库(课程作业).zip」这样的资源包。解压之后通常能看到一个 Eclipse 或 IDEA 的 Web 工程目录外加一个.sql数据库脚本文件。这个标题背后对应的是一套典型的 JavaWeb 三层架构应用前端用 JSP 或 HTMLServlet 渲染页面中间层用 Servlet 或 Spring 框架处理业务逻辑底层用 JDBC 或 MyBatis 操作 MySQL 数据库。选课系统的核心业务不复杂但麻雀虽小五脏俱全——用户登录鉴权、课程列表展示、选课退课、容量控制、已选课程查询这几条线基本覆盖了 JavaWeb 开发中最常见的增删改查场景。如果你正在做课程作业、准备答辩、或者想找一个完整的 JavaWeb 项目练手这套源码的参考价值在于它把「数据库设计 → 后端接口 → 前端页面」的完整链路串起来了而不是只给你一个孤立的代码片段。适合的人群很明确刚学完 Servlet 和 JSP、需要一个小型完整项目来巩固的在校生以及需要快速搭出选课功能原型的开发者。2. 选课系统的数据库设计与 SQL 脚本落地2.1 四张核心表的设计思路与建表语句选课系统的数据库设计围绕「学生」「课程」「选课记录」三个实体展开通常需要四张表学生表、课程表、选课关系表外加一张管理员或用户角色表。学生表和课程表各自存储基本信息选课关系表负责维护学生与课程之间的多对多映射。这里有一个设计决策需要提前想清楚选课关系表是只存student_id和course_id还是额外存选课时间、成绩、状态等字段。我的建议是至少加上选课时间和状态字段因为后续做退课、成绩录入、选课时间窗口控制时都会用到。-- 创建数据库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE course_selection; -- 学生表学号作为唯一标识密码存明文仅用于课程作业演示 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, student_name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 登录密码, major VARCHAR(100) DEFAULT NULL COMMENT 专业, grade VARCHAR(10) DEFAULT NULL COMMENT 年级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表capacity 控制选课人数上限selected_count 做冗余计数 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher VARCHAR(50) DEFAULT NULL COMMENT 授课教师, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT DEFAULT 50 COMMENT 容量上限, selected_count INT DEFAULT 0 COMMENT 已选人数, schedule VARCHAR(100) DEFAULT NULL COMMENT 上课时间地点 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表联合主键防止同一学生重复选同一门课 CREATE TABLE selection ( student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, status TINYINT DEFAULT 1 COMMENT 1已选 0已退, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里有几个参数值得展开说。utf8mb4而不是utf8是因为 MySQL 的utf8实际上是阉割版存不了 emoji 和部分生僻字课程作业里学生姓名偶尔会有生僻字用utf8mb4省心。selection表用(student_id, course_id)做联合主键这是最直接的防重复选课手段——数据库层面直接拒绝重复插入比在 Java 代码里先查再插要可靠得多。course表里的selected_count是一个冗余字段理论上可以通过SELECT COUNT(*) FROM selection WHERE course_id? AND status1算出来但每次选课都去 count 一遍在并发场景下性能差且容易出竞态所以用冗余计数配合事务来维护。2.2 初始化数据与 SQL 脚本导入的完整流程建完表之后需要插入一些测试数据否则系统跑起来是空的没法验证。初始化数据要覆盖几种典型场景正常学生、已选满的课程、可选课程、已退课的记录。-- 插入测试学生 INSERT INTO student VALUES (2021001, 张三, 123456, 计算机科学与技术, 2021), (2021002, 李四, 123456, 软件工程, 2021), (2021003, 王五, 123456, 网络工程, 2021); -- 插入测试课程注意有一门课已选满 INSERT INTO course VALUES (CS101, Java程序设计, 陈老师, 3.0, 50, 0, 周一1-2节 教A101), (CS102, 数据结构, 刘老师, 4.0, 45, 0, 周二3-4节 教B202), (CS103, 操作系统, 赵老师, 3.5, 40, 0, 周三5-6节 教A303), (CS104, 计算机网络, 孙老师, 3.0, 2, 2, 周四1-2节 教C101); -- 插入选课记录CS104 已被两人选满 INSERT INTO selection (student_id, course_id, status) VALUES (2021001, CS104, 1), (2021002, CS104, 1);导入流程上如果你用的是 MySQL 命令行直接source /path/to/init.sql即可如果用 Navicat 或 DataGrip打开 SQL 文件全选执行。这里有个常见的翻车点SQL 文件里如果先写了CREATE DATABASE但后面没有USE语句执行建表时会报「No database selected」。另一个坑是外键约束导致插入顺序必须正确——必须先插student和course再插selection否则外键检查会直接拒绝。如果导入时报Cannot add or update a child row八成是插入顺序反了或者引用了不存在的 ID。3. JavaWeb 后端从 JDBC 工具类到选课核心逻辑3.1 数据库连接池配置与 JDBC 工具类封装JavaWeb 项目里最忌讳的就是在每个 Servlet 里都写一遍DriverManager.getConnection()。正确的做法是抽一个工具类配合连接池来管理连接。课程作业级别的项目用 Druid 或者 HikariCP 都行如果不想引入额外依赖自己写一个简单的静态连接池也可以但生产环境绝对不要那么干。// DBUtil.java - 基于 HikariCP 的连接池工具类 public class DBUtil { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/course_selection?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); // 课程作业 10 个连接足够 config.setMinimumIdle(2); // 保持最少 2 个空闲连接 config.setConnectionTimeout(3000); // 获取连接超时 3 秒 config.setIdleTimeout(600000); // 空闲连接 10 分钟后回收 dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源的标准写法注意关闭顺序ResultSet - PreparedStatement - Connection public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }maximumPoolSize设成 10 是因为课程作业的并发量极低设太大反而浪费资源。connectionTimeout设 3000 毫秒是为了在数据库没启动时快速失败而不是让请求线程一直挂着。JDBC URL 里的serverTimezoneAsia/Shanghai是必须的MySQL 8.x 驱动如果不指定时区启动时会直接抛The server time zone value xxx is unrecognized异常这是新手最常遇到的翻车点之一。characterEncodingutf8mb4确保 Java 端和数据库端的字符集一致避免中文课程名变成问号。3.2 选课接口的事务控制与并发防超选选课这个操作看起来简单实际上涉及三个步骤检查课程是否已满、检查学生是否已选过、插入选课记录并更新课程已选人数。这三步必须在一个事务里完成否则并发场景下会出现超选——两个学生同时看到还剩 1 个名额同时插入结果选了 2 个人。// SelectionService.java - 选课核心逻辑 public String selectCourse(String studentId, String courseId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步用 SELECT ... FOR UPDATE 锁定课程行防止并发超选 String checkSql SELECT capacity, selected_count FROM course WHERE course_id ? FOR UPDATE; ps conn.prepareStatement(checkSql); ps.setString(1, courseId); rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return 课程不存在; } int capacity rs.getInt(capacity); int selectedCount rs.getInt(selected_count); if (selectedCount capacity) { conn.rollback(); return 课程已选满; } rs.close(); ps.close(); // 第二步检查是否已经选过联合主键也会兜底但提前检查给用户友好提示 String dupSql SELECT 1 FROM selection WHERE student_id ? AND course_id ? AND status 1; ps conn.prepareStatement(dupSql); ps.setString(1, studentId); ps.setString(2, courseId); rs ps.executeQuery(); if (rs.next()) { conn.rollback(); return 你已经选过这门课; } rs.close(); ps.close(); // 第三步插入选课记录 更新已选人数 String insertSql INSERT INTO selection (student_id, course_id, status) VALUES (?, ?, 1); ps conn.prepareStatement(insertSql); ps.setString(1, studentId); ps.setString(2, courseId); ps.executeUpdate(); ps.close(); String updateSql UPDATE course SET selected_count selected_count 1 WHERE course_id ?; ps conn.prepareStatement(updateSql); ps.setString(1, courseId); ps.executeUpdate(); conn.commit(); // 三步全部成功才提交 return 选课成功; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return 系统异常选课失败; } finally { DBUtil.close(conn, ps, rs); } }这段代码的关键在于SELECT ... FOR UPDATE。它会在事务内对查询到的课程行加排他锁其他事务想对同一行加锁时必须等待直到当前事务提交或回滚。这样就保证了「检查容量」和「更新计数」之间不会有其他事务插进来。如果你把FOR UPDATE去掉在并发测试下大概率会看到selected_count超过capacity的情况。另一个细节是conn.setAutoCommit(false)之后所有 SQL 都在同一个事务里任何一步失败都要rollback()否则会出现选了课但人数没加、或者人数加了但选课记录没插入的数据不一致。退课逻辑是选课的逆操作把status置为 0 并selected_count - 1同样需要事务包裹。4. 前端页面与 IDEA 运行配置让系统真正跑起来4.1 JSP 页面渲染课程列表与选课交互前端部分如果用 JSP核心页面通常有三个登录页、课程列表页、已选课程页。课程列表页需要从数据库查出所有课程并渲染成表格每行带一个「选课」按钮。这里用 JSTL 标签库来循环渲染避免在 JSP 里写 Java 代码块。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle课程列表/title/head body h2可选课程/h2 table border1 cellpadding8 tr th课程编号/thth课程名称/thth教师/th th学分/thth容量/thth已选/thth操作/th /tr c:forEach items${courseList} varcourse tr td${course.courseId}/td td${course.courseName}/td td${course.teacher}/td td${course.credit}/td td${course.capacity}/td td${course.selectedCount}/td td c:choose c:when test${course.selectedCount course.capacity} span stylecolor:red;已满/span /c:when c:otherwise a hrefSelectServlet?courseId${course.courseId}选课/a /c:otherwise /c:choose /td /tr /c:forEach /table /body /htmlc:choose标签在这里做前端层面的容量判断已满的课程直接显示红色「已满」而不是给一个可点击的选课链接。但要注意前端判断只是提升用户体验后端SelectionService里的容量检查才是真正的防线——用户完全可以在浏览器地址栏直接敲SelectServlet?courseIdCS104绕过前端。所以永远不要信任前端传来的任何状态后端必须重新校验。${course.selectedCount}这种 EL 表达式依赖 JavaBean 的 getter 方法你的Course实体类里必须有getSelectedCount()方法否则页面会报PropertyNotFoundException。4.2 IDEA 运行 JavaWeb 项目的完整配置步骤很多人拿到源码后在 IDEA 里跑不起来八成是 Tomcat 配置或者项目结构的问题。下面是从零跑通一个 JavaWeb 项目的标准流程。第一步确认项目结构。IDEA 里的 Web 项目需要正确标记web目录为 Web 资源目录。打开File → Project Structure → Modules检查是否有 Web FacetWeb Resource Directory是否指向你的web或webapp目录Deployment Descriptor是否指向web.xml。如果这里没配好Tomcat 启动后会直接 404。第二步配置 Tomcat Run Configuration。点击Run → Edit Configurations → → Tomcat Server → Local在Server标签页选择本地 Tomcat 安装目录在Deployment标签页点击 → Artifact选择xxx:war exploded。用war exploded而不是war这样修改 JSP 后不用重新打包就能生效。第三步检查依赖。在Project Structure → Libraries里确认mysql-connector-java、servlet-api、jstl这三个包都加进去了。servlet-api的 scope 要设成provided因为 Tomcat 自带了这个包打进去反而会冲突。mysql-connector-java的版本要和你本地 MySQL 版本匹配MySQL 8.x 必须用 8.x 的驱动用 5.x 的驱动连 8.x 数据库会报Unknown system variable query_cache_size。第四步启动并验证。Tomcat 启动后控制台如果出现Server startup in xxx ms说明启动成功。访问http://localhost:8080/你的项目名/看是否能打开登录页。如果报 500 错误先看 IDEA 控制台的异常堆栈最常见的是ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动没加或者Access denied for user数据库密码不对。注意如果你的项目用的是javax.servlet包Tomcat 版本不要超过 9.x。Tomcat 10 开始把包名改成了jakarta.servlet老项目跑在 Tomcat 10 上会大面积报ClassNotFoundException。5. 避坑与排查选课系统最常见的五个翻车现场5.1 中文乱码从数据库一路乱到浏览器现象课程名在数据库里看是正常的但页面上显示成????或者æ•°æ®。原因通常出在三个环节中的某一个数据库连接字符集、JSP 页面编码、或者 Tomcat 的 URI 编码。解决方法是逐层排查。数据库连接 URL 加characterEncodingutf8mb4JSP 页面头部加% page contentTypetext/html;charsetUTF-8 %如果用的是 GET 请求传中文参数还需要在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。POST 请求则在 Servlet 里第一行写request.setCharacterEncoding(UTF-8)。三个地方都对齐了乱码基本就消失了。5.2 选课成功但已选人数没变现象学生选了课selection表里有记录但course表的selected_count没加。原因通常是插入选课记录和更新计数不在同一个事务里或者更新语句执行失败但异常被吞了。排查时先看SelectionService里两步操作是否在setAutoCommit(false)之后、commit()之前。另一个可能是UPDATE语句的WHERE条件写错了比如把course_id写成了idSQL 不报错但影响行数为 0。建议在executeUpdate()之后打印返回值如果是 0 就说明没更新到任何行。5.3 并发选课导致超选现象课程容量设了 2但选课记录有 3 条。原因是没有用SELECT ... FOR UPDATE锁行两个线程同时读到selected_count1都判断没满都执行了插入和加一。解决办法就是在事务内用FOR UPDATE锁定课程行。如果不想用悲观锁也可以用乐观锁UPDATE course SET selected_count selected_count 1 WHERE course_id ? AND selected_count capacity然后检查影响行数是否为 1为 0 就说明被抢完了回滚事务返回「已满」。5.4 外键约束导致删课失败现象管理员想删除一门课程报Cannot delete or update a parent row: a foreign key constraint fails。原因是selection表里有学生选过这门课外键挡住了删除。解决方式有两种一是先删selection里对应的记录再删课程二是把外键的ON DELETE行为改成CASCADE删课程时自动删选课记录。课程作业里建议用第一种手动控制更安全避免误删。5.5 Tomcat 启动报端口占用现象启动 Tomcat 时报Address already in use: bind。原因是 8080 端口被其他程序占了常见的是之前没关干净的 Tomcat 进程或者某个开发工具。Windows 下用netstat -ano | findstr 8080找到占用进程的 PID然后taskkill /PID xxx /F杀掉。Mac 或 Linux 用lsof -i :8080找到进程再kill -9。如果不想杀进程也可以在 IDEA 的 Tomcat 配置里把端口改成 8081 或其他空闲端口。6. 从课程作业到能拿出手的项目三个进阶改造方向课程作业级别的选课系统跑通之后如果你想让它从一个「交完就忘」的作业变成一个能写进简历的项目有三个方向值得投入时间。第一个方向是把 JDBC 换成 MyBatis 或 MyBatis-Plus。现在企业里直接用 JDBC 写 DAO 的项目已经很少了MyBatis 的Mapper接口加 XML 映射文件是更主流的做法。改造时先把DBUtil换成 MyBatis 的SqlSessionFactory然后把每个 DAO 方法改成 Mapper 接口方法。这个过程能让你真正理解 ORM 框架解决了什么问题——不是「不用写 SQL」那么简单而是结果集映射、参数绑定、缓存管理这一整套东西。第二个方向是加一层简单的权限控制。课程作业里的登录通常只区分「学生」和「管理员」但实现方式往往只是在 Session 里存一个角色字符串然后在 JSP 里用c:if判断。更规范的做法是用 Filter 做统一的权限拦截把「哪些 URL 需要什么角色」配置在web.xml或注解里。这样新增角色或调整权限时不用改每个页面。第三个方向是补上接口文档和单元测试。给每个 Servlet 或 Controller 写 JUnit 测试用 H2 内存数据库替代 MySQL 做测试环境这样每次改代码都能快速验证核心逻辑没被改坏。接口文档用 Swagger 或 Knife4j 自动生成答辩时打开文档页面比对着代码讲要直观得多。我自己做这类项目最大的教训是不要一上来就想着把功能做全。先把「登录 → 查课 → 选课 → 退课」这条主链路跑通数据库建好、事务写对、页面能正常显示这四步做扎实了后面加功能都是顺水推舟。反过来如果主链路里事务没控制好、字符集没统一后面每加一个功能都会踩一遍同样的坑。希望帮到你。本文还有配套的精品资源点击获取