简介在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完成这个项目你能亲手处理HttpServletRequest和HttpServletResponse深刻理解会话Session和上下文Application的作用域这对你后续理解Spring中的Controller、RequestMapping有极大帮助。数据库方面MySQL是毫无争议的选择。它开源、免费、社区活跃是学习关系型数据库的绝佳平台。选课系统涉及的学生、课程、选课记录等都是典型的结构化数据非常适合用MySQL的表和关系来建模。通过这个项目你将熟练掌握SQL语句的编写、JDBC的连接与操作、以及事务处理的基本概念例如学生选课时需要同时更新选课记录和课程剩余名额这两个操作必须在一个事务中完成。2.2 数据库设计表结构是系统的骨架数据库设计是项目的重中之重表结构设计得好后续业务开发事半功倍。核心表通常包括以下几张用户表 (user/account): 存储学生、教师、管理员的基础信息。通常通过一个role字段如0-学生1-教师2-管理员来区分身份。这里有一个设计技巧可以将三类用户的共性字段如登录名、密码、真实姓名放在此表而特有信息如学生的所属班级、教师的职称通过外键关联到各自的详情表或者直接使用可空字段。对于初学者放在一张表里更简单直接。课程表 (course): 存储课程的基本信息如课程编号、名称、学分、授课教师ID外键关联用户表、课程容量、已选人数、课程介绍等。已选人数这个字段非常关键它是实现“课程容量控制”和“避免超选”的核心。选课表 (sc/student_course): 这是连接学生和课程的纽带是典型的“多对多”关系中间表。字段包括学生ID、课程ID、选课时间以及后续的成绩字段。这张表记录了每个学生的每一次选课行为。设计心得在course表中维护已选人数而不是每次都在sc表中COUNT是一种典型的“空间换时间”和保证数据一致性的做法。每次学生选课或退课都需要在同一个事务中更新sc表和course表的已选人数。这能极大提升查询课程列表时“判断是否已满”的性能。其他辅助表如班级表、开课计划表等可以根据系统复杂度增加。下图展示了核心表之间的简化关系 注此处用文字描述表关系因禁止使用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实现登录控制在LoginServlet的doPost方法中我们需要获取请求参数用户名、密码、验证码。校验验证码将用户输入的验证码与Session中存储的验证码对比防止暴力破解。这是很多初学者会忽略的安全步骤。调用UserService的login方法进行身份验证。UserService内部会调用UserDao查询数据库。验证成功后将用户对象至少包含用户ID、姓名、角色存入HttpSession中。例如session.setAttribute(loginUser, user);根据用户角色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) 的核心流程参数与状态校验从Session获取当前学生ID从请求获取课程ID。检查学生是否已选该课程查询sc表检查课程是否存在且状态为“可选”。关键并发控制检查课程当前已选人数 (currentSelected) 是否小于课程容量 (capacity)。这里存在一个时间窗口问题在检查之后和更新之前数据可能已被其他请求修改。使用数据库事务与行锁这是解决问题的关键。我们需要在一个数据库事务中完成以下操作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会在查询时对这条课程记录加上行锁直到当前事务提交或回滚其他事务无法修改这条记录从而彻底杜绝了超选的可能。返回结果根据操作结果向前端返回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方法获取分页数据 PageInfoCourse pageInfo courseService.getCourseByPage(start, pageSize); // 将pageInfo对象存入request转发到JSP request.setAttribute(pageInfo, pageInfo); request.getRequestDispatcher(/admin/courseList.jsp).forward(request, response);在CourseService和CourseDao中需要执行两条SQL查询总记录数SELECT COUNT(*) FROM course查询当前页数据SELECT * FROM course LIMIT ?, ?MySQL语法2. JSP页面展示与分页控件在courseList.jsp中使用JSTL和EL表达式遍历pageInfo.list。分页控件的逻辑是计算总页数并生成上一页、下一页、以及若干数字页码的链接。%-- 遍历课程列表 --% c:forEach items${pageInfo.list} varcourse 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 hrefjavascript:deleteCourse(${course.id})删除/a /td /tr /c:forEach %-- 分页控件 --% div classpagination c:if test${pageInfo.pageNum 1} a hrefcourseQuery?pageNum${pageInfo.pageNum-1}上一页/a /c:if c:forEach begin1 end${pageInfo.pages} varp c:choose c:when test${p pageInfo.pageNum} span classcurrent${p}/span /c:when c:otherwise a hrefcourseQuery?pageNum${p}${p}/a /c:otherwise /c:choose /c:forEach c:if test${pageInfo.pageNum pageInfo.pages} a hrefcourseQuery?pageNum${pageInfo.pageNum1}下一页/a /c:if /div3. 删除操作的注意事项删除课程并非简单地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?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/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模式数据访问层的标准化DAOData Access Object模式将底层数据访问逻辑与业务逻辑分离。我们为每个实体类如User,Course定义一个接口和一个基于JDBC的实现。 以UserDao为例public interface UserDao { User findByUsernameAndPassword(String username, String password) throws SQLException; User findById(int id) throws SQLException; ListUser 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 prefixc urihttp://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}%-- 学生 --% lia hrefselectCourse我要选课/a/li lia hrefmyCourse我的课程/a/li /c:when c:when test${sessionScope.loginUser.role 1}%-- 教师 --% lia hrefmyTeaching我的授课/a/li lia hrefinputGrade录入成绩/a/li /c:when /c:choose /ul5.2 表单验证与Ajax应用前端基础验证使用HTML5属性如required,pattern或简单的JavaScript在数据提交前进行基本校验如非空、邮箱格式、数字范围。这能快速给用户反馈减少无效请求。input typetext nameusername required placeholder请输入用户名 input typenumber namecredit min1 max10 requiredAjax提升体验对于“检查用户名是否重复”、“选课操作”等场景使用Ajax实现局部刷新能极大提升用户体验。例如选课按钮点击后通过Ajax发送请求到SelectCourseServletServlet处理完成后返回一个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需要设置响应内容类型并输出JSONresponse.setContentType(application/json;charsetUTF-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 环境准备与项目部署开发环境你需要安装JDK建议JDK 8或11、一个IDEIntelliJ IDEA或Eclipse、MySQL数据库和一个Servlet容器如Tomcat。IDEA集成了Tomcat配置非常方便。数据库初始化在MySQL中创建一个名为student_course的数据库然后运行项目附带的SQL脚本通常是一个.sql文件创建所有表并插入一些初始测试数据如管理员账号、样例课程、学生和教师账号。项目配置数据库连接修改DBUtil工具类中的URL、USER、PASSWORD确保与你的本地MySQL配置一致。字符编码在web.xml中配置一个字符编码过滤器统一处理请求和响应的编码为UTF-8避免中文乱码。filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.yourdomain.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingTomcat部署在IDEA中配置一个本地Tomcat服务器将项目的Artifact通常是war exploded添加到部署中。启动TomcatIDEA会自动将项目部署到服务器并打开浏览器。6.2 系统测试要点测试不能只靠点一点。你需要系统性地验证功能测试以不同角色学生、教师、管理员登录逐一测试所有设计的功能点增、删、改、查、选课、退课、成绩录入等检查结果是否符合预期。边界测试测试极端情况。例如课程容量为0时能否被选择已选人数等于容量时下一个学生选课是否被正确拒绝输入超长的课程名或负的学分系统如何处理并发测试重点模拟多个用户同时选同一门只剩一个名额的课。这是检验你事务和锁机制是否正确的“试金石”。可以用JMeter等工具模拟或者写简单的多线程测试代码。安全测试尝试在登录页面输入SQL注入代码如 OR 11检查系统是否会异常登录。尝试未登录直接访问受保护的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页面显示中文乱码。全方位解决方案确保所有JSP文件页面编码为UTF-8在JSP文件头部添加% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8%。配置全局字符编码过滤器如上文web.xml配置所示。数据库连接字符串指定编码JDBC URL中加入?useUnicodetruecharacterEncodingUTF-8。确保数据库和表的字符集为UTF-8创建数据库时使用CREATE DATABASE dbname CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。Servlet获取参数前设置编码对于POST请求在doPost方法最开始调用request.setCharacterEncoding(UTF-8);。对于GET请求可能需要修改Tomcat的server.xml中Connector的URIEncodingUTF-8属性。问题3选课超卖即课程名额超了。根本原因没有处理好并发。在高并发下两个线程几乎同时执行“查询剩余名额-判断有名额-执行选课”的流程都判断有名额然后都执行了插入和更新。解决方案如3.2节所述必须在数据库事务中使用悲观锁SELECT ... FOR UPDATE或乐观锁通过版本号字段。这是解决此类问题的唯一正确姿势。仅仅在Java代码里用synchronized关键字是没用的因为多个请求可能由不同的JVM进程或服务器处理。问题4数据库连接用尽报Too many connections错误。原因数据库连接没有正确关闭。每个Connection、PreparedStatement、ResultSet都是资源必须在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包或者将源码和数据库脚本整理好这就是你简历上“学生选课管理系统”项目的最终成果。它不仅是一段代码更是你解决实际问题能力的证明。本文还有配套的精品资源点击获取