☰
JSP+Servlet+JavaBean选课系统:老架构实战与避坑指南
2026/9/26 18:00:35 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的数据库设计课程设计完整项目,采用jsp+servlet+javabean技术路线,基于Tomcat8.5、Eclipse与SQL Server 2017开发,适合作为课程设计、毕业设计或Java Web入门练手参考。项目围绕高校选课管理系统展开,覆盖管理员、教师、学生三类角色:管理员可管理教师、学生与课程信息并修改密码;教师可查看任课信息、开设或取消课程、按班级或课程统计学生成绩与排名、录入成绩并自动更新学分;学生可查看课程、选课退选及按学年查询成绩。资源包共237个文件,以js脚本、java源码、css样式、jsp页面及png、jpg图片为主,另含xml配置与字体资源,压缩包约5.95MB,并附有说明文档、E-R图与实体关系模型,便于理解数据库设计思路。目前已有1052人学习下载,可帮助读者快速掌握Java Web分层开发与数据库建模方法。

1. 高校选课管理系统拆包:jsp+servlet+javabean 这套老架构还值不值得跑一遍

如果你正在做数据库设计课程设计,或者被安排了一个 JavaWeb 大作业,大概率绕不开“选课管理系统”这个题目。我拿到这份资源的时候,第一反应是:jsp+servlet+javabean,Tomcat 8.5,SQL Server 2017,Eclipse 2019——这套组合放在今天确实不算新,但它恰好是很多高校课程设计、头歌实训、毕设选题里反复出现的标准模板。换句话说,它不是让你追新技术,而是让你把 MVC 三层结构、数据库表设计、E-R 图、事务处理这些基本功一次性跑通。

这份资源包含完整源码、说明文档、E-R 图和实体关系模型,角色分管理员、教师、学生三端。管理员管教师、学生、课程和密码;教师看任课信息、开课退课、查班级均绩排名、录成绩并自动更新学分;学生看个人信息、选课退选、按学年查成绩。Web 服务器 Tomcat 8.5,数据库 SQL Server 2017,技术路线就是 jsp+servlet+javabean。适合谁?适合需要交课程设计、想理解传统 JavaWeb 项目怎么分层、或者想拿一个能跑通的选课系统做二次开发的人。不适合想学 Spring Boot 的人,但如果你连 servlet 生命周期都没搞明白,这套代码反而是最好的教材。

2. 环境搭建与数据库还原:Tomcat 8.5 和 SQL Server 2017 怎么配才不翻车

2.1 为什么选 Tomcat 8.5 而不是 Tomcat 10

Tomcat 8.5 对应的是 Servlet 3.1 规范,而 Tomcat 10 已经切到 Jakarta EE 9,包名从javax.servlet变成jakarta.servlet。这份资源里的 servlet 代码用的是javax.servlet,如果你直接扔进 Tomcat 10,启动就报 ClassNotFoundException。常见做法是:要么老老实实用 Tomcat 8.5,要么把代码里所有javax.servlet改成jakarta.servlet,但后者工作量不小,还容易漏。我一般会建议课程设计阶段直接用 Tomcat 8.5,省去迁移的麻烦。

Eclipse Java 2019 版本对 Tomcat 8.5 的支持很成熟,新建 Dynamic Web Project 时 target runtime 选 Tomcat 8.5,Dynamic web module version 选 3.1,这样生成的 web.xml 和 servlet 映射才不会出玄学问题。

2.2 数据库还原:先建库再跑脚本

SQL Server 2017 还原这份资源的数据库,步骤不复杂,但有几个参数容易设错。先创建一个空数据库,字符集用默认的Chinese_PRC_CI_AS,排序规则别乱改。然后执行资源里的建表脚本。

