资讯动态

JavaWeb学生选课系统实战:从Servlet到数据库事务的完整开发指南

发布时间:2026/8/28 1:29:58 来源:尧图企业网站定制
简介管理信息系统MIS是计算机科学中用于处理日常业务操作的核心软件类型其设计通常遵循经典的三层架构模式即表现层、业务逻辑层和数据访问层。这一架构通过分离关注点来提升代码的可维护性和可扩展性。在Java Web开发领域Servlet作为处理HTTP请求与响应的基础技术组件是理解Web应用程序生命周期和MVC设计模式原理的基石。结合JDBC进行数据库操作开发者能够深入掌握SQL执行、连接池管理以及保证数据一致性的关键机制——数据库事务。这些技术的工程实践价值在于它们为构建稳定、可靠的企业级应用提供了底层支撑例如在高并发场景下的资源争用问题就需要通过事务控制与锁机制来妥善解决。典型的应用场景包括各类需要处理复杂业务规则、用户权限管理和数据关联关系的系统如本文所探讨的学生选课管理系统它完整涵盖了用户鉴权、业务闭环和并发控制等核心实践。1. 项目概述与核心价值最近在整理硬盘翻出来一个压箱底的老项目——一个用JavaWeb技术栈实现的学生选课管理系统。这个项目虽然技术栈不算新潮但麻雀虽小五脏俱全从数据库设计到前后端交互再到业务逻辑的完整闭环非常适合用来理解一个典型的管理信息系统MIS是如何从零搭建起来的。无论是计算机专业的学生做课程设计、毕业设计还是刚入行的Java开发者想找一个完整的项目来练手、巩固基础这个项目都能提供一条清晰的路径。它涵盖了用户登录鉴权、学生选课退课、教师发布课程、管理员管理用户和课程等核心功能数据库设计也包含了典型的关联关系。接下来我就把这个项目的完整实现思路、关键技术细节以及我在开发过程中踩过的坑和总结的经验毫无保留地分享出来。2. 系统整体架构与设计思路拆解2.1 技术选型背后的考量这个项目采用经典的JavaWeb三层架构表现层Web Layer、业务逻辑层Service Layer、数据访问层DAO Layer。技术栈选择上我用了Servlet JSP JDBC MySQL的组合。很多人可能会问为什么不用更流行的Spring Boot我的考虑是对于学习而言从最基础的Servlet开始能让你更透彻地理解Web请求的生命周期、MVC模式的思想本源。当你亲手用Servlet处理过请求和响应用JDBC写过原生的SQL和事务控制再过渡到Spring MVC和MyBatis乃至Spring Boot你会对框架帮你做了什么、为什么这么做有更深刻的理解而不是仅仅停留在配置和注解的层面。这就像学开车先用手动挡摸清了离合、油门、档位的配合再开自动挡会感觉格外轻松并且应对复杂路况时心里更有底。数据库选择MySQL一是因为它开源、免费、资料丰富二是它的应用极其广泛从这种中小型项目到互联网大厂的部分业务场景都在用学好了不吃亏。前端用JSP配合JSTL标签库和EL表达式虽然现在前后端分离是主流但JSP在服务器端渲染、数据绑定的学习上依然有其直观的优势。整个项目用Maven进行依赖和构建管理这几乎是现代Java项目的标配早点熟悉pom.xml的配置只有好处没有坏处。2.2 核心功能模块与业务流程设计系统主要围绕三类用户展开学生、教师、管理员。他们的核心诉求和业务流程构成了系统的骨架。学生端的核心流程是“选课-查看-退课”。学生登录后应能查看所有可选课程需考虑时间冲突、先修课限制、人数是否已满等业务规则进行选课操作并能在个人课表中查看已选课程在特定时间窗口内如开学前两周进行退课。这里面的状态流转和业务规则校验是业务逻辑层的重点。教师端的核心是“课程管理”。教师可以发布自己负责的新课程填写课程名、简介、学分、容量、上课时间地点等可以查看自己名下课程的学生选课名单并可能在期末录入成绩虽然本版本可能不包含成绩管理但数据结构上应预留扩展性。管理员端扮演“上帝视角”负责基础数据的维护。包括学生、教师账号的增删改查所有课程信息的审核与管理例如禁用一门问题课程以及系统基础数据的初始化。这三条业务线看似独立但在数据层面紧密耦合。例如一门课程course表由一位教师teacher表创建被多名学生student表选择这就产生了student_course这样的关联表来记录选课关系。如何高效、安全地处理这些关联数据的增删改查特别是涉及事务的操作如学生选课需要同时减少课程余量、增加学生选课记录是数据库和代码设计的难点。2.3 数据库设计表结构与关系图谱数据库设计是系统的基石设计得好后续开发事半功倍设计得差则处处掣肘。我遵循了基本的数据库范式但也没有过度设计在性能和清晰度之间做了权衡。核心表大概有这几张用户基础表 (user): 这是一个通用表通过user_type字段如0-管理员1-教师2-学生来区分角色。包含id,username,password加密存储,real_name,user_type等字段。这种设计减少了表的数量但可能牺牲了不同角色特有字段的扩展性。另一种方案是为每种角色建独立的表并通过外键关联到user表。我这里采用了单表设计主要是为了简化登录和权限验证逻辑。学生表 (student): 包含student_id学号可与user.id关联或作为登录账号、class_name班级、enrollment_year入学年份等。与user表通过user_id关联。教师表 (teacher): 包含teacher_id工号、title职称、department院系等。同样与user表关联。课程表 (course): 这是核心表之一。字段包括course_id课程号、course_name、teacher_id外键关联教师、credit学分、total_capacity总容量、selected_count已选人数需实时更新、schedule_time上课时间如“周一 1-2节”、location地点、description简介、status状态如1-开放选课0-已关闭。选课关系表 (student_course): 这是典型的“多对多”中间表。字段包括id主键、student_id、course_id、select_time选课时间、score成绩可为空为后续扩展留口子。这里(student_id, course_id)应设置唯一约束防止同一学生重复选择同一门课。注意关于course表中的selected_count字段这是一个“冗余”字段其数据完全可以通过SELECT COUNT(*) FROM student_course WHERE course_id ?查询得到。我之所以加上它是为了在高并发选课场景下避免频繁进行COUNT聚合查询提升“查询课程余量”的性能。但这带来了数据一致性的挑战每次增删student_course记录时都必须同步更新course.selected_count。这必须在一个数据库事务中完成否则会导致数据不一致。这是典型的“用空间换时间”和“保证事务一致性”的权衡。3. 核心模块实现与关键技术细节3.1 用户登录、鉴权与会话管理这是系统的安全门户。流程是用户提交用户名密码 - 服务器验证 - 创建会话Session - 后续请求通过Session校验权限。密码处理绝对不要在数据库明文存储密码我采用MD5加盐Salt加密。虽然现在更推荐BCrypt或PBKDF2但MD5加盐对于学习项目足够说明问题。盐值是一个随机字符串与密码拼接后再进行MD5哈希。这样即使两个用户密码相同存储在数据库的哈希值也不同能有效抵御彩虹表攻击。// 示例密码加密工具类方法 public static String encrypt(String password, String salt) { String saltedPassword salt password; return DigestUtils.md5Hex(saltedPassword); // 使用Apache Commons Codec }用户注册或修改密码时调用此方法生成密文存入数据库。登录时用同样的盐值对用户输入的密码进行加密再与数据库存储的密文比对。会话管理用户登录成功后我会将用户的主要信息如user_id,username,user_type存入HttpSession对象中。HttpSession session request.getSession(); session.setAttribute(currentUser, userObject);对于需要登录才能访问的Servlet如/student/selectCourse我会在doGet或doPost方法开头进行拦截检查User user (User) session.getAttribute(currentUser); if (user null || user.getUserType() ! UserType.STUDENT) { response.sendRedirect(/login.jsp?errorunauthorized); return; // 非常重要直接返回不再执行后续业务代码 }权限控制除了在每个Servlet里手动判断更优雅的方式是使用过滤器Filter。可以编写一个AuthenticationFilter配置它拦截所有需要权限的路径如/student/*,/teacher/*,/admin/*在过滤器中统一进行Session和角色校验。这样避免了在每个Servlet中重复校验代码符合DRYDon‘t Repeat Yourself原则。3.2 学生选课业务逻辑与并发控制这是系统最核心、也最容易出问题的业务。选课的基本逻辑是检查课程状态是否开放、检查是否已选过该课程、检查课程容量是否已满、检查时间是否冲突可选但很重要然后执行选课操作。关键SQL与事务-- 1. 查询课程信息悲观锁或乐观锁基础 SELECT selected_count, total_capacity, status FROM course WHERE course_id ? FOR UPDATE; -- 使用 FOR UPDATE 进行行级锁防止其他事务同时修改该课程数据。这是悲观锁做法。 -- 2. 检查是否已选 SELECT COUNT(*) FROM student_course WHERE student_id ? AND course_id ?; -- 3. 检查时间冲突假设有简单的schedule_time字符串字段 -- 这需要更复杂的业务逻辑可能需要另一张课程时间详情表。此处简化。 -- 4. 如果所有检查通过执行插入和更新必须在同一事务中 START TRANSACTION; INSERT INTO student_course (student_id, course_id, select_time) VALUES (?, ?, NOW()); UPDATE course SET selected_count selected_count 1 WHERE course_id ?; COMMIT;实操心得高并发选课的坑与解决思路如果选课系统面向大量学生同时抢少量热门课程比如“名师公选课”上面的FOR UPDATE悲观锁会成为性能瓶颈导致大量请求串行化数据库连接池被迅速占满。我遇到过测试时一抢课系统就卡死的情况。优化方案1乐观锁。在course表增加一个version版本号字段。更新时带上版本号条件。UPDATE course SET selected_count selected_count 1, version version 1 WHERE course_id ? AND version ? AND selected_count total_capacity;执行后检查受影响的行数executeUpdate()的返回值。如果为0说明更新失败可能是版本号不对或容量已满则回滚事务提示用户“选课失败请重试”。这种方式并发度高但用户体验可能稍差需要前端配合重试。优化方案2队列削峰。将选课请求放入消息队列如RabbitMQ、RocketMQ后端Worker逐个处理。这能保证数据库压力平稳顺序处理绝对避免超卖但系统复杂度上升且选课结果不是实时的。对于课程设计级别的项目使用乐观锁是一个在复杂度和效果上取得较好平衡的选择。务必在service层方法上添加Transactional注解如果用了Spring或手动管理JDBC事务确保INSERT和UPDATE是一个原子操作。3.3 前后端数据交互与JSP页面渲染前端页面主要使用JSP。JSP中应尽量避免嵌入大量的Java代码% ... %而是使用JSTLJSP Standard Tag Library和ELExpression Language表达式来展示数据使页面更清晰。Servlet处理数据并传递到JSP// 在Servlet的doGet方法中查询学生已选课程列表 ListCourse selectedCourses courseService.getCoursesByStudentId(studentId); request.setAttribute(courseList, selectedCourses); // 将数据放入请求域 request.getRequestDispatcher(/student/myCourses.jsp).forward(request, response); // 转发到JSPJSP页面使用JSTL和EL渲染% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table trth课程号/thth课程名/thth学分/thth操作/th/tr c:forEach items${courseList} varcourse tr td${course.courseId}/td td${course.courseName}/td td${course.credit}/td td c:if test${course.canDrop} !-- 业务逻辑判断是否在退课期内 -- a href/student/dropCourse?courseId${course.courseId} onclickreturn confirm(确定退课)退课/a /c:if /td /tr /c:forEach /table表单提交与中文乱码这是新手必踩的坑。POST提交表单时如果包含中文服务器端接收到的往往是乱码。解决方案是配置一个字符编码过滤器CharacterEncodingFilter或者在每个Servlet的doPost方法最开始添加request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8);同时确保你的JSP页面、数据库连接字符串都统一使用UTF-8编码。3.4 数据库连接池与DAO模式封装直接在每个Servlet中创建和关闭数据库连接Connection是极其低效和危险的会导致连接泄露最终拖垮数据库。必须使用数据库连接池。我选用的是Apache DBCP2或者HikariCP性能更好在context.xmlTomcat配置或通过代码进行配置。DAOData Access Object模式将数据库操作封装在独立的DAO类中Service层调用DAOServlet调用Service。这样分层清晰便于维护和单元测试。例如CourseDao接口定义findById,save,updateSelectedCount等方法CourseDaoImpl用JDBC实现。一个常见的JDBC工具类用来获取连接、释放资源。注意获取的连接来自连接池。public class JdbcUtil { private static DataSource dataSource; // 连接池数据源 static { // 初始化连接池例如使用HikariConfig } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 反向关闭资源注意处理异常但通常不抛出 } }在DAO实现中模式化地使用Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { conn JdbcUtil.getConnection(); pstmt conn.prepareStatement(SELECT * FROM course WHERE status ?); pstmt.setInt(1, 1); rs pstmt.executeQuery(); // ... 处理结果集 } catch (SQLException e) { // 记录日志根据需要抛出运行时异常或业务异常 throw new DaoException(查询课程失败, e); } finally { JdbcUtil.close(conn, pstmt, rs); // 确保资源被释放回连接池 }4. 项目部署、测试与常见问题排查4.1 本地开发环境搭建与项目导入必备软件JDK 8或11、Maven 3.6、MySQL 5.7、Tomcat 9、IntelliJ IDEA或Eclipse。数据库初始化在MySQL中创建一个数据库如course_selection_db然后运行项目sql目录下的初始化脚本init_schema.sql和init_data.sql创建表并插入一些测试数据如管理员账号、测试学生教师账号、样例课程。导入项目在IDEA中选择“Open”或“Import Project”指向项目的pom.xml文件IDEA会自动识别为Maven项目并下载依赖。配置数据库连接修改项目src/main/resources目录下的db.properties或jdbc.properties文件填写正确的MySQL地址、端口、数据库名、用户名和密码。配置Tomcat在IDEA中“Add Configuration”添加一个“Tomcat Server Local”将“Deployment”选项卡中的“Artifact”设置为项目生成的WAR包。通常IDEA的Maven工具窗口运行package命令后会在target目录下生成项目名.war文件。启动与访问启动Tomcat在浏览器访问http://localhost:8080/你的项目上下文路径/login.jsp。上下文路径在Tomcat配置的Deployment中设置默认为/即直接访问项目名。4.2 功能测试要点与场景设计测试不能只走“阳光大道”要专门去走“泥泞小路”。以下是一些必须测试的场景边界测试学生选课尝试选择一门容量已满的课程系统应提示“容量已满”。学生选课尝试重复选择同一门课程系统应提示“已选过该课程”。学生退课尝试在退课截止时间后退课系统应禁止并提示。教师发布课程尝试设置课程容量为0或负数系统应做前端和后端双重校验。并发测试模拟抢课使用JMeter或Postman的Runner功能模拟几十上百个并发请求同时抢一门只有10个容量的课。检查最终选课成功人数是否正好为10数据库selected_count是否正确有无超卖。异常流测试登录时输入错误密码。访问/admin/manager.jsp而未登录或使用学生账号登录后访问该页面应被重定向到登录页或无权限页。在表单提交时通过浏览器开发者工具禁用JavaScript校验提交空数据或非法数据后端是否仍有健壮的校验。分页测试如果课程列表或学生名单实现了分页测试首页、末页、中间页、每页条数切换是否正常。4.3 常见问题排查与调试技巧开发过程中你肯定会遇到各种问题。这里记录几个典型的问题启动Tomcat时报告“java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver”排查这是最经典的驱动包问题。首先检查pom.xml中是否声明了MySQL Connector/J的依赖。dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version !-- 版本需与你的MySQL匹配 -- /dependency解决如果依赖存在检查是否成功下载。在IDEA的Maven工具窗口点击“Reimport”。另外确保Tomcat部署的WAR包包含了这个依赖。对于Tomcat通常需要将依赖包打到WAR的WEB-INF/lib下使用Maven的war打包方式默认会包含。问题页面显示中文乱码或插入数据库后中文变成问号“??”系统性排查JSP页面确保页面开头有% page contentTypetext/html;charsetUTF-8 languagejava %。Servlet请求在doPost方法第一行设置request.setCharacterEncoding(UTF-8)。数据库连接JDBC连接URL中要指定字符集如jdbc:mysql://localhost:3306/course_db?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai。数据库与表创建数据库和表时指定字符集为utf8mb4推荐支持更全的字符如emoji。技巧在Servlet中接收到参数后立刻打印到控制台或日志看是否是乱码从而定位问题发生在哪一环节。问题点击按钮提交表单后页面没反应浏览器控制台报404或500错误排查404路径错误。检查Servlet在web.xml中的配置如果用了WebServlet注解则检查注解路径以及JSP页面中表单的action属性或链接的href属性是否写对了。注意相对路径和绝对路径。建议在JSP中使用${pageContext.request.contextPath}来获取应用上下文路径如action${pageContext.request.contextPath}/student/selectCourse。500服务器内部错误。这是最需要关注的。立刻查看Tomcat的日志在IDEA的Run控制台或Tomcat的logs/catalina.out文件。错误堆栈信息会明确指出是哪一行代码出了问题是空指针异常NullPointerException、SQL异常SQLException还是其他。根据错误信息去定位代码。问题事务似乎没生效部分操作成功了部分失败了排查首先确认你的操作是否真的在一个数据库事务内。对于纯JDBC你需要手动设置conn.setAutoCommit(false)并在所有操作成功后conn.commit()在catch块中conn.rollback()。确保在finally块中恢复autoCommit状态并关闭连接。更深层原因如果你在Service层调用了多个DAO方法但每个DAO方法都自己获取和关闭了连接那么它们使用的是不同的Connection对象自然无法组成一个事务。解决方案使用“ThreadLocal”模式将Connection对象与当前线程绑定确保在同一个线程即同一个请求中Service和DAO使用的是同一个Connection。或者直接引入Spring框架利用其声明式事务管理Transactional完美解决此问题。5. 从项目实践到技能进阶的思考做完这个基础版本并不意味着结束而是一个开始。你可以基于此进行各种方向的扩展和深化这才能真正提升你的工程能力。方向一引入框架重构项目Spring Spring MVC MyBatis (SSM)这是国内最主流的JavaWeb框架组合。尝试用Spring的IoC容器管理Service和DAO对象用Spring MVC的Controller替代Servlet用MyBatis的Mapper接口和XML文件替代手写JDBC。你会体会到依赖注入、AOP、声明式事务管理等概念带来的便利。Spring Boot更进一步使用Spring Boot简化配置快速搭建一个可独立运行的、生产级别的应用。你会接触到application.properties配置、Starter依赖、自动配置等现代Spring开发理念。方向二前端技术升级将JSP替换为前后端分离架构。后端提供RESTful API使用Spring MVC的RestController前端使用Vue.js或React等现代框架。这需要你学习HTTP API设计、JSON数据格式、以及前端构建工具如Webpack。你会对网络应用的分层有更深刻的认识。方向三提升系统质量与可维护性单元测试为Service层和DAO层编写JUnit单元测试使用Mockito模拟依赖保证核心业务逻辑的正确性。日志引入SLF4J Logback日志框架替代System.out.println()为系统添加可分级、可配置的日志输出便于线上问题排查。代码规范与重构审视自己的代码是否有过长的方法重复的代码块不清晰的命名尝试进行重构提高代码的可读性和可维护性。这个学生选课管理系统项目就像一块很好的“磨刀石”。它涉及了Web开发的完整链路需求分析、数据库设计、后端业务逻辑、前端展示、部署测试。通过亲手实现它并尝试去解决其中遇到的并发、事务、编码、调试等问题你所获得的经验远比单纯看教程要扎实得多。最后在开发中养成记录的习惯把遇到的问题和解决方案就像本文这样整理下来这将成为你个人最宝贵的知识财富。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价