☰
JavaWeb学生选课管理系统源码部署:JSP+Servlet+JDBC+MySQL全链路解析
2026/10/7 14:07:59 网站建设 项目流程

简介:一套基于Servlet和JSP技术实现的学生选课管理系统,面向计算机相关专业需要毕业设计参考或项目实战练习的Java学习者,覆盖管理员、教师、学生三类角色的核心业务场景。系统后端采用JavaWeb与MySQL,前端结合JSP、CSS、Bootstrap和jQuery,完整实现学生信息管理、教师课程管理、成绩录入与查询、学生选课等典型功能,并内置权限控制逻辑,适合作为毕设方案二次开发。压缩包有112个文件,含22个Java源码类、22个class编译文件、21个JSP页面、10个jar依赖,以及数据库SQL脚本、样式表、脚本文件、图片和项目配置,总大小约2.39MB,导入开发工具并配置数据库即可运行。该资源已有554人学习下载,对正在完成毕业设计或希望快速掌握传统JavaWeb分层开发的读者,是一份可直接运行、便于改造的完整范例。

1. 学生选课管理系统这套javaWeb源码,到底能不能直接上手跑?

很多人在网上下载了“javaWeb学生选课管理系统(完整源码+数据库)”这类资源,第一反应是导入IDEA,结果要么一堆红叉、要么Tomcat起不来、要么数据库脚本导入直接报错,最后不得不去求助“idea运行javaweb项目配置”的帖子。这套系统说白了就是一个JSP+Servlet+JDBC+MySQL的经典三层架构课程设计,主要解决学生在线选课、退课,管理员维护课程信息和查看选课统计的需求。它不像Spring Boot那么讲究工程化,但胜在技术栈直接对应课本,非常适合刚学完Servlet和JDBC的初学者、需要交课程设计或毕业设计案例的人,以及想完整看一遍“请求→Servlet→DAO→数据库→JSP渲染”全链路的转行者。

但拿到源码只是开始,能不能跑起来、跑起来之后怎么验证功能是对的,才是真正见功夫的地方。我下面会按“拆解系统→设计数据库→读懂核心代码→在IDEA里跑通→避坑→优化”这条线,把整套流程讲透。

2. 拆开这套学生选课管理系统:功能模块、技术选型与源码结构

2.1 三个角色、两条业务线:功能需求先理清

任何一个学生选课管理系统,核心角色就两个:学生和管理员。有的版本会加一个“教师”角色,用来维护课程和录入成绩,但最常见的课程设计版本还是以学生和管理员为主。

学生这条业务线:登录、浏览可选课程、选课、退课、查看自己已选的课程列表。注意这里有个隐含需求——选课之前要判断课程是否已满,退课之后要释放名额,不然会出现“选课人数超过容量”这种数据不一致问题。

管理员这条业务线:登录、课程的增删改查、查看某门课程的选课学生名单、统计每门课的选课人数。管理员和学生虽然都“登录”,但权限完全不同,学生不能访问管理员页面,这就要求在Servlet层或Filter层做权限控制。

拿到一套源码,第一步不是急着跑,而是先把web.xml里的URL映射和Servlet类名过一遍,弄清楚每个入口对应哪个Servlet,页面跳转的路径是怎么设计的。否则后面排查问题的时候,你会连“这个请求到底走了哪个类”都搞不清楚,全部靠猜测,效率极低。

2.2 为什么是JSP+Servlet+JDBC而不是Spring Boot

“javaWeb项目完整案例mysql”这个关键词下,大多数资源还是JSP+Servlet的老骨架。很多初学者会问:现在企业里都Spring Boot了,学这个是不是过时了?

我的看法是:课程设计这个场景,JSP+Servlet恰恰是最合适的。第一,它直接对应学校教材的章节,答辩的时候老师问“请求是怎么从页面到数据库再返回的”,你能用手画出来整个链路。第二,它没有Spring的自动配置黑匣子,所有连接都要自己写,所有对象都要自己new,反而能逼着你理解HTTP、Session、JDBC这些基础概念。第三,Tomcat部署一个war包就能跑,不像Spring Boot还得理解内嵌容器。等你把这套系统调通,再去看Spring Boot里的DispatcherServlet、MyBatis这些封装,会轻松很多。

