☰
Servlet+JSP学生选课系统:从部署到改造的JavaWeb项目实战
2026/10/7 6:15:14 网站建设 项目流程

简介:这是一套基于JavaWeb技术栈实现的学生选课系统,面向计算机相关专业毕业设计学生及需要项目实战的Java学习者,可解决课程管理、选课、成绩录入等常见业务场景的完整开发需求。系统采用Servlet与JSP及MySQL架构,前端结合Bootstrap和jQuery,支持管理员、教师、学生三种角色,覆盖学生与教师信息管理、课程信息管理、课程查看与选课、成绩录入与查询、个人信息查看等功能。资源包共有一百一十二个文件,以Java源码、JSP页面、class编译文件为主,同时包含十个jar依赖库、SQL数据库脚本、CSS与JS前端样式及少量图片字体资源,整包仅二点三九MB,已通过严格调试可直接运行并作为毕业设计项目使用。目前已有四千二百七十三人学习下载,适合用于毕设选题参考、项目实战练习以及JavaWeb开发流程的完整学习。

1. 还在用 Servlet + JSP 的选课系统:为什么值得你把它跑起来

说到 JavaWeb 项目,很多人第一反应是 Spring Boot。但这套学生选课系统用的是最经典的 Servlet + JSP + MySQL + Bootstrap 组合,JDK 1.8,配一个 Tomcat 就能跑。不是它过时,而是它正好覆盖了两类诉求:正在做毕设、需要一套能直接答辩的完整案例的在校生,以及学完 JavaWeb 基础、想看看项目从登录过滤器到 DAO 层怎么串起来的自学者。系统把管理员、教师、学生三种角色拆成三条权限线,选课、成绩录入、课程管理全是能点能跑的界面,不是那种堆了一堆类名却起不来的半成品。

2. 技术选型与服务端分层:为什么坚持用 Servlet,源码该从哪看起

2.1 Servlet + JSP + JDK 1.8:组合的边界与选型理由

先交代一个常被忽略的事实:Spring Boot 的 DispatcherServlet 本身也是一个 Servlet。Servlet 规范是 JavaWeb 的地基,这套系统直接在地基上盖楼,没有框架黑匣子。对选课这种并发量不大、业务规则清晰的场景,Servlet + JSP 反而比 Spring Boot 更直白:请求进来走哪个 Servlet,Servlet 转发到哪个 JSP,全程一眼能看穿。

那为什么不选 SSM 或 Spring Boot?SSM 要维护 XML 映射文件,三张表的小系统写 mapper 比写业务还费劲;Spring Boot 自动配置把数据库连接、视图解析全部藏起来了,答辩时被追问一句「Tomcat 是怎么启动的」很容易卡壳。Servlet + JSP 的每一步都是显式的,适合用来证明你理解 JavaWeb 而不是只会用框架。

另外两个配套选择也值得说:JDK 1.8 是这套技术栈最稳定的版本,lambda 能用又不会引入 module 系统的麻烦;前端用 Bootstrap 而不是 Vue/React,是为了让 JSP 后端渲染和静态页面样式各司其职,列表、表单、弹窗全是现成组件,改样式几行 CSS 的事。对这些管理系统,前端复杂度越低,越能把答辩时间留给业务逻辑。

选型代价也很清楚:没有声明式事务、没有 ORM,JDBC 连接要手动开手动关。所以这套技术栈适合单体、校园网规模、几千条数据以内的管理系统。如果目标是高并发抢课,那该上 MQ、Redis,这不是这套源码的定位。

2.2 三种角色一个登录口:权限与表结构怎么落

登录相关逻辑集中在 UserServlet 和 LoginFilter。这类项目最常见的表结构是:一张 user 表存账号、密码、角色(admin / teacher / student),student、teacher 表存各自资料,course 表存课程,student_course 表存选课关系和成绩。成绩录入、成绩查询都是围绕 student_course 的 score 字段展开的,这一条能对上教师和学生两个角色的核心功能,也解释了为什么源码里会单独放一个 StudentCourse 实体类。

角色不同,操作入口不同,但一般不会写死多个主页,而是复用一套页面按角色渲染菜单。管理员能看到学生信息管理、教师信息管理、课程信息管理;教师能看到自己教的课程、选课学生名单、成绩录入入口;学生只能看到课程列表、选课按钮和自己的成绩。权限粒度控制在「菜单级」,对毕设来说刚好,做细反而增加调试负担。