-- 创建数据库 CREATE DATABASE CourseSelectionDB ON PRIMARY ( NAME = 'CourseSelectionDB_data', FILENAME = 'D:\SQLData\CourseSelectionDB_data.mdf', SIZE = 10MB, MAXSIZE = 200MB, FILEGROWTH = 10MB ) LOG ON ( NAME = 'CourseSelectionDB_log', FILENAME = 'D:\SQLData\CourseSelectionDB_log.ldf', SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB ); GO -- 用户表:区分管理员、教师、学生 CREATE TABLE Users ( user_id VARCHAR(20) PRIMARY KEY, password VARCHAR(50) NOT NULL, role VARCHAR(10) NOT NULL CHECK (role IN ('admin','teacher','student')), name NVARCHAR(20) NOT NULL ); GO -- 课程表 CREATE TABLE Courses ( course_id VARCHAR(20) PRIMARY KEY, course_name NVARCHAR(50) NOT NULL, credit INT NOT NULL, teacher_id VARCHAR(20) FOREIGN KEY REFERENCES Users(user_id) ); GO -- 选课表:记录学生选课和成绩 CREATE TABLE Enrollments ( enroll_id INT IDENTITY(1,1) PRIMARY KEY, student_id VARCHAR(20) FOREIGN KEY REFERENCES Users(user_id), course_id VARCHAR(20) FOREIGN KEY REFERENCES Courses(course_id), score DECIMAL(5,2) NULL, enroll_year VARCHAR(10) NOT NULL ); GO

这段脚本里,Users表用role字段区分三种角色,避免建三张结构相似的表。Enrollments表的score允许为空,因为选课的时候还没成绩,教师录入后才更新。enroll_year用来支持“按学年查询考试成绩”这个功能。注意外键约束的顺序:先建Users,再建Courses,最后建Enrollments,否则外键引用会失败。

2.3 JDBC 连接配置:SQL Server 的坑比 MySQL 多

SQL Server 的 JDBC 驱动和 MySQL 不一样,类名是com.microsoft.sqlserver.jdbc.SQLServerDriver,URL 格式也特殊。资源里一般会有一个DBUtil.java或者db.properties,我习惯把连接参数单独抽出来。

// DBUtil.java public class DBUtil { private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; private static final String URL = "jdbc:sqlserver://localhost:1433;databaseName=CourseSelectionDB;encrypt=false"; private static final String USER = "sa"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new SQLException("SQL Server 驱动未找到", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } }

encrypt=false这个参数在 SQL Server 2017 之后的驱动里必须加,否则会报 SSL 握手错误。databaseName后面直接跟库名,不要写成jdbc:sqlserver://localhost:1433;CourseSelectionDB,那是 MySQL 的写法。端口默认 1433,如果你装 SQL Server 的时候改了实例名,URL 里还要加instanceName。

提示:SQL Server 配置管理器里要确保 TCP/IP 协议已启用,并且 IP 地址里的 TCP 端口是 1433。很多人装完默认是禁用状态,连不上就以为是代码问题。

3. 三端功能怎么落地:servlet 分发与 javabean 数据封装

3.1 用 javabean 承载数据,别在 servlet 里拼字符串

这份资源的核心分层是 jsp 做展示、servlet 做控制、javabean 做数据模型。我见过不少课程设计把数据库查询结果直接塞进HttpServletRequest的 attribute 里当 Map 用,短期能跑,但代码一多就乱。正确的做法是每个实体一个 javabean。

// User.java public class User { private String userId; private String password; private String role; private String name; public User() {} public User(String userId, String password, String role, String name) { this.userId = userId; this.password = password; this.role = role; this.name = name; } // getter 和 setter 省略 }

javabean 的要求是:无参构造方法、私有字段、public 的 getter/setter。jsp 页面里用<jsp:useBean>或者 EL 表达式${user.name}取值时,底层就是靠反射调用 getter。如果你字段名写成userName但 getter 写成getUsername(),EL 表达式会取不到值,这种大小写问题排查起来很费时间。

3.2 servlet 按角色分发请求

三端功能多,但 servlet 不需要写几十个。常见做法是用一个LoginServlet处理登录,根据role字段把 session 里的用户信息设好,然后重定向到对应的主页。后续每个功能模块一个 servlet,比如TeacherServlet、StudentServlet、CourseServlet。

// LoginServlet.java 核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userId = request.getParameter("userId"); String password = request.getParameter("password"); UserDao dao = new UserDao(); User user = dao.login(userId, password); if (user == null) { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } HttpSession session = request.getSession(); session.setAttribute("currentUser", user); switch (user.getRole()) { case "admin": response.sendRedirect("admin/index.jsp"); break; case "teacher": response.sendRedirect("teacher/index.jsp"); break; case "student": response.sendRedirect("student/index.jsp"); break; default: response.sendRedirect("login.jsp"); } }

