
简介这是一份基于JavaWeb的学生选课系统毕业设计源码包面向计算机、人工智能、自动化等专业的学生和开发者适合用作期末课程设计、课程大作业或毕业设计参考。项目是个人答辩作品评审分达到95分代码已通过调试测试可正常运行系统涵盖学生管理、课程管理、选课操作等典型功能模块。压缩包共198个文件总大小约10.41MB目录中包含35个Java后端源码、29个JSP动态页面、23个JAR依赖库、15个XML配置、12个CSS样式及SQL数据库脚本等同时配有Gradle工程文件与前端脚本结构清晰方便快速导入和二次开发。整体具备较高的学习借鉴价值使用者既可对照源码理解JavaWeb分层开发思路也可在此基础上修改扩展实现不同业务需求。目前已有437人浏览学习适合从事JavaWeb开发的学习者与入门进阶人群。1. 学生选课系统源码包课程作业项目到底是个什么形态很多同学下载这个“基于 JavaWeb 的学生选课系统源码sql数据库(课程作业).zip”时满脑子是“双击就能跑”结果导入 IDEA 之后在报红和 Tomcat 启动失败之间反复横跳。这类源码包是高校 Java 课程设计的典型交付物一个 Servlet JSP MySQL 的 Web 应用功能围绕学生登录、浏览课程、选课退课、教师维护课程信息展开核心判定就两件事——这门课有没有满员、和你已选的课有没有时间冲突。这篇笔记按我接手这类作业项目的顺序来写先看清包里是什么再配置环境跑通然后读懂选课链路最后用 SQL 脚本校验业务逻辑。适合正在赶课程作业、以及需要给别人讲清楚这套系统怎么运作的人。新手可以照着一步步走熟手可以直接跳到第 5 章看坑。2. 把源码包变成能跑的项目导入 IDEA、配置 Tomcat 与 MySQL 的三个必改点2.1 为什么课程作业几乎全是 ServletJSPMySQL先看技术选型看过几届学生的课程设计后我几乎可以断定这类 JavaWeb 选课系统八成是 Servlet JSP MySQL Tomcat 的组合少数用 MyBatis 替换了纯 JDBC。不是因为 Spring Boot 不好而是课程进度通常恰好停在“刚讲完 Servlet、会话管理和 JSP”的位置作业就下来了。这个选型的好处是代码链路短一页纸能讲清楚请求怎么走答辩时好说坏处是配置敏感换台电脑、换个 Tomcat 版本就可能跑不起来。这里有一个最容易忽略的版本匹配问题。Tomcat 版本、JDK 版本和 Servlet API 有对应关系不匹配的话项目能编译但启动时报ClassNotFoundException或给你一个莫名其妙的 404。我现在的组合是 JDK 8 Tomcat 9.0.x MySQL 5.7/8.0对应 Servlet 4.0 规范。如果你用 JDK 8 去配 Tomcat 10项目里那些javax.servlet.*的 import 全会挂因为 Tomcat 10 把命名空间换成了jakarta.*。JDK 版本建议 TomcatServlet API需要注意的事JDK 8Tomcat 9.0.x4.0课程作业最稳的组合演示不折腾JDK 11Tomcat 9.0.x4.0编译版本调到 11其余不变JDK 8Tomcat 10.1.x6.0项目用javax.*会直接启动失败所以拿到源码包的第一步不是跑而是先看里面用的是哪个 Servlet API。去pom.xml或WEB-INF/lib目录下找到servlet-api.jar再确认自己本机 Tomcat 的版本。别小看这一步它决定了后面所有配置动作。2.2 IDEA 导入项目与三个必改点db 配置、Artifacts、SDKIDEA 导入方式不复杂File Open选中解压后的文件夹等它识别结构。如果这个项目不是 Maven 工程IDEA 不会自动给你标好目录角色。你要手动到Project Structure里把src/main/java标记为 Sources把src/main/webapp标记为 Web Resources Directory。这一步没做对后面代码全是红色下划线。接下来动三个必改的配置。第一处是数据库连接常见做法是项目里放一个db.properties或jdbc.properties我见过最多的问题是把密码和数据库名写死成作者的本地值。你需要改成自己 MySQL 里的实际库名、用户名和密码jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456这段配置里course_db必须和后面导入的 SQL 脚本建的库名一致serverTimezoneAsia/Shanghai是为了避开 MySQL 8.0 的时区报错characterEncodingutf8是防止中文课程名乱码。如果你用的 MySQL 版本是 5.7驱动类名可以保留com.mysql.jdbc.Driver但 8.0 必须写成com.mysql.cj.jdbc.Driver。改完这一处数据库连接就算打通了。第二处是 Tomcat 部署配置。在 IDEA 的Run Edit Configurations里添加一个 Tomcat Server Local然后在 Deployment 标签页把当前项目的 war exploded 添加到 Application Context。这里踩得最多的坑是把 Application Context 写成/结果启动后浏览器访问http://localhost:8080/项目名/时资源路径全对不上CSS、JS 全部 404。第三处是 SDK 与语言级别。Java Web 课程作业对 JDK 版本要求不高但你必须让Project SDK、Module SDK和Tomcat三者在同一个大版本下。我一般会把语言级别固定在 8因为很多老项目用了 Java 8 之后才有的特性级别设高了反而不兼容。2.3 最小启动路径按这个顺序跑通再谈改代码跑通这套系统我一般按四步走顺序不要乱。第一步用 Navicat 或命令行执行附带的 SQL 脚本把数据库建好。第二步确认db.properties里的密码、库名已经改成你本机的值。第三步在 IDEA 里配置好 Tomcat 后先点一下 Build确保没有编译错误。第四步以 Debug 模式启动 Tomcat控制台出现Server startup in相关日志后访问http://localhost:8080/course_war_exploded/。提示入口页面通常是login.jsp或index.jsp如果打开是 404先去看 Tomcat 的 webapps 目录里是否真的生成了部署目录再去对一下 Artifact 名称和访问路径的大小写。启动失败时优先看 Tomcat 的 localhost 日志不是看 IDEA 控制台那一小段报错。控制台里出现的ClassNotFoundException通常指向两个原因项目里某个 jar 没进 WEB-INF/lib或者 Tomcat 版本和 Servlet API 不匹配。把lib目录下的 jar 包和pom.xml里的依赖对比一遍配好后再启动绝大多数问题都能消失。3. 读懂选课系统的核心链路登录、去重与冲突检测的代码实现3.1 请求分层Servlet 接收参数Service 做判断DAO 操作数据库拿到能跑的项目后不要急着改界面。先找一条完整请求链读一遍比如“选课”这个动作。典型的分层是浏览器提交表单到 ServletServlet 从HttpServletRequest里取出参数调用 Service 层方法Service 里做业务判断再通过 DAO 访问 MySQL最后把结果放回 request 域转发给 JSP 渲染。我一般会先打开CourseServlet看它的路由。很多课程作业用的是WebServlet注解少数用web.xml配置映射两种都行。下面是我见过最常见的路由写法WebServlet(name CourseServlet, urlPatterns {/course}) public class CourseServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { // 查询当前学生未选的课程列表 int studentId ((User) request.getSession().getAttribute(loginUser)).getUserId(); request.setAttribute(courses, courseService.findAvailableCourses(studentId)); request.getRequestDispatcher(/WEB-INF/jsp/course_list.jsp).forward(request, response); } } }这段代码的逻辑很直白action参数决定 Servlet 做什么list对应课程列表页。studentId是从 session 里的loginUser对象取的这说明登录状态靠 session 维持也是课程作业几乎统一的作法。getRequestDispatcher后面那个路径如果带/WEB-INF/jsp说明 JSP 放在 WEB-INF 下面用户不能直接通过地址栏访问只能被 Servlet 转发进去这是一种常见的安全性处理。你在改造时如果想新增一个“我的选课”功能照这个模式加一个actionmy分支就行。3.2 选课动作重复、满员与冲突检测三重判断的代码顺序选课是整个系统最值得看的核心方法。我见过很多学生在此处只有“插入一条选课记录”的逻辑那是不及格的做法。一个严谨的选课 Service 方法至少要按顺序做四件事public Result selectCourse(int studentId, int courseId) { // 1. 课程存在且可选 Course course courseDao.findById(courseId); if (course null || course.getStatus() ! 1) { return Result.fail(课程不存在或不在选课期); } // 2. 防止重复选课 Selection exist selectionDao.findByStudentAndCourse(studentId, courseId); if (exist ! null) { return Result.fail(已选过这门课不能重复提交); } // 3. 容量检查已选人数 vs 课程容量 if (course.getSelectedCount() course.getMaxCount()) { return Result.fail(选课人数已满); } // 4. 时间冲突同一学生不能有两门课在同时段 if (selectionDao.hasTimeConflict(studentId, course)) { return Result.fail(与已选课程存在时间冲突); } selectionDao.insert(studentId, courseId, new Date()); courseDao.increaseSelectedCount(courseId); return Result.ok(); }代码里的注释其实对应了四种常见的作业验收点。第一层查课程状态防止有人绕过页面直接构造请求选一门被停开的课第二层用“学生课程”唯一记录去重这是最容易被攻击的接口建议在t_selection表上建唯一索引双保险第三层是容量控制但要注意这里的course.getSelectedCount()是从课程表读出来的缓存值它有并发问题后面第 5 章单独讲第四层是时间冲突SQL 写法在下一章详细展开。这个方法的返回值Result是课程作业里常见的一个包装类里面放一个boolean或int状态码再加一条提示消息。你在给别人讲解时可以直接拿这个方法的四层判断当主线比对着页面讲效果好得多。3.3 前端到后端JSP 表单的参数传递与隐藏字段选课页面上通常是一个课程列表每行一个“选课”按钮。很多初学同学看不懂为什么按钮能提交不同课程的数据秘密就在隐藏字段里form action${pageContext.request.contextPath}/course methodpost input typehidden nameaction valueselect input typehidden namecourseId value${c.courseId} button typesubmit classbtn btn-primary选这门课/button /form这个表单提交后Servlet 里用request.getParameter(courseId)就能取到对应的课程编号。${c.courseId}是 JSP 的 EL 表达式它读取的是 Servlet 在 request 里放的课程对象列表所以循环输出时每个按钮自动绑定了不同的课程 ID。这里有一个特别容易翻车的细节如果表单的method写成了get课程 ID 会暴露在 URL 上浏览器回退时还会出现重复提交问题。改成post能规避大部分重复提交但并不能完全防止双击所以服务端去重逻辑才是真正的防线。在调试这类页面时我会先不点按钮直接在浏览器地址栏输入http://localhost:8080/项目名/course?actionlist看看列表能不能出来。能出来说明 Servlet 链路正常再点按钮去测选课逻辑可以把问题快速定位到“前端”还是“后端”省掉很多无意义的排查时间。4. 从 SQL 脚本看表结构选课约束怎么用数据库设计落地4.1 五张核心表用户、学生、课程、选课记录、班级/院系SQL 脚本是这套系统的另一半灵魂。我拿到手会先数表看作者是把用户和学生的信息混在一张表里还是分成了多张表。常见设计是五张表左右表名作用关键字段t_user登录账号user_id, username, password, rolet_student学生信息student_id, user_id, name, dept_idt_course课程信息course_id, course_name, credit, teacher_name, week_day, start_section, end_section, max_count, selected_countt_selection选课记录selection_id, student_id, course_id, select_timet_department院系/班级dept_id, dept_name要注意的是t_user里的role字段决定了登录后的跳转方向学生角色进学生主页管理员角色进课程维护页。有的项目用一个status或is_admin代替本质一样。选课关系是“多对多”t_selection就是中间表每个学生可以选多门课每门课可以被多个学生选。建表语句里我特别留意三个约束。一是t_selection表上有没有唯一约束UNIQUE KEY uk_stu_course(student_id, course_id)没有的话说明重复选课只能靠代码判断数据库层面拦不住。二是t_course表的selected_count是不是有DEFAULT 0没有的话插入课程时必须手动赋值容易漏。三是t_selection的course_id和student_id有没有外键有外键的好处是删除课程时不会被孤儿记录干扰坏处是删除顺序必须对应很多同学在初始化数据时就是因为外键顺序不对而导入失败。4.2 两段核心 SQL查可选课程与检测时间冲突“查当前学生可以选的课程”是列表页的查询几乎每个选课系统都有。我见过最直白的写法是“先查所有课程再在 Java 里循环判断有没有选过”这在小数据量作业里能用但数据一多就是慢 SQL 的典型。更常见也更合理的写法是用LEFT JOIN一次查出未选课程SELECT c.* FROM t_course c LEFT JOIN t_selection s ON s.course_id c.course_id AND s.student_id 1 WHERE s.selection_id IS NULL AND c.selected_count c.max_count;这段 SQL 的关键在LEFT JOIN的AND s.student_id 1它把“当前学生”的选课记录关联到课程表上。如果该学生没选过这门课s.selection_id就是 NULLWHERE s.selection_id IS NULL正好把这些课程筛出来。后面再加selected_count max_count过滤满员状态。我一般会让你在t_selection(student_id, course_id)上建索引否则学生一多这个 JOIN 会扫描大量无关记录。时间冲突检测是另一个容易被写错的地方。每门课有week_day星期几、start_section起始节次、end_section结束节次。判断新课程与已选课程冲突不能只看week_day相同还要比较节次区间是否有交集SELECT COUNT(*) FROM t_selection s JOIN t_course c ON c.course_id s.course_id JOIN t_course new_c ON new_c.course_id 5 WHERE s.student_id 1 AND c.week_day new_c.week_day AND c.start_section new_c.end_section AND c.end_section new_c.start_section;start_section new_c.end_section和end_section new_c.start_section这两个条件放在一起就是闭区间交集判断。我指导过的学生里至少有三个在这里写成了“两个节次全部相等”导致选修课和已选课部分重叠时被放过。这个 SQL 还可以把new_c.course_id 5换成参数放到 DAO 的hasTimeConflict(int courseId)方法里配合上一章的 Service 代码直接使用。4.3 初始化脚本里的“隐藏考点”造数据也要有目的SQL 脚本除建表外往往还带一批 INSERT 语句。这些数据看起来只是让页面有东西可看实际上也承担着验证逻辑的作用。聪明的作者会在初始化脚本里造好“同一时段的两门课”用来演示冲突检测也会造一门selected_count已经等于max_count的课用来演示满员拦截。我拿到脚本后会先把这些测试数据标记出来而不是全部清空重造。如果你决定自己重造数据我建议至少造这几类场景一门已满员的课、两门时间重叠的课、一门已被当前学生选过的课。每造一条就在页面上点一次选课看服务端是否正确拒绝。这样一来答辩时老师随便点你心里都有底。不要一股脑插入几十条没有章法的课程那样只会让列表页翻到手软核心逻辑反而得不到展示。5. 常见问题与排查让这套 JavaWeb 项目在别的机器也能跑的 5 个避坑记录5.1 导入后代码大片报红但实际不是代码的问题现象项目从 zip 解压后导入 IDEAJava 文件里import javax.servlet.*和数据库相关的类全部标红编译列表里几十个错误。原因绝大多数情况下不是源码有 bug而是 IDEA 没有识别这个项目的依赖。课程作业很少带完整的 Maven 依赖管理要么是一个纯 web 项目要么 lib 目录里的 jar 没有导入到模块依赖中。解决打开Project Structure Modules Dependencies确认是否有WEB-INF/lib目录的依赖。没有就点加号选 JARs or directories把 lib 目录整个加进去。如果是 Maven 项目先点击右侧 Maven 面板的刷新按钮让依赖重新下载再不行就去File Invalidate Caches清缓存重启。记住IDEA 的报红有一半是缓存和依赖索引的锅。5.2 数据库连不上Navicat 能连程序却一直超时现象Navicat 用 root/123456 能正常连上 MySQL但启动 Tomcat 后程序报Communications link failure或Access denied。原因两种可能。第一种db.properties里的密码对不上当前 MySQL 用户第二种MySQL 8.0 默认的认证方式是caching_sha2_password而项目里用的老版本 JDBC 驱动不认识它。解决先确认密码再确认驱动。把 MySQL 8.0 的用户认证方式改回mysql_native_password是可用的笨办法但我更建议直接把驱动升级到mysql-connector-java 8.0.3x或com.mysql:mysql-connector-j最新版同时把db.properties里的 driver 写完整。改完后如果还报时区错就把 URL 上加serverTimezoneAsia/Shanghai。这一条是我见过出现频率最高的翻车点几乎所有把项目从作者电脑搬到新电脑的同学都会遇上。5.3 选课人数超卖双击提交塞进五十几个人现象课程容量是 30页面显示已选 29连续点击选课按钮后选课记录表出现 33 条记录selected_count却停在 30。原因先执行 SELECT 检查selected_count max_count后执行 INSERT 更新记录中间隔了网络和线程切换。两个请求同时通过检查然后各自 INSERT容量就被绕过了。这就是典型的“先查后写”并发问题代码逻辑单看没问题但真实运行时会翻车。解决课程作业层面最简单的做法是在INSERT INTO t_selection前加唯一索引兜底重复数据会被数据库拒绝更贴近工程的做法是给selected_count的更新写成一个原子操作UPDATE t_course SET selected_count selected_count 1 WHERE course_id ? AND selected_count max_count;UPDATE语句本身是按行加锁的WHERE里带上selected_count max_count后只有没满员时才会更新成功。如果更新影响行数是 0就说明课程已经满了再回滚选课记录。用这个 SQL 替换原来的increaseSelectedCount超卖问题就能在数据库层面被拦住。5.4 改完表结构页面数据对不上现象按照自己的需求加了t_course的一个字段比如course_type页面明明重新部署了却一直显示默认值或干脆报错Unknown column。原因IDEA 里经常出现改了 SQL 脚本但没重新执行或 Tomcat 的 deployment 使用的是旧的编译输出目录。还有一种隐蔽原因t_course实体类里的字段名和数据库列名不一致比如数据库列名是下划线course_typeJava 实体里却写的courseType没有开启驼峰映射的 MyBatis 和 JDBC 都会匹配不上。解决先在 Navicat 里确认表结构真的改过来了再重启 Tomcat 而不是热部署。如果是纯 JDBC 项目检查ResultSet取值时用的列名如果是 MyBatis检查mapUnderscoreToCamelCase是否开启。字段名对齐这个点每届都有同学踩改的时候最好一次把实体类、DAO SQL、页面显示三处全部同步。5.5 演示现场中文乱码URL 参数和页面各有一套编码现象本地运行一切正常换到教室电脑上演示时搜索课程名输入“数据库”页面结果里中文全是问号。原因Tomcat 8 之后默认 URI 编码是 UTF-8但项目里的 JSP 页面可能写的是GBK或者数据库连接 URL 上没带characterEncodingutf8。三者任何一处不一致中文参数就会在传递过程中变成乱码。解决统一编码。JSP 页面顶部加上% page contentTypetext/html;charsetUTF-8 languagejava %数据库 URL 上加characterEncodingutf8Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。这三处设成一致之后再把表和字段的 collation 改成utf8mb4_general_ci中文问题基本绝迹。演示前最好让同学用另一个浏览器开一次双保险。6. 提交课程设计前按这份清单自检一遍别把翻车留到现场演示做完功能不等于能交付。我一般会在提交前半小时跑一组“验收用例”把所有边界条件一次性过掉。你不用写自动化测试打开页面手动点就行但顺序要对。用例操作步骤预期结果重复选课对同一门课连续点两次选课第一次成功第二次提示“已选过”满员拦截找一门已满课程点选课提示“人数已满”记录表不新增时间冲突选一门与已选课同时段的新课提示“时间冲突”无法选课退课回补退掉一门课再重新选名额立即回补可再次选上会话过期直接访问课程列表 URL跳回登录页最后一个用例尤其容易忽视。很多学生只在登录状态下点页面从没试过不登录直接访问/course?actionlist。如果你没在 Servlet 里写 session 校验这个 URL 会直接打开列表页甚至报空指针老师随便点一下就露馅。我见过的稳妥做法是在相关 Servlet 的doGet和doPost开头加一段统一的登录检查取不到loginUser就重定向到login.jsp。另外一个血泪经验是把数据库脚本重新执行一遍再打包。很多同学项目跑得好好的是因为本地数据库里残留了手工改过的数据一旦换电脑重新导入原始 SQL页面直接空白。你提交前删掉当前库用附带的 SQL 脚本从头建一次再按上面的用例走一遍能过就说明这套源码包在陌生环境里也有自愈能力。这比临时补功能更能保住印象分。我自己的习惯是最后再看一眼db.properties里的密码如果里面写的是作者同学的真实密码我会改成自己的再打包并顺手在注释里标清楚“本机测试密码”。这种小的细节答辩老师问起来反而是加分项。课程作业的源码包不追求复杂追求的是“换个环境也能稳稳跑起来”。希望这份排查清单能帮你少走一次弯路祝演示顺利。本文还有配套的精品资源点击获取