成绩录入这条线在 TeacherServlet 里:教师进课程后看到学生名单,提交分数时更新 student_course.score。这里有个权限边界要注意,TeacherServlet 里通常会判断当前课程是否属于登录教师本人,否则教师可以改任何课程的成绩,这是常见越权漏洞,也是答辩时能主动讲出来的加分点。

还有一点容易被新手绕晕:LoginFilter 只回答「有没有登录」,不回答「能不能做这件事」。具体操作是否越权,由 Servlet 内部拿 session 里的角色判断。两层各管一头,职责分明,改起来也快。

2.3 核心文件调用链:压缩包打开后先看哪几个

先看类文件分布。这套资源的核心是六个 Servlet 和四个数据类,把层次和职责摆开是这样的:

层次类文件对应职责
控制层UserServlet登录登出、个人资料
控制层StudentServlet学生资料维护(管理员操作)
控制层TeacherServlet教师资料维护、成绩录入
控制层CourseServlet课程列表、选课入口
数据层StudentDao / TeacherDao学生、教师表 CRUD
数据层CourseDao课程表 CRUD
数据层StudentCourseDao选课关系、成绩读写
横切LoginFilter登录状态拦截
实体StudentCourse选课关系实体

拿到项目我一般不建议先开 index.jsp,按这个顺序读更快:先看 SQL 脚本里的表和初始化数据,确认有几个角色、几门课;再看 LoginFilter 怎么拦截;然后挑一条完整链路,比如学生选课,从 CourseServlet 追到 StudentCourseDao;最后打开对应 JSP 看渲染。两小时能把整套系统讲明白,比漫无目的翻代码高效得多。

顺带说一句读代码的节奏。第一遍只需要把「谁调用谁」画在脑子里,不用深挖每行 JDBC;第二遍再回到 StudentCourseDao,把连接管理、结果集映射看细。两遍下来,这套系统的并发边界、可扩展点在哪里,心里基本有数。

提示:压缩包里的 .class 是编译产物,真正要改的是 src 下对应的 .java 源文件,两者在工程目录里是对应存在的,别拿反编译工具去折腾 class。

3. 核心代码拆解:登录拦截、选课链路与 Bootstrap 页面的串法

3.1 LoginFilter 与 UserServlet:登录态怎么产生、怎么拦截

先看登录态怎么来。UserServlet 接收用户名密码,校验通过后把 User 对象放进 session。常见写法是:

@WebServlet("/login") public class UserServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 查 user 表校验账号密码,成功返回 User,失败返回 null User user = userDao.findByUsernameAndPassword(username, password); if (user == null) { req.setAttribute("error", "账号或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } }

逻辑说明:登录失败用 forward 转发回登录页,地址栏不变,方便在页面上回显 error 提示;登录成功用 sendRedirect 重定向到主页,避免刷新页面时表单重复提交。具体查表这段,如果压缩包里没有独立的 UserDao,那多半是把校验写在 UserServlet 内部或复用了现有 Dao,不影响整条链路理解。

有了登录态,LoginFilter 就负责把没登录的请求挡在门外:

@WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); if (uri.endsWith("login.jsp") || uri.contains("/login") || uri.endsWith(".css") || uri.endsWith(".js")) { chain.doFilter(request, response); return; } Object user = req.getSession().getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

逻辑说明:过滤器先放行登录页、登录接口和静态资源,再检查 session。注意静态资源必须放行,否则登录页 CSS/JS 全被拦截,样式全丢,出现「能登录但页面丑得没法看」的翻车现场。

参数说明:@WebFilter("/*") 拦截所有路径,也可以用 web.xml 里的<filter>/<filter-mapping>声明,老项目两种写法都有,效果一致。判断登录态时建议用 getSession(false),没登录时返回 null,而 getSession() 会当场创建一个空 session,既浪费内存还绕不过拦截;登录成功后主动 setAttribute 存 loginUser,这个 key 要和过滤器里取的一致,否则永远认为你没登录。

3.2 三层调用链:CourseServlet 与 StudentCourseDao 的配合

以学生选课为例,完整链路是「课程列表点按钮 → CourseServlet → StudentCourseDao → 结果页」。CourseServlet 里有两个关键动作:加载课程列表、处理选课请求。列表加载常见写法:

@WebServlet("/course/list") public class CourseServlet extends HttpServlet { private CourseDao courseDao = new CourseDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Course> courseList = courseDao.findAll(); req.setAttribute("courseList", courseList); req.getRequestDispatcher("/course_list.jsp").forward(req, resp); } }

