JavaWeb学生选课系统:从Servlet/JSP到数据库事务的实战开发指南
2026/9/19 22:39:19 网站建设 项目流程

简介:在JavaWeb开发领域,Servlet和JSP是构建动态Web应用的基石,它们定义了请求-响应的核心模型。理解其原理,是掌握后续Spring等高级框架的前提。通过JDBC操作MySQL数据库,开发者能够实现数据的持久化存储与业务逻辑的落地,其中数据库事务管理是保证数据一致性的关键技术,尤其在处理并发操作时价值凸显。这些技术共同支撑了诸如学生选课管理系统这类经典应用场景的开发。本文以选课系统为例,深入探讨了在Servlet/JSP技术栈下,如何运用JDBC和数据库事务解决高并发下的选课超卖问题,并详细拆解了用户会话管理、密码安全存储等关键模块的实现,为初学者提供了一个从设计到部署的完整工程实践范本。

1. 项目概述:一个典型的JavaWeb选课系统意味着什么?

如果你是一名计算机相关专业的学生,或者刚入行的Java开发者,那么“学生选课管理系统”这个名字你一定不陌生。它几乎是每个JavaWeb初学者在掌握了Servlet、JSP、JDBC之后,第一个有完整业务逻辑、需要前后端联动的“毕业设计级”练手项目。我当年也是从这个项目开始,真正理解了MVC模式、数据库设计、会话管理和业务流转。这个项目标题“javaWeb学生选课管理系统(完整源码+数据库)”背后,不仅仅是一堆代码和SQL文件,它代表着一个从理论到实践的完整闭环,涵盖了用户角色管理、核心业务流程、数据持久化和基础Web安全等关键知识点。

简单来说,这个系统模拟了大学里学生在线选课的核心场景。通常,它涉及三类主要用户:学生、教师和管理员。学生可以查看可选课程、进行选课、退课,并查询自己的课表与成绩;教师可以发布自己教授的课程、管理选课学生名单、录入成绩;管理员则负责最底层的系统维护,包括学生/教师账号管理、课程信息管理、开课计划安排等。整个系统的业务逻辑围绕“课程”这一核心实体展开,通过状态(如可选、已满、已选、已结课)来控制业务流程。对于学习者而言,成功复现并理解这个项目,意味着你基本掌握了使用Java技术栈(Servlet/JSP + JDBC + MySQL)开发一个具有增删改查(CRUD)功能的动态Web应用的能力,这是迈向企业级开发非常扎实的一步。

2. 系统核心架构与设计思路拆解

在动手敲代码之前,理清系统的设计思路至关重要。一个结构混乱的项目,即使功能都能跑通,其代码也如同乱麻,难以维护和扩展。对于这个选课系统,我们采用经典的三层架构(表现层、业务逻辑层、数据访问层)来组织代码,这不仅是行业最佳实践,也能让初学者清晰地看到数据是如何从浏览器页面流转到数据库,再返回页面的。

2.1 技术栈选型:为什么是Servlet/JSP + MySQL?

看到“javaWeb”这个关键词,很多同学可能会疑惑:现在不都流行Spring Boot吗?为什么还要学“过时”的Servlet/JSP?我的观点是,Servlet/JSP是理解JavaWeb的基石。Spring MVC等现代框架在底层封装和简化了Servlet,但核心请求-响应模型、过滤器、监听器等概念一脉相承。直接从框架入手,就像还没学会走路就去开车,遇到复杂问题很容易不知其所以然。用原生Servlet/JSP完成这个项目,你能亲手处理HttpServletRequestHttpServletResponse,深刻理解会话(Session)和上下文(Application)的作用域,这对你后续理解Spring中的@Controller@RequestMapping有极大帮助。

数据库方面,MySQL是毫无争议的选择。它开源、免费、社区活跃,是学习关系型数据库的绝佳平台。选课系统涉及的学生、课程、选课记录等,都是典型的结构化数据,非常适合用MySQL的表和关系来建模。通过这个项目,你将熟练掌握SQL语句的编写、JDBC的连接与操作、以及事务处理的基本概念(例如,学生选课时,需要同时更新选课记录和课程剩余名额,这两个操作必须在一个事务中完成)。

2.2 数据库设计:表结构是系统的骨架

