
简介JavaWeb学生选课系统是一份适合课程设计与毕业设计场景的完整项目源码覆盖学生、教师、管理员三类角色的日常教务管理功能。压缩包内共包含305个文件总体积约6.94MB其中Java源码与JSP页面构成后端业务与前端页面主体CSS和JavaScript负责界面样式与交互SQL脚本提供数据库表结构JAR包保证运行依赖PNG/GIF截图则配合图文和视频部署教程展示导入、配置及运行全过程。系统功能较全面学生可注册登录、查看选课与成绩、浏览公告教师可维护课程、查询学生选课情况并对学生成绩执行增删改查管理员能够统一管理学生、教师、班级、课程、选课、成绩和公告也支持修改密码与登录注册。项目经历过Bug修复更新并配有详细的视频部署教程能明显减少Eclipse或IDEA中导入项目、配置数据库时的常见问题源码目录结构清晰适合在此基础上继续扩展。目前已有3560人学习/下载对于需要上手JavaWeb项目实战或准备答辩的学习者很有参考价值。1. 为什么学生选课系统是JavaWeb课设里最值得动手的项目不是玩具是完整业务闭环JavaWeb学生选课系统项目源码在网上流传多年形态却分裂成两种一种是JSPServletMySQL的课设标准件另一种是Spring Boot的工程化版本。不管哪种业务闭环都是一样的——学生登录、浏览课程、选课退课、查成绩管理员维护课程老师录入成绩。对正在做课程设计的人来说这套东西比刷一百道Java题有用因为它把Servlet生命周期、JDBC事务、HTTP状态管理全串起来了。更难得的是标题明确写了附带图文和视频部署教程这意味着你不需要自己摸索IDEA怎么认Web项目、Tomcat怎么部署照着视频点就能跑起来。但视频只保证能跑不保证跑通后你懂。下面我从部署者视角把框架选型、数据表、启动步骤、核心代码和踩坑一次讲透让你既拿到能交差的项目也拿到能面试讲清楚的项目。2. 框架选型与数据表设计先认清是哪种JavaWeb再谈部署2.1 先认技术栈JSP/Servlet还是Spring Boot打开压缩包第一件事JavaWeb这个概念大得像条河。从高校课程节奏看学生选课系统大部分是Servlet JSP JDBC MySQL Tomcat的组合但从招聘市场需求看越来越多源码换成了Spring Boot。哪来的大部分是网上流传的JavaWeb项目完整案例mysql被二次开发后挂上了课设源码的名头。如果你下载的压缩包里没有pom.xml只有src、WebRoot或src/main/webapp和一堆jar包那基本就是Servlet课设。如果有pom.xml先用文本搜索在pom里搜spring-boot-starter-web搜得到就是Spring Boot部署路径完全不同。这一步很重要因为视频教程的第1秒通常就是解压后导入IDEA默认你已经认出了项目类型。带图文和视频的课设源码绝大多数是用IDEA Tomcat MySQL的组合讲部署的。理由很现实脱离Maven不依赖外网下载依赖机房离线环境也能跑课设考试考的就是Servlet生命周期、请求转发、重定向、Filter这些点换成Spring Boot反而不容易对号入座。但如果你想毕业后找工作Spring Boot早晚要会我放在最后一章讲迁法。识别技巧我一般这么干先找web.xml位置在WEB-INF/web.xml。有这个文件且里面配置了servlet-mapping就按JSP/Servlet走如果只有pom.xml就在pom里找spring-boot-starter-web。注意有些老工程新壳项目同时保留了web.xml和pom.xml但主启动类上带着SpringBootApplication以这个注解为准。还有一种最容易被忽略的情况工程看起来有web.xml但IDEA导入后右下角提示Unregistered web.xml这是因为你的Module没有添加Web模块后续部署会失败具体见3.4。提示拿到源码先别改一行代码。先识别技术栈再对着视频教程部署一次跑通了再谈改需求这是课设里最省时间的顺序也避免后面找后悔药。为什么我建议你选择JSPServlet版本作为第一次部署对象第一视频教程每一步都是按它拍的你照着做成功率最高。第二它把SQL和JDBC裸着写你能看见每一次查询和每一次事务而不是被MyBatis的代理藏起来。第三课设答辩老师手里的验收单关注的往往是你能不能讲清楚选课和退课的数据变化JSPServlet代码短、路径清晰讲起来最顺。第四这类项目的部署教程网络上存量最大哪怕你那版视频画质糊了搜idea运行javaweb项目配置也能找到同款操作的截图图文教程通常还会标记出按钮位置。而Spring Boot版本方便二次开发但部署被内嵌Tomcat和jar/war的选择绕进来对新手更容易翻车。2.2 数据表设计五张核心表一张联合唯一索引拦住重复选课先想清楚业务到数据表的映射。角色有三个学生、教师、管理员。功能有学生选课、退课、查成绩教师录入成绩、维护课程管理员管理用户。由此最小表结构是五张用户表、学生表、教师表、课程表、选课表。很多精简版只剩三张user、course、selection教师挂在course上学生挂在user上也能跑。但如果你打算拿这个去面试建议用五张因为面试官一问教师和课程是一对多还是多对多三张表很难自圆其说。常见做法是源码包里自带student_course.sql我按规范写一版供对照CREATE DATABASE IF NOT EXISTS student_course DEFAULT CHARSET utf8mb4; USE student_course; CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, role ENUM(admin,teacher,student) NOT NULL DEFAULT student, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, stu_no VARCHAR(20) NOT NULL UNIQUE, stu_name VARCHAR(30) NOT NULL, class_name VARCHAR(50), phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_teacher ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, tea_no VARCHAR(20) NOT NULL UNIQUE, tea_name VARCHAR(30) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL, teacher_id INT NOT NULL, capacity INT NOT NULL DEFAULT 50, selected_count INT NOT NULL DEFAULT 0, credit DECIMAL(3,1) DEFAULT 2.0, course_time VARCHAR(100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,1) DEFAULT NULL, UNIQUE KEY uk_stu_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段SQL每一句都值得看。创建库时写出utf8mb4比单独写utf8多覆盖Emoji和生僻字MySQL8默认就是utf8mb4MySQL5.7需要显式声明。所有表都指定ENGINEInnoDB因为选课涉及事务MyISAM不支持事务一旦在选课时做到一半崩溃数据会停留在不一致状态。逐张表说职责tb_user只放登录凭证不放业务字段。role用ENUM(admin,teacher,student)从数据库层面限制角色名只能取这三个值username加唯一索引登录查询时能走索引。tb_studentuser_id逻辑关联tb_user.idstu_no是学号保持唯一。不建物理外键是为了后续导测试数据方便。tb_teacher和tb_student对称tea_no是教师工号。tb_courseteacher_id逻辑关联teachercapacity是开课容量selected_count是冗余计数用它做列表页显示不必每次COUNT选课表。tb_selection核心。联合唯一键uk_stu_course从数据库层面保证同一学生对同一课程只能有一条选课记录。这个键是防重复选课的兜底即使应用层并发把容量判断穿透了这条唯一键也会抛异常让事务回滚。几个字段参数值得说明credit用DECIMAL(3,1)可以存2.5学分三位精度足够score默认NULL老师没录入成绩前是NULL录入后再UPDATEcourse_time是VARCHAR(100)存周一3-4节这种描述不要用DATETIME因为排课是周规律重复的不是某个时间点。select_time用DATETIME DEFAULT CURRENT_TIMESTAMP插入时省一行Java代码。注意如果代码里习惯用DateUtils.setCreateTime()那这一列的默认值就别依赖数据库两边只留一种写法否则会出现插入后时间存NULL的怪事。这张表里最需要你理解的是selected_count。它让列表页变快但也引入一致性风险如果选课成功后没有同时UPDATE selected_count列表页显示的人数就比选课表真实记录少。所以选课必须是一个事务插入tb_selection 更新tb_course.selected_count要么都成功要么都回滚。第4章的代码就是这么写的。更稳妥的做法是连capacity也一起条件更新后面我会给出原子SQL。另外索引设计要背下来PRIMARY KEY (id)、UNIQUE KEY (username)、UNIQUE KEY (stu_no)、UNIQUE KEY (tea_no)、UNIQUE KEY uk_stu_course。前几个是业务主键和业务唯一键最后一个是防重复选课的底线。如果某个源码没有uk_stu_course你可以补上ALTER TABLE tb_selection ADD UNIQUE KEY uk_stu_course (student_id, course_id); 注意补之前先查有没有重复数据有重复会加不上。源码包里的SQL文件名不一定是student_course.sql常见还有schema.sql、init_db.sql、db.sql。快速判断是否对应这个项目打开文件搜tb_course搜到就算对。有些压缩包会把多个课设源码打在一起里面有好几个SQL必须挑对不然导入后表结构对不上登录时报Unknown column。这也是很多人照着视频操作却连不上数据库的隐性原因。我习惯把SQL里的表名和代码里的SQL语句交叉核对一遍尤其是tb_selection的字段名selection_time还是select_time不少源码差异就藏在这种命名缝里。3. 部署全流程在IDEA里导入源码、连接MySQL、启动Tomcat一次跑通3.1 环境准备JDK、Tomcat、MySQL版本怎么搭才不打架这个项目最怕的不是代码是环境版本各说各话。推荐组合如下组件推荐版本为什么这么选JDK1.8.0_211及以上大部分课设代码没有Java9特性1.8最稳Tomcat8.5/9都默认支持1.8Tomcat8.5.x / 9.0.x9.0对应Servlet 4.0兼容javax.命名不要用Tomcat 10那要jakarta.MySQL5.7.x / 8.0.x两种都行MySQL8要换8.0.x驱动并加serverTimezoneIDEA2020.1 ~ 2023.x新版本也能用但菜单位置略有区别社区版要装Smart Tomcat插件版本不齐不是不能跑而是踩坑概率成倍增加。我第一次给学弟调项目他电脑是JDK17 Tomcat10 MySQL8然后拿着JDK8时代的源码光是javax/servlet就报了十几个类找不到最后换成Tomcat9和JDK8后十分钟内跑通。这个教训我记到现在。先确认当前环境java -version mysql --versionjava -version输出里带1.8.0_xxx就是JDK8mysql --version输出类似Ver 14.14 Distrib 5.7或8.0.3x。如果java版本不对后装一个JDK8并设置JAVA_HOME即可。如果MySQL没装也可用Docker启动一个MySQL容器但视频教程很可能默认你本机有MySQL并用Navicat连localhost:3306第一次课设建议直接本机装少一层容器网络问题。Docker适合已经熟悉端口映射和日志排查的人。3.2 导入数据库用source导入SQL脚本而不是复制粘贴找到源码包里的SQL后打开终端或cmd执行mysql -uroot -p -e source D:/student_course.sql-e是进入MySQL客户端后执行命令source是MySQL的客户端内部命令会按文件内容逐条建库建表。路径用正斜杠Windows用反斜杠容易被转义成换行符。如果你想先登录再导入可以这样mysql -uroot -p source D:/student_course.sql;参数说明-p后面会提示输入密码如果密码是123456也可以-p123456但中间不能有空格这会暴露在进程列表里不建议。导入完成后执行SHOW TABLES;确认至少出现五张表。另一种常见做法是打开Navicat右键连接运行SQL文件。用Navicat时记得在高级里勾选遇到错误继续否则某个索引语句报错会中断整批导入。导入后看表名前缀有的是tb_有的是t_要确认DBUtil里的SQL和导入的表名一致不一致会报Table doesnt exist。3.3 修改数据库连接配置三个文件里必有一处写死MySQL账号密码代码jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_course?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码这段通常存在于db.properties、jdbc.properties或JDBCUtils.java的静态代码块里。如果你下载的版本是Maven工程配置文件在src/main/resources/application.properties字段名变成spring.datasource.*结构类似只是键名不同。改完后不要急着启动先确认driver class写对com.mysql.cj.jdbc.Driver是MySQL8专用com.mysql.jdbc.Driver是MySQL5.7及以前或旧驱动兼容名。你用MySQL8却写了旧driver会报ClassNotFoundException或Loading class com.mysql.jdbc.Driver. This is deprecated.的警告。参数说明serverTimezoneAsia/Shanghai解决MySQL8时区报错useUnicodetrue和characterEncodingutf8是中文不乱码的前置条件useSSLfalse关闭SSL握手不是为了安全而是减少控制台刷警告。如果你的MySQL密码里有或#在properties里直接写一般能用但如果报Access denied就试一下把#写成%23。如果有多个角色账号注意看这套源码是不是用同一个连接串有的会区分student库和admin库别只改了一半。3.4 在IDEA里配置Tomcat并启动war exploded和Application context是核心部署步骤按以下顺序走每一步都是给下一步打地基File - Open选中解压后的项目根目录等IDEA右下角Event Log提示导入完成。如果根目录有pom.xml右键pom.xml - Add as Maven Project让IDEA下载依赖。CtrlShiftAltS打开Project StructureProject页签里把Project SDK改成1.8Language level 8。Modules页签选中当前模块点 - Web这时会出现Web Resource Directory把它指定到src/main/webapp或WebRoot对应的父目录。这一步解决Unregistered web.xml问题。Artifacts页签点 - Web Application: Exploded - From Modules让IDEA生成部署目录。Run - Edit Configurations - - Tomcat Server - LocalServer页签里选Tomcat主目录Deployment页签里点Artifact选第5步生成的war exploded。Application context这一栏填 /student_course表示访问根路径。如果想直接访问首页可以填 /。点击右上角Debug运行控制台出现Server startup in xxx ms后浏览器打开http://localhost:8080/student_course/login.jsp。为什么用war exploded而不是warExploded是解压后的文件夹IDEA可以直接在上面更新静态资源改了JSP不用重新打包重启war是整包适合发布到生产课设演示用Exploded。Application context是浏览器URL里的路径很多教程让你填 /我也推荐省得每次访问都带项目名。但你真要填 /注意LoginFilter里的redirect路径不要写死成/student_course/login.jsp要用request.getContextPath()动态拼否则改context又要改一遍代码。如果你用的IDEA是社区版没有Application Servers面板下载Smart Tomcat插件把Tomcat路径和Deployment选中到webapp目录效果一样。这一步别省很多人卡在没有Tomcat Server选项。如果你本机装的是Tomcat10下一章的坑一定会遇到建议现在就换成Tomcat9。4. 核心代码走读登录拦截和选课事务怎么写才经得起面试追问4.1 登录与权限拦截一个Filter管住所有页面登录流程本身不复杂login.jsp表单提交到LoginServletLoginServlet用username和password去tb_user查记录查出来后session.setAttribute(loginUser, user)再重定向到主页。但很多课设版本喜欢在每个页面里写重复的session判空效率低且容易漏。正规做法是抽一个Filter在请求进入Servlet之前统一判断WebFilter(/*) public class LoginFilter implements Filter { private ListString whiteList Arrays.asList(/login.jsp, /LoginServlet, /register.jsp, /static/); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); String contextPath request.getContextPath(); if (uri.startsWith(contextPath /login.jsp) || uri.startsWith(contextPath /LoginServlet) || uri.startsWith(contextPath /static/) || uri.equals(contextPath /)) { chain.doFilter(request, response); return; } if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(contextPath /login.jsp); return; } chain.doFilter(request, response); } }逻辑说明session用getSession(false)表示没有会话时返回null而不是创建一个新会话避免攻击者还没登录就被分配Session也防止静态资源请求无意义地创建会话。白名单要放行登录页、登录接口、静态资源uri.startsWith(contextPath /...) 是防止Project context不是根路径时把/login.jsp判断成别的。如果登录接口被拦会出现登录页能打开但提交表单后又跳回登录页的循环翻车现场。参数说明WebFilter(/*)是Servlet 3.0注解方式兼容Tomcat7课设项目可以在web.xml里配置同效果filter-mapping。白名单的匹配规则每个源码不同有的用HashSet做精确匹配你拿到源码后先看它匹配的是不是带contextPath的完整URI这里最容易出问题。角色权限也可以在Filter里补String role (String) session.getAttribute(role); if (uri.startsWith(contextPath /admin/) !admin.equals(role)) { response.sendRedirect(contextPath /login.jsp); return; }把角色字符串存进Session而不是每次查库能省一次DB访问但角色变更后需要重新登录才能生效。对课设来说这已经是加分项。4.2 选课事务从先查再插到原子更新的演进选课最容易出错的地方不是SQL是并发。两个学生同时抢最后一个名额如果代码逻辑是先查selected_count小于capacity再insert会同时查到两个最后课程超限。带视频教程的课设源码多半是先查再判再插的写法因为单个用户演示没问题但面试官一问并发就露馅。建议在部署后直接把代码改成下面这种锁行版本public boolean selectCourse(int studentId, int courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); String checkSql SELECT capacity, selected_count FROM tb_course WHERE id ? FOR UPDATE; PreparedStatement ps conn.prepareStatement(checkSql); ps.setInt(1, courseId); ResultSet rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity rs.getInt(capacity); int selected rs.getInt(selected_count); if (selected capacity) { conn.rollback(); return false; } String insertSql INSERT INTO tb_selection(student_id, course_id) VALUES (?, ?); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setInt(1, studentId); ps2.setInt(2, courseId); ps2.executeUpdate(); String updateSql UPDATE tb_course SET selected_count selected_count 1 WHERE id ?; PreparedStatement ps3 conn.prepareStatement(updateSql); ps3.setInt(1, courseId); ps3.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.release(conn); // 关闭顺序ResultSet - PreparedStatement - Connection } }逻辑说明setAutoCommit(false)以后所有SQL都在同一个事务里SELECT ... FOR UPDATE会锁住tb_course的这一行第二个并发事务会阻塞到第一个提交或回滚从数据库层面避免超选。插入选课记录和更新selected_count放在同一事务要么都成功要么都回滚selected_count不会和真实记录漂移。finally里DBUtil.release(conn)再关PreparedStatement和ResultSet否则循环选课几次后MySQL会报Too many connections。参数说明TRANSACTION_READ_COMMITTED是最常用的隔离级别避免读到别的会话未提交的超选数据如果不确定可以不加默认InnoDB是REPEATABLE_READ对课设影响不大但加上说明你懂隔离级别面试能加分。ps.executeUpdate()返回的int表示影响行数如果为0说明插入失败可以return false。再给一个不需要手写锁的简化优化把查容量更新计数合并成一条原子SQLUPDATE tb_course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity如果返回影响行数等于1说明成功占到一个名额然后再INSERT选课记录。注意INSERT失败会导致占的名额没被消费需要回滚或把selected_count减回去所以实际还要配合事务。这条SQL的价值是判断和自增发生在数据库同一语句内天然原子比先查再插更稳。如果源码是MyBatis就在Service方法上加Transactional把这条UPDATE放在INSERT之前。课设答辩时能讲出这一句基本稳了。5. 部署避坑记录五条血泪经验按现象、原因、解决排好5.1 端口占用和Tomcat版本不匹配一启动就报错现象IDEA里Tomcat启动日志出现Port 8080 is already in use网页打不开或者点Debug后状态码一直是404。原因8080被另一个进程占用。常见占用者之前启动的Tomcat没关掉、XAMPP里的Apache、某个开发工具的预览服务甚至是Windows的万维网发布服务。解决先确认谁占的。Windows在CMD里执行netstat -ano | findstr :8080最右一列是PID然后taskkill /F /PID 4567杀掉。如果不想杀也可以在Tomcat/conf/server.xml里把Connector port改成8081但要同步改IDEA的HTTP port和访问地址。注意server.xml里有三处端口8005是shutdown8009是AJP8080是HTTP。新手只改8080即可别把shutdown端口改没否则关闭Tomcat会告警。如果你用Docker部署Tomcat端口映射要改docker run -p 8081:8080里的宿主端端口杀进程不生效要在docker-compose里改。现象Tomcat10启动项目控制台抛NoClassDefFoundError: javax/servlet/http/HttpServletRequest或ClassNotFoundException: javax.servlet.Filter。原因Tomcat10把Servlet API从javax.迁移到jakarta.老课设代码里全是javax.servlet.两个包不兼容Tomcat9及以前还是javax.。解决卸载Tomcat10安装Tomcat9.0.x设置好CATALINA_HOME后重启。如果不想卸载可以全局替换源码里的javax.servlet为jakarta.servlet但涉及JSP、Filter、Listener多处替换后还可能踩别的坑优先换Tomcat版本。检查当前Tomcat版本可以在命令行执行catalina version或在IDEA的Run Configuration里看Application server路径。5.2 数据库连不上和中文乱码跑起来也闹心现象启动时从JDBCUtils里报Public Key Retrieval is not allowed或Access denied for user rootlocalhost。原因MySQL8默认用caching_sha2_password认证旧版mysql-connector-java驱动不认识或者连接串没带allowPublicKeyRetrievaltrue和useSSLfalse。解决把驱动换成mysql-connector-java 8.0.33放到WEB-INF/lib连接串带上两项参数。如果还是拒连进MySQL执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES; 同时确认db.properties里的密码和实际密码一致别把源码自带的默认密码123456当成你本机的密码。如果你是Spring Boot项目驱动版本在pom里管理不要手动往lib里塞注意mysql的artifactId在旧版是mysql:mysql-connector-java新版是com.mysql:mysql-connector-j包名前缀不同混淆了也会连不上。现象JSP显示???或数据库里存的名字变成控制台日志也乱。原因编码链路里至少有一环不一致。JSP的pageEncoding不是UTF-8Tomcat的URIEncoding默认是ISO-8859-1MySQL表字符集不是utf8mb4连接串缺characterEncoding。解决按顺序检查。JSP页头改成% page contentTypetext/html;charsetUTF-8 %Tomcat server.xml的Connector增加URIEncodingUTF-8把库和表都转成utf8mb4ALTER DATABASE student_course CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE tb_course CONVERT TO CHARACTER SET utf8mb4; 最后确认db.properties里有characterEncodingutf8。这一条是典型的黑匣子不按顺序排查会一直翻车。现象页面报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver但WEB-INF/lib下确实有mysql-connector-java-8.0.33.jar。原因IDEA构建部署包时没有把lib目录的jar复制进去。很多从Eclipse搬过来的项目lib目录在WebRoot/WEB-INF/libIDEA不会自动把它注册成Web资源。解决CtrlShiftAltS打开Project Structure - Artifacts - 选中部署包 - 找到WEB-INF/lib节点 - 右键Add Copy of - 把lib里的所有jar加进去重启Tomcat。还有一些源码用了c3p0连接池c3p0-config.xml里配置了多个数据源需要把每个数据源的driver和url全部改对否则只改第一处运行时还是报驱动找不到。如果你用的是Maven引入的驱动检查pom依赖是否下载成功以及IDEA的Maven设置里是否勾选了Delegate IDE build/run actions to Maven。这些坑单独看都不难但连在一起就能卡掉大半天。我的习惯是每改一个配置先启动一次看下一个报错而不是一次改完再启动。这样才能把现象-原因-解决对上号。6. 部署成功之后加一个限选人数校验顺便想清楚要不要迁移Spring Boot6.1 用原子更新做限选人数校验把代码改成真正可靠跑通后我建议你把选课Service里的先查再插改成下面这条原子更新作为对这个项目的第一处功能升级UPDATE tb_course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity在Java里执行后若返回1说明占座成功继续执行INSERT选课记录若返回0说明名额已满直接返回课程已满。注意在JDBC代码里让这两条SQL共享一个事务INSERT失败时把UPDATE回滚否则会出现selected_count加1了但选课记录没生成的情况。你可以用第4章的事务模板把SELECT ... FOR UPDATE换成这条UPDATE代码还能更短。验证方法找一节capacity1的课程用两个学生账号在无头浏览器或Postman里同时提交选课最终课程表selected_count不会超过1选课表最多一条记录。这个实验做完你对并发的理解就上了一个台阶。6.2 迁移Spring Boot的思路毕业后简历要能写这一笔如果你准备找工作可以考虑把JSPServlet这套迁到Spring Boot。常见做法是新建一个Spring Boot项目把DAO层的JDBC代码迁到MyBatis-Plus Mapper把LoginFilter改成HandlerInterceptor把Session改为Spring MVC的HttpSession本质不变页面可以直接沿用JSP但Spring Boot对JSP的支持需要额外配置不如直接换Thymeleaf。数据库表不用动SQL几乎原样搬。注意一点原来的WebFilter在Spring Boot里依然能被扫描但拦截顺序不好控制用HandlerInterceptor注册更稳。迁移不是必须的。课程设计交JSPServlet也能拿高分真正决定水平的是你讲不讲得清事务、并发和Filter。我每次带人做项目都要求先按教程部署跑通再改一块业务代码验证自己读懂。这个习惯让我躲过了很多换台机器就启动失败的尴尬。希望帮到你。本文还有配套的精品资源点击获取