逻辑说明:Dao 查出来的 List 放进 request,forward 给 JSP 渲染。这里用 forward 而不是重定向,因为数据在 request 作用域里,重定向会丢。这也是 JSP 后端渲染和前后端分离最大的区别:页面是在服务端拼好再发出去的。

选课请求的处理是同一类 Servlet 里的另一个动作。不少项目把 /course/list 和 /course/select 配在同一个 Servlet 上,在 doGet 里根据路径分流,也有项目拆成两个 Servlet,逻辑是一样的:

// 地址形如 /course/select?courseId=5 String path = req.getServletPath(); if ("/course/select".equals(path)) { Integer courseId = Integer.valueOf(req.getParameter("courseId")); Integer studentId = getLoginUserId(req); // 从 session 取当前学生的 id boolean ok = studentCourseDao.addStudentCourse(studentId, courseId); if (ok) { resp.sendRedirect(req.getContextPath() + "/course/list"); } else { req.setAttribute("msg", "选课失败:可能已选过或课程人数已满"); req.getRequestDispatcher("/course_list.jsp").forward(req, resp); } }

逻辑说明:courseId 来自前端按钮,studentId 必须从 session 拿而不是从 URL 参数拿,这是防止越权的关键——用户改 URL 里的数字没用,身份以服务端 session 为准。getServletPath() 分流是 Servlet 时代最常用的多动作单类写法,等价于 Spring MVC 里的方法映射,只是要自己写分支。

对应的 StudentCourseDao 里是经典 JDBC:

public boolean addStudentCourse(int studentId, int courseId) { String selectSql = "SELECT COUNT(*) FROM student_course WHERE student_id=? AND course_id=?"; String insertSql = "INSERT INTO student_course(student_id, course_id, create_time) VALUES(?,?,NOW())"; // 先查是否已选,count > 0 直接返回 false // 未选则 PreparedStatement 执行 insert // finally 里依次关闭 rs、ps、conn }

逻辑说明:先查后插是选课系统最基本的幂等保护,防止同一学生在列表页连点两次按钮产生重复记录。PreparedStatement 用 ? 占位符传参,SQL 和参数分离,既防注入又可读。create_time 交给数据库 NOW() 生成,不在 Java 里拼时间字符串,是 JDBC 时代的标准做法。

3.3 JSP + Bootstrap:列表渲染、选课按钮与提示消息

前端是 Bootstrap + jQuery,JSP 负责把 Dao 查出来的数据渲染成表格。页头引入 JSTL 标签:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

课程列表主体用 Bootstrap 表格,按角色控制按钮显示:

<table class="table table-hover"> <thead> <tr><th>课程名</th><th>教师</th><th>学分</th><th>操作</th></tr> </thead> <tbody> <c:forEach items="${courseList}" var="course"> <tr> <td>${course.courseName}</td> <td>${course.teacherName}</td> <td>${course.credit}</td> <td> <c:if test="${sessionScope.loginUser.role == 'student'}"> <a href="${pageContext.request.contextPath}/course/select?courseId=${course.id}" class="btn btn-primary btn-sm" onclick="return confirm('确定选这门课?')">选这门课</a> </c:if> </td> </tr> </c:forEach> </tbody> </table>

逻辑说明:c:forEach 循环渲染表格行,c:if 按角色决定是否显示选课按钮。confirm() 是 jQuery 时代最常用的防误点方案,点取消返回 false,链接不会跳转。

参数说明:${course.xxx} 取值依赖实体类属性名,Servlet setAttribute 的 key 和 JSP 里写的 key 必须完全一致,否则页面空白且不报错。URL 里用 ${pageContext.request.contextPath} 拼绝对路径,而不是写死 /student_course,这样以后改 context 不用全局替换。这类渲染问题要用浏览器查看源码定位,比盯控制台有效。

到这一步,登录、课程列表、选课、结果反馈的闭环就通了。成绩录入、学生管理逻辑完全同构:Dao 多写 update,Servlet 多一个路由,JSP 多一个表单,半小时内能摸清全部套路。

4. 在 IDEA 里跑起这套 JavaWeb 项目:Tomcat、Navicat 与连接参数一次对齐

4.1 数据库脚本导入:先建空库,再跑 SQL

压缩包里的 SQL 脚本就是整套系统的数据基础。我习惯先在 Navicat 建一个空库,再导入脚本,避免脚本自带的建库语句和本地已有库冲突:

  1. Navicat 新建连接,字符集选 utf8mb4;
  2. 创建数据库,名字和脚本里一致,比如 student_course;
  3. 右键数据库 → 运行 SQL 文件,选择 .sql,等待执行完成;
  4. 重点核对三张核心表:user、course、student_course。

导入后顺手看一眼脚本里的 INSERT 语句,确认三种角色的账号密码都在。有些精简版脚本只初始化了管理员,教师和学生要靠自己手动加,这个差异会影响你后面验证三角色流程。建议在 Navicat 里执行一条 SELECT * FROM user 确认账号在,再进 IDEA 折腾,数据库这层通了,后面问题少一半。

4.2 JDBC 连接参数:driver、url 和时区三个坑位

连接配置在 jdbc.properties 或 DBUtil.java 里,核心四参数:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_course?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的数据库密码

参数说明:driver 类名由 MySQL 版本决定,MySQL 5.x 用 com.mysql.jdbc.Driver,MySQL 8.x 要换成 com.mysql.cj.jdbc.Driver,否则启动报 ClassNotFoundException;url 里 serverTimezone 是 MySQL 8 的强制要求,不配直接报时区异常;characterEncoding=utf8 管中文写入,useSSL=false 关掉本地连接的 SSL 握手告警。密码写错是最常见的连不上原因,报错信息里一般会直接提示 Access denied。

参数典型取值常见误配
drivercom.mysql.cj.jdbc.Driver8.x 还在用旧类名
urljdbc:mysql://localhost:3306/student_course端口不对、库名不对
serverTimezoneAsia/Shanghai缺失导致时区异常
characterEncodingutf8配置了但库表是 latin1 仍乱码

4.3 IDEA 配置 Tomcat:JDK、Artifact、上下文路径

在 IDEA 里跑 JavaWeb 项目,配置点就三个:JDK、Tomcat、Artifact。按下面顺序走,能避开大部分启动翻车:

  1. File → Project Structure → Project,SDK 选 1.8,Language level 选 8;
  2. 检查依赖:lib 目录下的 mysql-connector、jstl 等 jar 是否已被识别,没有就手动 Add as Library;
  3. 打开 Run/Debug Configurations,新增 Tomcat Server → Local,Deployment 标签页添加 Artifact,Application context 填 /student_course;
  4. 先编译一次确认 target 里有 classes,再点启动。

启动后访问 http://localhost:8080/student_course/login.jsp 打开登录页。这里最容易犯的错是 Application context 填的和 URL 对不上,context 是 /student_course,URL 就必须带这个前缀,前后不一致就是一片 404。

启动时先看 IDEA 的 console。常见三种输出要会识别:Tomcat started 表示容器起来了;Deployment of web application has finished 表示项目部署完成;Artifact 相关报错说明 target 里没编译出 classes。8080 端口被占会直接提示 Port was already in use,改 Tomcat 配置里的 HTTP port,或者把占用端口的进程清掉。控制台里的堆栈要看 Cause by 那一行,真正原因通常写在最后面,前面的框架日志大多是噪音。

4.4 三种账号各过一遍主流程

数据库脚本初始化了管理员、教师、学生三种账号,具体用户名密码看 SQL 里的 INSERT 语句。登录后按角色各验证一条链路:

角色验证动作预期结果
管理员新增一门课程、改一名学生资料列表刷新后有变化
教师进课程 → 学生名单 → 录入成绩保存后提示成功
学生选一门课 → 查成绩选课成功、成绩显示

我验证时有个习惯:学生端选课后,去 Navicat 里查 student_course 表,确认插入的 student_id、course_id 是对的。数据库层验证通过,代码基本就稳了。还有一个小坑:JSP 里所有静态资源引用、链接跳转,最好都写成 ${pageContext.request.contextPath} 开头的绝对路径。如果源码里看到资源引用没带 contextPath,跑通后建议顺手改掉,不然以后改 context 时图片、CSS、跳转链接全乱。

5. 避坑实录:JavaWeb 项目跑不起来的五个高频问题

5.1 Tomcat 10 部署老项目:404 或 ClassNotFound

现象:项目在别人机器上能跑,自己用 Tomcat 10 启动,访问就 404,控制台报 Servlet 相关类找不到。

原因:JDK 1.8 时代的项目用的是 javax.servlet 包,Tomcat 10 起全面切到 jakarta.servlet,包名不兼容,Servlet 注解和 API 全部失效。代码没变,容器版本变了。

解决:换 Tomcat 8.5 或 9.0.x,不要用 10 及以上。我接手任意一个 JavaWeb 老项目,第一步就是确认容器版本,只要源码里出现 javax.servlet,默认配 Tomcat 9。

5.2 MySQL 8 连不上:驱动名和时区双报错

现象:登录页正常打开,一提交账号密码就报 ClassNotFoundException: com.mysql.jdbc.Driver,或 The server time zone value 异常。

原因:本地装的是 MySQL 8,驱动旧类名不再兼容;同时 JDBC 要求 url 显式指定时区。两个坑经常一起出现。

解决:driver 换成 com.mysql.cj.jdbc.Driver,url 末尾加 serverTimezone=Asia/Shanghai。两条同时改,改完重启 Tomcat。只改一半还会报另一个错,这也是为什么我建议直接按第 4.2 节的参数表逐项核对。

5.3 中文乱码:JSP、请求、库表三处各自为政

现象:中文用户名存进数据库变问号;或页面显示一堆乱字符。

原因:三处编码不一致。JSP 页面声明 UTF-8,但请求没设置编码;或数据库连接没带 characterEncoding=utf8;或库表字符集是 latin1。

解决:统一成 UTF-8。Navicat 里把库和表字符集改成 utf8mb4;jdbc.url 加 characterEncoding=utf8;Servlet 读参数前执行 request.setCharacterEncoding("UTF-8")。三处缺一处,乱码就可能换个位置复发。

5.4 JSTL 标签原样输出或 $ 取不到值

现象:页面打开显示<c:forEach>原样代码,或者 ${course.courseName} 位置空白,控制台毫无报错。

原因:不识别 JSTL 标签说明缺 jstl 依赖;取不到值说明 Servlet setAttribute 的 key 和 JSP 里的 key 拼写不一致。EL 表达式对空值默认输出空串,所以静默失败。

解决:确认 WEB-INF/lib 或依赖里有 jstl.jar 和 standard.jar;再用浏览器查看源码对比渲染结果,逐个核对 key。这类问题盯控制台没用,要看渲染后的 HTML。

5.5 选课重复提交:刷新页面插了两条记录

现象:学生选课后手动刷新页面,student_course 表里出现两条相同记录。

原因:选课走的是 GET 链接,浏览器刷新会重放上一次请求。如果 Dao 层没做先查后插的幂等校验,就会插两条。

解决:StudentCourseDao 的 insert 前先查 count,已存在直接返回 false;更稳妥是把选课改成 POST 表单提交,避免 GET 被无意识刷新。答辩演示时网络一卡,手一抖就多一条记录,这个坑最容易在现场翻车。

老项目的问题集中在环境和编码两头,真正的业务逻辑报错反而少见。遇到问题先按「容器版本 → 数据库连接 → 编码 → 依赖」四步排查,比反复看业务代码效率高得多。

6. 把它改造成答辩加分项:限选人数校验与验收自查清单

6.1 加一个「限选人数」校验

原版选课只有幂等校验。答辩时老师常问「怎么防止课程选爆?」,加一个人数上限校验就能回应这个问题。在 StudentCourseDao 里,插入前先查目标课程的已选人数和上限:

String countSql = "SELECT COUNT(*) FROM student_course WHERE course_id=?"; String maxSql = "SELECT max_num FROM course WHERE id=?"; // 已选人数 >= max_num 时返回 false,提示课程已满

两条查询在同一个连接里执行,避免并发时读到不一致的数据。这个改动不碰 JSP,只在 Dao 层加判断、Servlet 改一下提示文字,十分钟能改完。

6.2 答辩前按这张清单自查

检查项操作通过标准
登录拦截未登录直接访问 course/list被踢回 login.jsp
角色隔离三种账号分别登录菜单与数据范围匹配
成绩闭环教师录入,学生查询分数一致
重复选课同一学生连点两次选课数据库只有一条
中文完整性新增带中文的课程列表与库表不乱码

我从那以后每次拿到一套 JavaWeb 项目,都强制先走一遍「数据库脚本 → 容器版本 → 登录链路」再动手改代码。版本对不上、编码不统一的项目,十个里有九个是环境问题,地基摆正了再谈扩展,会少走很多弯路。这套 Servlet + JSP 的选课系统,复杂度刚好够完整走一遍从部署到改造的过程,结构不花哨,适合反复拆开重装。希望帮到你,跑通之后你会发现,JavaWeb 老项目远没有想象中难啃。

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

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

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

立即咨询