当然,如果你拿到的源码是Maven工程并且用了Spring MVC,那也别拒绝,这代表它更像是企业级项目的简化版,跑通之后价值更高。但本文后面以最常见的“纯JSP+Servlet+JDBC+MySQL”版本为主线来讲,因为大多数“完整源码+数据库”的资源都是这个形态。

2.3 拿到源码先看这五个文件:目录结构清单

解压源码包之后,先别急着导入IDEA,花五分钟对着目录结构认一遍文件。常见的目录结构长这样:

路径作用
src/entity/实体类,对应数据库表的Java对象,例如Student.java、Course.java
src/dao/数据访问层,封装JDBC连接和增删改查SQL
src/servlet/控制器层,接收HTTP请求,调用DAO,跳转JSP
src/util/工具类,通常是DBUtil数据库连接工具
WebContent/JSP页面、CSS、JS、图片等静态资源
sql/或者根目录下的.sql文件数据库初始化脚本,建库建表+测试数据
WebContent/WEB-INF/web.xml核心配置文件,Servlet映射、欢迎页、Filter配置

这里最容易犯的错误是只关注src目录里的Java代码,忽略了sql脚本和web.xml。实际上,一个javaWeb项目能不能跑起来,sql脚本决定数据层是否正常,web.xml决定请求是否能被正确分发。很多“完整源码”下载下来发现缺web.xml,或者sql脚本是空文件的情况我都见过,所以我建议你解压之后先确认这三个东西都在:源码目录、sql脚本、web.xml。

3. 先把数据库跑起来:选课管理系统的建库脚本与三张核心表

3.1 从student、course、student_course说起:表结构设计

学生选课管理系统的数据库很简单,但设计得好不好直接影响后面写代码的复杂度。核心就三张表:student学生表、course课程表、student_course选课关联表。设计规范一点,三张表就够了。

student表常见字段:id主键自增、stu_no学号、stu_name姓名、password密码、major专业。course表常见字段:id主键自增、course_no课程编号、course_name课程名、teacher授课教师、credit学分、capacity容量、selected已选人数。student_course表常见字段:id、student_id学生ID、course_id课程ID、select_time选课时间,而且必须加一个联合唯一索引UNIQUE KEY uk_stu_course (student_id, course_id)。

这个联合唯一索引非常重要。很多初版源码没有这个索引,导致同一个学生可以重复选同一门课,数据表里出现两条一模一样的记录。你可能会说“我的Service层已经判断过了”,但程序判断总有漏洞,数据库层面的唯一约束是兜底的。加了这个索引之后,就算代码漏判,数据库也会直接报Duplicate entry错误,至少数据不会脏。

课程表里的selected字段特别值得说一句。它表示“已选人数”,每次有人选课成功就+1,退课就-1。这个字段的存在是为了在选课列表页直接显示“还剩多少个名额”,避免每次都去count选课表。但如果你的源码里没有这个字段,而是在查询的时候临时统计选课人数,也完全可以,只不过SQL会稍微复杂一点。两种方案各有取舍:用冗余字段读写快,但是要维护一致性;用实时统计不会出错,但课程多的时候查询会慢。课程设计阶段,冗余字段+事务控制更常见。

3.2 导入MySQL的完整步骤与常用命令

拿到sql脚本之后,导入数据库的操作其实可以全程在黑窗口里完成。先确保你的MySQL服务是启动状态,然后用root登录:

mysql -uroot -p

输入密码之后,先创建一个数据库,再切换到该库,最后执行source命令导入脚本:

CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARSET utf8mb4; USE course_selection; SOURCE /路径/你的sql脚本文件.sql;