数据库设计是项目的重中之重,表结构设计得好,后续业务开发事半功倍。核心表通常包括以下几张:

  1. 用户表 (user/account): 存储学生、教师、管理员的基础信息。通常通过一个role字段(如0-学生,1-教师,2-管理员)来区分身份。这里有一个设计技巧:可以将三类用户的共性字段(如登录名、密码、真实姓名)放在此表,而特有信息(如学生的所属班级、教师的职称)通过外键关联到各自的详情表,或者直接使用可空字段。对于初学者,放在一张表里更简单直接。
  2. 课程表 (course): 存储课程的基本信息,如课程编号、名称、学分、授课教师ID(外键关联用户表)、课程容量、已选人数、课程介绍等。已选人数这个字段非常关键,它是实现“课程容量控制”和“避免超选”的核心。
  3. 选课表 (sc/student_course): 这是连接学生和课程的纽带,是典型的“多对多”关系中间表。字段包括学生ID、课程ID、选课时间,以及后续的成绩字段。这张表记录了每个学生的每一次选课行为。

设计心得:在course表中维护已选人数,而不是每次都在sc表中COUNT,是一种典型的“空间换时间”和保证数据一致性的做法。每次学生选课或退课,都需要在同一个事务中更新sc表和course表的已选人数。这能极大提升查询课程列表时“判断是否已满”的性能。

  1. 其他辅助表:如班级表开课计划表等,可以根据系统复杂度增加。

下图展示了核心表之间的简化关系: (注:此处用文字描述表关系,因禁止使用Mermaid图表) 学生 (user) 和课程 (course) 之间存在多对多关系,通过选课表 (sc) 进行关联。一个学生可以选多门课,一门课可以被多个学生选。课程表中的teacher_id外键关联到用户表中的教师记录。

2.3 目录结构规划:让代码各就各位

一个清晰的目录结构是专业性的体现。在IDE(如IntelliJ IDEA)中创建项目后,建议按如下方式组织:

StudentCourseSystem/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yourdomain/ │ │ │ ├── controller/ // Servlet,处理请求 │ │ │ ├── service/ // 业务逻辑层 │ │ │ │ └── impl/ // 业务逻辑实现类 │ │ │ ├── dao/ // 数据访问层接口 │ │ │ │ └── impl/ // DAO实现类(JDBC操作) │ │ │ ├── model/ // 实体类(POJO),对应数据库表 │ │ │ ├── filter/ // 过滤器,如登录验证、字符编码 │ │ │ └── util/ // 工具类,如数据库连接工具 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml // 部署描述符 │ │ │ └── lib/ // 依赖的JAR包 │ │ ├── css/ // 样式表 │ │ ├── js/ // JavaScript文件 │ │ ├── images/ // 图片资源 │ │ └── views/ // JSP页面,按模块分文件夹 │ │ ├── student/ // 学生相关页面 │ │ ├── teacher/ // 教师相关页面 │ │ ├── admin/ // 管理员相关页面 │ │ └── common/ // 公共页面(如登录、头部、尾部) └── pom.xml // Maven配置文件(如果使用Maven)

这种分层结构将控制逻辑、业务计算、数据访问和页面展示分离,符合单一职责原则。当需要修改数据库操作时,你只需要关注dao.impl包;修改业务规则,则只需改动service包。

3. 核心功能模块实现详解

有了清晰的设计蓝图,我们就可以开始动手实现核心功能了。这里我将挑选几个最具代表性、也最容易踩坑的模块,深入讲解其实现细节和背后的思考。

3.1 用户登录与会话管理

登录是系统的门户,也是安全的第一道防线。实现一个健壮的登录模块,远不止是查询数据库比对用户名密码那么简单。

Servlet实现登录控制:LoginServletdoPost方法中,我们需要:

  1. 获取请求参数(用户名、密码、验证码)。
  2. 校验验证码(将用户输入的验证码与Session中存储的验证码对比,防止暴力破解)。这是很多初学者会忽略的安全步骤。
  3. 调用UserServicelogin方法进行身份验证。UserService内部会调用UserDao查询数据库。
  4. 验证成功后,将用户对象(至少包含用户ID、姓名、角色)存入HttpSession中。例如:session.setAttribute("loginUser", user);
  5. 根据用户角色(user.getRole()),使用response.sendRedirect()跳转到不同的主页(如/student/index.jsp/admin/main.jsp)。

