简介:这是一套面向高校计算机相关专业学生与Java Web初学者的网上选课系统完整项目包,以JSP结合SQL数据库实现,可作为毕业设计选题、课程设计作业或个人技术练手的参考方案,也适合小型团队对照搭建同类教务管理模块。压缩包共482个文件,约18.77MB,其中70个jsp页面构成选课、课程信息、学生与教师管理等核心功能,配套163个gif与107个jpg界面素材、30个class与10个jar运行依赖,另有11个db数据库文件及mdf、ldf数据文件,并附源代码、论文文档与答辩PPT,形成从编码到答辩的完整链路。资源已有219人学习下载,读者可借此梳理JSP+SQL的典型分层结构、数据库表设计与页面跳转逻辑,理解选课系统的业务闭环,同时参考论文与PPT组织自己的设计说明与答辩材料,降低从零搭建的门槛。
1. 从一份 JSP+SQL 选课系统源码说起:它到底能帮你解决什么
如果你正在做 JavaWeb 课程设计,或者需要一套能跑通、能改、能写进简历的 JSP+SQL 网上选课系统,那这份「源代码+论文+答辩PPT」打包资料的价值,不在于它有多复杂,而在于它把「学生选课」这条业务链完整跑通了:学生登录、课程列表、选课退课、名额扣减、教师查看名单、管理员维护课程。整套东西用的是 JSP+Servlet+JDBC+SQL Server/MySQL 这套经典组合,没有 Spring Boot 那种一层套一层的封装,反而更适合拿来理解请求怎么从浏览器走到数据库、事务怎么保证选课不超卖。热搜里常出现「jsp个人信息展示页面」「sql语句去重」「java基础」这些词,其实都指向同一件事:大家要的不是花哨架构,而是一份能看懂、能改、能答辩的源码。这篇笔记就按「先跑起来、再拆业务、最后避坑」的顺序,把这份资料怎么用、参数怎么调、哪里最容易翻车讲清楚。
2. 把 JSP+SQL 选课系统在本地跑起来:环境、建库与最小验证
2.1 环境选型:为什么这套源码更适合 JDK8 + Tomcat8.5
拿到一份 JSP+SQL 的 JavaWeb 源码,第一件事不是急着改代码,而是把运行环境对齐。这类课程设计级项目绝大多数是在 JDK8 时代写的,Servlet 还是javax.servlet包名,Tomcat 用 8.5 或 9.0 最稳。如果你直接上 JDK17 配 Tomcat10,会立刻遇到javax.servlet does not exist的编译错误,因为 Tomcat10 已经换成了jakarta.servlet,包名对不上,这不是代码写错了,是环境代差。
我一般会这样配:JDK 用 1.8.0_xxx,Tomcat 用 8.5.x,IDE 用 Eclipse 或 IntelliJ IDEA 都行,数据库优先 MySQL 5.7/8.0,如果源码里写的是 SQL Server 就装 SQL Server Express。判断依据很简单——打开源码里的WEB-INF/web.xml,看servlet-class前面是javax还是jakarta,是javax就老老实实降版本。数据库连接信息通常在src下的db.properties或某个DBUtil.java里,先找到它,后面所有配置都围绕这个文件改。
提示:不要一上来就升级依赖。课程设计源码的依赖版本是「能跑就行」,升级往往带来一堆兼容问题,先把原版跑通再谈优化。
2.2 建库建表:从 SQL 脚本到能登录的第一个账号
源码包里一般会带一个.sql文件,这是整个系统的地基。导入之前先确认三件事:字符集、库名、账号密码。用 Navicat 或命令行都行,命令行更直观:
# 登录 MySQL,注意 -u 和 -p 之间不要加空格 mysql -uroot -p # 创建数据库,字符集用 utf8mb4,避免中文课程名乱码 CREATE DATABASE course_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换到该库 USE course_selection; # 导入源码包里的 sql 脚本,路径按实际改 source /path/to/course_selection.sql; # 验证表是否建好,通常会有 student、course、sc(选课关系)三张核心表 SHOW TABLES;导入完成后,重点看三张表:student(学生)、course(课程)、sc或selection(选课关系)。选课系统的核心逻辑全在这三张表的关系上——学生和课程是多对多,中间表就是选课记录。很多源码的初始账号是admin/123456或student/123456,具体看 SQL 脚本里INSERT INTO student那几行。如果登录报「用户名或密码错误」,先别怀疑代码,去数据库里SELECT * FROM student;看一眼密码字段是明文还是 MD5,这决定了你后面登录逻辑要不要改。
2.3 改配置、起服务、验证选课主流程
数据库通了之后,改连接配置。找到DBUtil.java或db.properties,把 URL、用户名、密码换成你本地的:
// 典型的 JDBC 连接配置,注意 URL 里的库名和参数 private static final String URL = "jdbc:mysql://localhost:3306/course_selection?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; // 加载驱动,MySQL 8.0 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);这里的参数每一个都有讲究:useUnicode=true&characterEncoding=utf8保证中文不乱码,useSSL=false避免本地连接时的 SSL 警告,serverTimezone=Asia/Shanghai是 MySQL 8.0 必须加的,不加会报时区错误。驱动类名也要对上版本,8.0 用com.mysql.cj.jdbc.Driver,5.x 用com.mysql.jdbc.Driver,写错了就是ClassNotFoundException。
配置改完,把项目部署到 Tomcat,启动后访问http://localhost:8080/项目名/login.jsp。验证顺序建议是:先用管理员账号登录,看课程列表能不能出来;再用学生账号登录,选一门课,然后去数据库SELECT * FROM sc;看有没有新增记录;最后退课,看记录有没有删掉。这三步走通,说明整条链路是活的,后面改功能才有底气。
3. 拆解选课核心业务:名额扣减、事务与 SQL 语句怎么写才不出错
3.1 选课名额扣减:为什么你的系统会超卖
选课系统最典型的 bug 就是超卖——课程限 50 人,结果选进去 52 个。根因在于「查名额」和「扣名额」是两步操作,中间有并发窗口。很多课程设计源码是这么写的:先SELECT remaining FROM course WHERE id=?,在 Java 里判断remaining > 0,再UPDATE course SET remaining = remaining - 1。单机测试看不出问题,一旦两个人同时点选课,就会双双通过判断,双双扣减。
正确做法是把判断和扣减合并到一条 SQL 里,用数据库的行锁保证原子性:
-- 原子扣减:只有剩余名额大于 0 时才扣,返回受影响行数 UPDATE course SET remaining = remaining - 1, selected_count = selected_count + 1 WHERE id = ? AND remaining > 0; -- 如果返回 0,说明名额已满,Java 层据此提示「选课失败」这条语句执行后,JDBC 的executeUpdate()会返回受影响行数。返回 1 表示扣减成功,返回 0 表示名额不足。这样就不需要在 Java 里先查后判断,避免了并发窗口。如果还想更严谨,可以把「插入选课记录」和「扣减名额」放进同一个事务:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一步:扣减名额,返回 0 直接回滚 PreparedStatement ps1 = conn.prepareStatement( "UPDATE course SET remaining = remaining - 1 WHERE id = ? AND remaining > 0"); ps1.setInt(1, courseId); if (ps1.executeUpdate() == 0) { conn.rollback(); return "名额已满"; } // 第二步:插入选课记录,唯一索引防止重复选课 PreparedStatement ps2 = conn.prepareStatement( "INSERT INTO sc(student_id, course_id) VALUES(?, ?)"); ps2.setInt(1, studentId); ps2.setInt(2, courseId); ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 return "选课成功"; } catch (Exception e) { if (conn != null) conn.rollback(); // 任何异常都回滚 return "选课失败"; } finally { conn.setAutoCommit(true); conn.close(); }事务的意义在于:扣名额成功但插记录失败时,能回滚把名额加回去,不会出现「名额少了但没人选上」的脏数据。sc表上建议加UNIQUE(student_id, course_id)唯一索引,这样即使前端重复提交,数据库也会拦住重复选课。
3.2 课程列表与去重查询:热搜里「sql语句去重」的真实用法
热搜里「sql语句去重」出现频率很高,放到选课系统里,典型场景是「一个学生选了多门课,查他选过的课程列表时出现重复行」。这通常是因为关联查询时sc表和course表连接条件没写对,或者一个课程有多个教师记录导致笛卡尔积。去重有两种思路:DISTINCT和GROUP BY。
-- 方式一:DISTINCT,适合简单去重 SELECT DISTINCT c.id, c.name, c.credit FROM course c JOIN sc s ON c.id = s.course_id WHERE s.student_id = ?; -- 方式二:GROUP BY,适合需要聚合的场景,比如统计每门课选课人数 SELECT c.id, c.name, COUNT(s.student_id) AS selected_count FROM course c LEFT JOIN sc s ON c.id = s.course_id GROUP BY c.id, c.name;DISTINCT作用于整行,只要有一列不同就不算重复,所以列要选准。GROUP BY更适合做统计,比如管理员页面要看「每门课选了多少人」,用LEFT JOIN保证没人选的课也显示为 0。这里有个坑:GROUP BY在 MySQL 8.0 之前默认不校验SELECT列是否都在GROUP BY里,8.0 之后开了ONLY_FULL_GROUP_BY会报错,所以c.name也要写进GROUP BY。
3.3 分页与模糊查询:课程列表多了之后怎么不卡
课程一多,一次性SELECT * FROM course全查出来,页面会又慢又长。分页是必须的,MySQL 用LIMIT:
-- 第 page 页,每页 size 条,offset 从 0 开始 SELECT * FROM course WHERE name LIKE CONCAT('%', ?, '%') ORDER BY id LIMIT ?, ?; -- 参数:name 关键字,offset = (page-1)*size,size 每页条数LIMIT后面两个参数,第一个是偏移量,第二个是条数。深分页时LIMIT 10000, 10会扫描前 10000 行再丢弃,性能差,优化方式是用WHERE id > 上一页最大id LIMIT 10。模糊查询的LIKE CONCAT('%', ?, '%')用PreparedStatement传参,既防注入又清晰。注意%放前面会导致索引失效,数据量大时考虑全文索引或搜索引擎,课程设计级别用LIKE足够。
4. 从源码到论文和答辩:资料怎么用才不浪费
4.1 论文与源码的对应关系:先跑通再写,别反过来
打包资料里的论文和答辩 PPT,价值在于帮你理清「系统做了什么、为什么这么做」。但顺序不能反——先照着源码把系统跑起来,再回头看论文,你会发现论文里写的「系统架构图」「E-R 图」「流程图」都能在代码里找到对应。比如 E-R 图里的学生、课程、选课三个实体,对应student、course、sc三张表;流程图里的「选课」分支,对应SelectionServlet里的if-else。
我一般建议这样用:第一步,跑通系统,把每个页面点一遍;第二步,对着论文的模块划分,在源码里找到对应的 Servlet 和 JSP;第三步,把论文里的「功能描述」和实际代码行为对一遍,有出入的地方就是你可以改进的点,也是答辩时能讲出深度的点。答辩 PPT 通常只有十几页,重点在「创新点」和「难点」,你可以把第 3 章里的事务扣名额、防超卖作为难点讲,比空泛地说「用了 JSP+SQL」有说服力得多。
4.2 二次开发方向:让课程设计不止于及格
一份能跑的选课系统只是起点,想拿高分或者写进简历,得做二次开发。几个投入产出比高的方向:第一,把密码明文改成 MD5 或 BCrypt 加盐存储,登录时比对哈希值,这是安全意识的体现;第二,加一个「选课时间窗口」控制,用Date比较判断当前是否在选课期内,超时不允许选课;第三,把 JDBC 直连改成数据库连接池,比如 Druid 或 HikariCP,配置initialSize、maxActive参数,能讲清楚「为什么连接池比每次新建连接快」。
// Druid 连接池最小配置示例 DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/course_selection?useSSL=false&serverTimezone=Asia/Shanghai"); ds.setUsername("root"); ds.setPassword("你的密码"); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大连接数 ds.setMaxWait(3000); // 获取连接超时时间,毫秒initialSize是启动时建好的连接数,maxActive是并发上限,maxWait是拿不到连接时的等待时间。这三个参数调好了,系统在多人同时选课时不会频繁创建销毁连接,响应更稳。这些改动都不大,但每一个都能在答辩时展开讲原理,比单纯「我做了个选课系统」有内容。
5. 避坑与排查:JSP+SQL 选课系统最常见的 5 个翻车现场
5.1 中文乱码:从页面到数据库一条链都要统一
现象:课程名、学生姓名显示成???或乱码。原因:JSP 页面、Servlet 响应、数据库连接、表字符集四处的编码不一致。解决:JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;Servlet 里request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8");JDBC URL 加characterEncoding=utf8;数据库和表用utf8mb4。四处统一,乱码基本消失。
5.2 空指针异常:getParameter拿不到值就直接用
现象:提交表单后报NullPointerException,定位到某行request.getParameter("xxx").trim()。原因:参数名写错、表单没提交该字段,或者 GET/POST 方式不匹配,导致getParameter返回null。解决:取值后先判空,String name = request.getParameter("name"); if (name != null) { ... }。更稳妥的做法是封装一个工具方法统一处理,避免每处都写判空。
5.3 驱动加载失败:ClassNotFoundException的三种可能
现象:启动或第一次连库时报ClassNotFoundException: com.mysql.jdbc.Driver。原因:一是驱动 jar 没放进WEB-INF/lib;二是驱动类名和 MySQL 版本不匹配(8.0 要用com.mysql.cj.jdbc.Driver);三是 Tomcat 没重新加载。解决:确认WEB-INF/lib下有mysql-connector-java-x.x.x.jar,按版本改类名,改完重启 Tomcat 并清理work目录。
5.4 选课重复提交:刷新页面就多选一次
现象:选课成功后按 F5 刷新,又插入一条选课记录。原因:表单提交后直接转发到结果页,刷新会重复提交最后一次请求。解决:提交后用response.sendRedirect()重定向到列表页,而不是forward;同时在sc表加唯一索引兜底。重定向让浏览器地址栏变成列表页 URL,刷新就不会重复提交。
5.5 数据库连接未关闭:跑一会儿就报连接数满
现象:系统用一段时间后报Too many connections。原因:Connection、PreparedStatement、ResultSet用完没关,连接泄漏。解决:在finally块里按「后开先关」的顺序关闭,或者用 try-with-resources 自动关闭。更彻底的方式是引入连接池,由池统一管理生命周期。
6. 进阶技巧:用一条 SQL 和一次压测验证你的选课系统到底稳不稳
前面把系统跑通、业务拆完、坑也排了,最后落到一个具体技巧上:怎么用最小成本验证你的选课系统在并发下不超卖。很多人做完课程设计就点几下页面,觉得没问题,但答辩老师一句「两个人同时选最后一门课会怎样」就能问住。我的习惯是写一个简单的多线程测试,直接打数据库层,不依赖页面。
// 模拟 100 个线程抢 10 个名额,验证是否超卖 int threadCount = 100; ExecutorService pool = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); // 让线程同时起跑 for (int i = 0; i < threadCount; i++) { final int studentId = i + 1; pool.submit(() -> { try { latch.await(); // 所有线程在这里等待,直到计数归零 // 调用你的选课方法,内部走事务扣名额 String result = selectionService.selectCourse(studentId, 1); System.out.println("学生" + studentId + ":" + result); } catch (Exception e) { e.printStackTrace(); } finally { latch.countDown(); } }); } latch.countDown(); // 主线程放行 pool.shutdown();CountDownLatch的作用是让 100 个线程尽量同时发起请求,制造真实并发。跑完之后去数据库查:SELECT remaining, selected_count FROM course WHERE id=1;,如果remaining是 0、selected_count是 10,说明扣减正确;如果remaining变成负数,说明你的 SQL 没有加AND remaining > 0,或者事务隔离级别有问题。这个测试不需要任何压测工具,几十行代码就能跑,答辩时把结果一摆,比说一百句「我考虑了并发」都管用。
再补一个验证点:把sc表的唯一索引加上,然后让同一个学生 ID 并发选同一门课,看最终是不是只有一条记录。如果出现两条,说明唯一索引没生效或者插入逻辑绕过了约束。这两个测试做完,你对这套 JSP+SQL 选课系统的理解就不止于「能跑」,而是「知道它在什么情况下会崩、为什么崩、怎么让它不崩」。
我自己踩过的最大坑,是早期做类似系统时觉得「课程设计而已,不用管并发」,结果答辩现场被要求演示两个人同时选课,当场翻车,名额扣成了负数。从那以后我养成了一个习惯:任何涉及「数量增减」的功能,先把扣减写成带条件的原子 SQL,再谈页面好不好看。希望帮到你。
本文还有配套的精品资源,点击获取