这里要注意三点。第一,路径里的反斜杠在Windows下要改成斜杠,否则会报找不到文件。第二,脚本文件的编码格式最好是UTF-8,如果是GBK,导入之后中文可能乱码,这也是“数据库中文乱码”问题最常见的来源。第三,如果脚本里已经写了CREATE DATABASE和USE语句,那你直接SOURCE就行,不用手动建库。

导入完成之后,用几条常用命令验证一下数据是否正常:

SHOW TABLES; SELECT COUNT(*) FROM student; SELECT * FROM course LIMIT 5;

看到有数据输出,说明数据库这块已经通了。如果SOURCE执行时报语法错误,大概率是下载的sql脚本是用MySQL 8.0的语法导出、而你的MySQL是5.7或更低版本。这时打开sql文件,检查有没有utf8mb4_0900_ai_ci这类字符集排序规则,把它批量替换成utf8mb4_general_ci再导入即可。

3.3 初始化数据为什么要带测试账号

一个设计得好的sql脚本,除了建表,还会预置几条测试数据。比如管理员账号admin/123456,学生账号2023001/123456,以及几门课程、每门课的容量和已选人数。

别小看这些测试数据。我见过不少下载下来的脚本只有建表语句没有任何数据,结果你启动项目之后连登录页面都进不去,因为数据库里根本没有这个用户。有测试数据的另一个好处是,你跑通登录之后立刻能看到课程列表里有内容,可以马上进行选课操作,整个系统是否正常的验证链路是连续的,不需要你自己再去手动INSERT一堆数据。

如果你拿到的脚本里确实没有测试数据,那就自己补上,这是最省事的路子:

INSERT INTO student (stu_no, stu_name, password, major) VALUES ('2023001', '张三', '123456', '计算机科学与技术'); INSERT INTO admin (admin_name, password) VALUES ('admin', 'admin123'); INSERT INTO course (course_no, course_name, teacher, credit, capacity, selected) VALUES ('C001', 'Java程序设计', '李老师', 3, 50, 0);

注意,admin表不是每个版本都有,有的系统把管理员和学生放在同一张表,用role字段区分角色。跑通之前先看清楚你的表结构,别拿着我的SQL往上套。

4. 看懂核心代码:登录校验、选课事务与权限拦截

4.1 登录Servlet:从request参数到session会话

登录功能是所有JavaWeb系统里最经典的入门案例,学生选课管理系统的登录逻辑也不外乎这几步:接收前端传来的用户名和密码,去数据库查询比对,成功就把用户信息放进session,失败就返回错误提示。下面这段是简化后的LoginServlet核心逻辑:

@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("user", user); if ("admin".equals(user.getRole())) { response.sendRedirect("admin/courseList.jsp"); } else { response.sendRedirect("student/courseList.jsp"); } } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }

这段代码的逻辑很直白:从请求里拿参数、查库、判断、跳转。三个容易忽略的细节:

第一,密码比对应该放在哪?我见过有些源码把密码比对写在Servlet里,也就是先把数据库里的密码查出来再在Servlet里equals。这样做不是不行,但最好把“按用户名查用户”和“密码校验”两层分开,否则以后想改成MD5加密存储时,修改范围会很大,搞不好就漏改一处导致登录全线崩盘。

第二,redirect和forward的区别。登录成功用sendRedirect,因为要改变URL地址栏,避免刷新页面时重复提交表单;登录失败用forward,因为需要把error信息带到JSP页面上。这个区别面试也常问,值得在心里过一遍。

第三,session里存什么。只存用户ID和角色就够了,不要存密码,更不要存整个User对象到JSP里直接getPassword展示出来。这既是安全习惯,也是给自己省麻烦。

4.2 选课操作的DAO层:一个坑位一个事务

选课这个操作看似简单,其实是一个典型的“先检查后更新”场景。学生点击选课,后端要做的完整逻辑是:判断课程是否存在→判断选课时间是否截止→判断课程是否已满→判断学生是否已经选过→插入选课记录→更新课程的selected字段。