关键技巧:使用过滤器实现全局登录验证单独在每个Servlet里检查Session太麻烦。我们应该创建一个LoginFilter,配置它拦截所有需要登录才能访问的路径(如/student/*,/teacher/*,/admin/*)。在过滤器的doFilter方法中,检查Session里是否存在loginUser。如果不存在,则重定向到登录页面。这样,所有受保护的资源都自动拥有了登录检查,代码干净又安全。

// LoginFilter 伪代码示例 HttpSession session = req.getSession(false); // false表示如果不存在则不创建新session if (session == null || session.getAttribute("loginUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); // 放行,继续执行后续的Servlet或过滤器

密码安全:绝对不要在数据库中明文存储密码!必须使用哈希算法(如MD5、SHA-256)进行加密存储。更安全的做法是“加盐”(salt),即为每个用户生成一个随机字符串,与密码拼接后再哈希。即使数据库泄露,攻击者也很难反推出原始密码。

// 一个简单的MD5加密工具方法示例 public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } // 注册或修改密码时:user.setPassword(md5(password)); // 登录验证时:查询到的user.getPassword().equals(md5(inputPassword))

3.2 学生选课业务逻辑与并发控制

选课是系统的核心业务,也是最容易出并发问题的地方。想象一下,一门热门课程只剩最后一个名额,两个学生同时点击“选课”。如果没有妥善处理,可能会导致“超选”。

选课Servlet (SelectCourseServlet) 的核心流程:

  1. 参数与状态校验:从Session获取当前学生ID,从请求获取课程ID。检查学生是否已选该课程(查询sc表),检查课程是否存在且状态为“可选”。
  2. 关键并发控制:检查课程当前已选人数 (currentSelected) 是否小于课程容量 (capacity)。这里存在一个时间窗口问题:在检查之后和更新之前,数据可能已被其他请求修改。
  3. 使用数据库事务与行锁:这是解决问题的关键。我们需要在一个数据库事务中完成以下操作:
    Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 使用 FOR UPDATE 或乐观锁机制查询课程信息,锁定该行记录 // 例如:SELECT capacity, selected FROM course WHERE id = ? FOR UPDATE Course course = courseDao.getCourseForUpdate(conn, courseId); // 2. 再次判断名额 if (course.getSelected() < course.getCapacity()) { // 3. 插入选课记录 scDao.insert(conn, studentId, courseId); // 4. 更新课程已选人数(原子操作) courseDao.incrementSelected(conn, courseId); conn.commit(); // 提交事务 // 返回选课成功 } else { conn.rollback(); // 名额不足,回滚 // 返回“课程已满”提示 } } catch (SQLException e) { if (conn != null) conn.rollback(); // 处理异常 } finally { DBUtil.close(conn); }
    FOR UPDATE会在查询时对这条课程记录加上行锁,直到当前事务提交或回滚,其他事务无法修改这条记录,从而彻底杜绝了超选的可能。
  4. 返回结果:根据操作结果,向前端返回JSON或设置请求属性,然后转发到JSP页面显示成功或失败信息。

前端交互优化: 在选课按钮点击后,最好立即将其置为禁用状态(disabled),并显示“处理中...”,防止用户重复提交。待服务器返回结果后,再更新页面状态。

3.3 管理员功能:课程与用户管理

管理员模块通常包含大量的列表展示、分页查询、新增、修改和删除操作。这里以“课程管理”为例,讲解如何实现一个功能完整的后台管理页面。

1. 分页查询课程列表:这是后台管理最常见的功能。我们需要从前端接收两个参数:pageNum(当前页码)和pageSize(每页条数,如10)。 在CourseQueryServlet中:

int pageNum = Integer.parseInt(request.getParameter("pageNum")); int pageSize = 10; // 计算起始索引 int start = (pageNum - 1) * pageSize; // 调用Service方法,获取分页数据 PageInfo<Course> pageInfo = courseService.getCourseByPage(start, pageSize); // 将pageInfo对象存入request,转发到JSP request.setAttribute("pageInfo", pageInfo); request.getRequestDispatcher("/admin/courseList.jsp").forward(request, response);

CourseServiceCourseDao中,需要执行两条SQL:

  • 查询总记录数:SELECT COUNT(*) FROM course
  • 查询当前页数据:SELECT * FROM course LIMIT ?, ?(MySQL语法)

2. JSP页面展示与分页控件:courseList.jsp中,使用JSTL和EL表达式遍历pageInfo.list。分页控件的逻辑是计算总页数,并生成上一页、下一页、以及若干数字页码的链接。

<%-- 遍历课程列表 --%> <c:forEach items="${pageInfo.list}" var="course"> <tr> <td>${course.id}</td> <td>${course.name}</td> <td>${course.teacherName}</td> <td>${course.selected}/${course.capacity}</td> <td> <a href="/admin/courseEdit.jsp?id=${course.id}">编辑</a> <a href="javascript:deleteCourse(${course.id})">删除</a> </td> </tr> </c:forEach> <%-- 分页控件 --%> <div class="pagination"> <c:if test="${pageInfo.pageNum > 1}"> <a href="courseQuery?pageNum=${pageInfo.pageNum-1}">上一页</a> </c:if> <c:forEach begin="1" end="${pageInfo.pages}" var="p"> <c:choose> <c:when test="${p == pageInfo.pageNum}"> <span class="current">${p}</span> </c:when> <c:otherwise> <a href="courseQuery?pageNum=${p}">${p}</a> </c:otherwise> </c:choose> </c:forEach> <c:if test="${pageInfo.pageNum < pageInfo.pages}"> <a href="courseQuery?pageNum=${pageInfo.pageNum+1}">下一页</a> </c:if> </div>

3. 删除操作的注意事项:删除课程并非简单地DELETE FROM course WHERE id=?。必须考虑业务关联性:如果一门课程已经有学生选了(在sc表中有记录),直接删除课程会导致数据不一致(存在“孤儿”选课记录)。因此,删除前需要检查外键约束,或者采用“逻辑删除”(在课程表中增加一个is_deleted字段,标记为已删除,而非物理删除)。对于初学者项目,可以在删除前进行关联查询,如果有选课记录,则提示管理员“无法删除已有学生选择的课程”。

4. 数据库操作与工具类封装

与数据库打交道是JavaWeb项目的日常。如何优雅、高效、安全地进行数据库操作,是衡量代码质量的重要标准。

4.1 JDBC工具类:告别重复代码

每个DAO方法都写一遍加载驱动、获取连接、释放资源,是极其枯燥且容易出错的。我们必须封装一个DBUtil工具类。

public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/student_course?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "yourpassword"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException("数据库驱动加载失败!"); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源(重载多个版本) public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } public static void close(Connection conn, Statement stmt) { close(conn, stmt, null); } }

重要提示:数据库连接是非常宝贵的资源,必须确保在使用完毕后关闭。务必在finally代码块中调用关闭方法,或者使用Java 7的try-with-resources语法。

4.2 DAO模式:数据访问层的标准化

DAO(Data Access Object)模式将底层数据访问逻辑与业务逻辑分离。我们为每个实体类(如User,Course)定义一个接口和一个基于JDBC的实现。 以UserDao为例:

public interface UserDao { User findByUsernameAndPassword(String username, String password) throws SQLException; User findById(int id) throws SQLException; List<User> findAll() throws SQLException; int insert(User user) throws SQLException; int update(User user) throws SQLException; int delete(int id) throws SQLException; } public class UserDaoImpl implements UserDao { @Override public User findByUsernameAndPassword(String username, String password) throws SQLException { String sql = "SELECT id, username, real_name, role FROM user WHERE username = ? AND password = ?"; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); // 注意:这里传入的密码应是加密后的 rs = pstmt.executeQuery(); if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealName(rs.getString("real_name")); user.setRole(rs.getInt("role")); return user; } return null; } finally { DBUtil.close(conn, pstmt, rs); } } // ... 其他方法实现 }

使用PreparedStatement而不是Statement,可以有效防止SQL注入攻击,并且性能更好。

4.3 事务管理:保证数据一致性

如前文“选课”例子所示,对于涉及多个数据库操作(如插入选课记录和更新课程人数)的业务,必须使用事务来保证原子性。我们的工具类可以进一步扩展,提供一个简单的事务管理方法,但更常见的做法是在Service层进行事务控制。对于复杂的项目,可以考虑使用像Spring这样的框架来管理声明式事务。但在原生Servlet/JSP项目中,手动管理事务是必须掌握的技能。

5. 前端页面交互与用户体验优化

虽然这是一个后端为主的项目,但一个友好、健壮的前端界面同样重要。我们使用JSP作为视图层,结合EL表达式、JSTL标签库以及一些基础的JavaScript和CSS。

5.1 使用JSTL与EL简化JSP

在JSP中直接编写Java代码(脚本片段<% %>)是过时且难以维护的做法。我们应该使用EL表达式${}来输出数据,使用JSTL标签(如<c:forEach>,<c:if>)来进行逻辑控制。 首先,需要在页面头部引入JSTL核心标签库:

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

然后就可以愉快地使用了:

<%-- 判断用户是否登录 --%> <c:if test="${empty sessionScope.loginUser}"> <jsp:forward page="/login.jsp"/> </c:if> <%-- 根据角色显示不同菜单 --%> <ul> <c:choose> <c:when test="${sessionScope.loginUser.role == 0}"><%-- 学生 --%> <li><a href="selectCourse">我要选课</a></li> <li><a href="myCourse">我的课程</a></li> </c:when> <c:when test="${sessionScope.loginUser.role == 1}"><%-- 教师 --%> <li><a href="myTeaching">我的授课</a></li> <li><a href="inputGrade">录入成绩</a></li> </c:when> </c:choose> </ul>

5.2 表单验证与Ajax应用

前端基础验证:使用HTML5属性(如required,pattern)或简单的JavaScript,在数据提交前进行基本校验(如非空、邮箱格式、数字范围)。这能快速给用户反馈,减少无效请求。

<input type="text" name="username" required placeholder="请输入用户名"> <input type="number" name="credit" min="1" max="10" required>

Ajax提升体验:对于“检查用户名是否重复”、“选课操作”等场景,使用Ajax实现局部刷新,能极大提升用户体验。例如,选课按钮点击后,通过Ajax发送请求到SelectCourseServlet,Servlet处理完成后返回一个JSON结果(如{"success": true, "message": "选课成功"}),前端JavaScript根据这个结果动态更新页面上的提示信息,而无需刷新整个页面。

// 使用原生JavaScript或jQuery发起Ajax请求示例(jQuery) $('#selectBtn').click(function(){ var courseId = $(this).data('course-id'); $.post('selectCourse', {courseId: courseId}, function(data){ if(data.success){ alert(data.message); // 更新页面状态,如禁用按钮,更新已选人数显示 $(this).prop('disabled', true).text('已选'); }else{ alert('选课失败:' + data.message); } }, 'json'); });

对应的Servlet需要设置响应内容类型并输出JSON:

response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); // 使用如Gson、Jackson或简单的字符串拼接生成JSON out.print("{\"success\": true, \"message\": \"选课成功\"}"); out.flush();

5.3 简单的样式与布局

使用CSS框架(如Bootstrap)可以快速搭建出美观、响应式的界面。将Bootstrap的CSS和JS文件引入项目,按照其网格系统和组件进行布局,能让你的项目瞬间变得专业起来。即使不用框架,至少也要编写一些基础CSS来规整表格、表单和按钮的样式,避免页面过于简陋。

6. 项目部署、测试与常见问题排查

代码写完只是第一步,能让项目在服务器上稳定运行,才是真正的完成。

6.1 环境准备与项目部署

  1. 开发环境:你需要安装JDK(建议JDK 8或11)、一个IDE(IntelliJ IDEA或Eclipse)、MySQL数据库和一个Servlet容器(如Tomcat)。IDEA集成了Tomcat,配置非常方便。
  2. 数据库初始化:在MySQL中创建一个名为student_course的数据库,然后运行项目附带的SQL脚本(通常是一个.sql文件),创建所有表并插入一些初始测试数据(如管理员账号、样例课程、学生和教师账号)。
  3. 项目配置
    • 数据库连接:修改DBUtil工具类中的URLUSERPASSWORD,确保与你的本地MySQL配置一致。
    • 字符编码:在web.xml中配置一个字符编码过滤器,统一处理请求和响应的编码为UTF-8,避免中文乱码。
    <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>com.yourdomain.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
    • Tomcat部署:在IDEA中,配置一个本地Tomcat服务器,将项目的Artifact(通常是war exploded)添加到部署中。启动Tomcat,IDEA会自动将项目部署到服务器并打开浏览器。

6.2 系统测试要点

测试不能只靠点一点。你需要系统性地验证:

  • 功能测试:以不同角色(学生、教师、管理员)登录,逐一测试所有设计的功能点(增、删、改、查、选课、退课、成绩录入等),检查结果是否符合预期。
  • 边界测试:测试极端情况。例如,课程容量为0时能否被选择?已选人数等于容量时,下一个学生选课是否被正确拒绝?输入超长的课程名或负的学分,系统如何处理?
  • 并发测试(重点):模拟多个用户同时选同一门只剩一个名额的课。这是检验你事务和锁机制是否正确的“试金石”。可以用JMeter等工具模拟,或者写简单的多线程测试代码。
  • 安全测试:尝试在登录页面输入SQL注入代码(如' OR '1'='1),检查系统是否会异常登录。尝试未登录直接访问受保护的URL(如/admin/main.jsp),看过滤器是否生效。

6.3 常见问题与排查技巧实录

在开发和调试过程中,你几乎一定会遇到下面这些问题。这里是我的“踩坑”记录和解决方案:

问题1:启动Tomcat后访问页面报404错误。

  • 可能原因A:项目没有成功部署。检查IDEA的Tomcat配置,Deployment选项卡里是否添加了正确的Artifact
  • 可能原因B:访问的URL路径不对。Servlet的访问路径由@WebServlet注解或web.xml中的<url-pattern>决定。例如,一个Servlet的url-pattern/login,那么完整访问路径可能是http://localhost:8080/你的项目名/login
  • 排查技巧:查看Tomcat启动日志,确认你的项目上下文路径(Context Path)是什么。在IDEA的Tomcat运行配置中,可以设置Application context

问题2:页面显示中文乱码。

  • 全方位解决方案
    1. 确保所有JSP文件页面编码为UTF-8:在JSP文件头部添加<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>
    2. 配置全局字符编码过滤器:如上文web.xml配置所示。
    3. 数据库连接字符串指定编码:JDBC URL中加入?useUnicode=true&characterEncoding=UTF-8
    4. 确保数据库和表的字符集为UTF-8:创建数据库时使用CREATE DATABASE dbname CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    5. Servlet获取参数前设置编码:对于POST请求,在doPost方法最开始调用request.setCharacterEncoding("UTF-8");。对于GET请求,可能需要修改Tomcat的server.xml中Connector的URIEncoding="UTF-8"属性。

问题3:选课超卖,即课程名额超了。

  • 根本原因:没有处理好并发。在高并发下,两个线程几乎同时执行“查询剩余名额->判断有名额->执行选课”的流程,都判断有名额,然后都执行了插入和更新。
  • 解决方案:如3.2节所述,必须在数据库事务中使用悲观锁(SELECT ... FOR UPDATE)或乐观锁(通过版本号字段)。这是解决此类问题的唯一正确姿势。仅仅在Java代码里用synchronized关键字是没用的,因为多个请求可能由不同的JVM进程或服务器处理。

问题4:数据库连接用尽,报Too many connections错误。

  • 原因:数据库连接没有正确关闭。每个ConnectionPreparedStatementResultSet都是资源,必须在finally块中关闭。
  • 解决方案:严格使用DBUtil.close()方法,并确保它在finally中执行。推荐使用try-with-resources语法(Java 7+),它能自动关闭实现了AutoCloseable接口的资源。
    try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { // ... 设置参数,执行查询 try (ResultSet rs = pstmt.executeQuery()) { // ... 处理结果集 } } catch (SQLException e) { e.printStackTrace(); } // 无需手动调用close,离开try块自动关闭

问题5:静态资源(CSS、JS、图片)加载不到。

  • 原因:Tomcat默认的Servlet会拦截所有请求。如果你的web.xml中配置了/*的过滤器或Servlet(如你的LoginFilter),它可能会拦截对静态资源的请求。
  • 解决方案:在过滤器的doFilter方法中,对静态资源的请求路径进行排除。
    String uri = ((HttpServletRequest) request).getRequestURI(); if (uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".png") || uri.endsWith(".jpg")) { chain.doFilter(request, response); // 静态资源,直接放行 return; } // 否则,进行登录检查等逻辑

把这个项目从头到尾实现一遍,你遇到的绝大多数问题都不会超出这个列表。每解决一个问题,你对JavaWeb开发的理解就会加深一层。最后,别忘了将完整的项目导出为WAR包,或者将源码和数据库脚本整理好,这就是你简历上“学生选课管理系统”项目的最终成果。它不仅是一段代码,更是你解决实际问题能力的证明。

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

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

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

立即咨询