
简介JavaWeb学生选课系统是一套面向高校课程设计与JavaWeb初学者的完整项目源码包帮助学习者掌握选课业务中的后端接口、数据库设计、多角色权限控制等核心开发环节。项目基于Eclipse环境构建运行于Tomcat8搭配MySQL5.5及以上数据库要求JDK1.8整体采用MVC分层架构并集成Servlet、JSP、JDBC及AJAX技术前端页面与后端逻辑分离便于阅读和二次开发。压缩包为zip格式仅2.83MB内部共161个文件包括36个Java源文件、36个Class编译文件、28个JSP页面、4个CSS样式、4个JS脚本、4个JAR依赖库、33张PNG图片、1个SQL数据库脚本及Eclipse工程配置其中Java负责业务处理JSP负责界面展示SQL用于初始化数据表导入工作空间即可对照运行。功能上覆盖管理员、教师、学生三大模块涉及用户账号管理、课程信息维护、选课时间与容量控制、选课冲突检测、作业提交、成绩查询等典型业务并包含课程容量、冲突检测、时间窗口等复杂选课逻辑代码中提供DBUtil数据库工具类和各业务Service处理逻辑便于按模块学习或改造扩展。已有1673人学习下载项目完成度较高、目录结构清晰是JavaWeb课程设计与毕业设计选题中可直接参考复用的优质示例。1. 项目概览与技术选型1.1 为什么选课系统是JavaWeb练手的黄金案例我带过不少初学JavaWeb的朋友问他们第一个完整项目做什么我基本都推荐学生选课系统。不是因为它名字常见而是它的业务复杂度卡得刚刚好——比单纯的增删改查有内容又不会像电商系统那样把订单、库存、支付、秒杀全搅进来让人一上来就懵掉。选课系统天然自带几个经典业务场景学生登录验证、课程列表分页、选课时的名额校验、重复选课拦截、退课后名额释放。这里面涉及的“登录会话管理”“事务控制”“唯一索引约束”“分页查询”几乎就是JavaWeb面试和实际开发里最常碰到的技术点。而且数据表的关系非常清晰学生和课程是多对多中间一张选课表就能把多表联查练得明明白白。另一个好处是扩展空间大。做完一个基础版本之后你可以顺手加一个教师模块让老师开课、加一个成绩模块录入分数或者把前端从JSP升级成Vue 接口这些都是看得见摸得着的成长路径。不像有些练手项目做完了就真完了你连重构都不知道怎么下手。1.2 技术栈选型先手写Servlet还是直接上框架很多初学者纠结现在企业都Spring Boot了我还有必要用JSP Servlet JDBC手写一套吗我的观点很明确——第一个项目必须手写第二个项目再上框架。原因不复杂。学生选课系统的核心难点不在“代码量”而在“理解请求走的每一步”。你用Servlet接收请求自己调DAO查数据库自己把数据set进request域再转发给JSP渲染这一条链路上的每一步你都会遇到实实在在的问题编码乱码、连接没关、事务没提交、Session失效。这些坑如果第一次踩是在Spring Boot里你连排查方向都没有因为框架帮你把很多细节藏起来了。手写一遍哪怕代码丑一点你对整个JavaWeb请求链路会有脱胎换骨的理解。技术栈我推荐这套经典组合JDBC Servlet JSP MySQL Tomcat再加一个Druid连接池。连接池一定要加直接写DriverManager.getConnection()在Demo里看着简单但并发一上来就会频繁创建和销毁连接系统性能会很难看。Druid还能顺便监控SQL执行情况调试时特别好用。前端就用JSP配合一点CSS别急着上前后端分离那是第二个项目该干的事。2. 数据库设计先把地基打牢2.1 三张核心表的设计思路我做这个项目时候的建表语句经过几次调整后稳定成这样你可以直接参考CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, password VARCHAR(64) NOT NULL, name VARCHAR(20) NOT NULL, major VARCHAR(50) COMMENT 专业, class_name VARCHAR(50) COMMENT 班级 ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, name VARCHAR(50) NOT NULL COMMENT 课程名称, teacher VARCHAR(20) COMMENT 授课教师, credit INT COMMENT 学分, max_student INT NOT NULL DEFAULT 50 COMMENT 限选人数, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, schedule VARCHAR(100) COMMENT 上课时间地点 ); CREATE TABLE sc ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) );三张表的职责很明确。student和course是基础数据sc表是中间关系表。这里有个小细节值得说sc表里我没有用student_no和course_no这种业务编号而是用student表的主键id和外键逻辑对应。好处是多表联查走主键索引效率高如果以后学号规则变了sc表不需要动。你可能会问为什么不用物理外键这是我在实际项目里踩出来的经验——教学项目里加物理外键看起来规范但后面你导入数据、做联表删除时会被约束纠缠而且真正生产环境很多团队是刻意不用物理外键的靠应用层保证数据关系。所以这里用逻辑外键更适合。2.2 约束设计防重复选课和超员选修选课系统最容易出问题的两个点是同一学生选同一门课选了两次以及课程人数超限。重复选课这个坑靠Java代码判断“有没有选过”是能拦住大多数情况但并发请求下会有漏网之鱼。真正的兜底是在数据库层面加唯一索引就是上面sc表里的UNIQUE KEY uk_student_course (student_id, course_id)。这个索引一建你就算代码逻辑漏了数据库也会直接报DuplicateEntry错误把脏数据挡在门外。人数超限就得靠事务解决了。course表里我专门维护了一个selected_count字段选课时执行的是“查空余名额、插入选课记录、更新已选人数”三步操作。这三步必须放在同一个事务里否则两个学生同时选最后一门课的最后一个名额时都会读到名额还剩1然后双双选课成功超员了。Java代码里用Connection关闭自动提交conn.setAutoCommit(false); try { // 查询当前已选人数判断是否有余量 // 插入选课记录 sc // 更新 course 的 selected_count 加 1 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; }有人会问把selected_count去掉每次查count(*)不是也行可以但课程列表页每次都要展示已选人数每行都count一次子查询数据量上来以后非常吃性能。维护一个冗余字段用事务保证一致性是性价比更高的方案。3. 开发环境搭建与第一个JavaWeb程序3.1 新版IDEA创建JavaWeb项目的正确姿势搜“JavaWeb”相关热搜时能看到很多人卡在环境搭建这一步尤其是2023版之后的IDEA创建JavaWeb项目的入口和以前不太一样网上的老教程经常对不上。我用的是新版IDEA创建项目时直接选Maven模板archetype选择org.apache.maven.archetypes:maven-archtype-webapp一路下一步生成项目骨架。如果你的IDEA里这个模板拉取特别慢也可以先创建一个普通的Maven项目然后在项目结构里手动添加Web支持效果一样。生成之后pom.xml里加依赖是第一步这几个坐标是跑通项目的基础dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependencyservlet-api和jsp-api这两个一定要用provided作用域因为Tomcat本身自带这些类。我之前遇到过有人把这两个依赖打成全量包部署到Tomcat后报各种类冲突排查半天才发现是重复依赖的问题。运行配置这块网上很多教程让你下载Tomcat后手动配置新版本IDEA其实已经内置了Tomcat集成。在Run/Debug Configurations里选择Tomcat Server点击Local指定本地Tomcat路径。Deployment选项卡里把Artifact选成xxx:war explodedApplication context设为/这样访问项目就不用带多余路径前缀调试体验好很多。3.2 数据库连接与驱动版本坑数据库连接推荐用Druid连接池配置写到db.properties代码里通过一个工具类读取避免每写一个DAO就重复一套获取连接逻辑driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/student_course?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue usernameroot password你的密码 initialSize5 maxActive20url这段是重点。MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver。serverTimezoneAsia/Shanghai不加的话连接时大概率报时区错误allowPublicKeyRetrievaltrue是应对MySQL 8.x公钥检索报错的characterEncodingutf8不写的话后面中文乱码会折磨你一整晚。这些都是我在实际项目里一个个踩过坑之后才稳定下来的参数。4. 核心功能实现流程4.1 登录验证与Session会话管理登录模块看着简单但它涉及整个项目的基础安全机制。我的实现方式是登录页面提交学号和密码到LoginServletServlet调用StudentDAO查询用户查到了就把student对象放进Session然后重定向到课程列表主页查不到就转发回登录页附带一条错误提示。Session这一步很关键因为JavaWeb的HTTP协议是无状态的你不记住“当前浏览器是谁”用户一刷新页面就变成游客了。我在选课系统的做法是登录成功后执行session.setAttribute(student, student)后面所有需要登录才能访问的页面都先检查Session里有没有这个属性。检查逻辑不要每个Servlet重复写用Filter统一拦截。我写了一个LoginFilter过滤所有请求放行登录页面、登录Servlet、CSS等静态资源其余请求一律先看Sessionpublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session ! null session.getAttribute(student) ! null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() /login.jsp); } }注意这里有个细节request.getSession(false)和request.getSession()的区别。传false表示如果当前没有Session就返回null而不是新建一个。你如果在这里写getSession()那所有没登录的请求也会被强制创建SessionFilter的判断就失去意义了。4.2 选课与退课事务处理和并发问题选课是整个系统复杂度的核心。前面数据库设计部分我说过选课要在一个事务里完成“校验余量、插入记录、更新人数”这三个动作。但落实到代码上还需要做一次“是否已选过”的校验。有人会把所有代码堆在一个Servlet里我强烈不建议。哪怕是小项目也至少拆成三层Servlet处理请求参数和页面跳转Service层处理业务逻辑DAO层只负责SQL执行。选课功能的Service层方法大概是这个结构public boolean selectCourse(int studentId, int courseId) throws SQLException { Connection conn JDBCUtils.getConnection(); try { conn.setAutoCommit(false); CourseDao courseDao new CourseDao(); ScDao scDao new ScDao(); // 1. 判断是否已选过 if (scDao.exists(conn, studentId, courseId)) { return false; // 重复选课 } // 2. 查询课程余量 Course course courseDao.findById(conn, courseId); if (course.getSelectedCount() course.getMaxStudent()) { return false; // 人数已满 } // 3. 插入选课记录 scDao.insert(conn, studentId, courseId); // 4. 已选人数 1 courseDao.increaseSelectedCount(conn, courseId); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); JDBCUtils.close(conn); } }这里给每个DAO方法都传了Connection参数就是为了保证四个步骤用的是同一个数据库连接。这是事务控制的核心——你如果每个DAO方法内部自己去获取连接事务就割裂了commit的只是最后一步前面失败的操作用rollback根本回滚不掉。退课的逻辑正好相反删除sc记录同时selected_count减一。但有一点值得提醒退课接口必须校验这条选课记录确实是当前登录学生本人的不能只凭请求参数里的学生id操作否则学生A可以退掉学生B的课。正确做法是从Session里拿当前登录学生的id去匹配。4.3 课程列表与分页查询课程列表如果课程数量多全查出来一次渲染到页面上页面会变得冗长数据库压力也大。分页是必须做的。分页查询的核心逻辑分两部分第一部分是查总记录数用来计算总页数第二部分是查当前页的数据用LIMIT实现。Service层大概是这个流程int pageSize 5; // 每页显示数量 int currentPage Integer.parseInt(request.getParameter(page) null ? 1 : request.getParameter(page)); int totalCount courseDao.count(); int totalPages (totalCount pageSize - 1) / pageSize; if (currentPage 1) currentPage 1; if (currentPage totalPages) currentPage totalPages; int offset (currentPage - 1) * pageSize; ListCourse courseList courseDao.findPage(offset, pageSize);对应的SQL就是SELECT * FROM course ORDER BY id LIMIT ?, ?LIMIT的前一个参数是offset也就是从第几条开始取计算公式是(currentPage-1)*pageSize后一个参数是查询条数。这个公式容易记混我通常把它理解为第一页之后每往后翻一页就要跳过pageSize条数据。页面底部还需要渲染页码导航我直接用JSP的forEach循环遍历总页数生成链接。这里有个小坑要提醒连接参数用拼接时在JSP或HTML里要写成amp;否则页面校验严格时会报错或者参数被截断。5. 常见问题与排查技巧实录5.1 高频报错速查表做这个项目的过程中我把遇到过的典型问题整理成了一张表后来带朋友做的时候基本照着这个排查现象原因解决方法ClassNotFoundException: com.mysql.jdbc.DriverMySQL 8.x驱动类路径变了配置文件改成com.mysql.cj.jdbc.Driver连接数据库时区报错驱动要求明确时区url加serverTimezoneAsia/Shanghai中文全部变成问号页面编码或传输编码不一致JSP统一UTF-8url加characterEncodingutf8Tomcat端口被占用上次没正常关闭或别程序占用改端口或杀掉占用进程页面404访问路径和Servlet映射不匹配检查web.xml/WebServlet路径和form action选课前检查人数正常选完后超员并发请求导致同时读到余量用事务包裹校验和更新必要时加行锁项目里新增Servlet后没生效IDEA里Tomcat没重新部署重新构建Artifact并重启Tomcat5.2 调试技巧让SQL和异常显形新手调试JavaWeb项目最痛苦的是“页面报错但看不到具体原因”。我的经验是先养成随手打印日志的习惯。在DAO的方法里执行SQL前打印一下参数执行后打印影响行数在Service层catch住异常后除了往上抛也要e.printStackTrace()看一眼完整堆栈。IDE控制台里的异常信息基本上就能定位九成问题。另一个好用的调试手段是直接看数据库端的情况。把Druid连接池的stat-view-servlet开启后访问/druid/index.html可以看到当前活跃连接数、SQL执行次数和慢SQL记录。我之前有一次页面响应特别慢用Druid监控一查发现某条SQL没有走索引全表扫描了给字段补上索引后立刻恢复。还有一点排查问题时要学会用浏览器开发者工具。按F12打开Network面板提交选课请求后看这个请求的Status和Response。如果是500说明服务器异常如果是302说明被重定向了看Location指向哪能帮你快速判断是登录拦截的问题还是路由跳转的问题。5.3 我的几点实操心得这个项目我前前后后带人做了很多遍最后分享几个特别容易被忽略的点。第一个是密码存储。教学项目里直接明文存密码还能用但我建议至少用MD5加盐处理一下代码不复杂但安全意识能提早建立。第二个是IDEA里连接数据库时尽量用Database面板直接建表、预览数据、跑SQL。这样可以快速验证表结构是否合理比来回切页面、重启Tomcat效率高得多。第三个是代码里所有用户输入都不能直接拼进SQL用PreparedStatement占位符这一点从第一个项目就该养成习惯等以后做真实项目能省掉无数SQL注入的风险。学生选课系统麻雀虽小五脏俱全。把这个项目完整地走一遍你对JavaWeb的技术全貌会有一个立体的认识再去看框架源码的时候很多原先抽象的概念都能对上号了。本文还有配套的精品资源点击获取