这六步里,任何一步都可能出问题。最经典的问题就是并发:两个学生同时点选课,都查到了“还剩1个名额”,然后同时执行INSERT,结果课程人数超了。要解决这个问题,代码层面必须加事务,把“插入选课记录”和“更新课程已选人数”绑定在一起,要么都成功,要么都失败:

public boolean selectCourse(int studentId, int courseId) { Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; try { conn = DBUtil.getConnection(); // 开启事务,关闭自动提交 conn.setAutoCommit(false); // 第一步:插入选课记录 String sql1 = "INSERT INTO student_course (student_id, course_id, select_time) VALUES (?, ?, NOW())"; ps1 = conn.prepareStatement(sql1); ps1.setInt(1, studentId); ps1.setInt(2, courseId); ps1.executeUpdate(); // 第二步:更新课程已选人数 String sql2 = "UPDATE course SET selected = selected + 1 WHERE id = ? AND selected < capacity"; ps2 = conn.prepareStatement(sql2); ps2.setInt(1, courseId); int rows = ps2.executeUpdate(); if (rows == 0) { // 影响行数为0,说明课程已满,回滚 conn.rollback(); return false; } // 都成功才提交 conn.commit(); return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps1, ps2); } }

很多人写选课事务的时候容易忽略一个细节:UPDATE语句里要带上AND selected < capacity这个条件。这样一来,即使两个请求同时到达数据库,也只能有一个UPDATE影响行数为1,另一个影响行数为0从而触发回滚。这是利用数据库行锁来防止超卖的关键,比在Java代码里加synchronized靠谱得多。

另一个细节是NOW()这个函数。MySQL里插入当前时间,NOW()和SYSDATE()都能用,但在有主从复制的场景下NOW()更稳,因为它返回的是语句开始执行的时间,而不是实时时间。课程设计阶段用NOW()已经足够,注意别写成CURRENT_TIMESTAMP常量,虽然结果一样,但阅读性差一些。

4.3 web.xml与Filter:学生凭什么进不了管理页

登录校验只是第一步,权限控制才是区分“能用”和“好用”的分水岭。如果学生把浏览器的URL直接改成/admin/courseManage.jsp,结果页面照样打开,那这套系统的权限就是摆设。

用Filter做拦截是标准做法。在web.xml里配置一个过滤器,拦截所有以.jsp结尾的请求,检查session里有没有用户,以及用户的角色是否匹配要访问的路径:

<filter> <filter-name>PermissionFilter</filter-name> <filter-class>com.cs.filter.PermissionFilter</filter-class> </filter> <filter-mapping> <filter-name>PermissionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

对应的Filter实现逻辑大致是:放行login.jsp和静态资源,其他请求都必须有user对象;访问admin开头的路径必须是admin角色;访问student开头的路径必须是student角色。这里有个坑:Filter的url-pattern写在/*时,它也会拦截login.jsp本身。如果你在Filter里忘记放行login.jsp,就会出现“访问登录页面也被重定向到登录页面”的死循环跳转。这是我见过发生率极高的配置失误,几乎每届学生都会在这上面翻一次车。

web.xml里除了Filter,还会配置Servlet映射和欢迎页。如果你拿到源码里web.xml没有配置任何Servlet映射,说明用的是@WebServlet注解方式。两种方式共存的时候要注意,同一个Servlet路径不要同时出现在web.xml和注解里,否则Tomcat启动时会报fatal error,整个项目直接起不来。

5. 在IDEA里跑通完整源码:Tomcat配置、数据库连接与避坑指南

5.1 用IDEA导入项目和配置Tomcat:照着做就能起来的步骤

数据库就绪之后,总算轮到在IDEA里跑通项目了。这一步是“idea运行javaweb项目配置”热搜词背后最集中的痛点,我按步骤说一遍。

第一步,导入项目。打开IDEA,File → New → Project from Existing Sources,选中你解压后的源码目录。如果项目下面有pom.xml,就选Maven;如果没有,就选Web目录直接导入。导入之后IDEA可能会提示“Unlinked Maven Project”,有pom就点右上角的刷新按钮让它下载依赖,没有pom就忽略。

第二步,配置项目结构。File → Project Structure → Modules,选中你的模块,点+号添加Web,把WebContent目录(有的叫webapp或webroot)指定为Web资源目录。然后切到Artifacts选项卡,点击+号选择Web Application: Exploded,从Modules里把刚才配置的Web模块加进去。这一步不做或者做错,后面运行Tomcat时十有八九报“404”或者“Error: Artifact not deployed”。

第三步,配置Tomcat。Run → Edit Configurations → 点+号 → Tomcat Server → Local。在Server选项卡里选择Tomcat安装目录,在Deployment选项卡里点+号选择Artifact,Application context填/或者/course,注意这个路径会拼在URL前面,填错了访问路径就不对。

第四步,改数据库连接配置。找到源码里的DBUtil工具类,确认jdbcUrl、用户名、密码这三点和你本机MySQL一致。用Navicat或命令行把数据库建好并导入脚本后,再来核对这行代码,别弄反顺序。连接串里建议加上useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai,三个参数少一个都可能踩坑。

Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/course_selection?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai", "root", "你的密码");

这里特别说明一下URL参数:“我的MySQL连接为什么报错”,九成是这三种情况——MySQL 8.0的驱动类名要写成com.mysql.cj.jdbc.Driver;时区不设置会报Server returns invalid timezone异常;不加上useSSL=false会看到一堆SSL告警日志刷屏,虽然不影响运行,但很碍眼。驱动JAR包如果导入不进来,检查一下lib目录是否被IDEA标记为库,右键lib目录选Add as Library。

第五步,启动Tomcat。点右侧的绿色三角号启动,控制台输出Server startup in xxx ms就说明启动成功了。访问http://localhost:8080/,就能看到登录页面。

5.2 数据库连接失败、中文乱码、端口占用:三个必查项

数据库连接失败是最常见的启动异常,现象有两种:第一种是IDEA控制台直接报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,说明驱动JAR包没放进来;第二种是Communications link failure,说明端口或者账号密码有问题。先ping一下数据库端口通不通,再确认MySQL服务有没有在运行,最后看账号密码。很多人密码里有特殊字符,比如@符号,在JDBC URL里要用URL编码格式写,否则解析会出错。

中文乱码是另一个高频问题,要分清是页面乱码还是数据库乱码。页面中文乱码,检查JSP文件头部有没有<%@ page contentType="text/html;charset=UTF-8" %>,以及Tomcat的server.xml里Connector有没有配URIEncoding="UTF-8"。数据库乱码,检查建库时字符集是不是utf8mb4,导入的sql文件本身编码是不是UTF-8。这两条排查路径可以并行,修改任意一处之后都要重启Tomcat加重新导入数据,只改一处往往不够。

端口占用问题,现象是启动时报Port 8080 was already in use。处理方式不必杀进程,直接改端口更简单——在Tomcat配置里把HTTP port改成8081,然后访问http://localhost:8081/即可。

5.3 六条血泪踩坑记录:现象、原因与一次性解决

下面这几条是我经手这类项目时遇到概率最高的坑,每一条都按“现象→原因→解决”写清楚,建议你对照排查。

第一个:Tomcat启动成功但访问404。现象是控制台没有报错,日志里也没有Exception,但浏览器访问首页就是404。原因是Artifact没有正确Deploy,或者Web资源目录没有关联到Module。解决方式:检查Deployment选项卡里有没有添加Artifact,检查Facets里关联的Web目录路径是否正确,修改后重启Tomcat。

第二个:登录成功后跳转到manage页面仍然跳到login。现象是登录显示成功,URL却永远停在login.jsp。原因是Filter拦截了所有JSP请求,但登录Servlet只放行了login.jsp,跳转到admin页面时Filter发现session里没有user就重定向了。解决方式:确认LoginServlet里登录成功执行了session.setAttribute("user", user),并且Filter的放行逻辑里对login.jsp和静态资源都做了排除。

第三个:MySQL 8.0版本导入旧版sql脚本报Unknown collation: utf8mb4_0900_ai_ci。原因是MySQL 5.7及更低版本不认识8.0的默认排序规则。解决方式:用文本编辑器打开sql文件,全局替换utf8mb4_0900_ai_ci为utf8mb4_general_ci,再重新导入。

第四个:选课页面点击选课没反应,控制台报Duplicate entry '1-2' for key 'student_course'。现象是第一次选课成功后,刷新页面再点一次同一个按钮就报这个SQL异常。原因是数据库层面加了联合唯一索引,而代码没捕获SQLIntegrityConstraintViolationException,导致错误直接抛给了浏览器。解决方式:在DAO层捕获该异常,返回“你已选过该课程”的业务提示。不要为了省事把唯一索引删掉,那是防超卖的最后防线。

第五个:JSP页面里写Java代码(Scriptlet)报错说变量找不到。现象是页面里<% for (Course c : courseList) { %>报编译错误。原因是Servlet转发时没有把courseList放到request或session里,JSP拿不到。解决方式:回到对应的Servlet查看该列表是否在转发前通过request.setAttribute("courseList", list)写入。

第六个:数据库连接正常,但更新数据后查不出来,或者查出来是旧数据。现象是选课成功后,课程列表页面的已选人数没变。原因可能性很多,但优先级最高的检查项是事务是否提交。在DAO层用了conn.setAutoCommit(false)却没调conn.commit(),事务会一直挂在那里,连接池里的连接都被这些未提交事务占满,后续请求全部卡住。解决方式:检查代码路径上是否有异常导致跳过commit,以及finally里是否正确关闭了连接。

6. 让选课系统扛住并发:从防重复选课到连接池替换

把上面这套系统跑通,其实只达到了“能用”的水平。如果这个课程设计要在答辩现场演示、或者老师要求加一个“选课高峰期不卡顿”的亮点,接下来这个优化就是你的加分项。

第一步,用JMeter或写一个简单的Java并发测试,模拟30个线程同时为同一个学生选同一门课。你会发现即使代码里有事务和selected < capacity判断,依然可能产生脏数据。原因在于两个并发的SELECT都读到了selected=49,然后都通过了业务层的“未满”校验,但事务提交时只有一条UPDATE生效,另一条UPDATE因为selected < capacity条件不成立而回滚,这时候业务层的返回结果和数据库最终记录是一致的,不会超卖。但如果你的源码里UPDATE语句漏了条件,那并发场景下必超卖无疑。这就是为什么我反复强调数据库条件是最底层兜底。

第二步,把JDBC的DriverManager连接方式换成连接池。纯JDBC每次请求都新建连接,并发上来之后性能很差。在src/lib下加入Druid或C3P0的JAR包,在util包里新增一个连接池初始化类,然后把DAO层的DBUtil.getConnection()替换为从连接池取连接。核心改动不到十行,但效果立竿见影。这一步做完,你可以在课程设计的说明文档里写“使用连接池管理数据库连接,避免了频繁创建销毁连接带来的性能开销”,答辩时这就是一个实打实的亮点。

另外,把登录密码从明文改成MD5加盐存储也是一个成本很低但很加分的优化。在sql脚本的初始数据里事先存好加密后的密码,在LoginServlet里对用户输入的密码做同样加密再比对,就能避免“数据库被人看到直接泄露所有账号”的尴尬。当然,MD5在今天已经不算安全了,答辩时可以提一句“生产环境会替换成BCrypt”,说明你了解密码存储的演进。实践建议是只加盐到固定长度,不要用随机盐,否则在登录时还得先查盐值再算哈希,绕来绕去容易把自己绕晕。

最后一章用一个我自己的习惯收尾:任何系统跑通之后,我都会人为地注入几个破坏性操作——重复点选课、管理员删掉正在被选的课程、学生退课后再次选课。把这些边界场景挨个试一遍,凡是报500错误的,都是可以在答辩前修复的隐患。这套“破坏性自测”帮我省了无数次当场翻车的尴尬,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询