这里用sendRedirect而不是forward,是为了避免刷新页面时重复提交登录表单。session.setAttribute("currentUser", user)之后,jsp 页面里用${currentUser.name}就能拿到当前登录人。注意 session 超时时间在web.xml里配置,默认 30 分钟,课程设计演示够用。

3.3 教师录入成绩与学分自动更新

教师端最核心的功能是录入成绩,并且自动更新学生已修总学分。这个逻辑不能只靠一条 UPDATE 语句,因为学分更新依赖于成绩是否及格。常见做法是在Enrollments表更新 score 之后,再查一次该学生所有及格课程的总学分,写回Users表或者单独的学生扩展表。

// ScoreServlet.java 录入成绩 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int enrollId = Integer.parseInt(request.getParameter("enrollId")); BigDecimal score = new BigDecimal(request.getParameter("score")); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 更新成绩 String updateScore = "UPDATE Enrollments SET score = ? WHERE enroll_id = ?"; PreparedStatement ps1 = conn.prepareStatement(updateScore); ps1.setBigDecimal(1, score); ps1.setInt(2, enrollId); ps1.executeUpdate(); // 查询该学生所有及格课程的总学分 String sumCredit = "SELECT SUM(c.credit) FROM Enrollments e " + "JOIN Courses c ON e.course_id = c.course_id " + "WHERE e.student_id = (SELECT student_id FROM Enrollments WHERE enroll_id = ?) " + "AND e.score >= 60"; PreparedStatement ps2 = conn.prepareStatement(sumCredit); ps2.setInt(1, enrollId); ResultSet rs = ps2.executeQuery(); int totalCredit = 0; if (rs.next()) { totalCredit = rs.getInt(1); } // 更新学生总学分 String updateCredit = "UPDATE Users SET total_credit = ? WHERE user_id = " + "(SELECT student_id FROM Enrollments WHERE enroll_id = ?)"; PreparedStatement ps3 = conn.prepareStatement(updateCredit); ps3.setInt(1, totalCredit); ps3.setInt(2, enrollId); ps3.executeUpdate(); conn.commit(); response.sendRedirect("teacher/scoreList.jsp?msg=success"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); response.sendRedirect("teacher/scoreList.jsp?msg=error"); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这段代码的关键点是事务。更新成绩和更新学分必须在一个事务里,否则成绩更新了但学分没更新,学生查成绩时就会对不上。conn.setAutoCommit(false)开启事务,conn.commit()提交,异常时conn.rollback()回滚。注意Users表里需要有一个total_credit字段,如果资源里没有,自己加一列INT DEFAULT 0。

注意:SQL Server 的 JDBC 驱动在事务回滚后,PreparedStatement 最好重新创建,不要复用。我踩过这个坑,回滚后复用同一个 ps 会报“该语句已关闭”。

4. 避坑与排查:这套老架构最容易翻车的五个地方

4.1 中文乱码:POST 和 GET 要分开处理

现象:登录时输入中文用户名,后台拿到的是乱码,或者 jsp 页面显示问号。

原因:Tomcat 8.5 默认的 URI 编码是 UTF-8,但请求体编码取决于页面。POST 请求如果不设request.setCharacterEncoding("UTF-8"),默认用 ISO-8859-1 解码。GET 请求在 Tomcat 8.5 里由server.xml的URIEncoding决定,默认不是 UTF-8。

解决:在每一个 servlet 的doPost第一行加request.setCharacterEncoding("UTF-8");server.xml的 Connector 标签里加URIEncoding="UTF-8"。jsp 页面头部统一写<%@ page contentType="text/html;charset=UTF-8" language="java" %>。

4.2 选课退选时并发导致学分算错

现象:两个学生同时选同一门课,课程容量只剩一个名额,结果两个人都选上了。

原因:先查容量再插入,中间没有加锁,两个线程都查到“还有名额”。

解决:在Courses表加一个current_count字段,选课时用UPDATE Courses SET current_count = current_count + 1 WHERE course_id = ? AND current_count < capacity,根据executeUpdate返回的影响行数判断是否选上。返回 1 就是选课成功,返回 0 就是已满。这个做法比先查后插可靠得多。

4.3 jsp 页面里 Java 代码太多,改一个字段要动五个文件

现象:学生信息展示页面里嵌了大段<% %>脚本,数据库字段一改,jsp 和 servlet 都要跟着改。

原因:没有用 EL 和 JSTL,直接在 jsp 里写 JDBC 查询。

解决:把数据库操作全部收进 DAO 层,jsp 只负责用${}取 request 或 session 里的 javabean。如果资源里已经有 DAO,检查一下 jsp 里还有没有漏网的<% Class.forName... %>,有就删掉。

4.4 Tomcat 启动报 404,但项目明明部署了

现象:Eclipse 里 Tomcat 启动正常,访问http://localhost:8080/项目名/login.jsp却 404。

原因:Eclipse 默认把项目部署到 workspace 的.metadata目录下,而不是 Tomcat 的webapps。有时候 context path 被改成了/,访问路径就变了。

解决:双击 Tomcat 服务器,在 Modules 标签里看 Path 是不是项目名。如果是/,访问时就不需要加项目名。另外检查web.xml里welcome-file-list有没有配login.jsp。

4.5 SQL Server 驱动版本和 JDK 不匹配

现象:ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver,但 jar 包明明放进WEB-INF/lib了。

原因:SQL Server 的 JDBC 驱动分mssql-jdbc-7.x和mssql-jdbc-9.x,7.x 支持 JDK 8,9.x 需要 JDK 11。Eclipse 2019 默认用 JDK 8 的话,放 9.x 的驱动会加载失败。

解决:JDK 8 用mssql-jdbc-7.4.1.jre8.jar,JDK 11 用mssql-jdbc-9.4.1.jre11.jar。放进去之后在 Eclipse 里右键项目 → Build Path → Configure Build Path → Libraries,确认 jar 在 Classpath 下。

5. 从能跑到好用:E-R 图验证与查询性能的一个小技巧

资源里附了 E-R 图和实体关系模型,很多人拿到就扔一边,其实这张图是验证数据库设计是否合理的最后一道防线。我一般会做一件事:把 E-R 图里的实体和关系逐个对照建表脚本,看有没有多对多关系被漏掉。比如“学生选课”是多对多,必须有一张中间表Enrollments,如果只靠Users和Courses两张表,选课记录就存不下来。再比如“教师开课”是一对多,Courses表里的teacher_id外键就是体现。如果 E-R 图里画了某个关系但建表脚本里没有对应字段,那查询的时候一定会出问题。

另一个容易被忽略的是索引。这份资源的查询里,教师端要“按行政班级查看学生均绩及排名”,学生端要“按学年查询考试成绩”,这两个查询都涉及Enrollments表的student_id和enroll_year字段。如果数据量到了几千条,不加索引会明显变慢。

-- 在 Enrollments 表上建复合索引 CREATE NONCLUSTERED INDEX IX_Enrollments_Student_Year ON Enrollments (student_id, enroll_year) INCLUDE (course_id, score);

INCLUDE里把course_id和score带上,这样查询成绩时可以直接从索引页取数据,不用回表。SQL Server 的执行计划里如果看到 Key Lookup,就说明缺了 INCLUDE 字段。这个技巧在课程设计答辩的时候提一句,比单纯说“我用了 JDBC”要加分。

验证方法也简单:在 SQL Server Management Studio 里开“显示实际执行计划”,跑一遍排名查询,看有没有 Index Scan。如果是 Scan 而不是 Seek,说明索引没命中,检查一下查询条件里的字段顺序和索引定义是否一致。

提示:索引不是越多越好,Enrollments表的插入操作会因为索引变慢。选课高峰期如果并发高,索引维护的开销要算进去。课程设计数据量小,加一两个关键索引就够了。

从那以后我每次拿到一个数据库课程设计,都会先把 E-R 图摊开,对着建表脚本一行行核对外键和中间表,再跑一遍执行计划看索引。这个习惯帮我省了很多答辩时被问“为什么这里查这么慢”的后悔药。希望帮到你。

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

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

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

立即咨询