资讯动态

JavaWeb学生选课系统实战:从Servlet到JDBC的完整搭建与避坑指南

发布时间:2026/9/24 18:47:39 来源:尧图企业网站定制
简介这套基于JSPServletMySQL实现的学生选课管理系统面向Java Web初学者和毕业设计、课程设计开发者支持教师、学生两个角色登录能有效解决选课管理中信息维护、学分统计等环节的常见问题。系统功能较为完整教师端可管理学生信息、课程信息和选课信息并设置必修学分的下限与上限学生端可进行选课、查看已选课程、修改个人信息还能查看未修够最低学分的学生列表。压缩包内共105个文件包括25个Java源文件、25个class编译文件、15个JSP页面、SQL数据库脚本、论文docx文档、高清演示mp4视频以及多张效果图片等资源整体约108.89MB目录结构清晰便于对照学习。目前已有4703人学习/下载。配套内容还包含数据库导入、源码在Eclipse或MyEclipse中的运行指导文档及视频教程有助于使用者快速跑通项目理解JSP、Servlet、JDBC与MySQL整合开发的完整流程。1. 学生选课系统为什么总跑不起来版本、环境与依赖三件事从网上下载一个写着“亲测可用”的 Java 课程设计案例源码导入 IDEA 后点运行十次里有八次是 Tomcat 起不来、页面 404 或者数据库连不上。学生选课管理系统就是这样一类典型的 JavaWeb 课设项目后端用 Servlet 处理请求页面用 JSP 渲染数据落在 MySQL 里登录时按学生、教师、管理员三种角色走不同的功能界面。它不算新却是把 Servlet 生命周期、Session、JDBC 事务这些 java 基础面试常考点一次性串起来的最短路径也正是它常年霸榜课设选题的原因。这篇文章不讲虚的就按“架构先立住、数据库能连上、登录能拦住、选课不出错、演示有底气”的顺序把这类系统从源码到能跑通的每个环节拆开讲。2. 先立住架构与角色模型MVC 分层和三种角色的权限边界决定后面能不能顺利跑通2.1 JSP、Servlet、MySQL 各干各的活一次登录请求的完整流转路径很多课设源码最大的问题不是功能少而是所有逻辑都堆在 JSP 页面里。打开一个login.jsp上面一半是 HTML下面一半是% ... %里写的 Java 代码能跑但改一个判断条件要翻三个页面答辩被问“你这里为什么这么写”时自己也说不清楚。我一般建议拿到任何 Servlet 项目第一件事就是看清分层JSP 负责页面渲染Servlet 负责接收请求和分发DAO 负责和 MySQL 打交道中间再隔一层 Service 用来放业务规则。一次登录请求的生命周期是这样的浏览器把表单 POST 到/login这个路径Tomcat 根据web.xml或WebServlet注解找到对应的 LoginServletServlet 从 request 里取出用户名和密码调用 UserService 去查数据库查到了就把用户对象和角色写进 session再重定向到该角色对应的主页查不到就把错误提示塞进 request转发回登录页。JSP 在这里只做一件事把 session 里的用户名显示在页面上。这就是 JSPServlet 的天然分工也是 JavaWeb 里最标准的 MVC 落地方式。请求流转里有一个新手最容易忽略的点JSP 本质上也是一个 Servlet。第一次访问某个 JSP 时Tomcat 会把它编译成.java再编译成.class所以第一次打开页面总是比后面慢这不是你的代码有问题。而 Servlet 直接写在 Java 类里编译时机由容器在启动时处理。两者在请求处理的底层逻辑上没有任何区别区别只在于 JSP 更擅长输出 HTMLServlet 更擅长写流程控制。理解了这一点你就知道为什么选课列表页用 JSP 写而“判断用户有没有登录”这种逻辑放在 Servlet 的 doGet 里更合适。2.2 学生、教师、管理员三种角色的功能边界与权限矩阵多角色登录是这个标题的核心卖点也是评分的重点。角色划分最常见的做法是三套入口页面登录成功后重定向到不同目录/student/、/teacher/、/admin/。目录划分不是给页面分类这么简单它是后面 Filter 做权限拦截的抓手只要 request 的 URI 带/admin/而角色不是 admin直接拦掉。三类角色的功能边界大致如下表做菜单的时候照着这个矩阵画就行角色能做什么不能做什么首页入口管理员学生/教师账号管理、课程上下架、选课统计不能实际参与选课/admin/main.jsp教师开设课程、修改课程信息、查看选课名单、录入成绩不能代替学生选课不能管理账号/teacher/main.jsp学生浏览课程、选课、退课、查看已选课程与成绩不能改课程容量、不能管理他人账号/student/main.jsp权限控制一共分两层。第一层是登录拦截没登录的用户访问任何业务页面都跳回登录页第二层是角色校验登录了但角色不对的请求直接返 403。只做第一层不做第二层的系统会出现学生改一下浏览器地址栏路径就能进入管理页面的低级事故这在演示时被老师点出来基本就告别高分了。我见过一份源码就是只拦了登录没验角色排查了一下午才找到问题所以写权限时要顺手把两层都做上。2.3 拿到源码包先核对三件事JDK、Tomcat 与 MySQL 版本错了直接白干标题说“亲测可用”但“可用”是相对于原作者的电脑环境而言的。你换一台机器JDK 版本不同、Tomcat 版本不同、MySQL 驱动不匹配结果就完全不同。拿到源码包后别急着导入 IDEA先打开命令行核对环境java -version # 查看 JDK 版本课设项目基本要求 1.8 javac -version # 编译器版本和 java -version 不一致会出怪问题 echo %JAVA_HOME% # Windows 下检查环境变量是否指向 JDK 安装目录 mysql --version # 查看 MySQL 版本5.7 和 8.0 的驱动类名不一样java 环境变量配置是这类项目最常见的翻车点。JAVA_HOME没配、配到了 JRE 目录、或者 PATH 里还留着旧版本路径Tomcat 启动时会直接报 “Java Runtime Environment not found” 或者闪退。排查方法很简单java -version能输出版本号但javac -version报找不到命令说明 PATH 里只有 JRE 没有 JDK去系统环境变量里把JAVA_HOME指到 JDK 目录不是 bin 目录再把%JAVA_HOME%\bin加进 PATH。版本核对完再导入 IDEA步骤按这个顺序走第一步看源码根目录有没有pom.xml有就是 Maven 工程直接 Open 让 IDEA 自动拉依赖没有就找WEB-INF/lib目录里面应该有 mysql-connector-java 的 jar。第二步File → New → Project from Existing Sources 选择源码根目录按向导点完。第三步File → Project Structure → Libraries → → Java手动把WEB-INF/lib下的 jar 全部加进工程不加的话启动时不报错点登录才报ClassNotFoundException: com.mysql.cj.jdbc.Driver。第四步Run → Edit Configurations → → Tomcat Server → Local在 Deployment 里加上项目 Artifact设置 Application context 为/course启动后访问http://localhost:8080/course/login.jsp。页面 404 九成是 Artifact 没部署到 Tomcat 的 Deployment 里这个坑我踩过不止一次。还有一类情况是源码压缩包解开后只有一个.war文件这也是常见分发方式需要手动在 IDEA 里建一个 Web Application 工程把 war 解压后的目录拷进去再重复上面的导入流程。导入过程琐碎但一次配好后面全通值得花半小时慢慢弄。3. 数据库设计与 JDBC 访问层六张表、连接参数与 DAO 写法一次到位3.1 六张核心表的建表 SQL字段、类型与唯一约束一次写对选课系统的表结构大方向上是固定的核心就六张账号表、学生详情表、教师详情表、课程表、选课记录表再加上一张可选的学期表。账号表统一存登录凭证用 role 字段区分角色这是多角色登录的数据基础比建三套登录表省事得多。下面是可直接执行的建表脚本CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_db; -- 账号表三种角色共用一个登录表role 区分身份 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL, -- admin / teacher / student real_name VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT NOW() ); -- 学生详情表与 sys_user 一一对应 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, major VARCHAR(50), class_name VARCHAR(50) ); -- 教师详情表 CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, teacher_no VARCHAR(20) NOT NULL UNIQUE, department VARCHAR(50) ); -- 课程表selected_count 冗余已选人数避免每次统计 count(*) CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_user_id INT NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, capacity INT NOT NULL DEFAULT 50, selected_count INT NOT NULL DEFAULT 0, begin_time DATETIME NOT NULL, -- 开放选课时间 end_time DATETIME NOT NULL, -- 截止时间 semester VARCHAR(20) ); -- 选课记录表唯一约束是防重复选课的第一道防线 CREATE TABLE sc ( id INT PRIMARY KEY AUTO_INCREMENT, student_user_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT NULL, select_time DATETIME DEFAULT NOW(), UNIQUE KEY uk_stu_course (student_user_id, course_id) );这里有两个设计点值得说。第一个是把账号独立成sys_user表而不是直接建student表带密码字段好处是登录逻辑只需要查一张表拿到 role 就能决定跳转方向不用先查学生表再查教师表再查管理员表。第二个是course表里冗余了selected_count字段牺牲一点存储换查询效率选课列表页要展示“已选/容量”这种进度条时一条 SQL 就取出来了不需要JOIN选课记录表再COUNT(*)这在课程数量大时性能差别很明显。外键我建议不加。课设评审老师看重的是逻辑正确外键约束在插入顺序上会引入一堆额外干扰比如删除教师时连带删课程还是置空处理不好反而扣分。用逻辑外键即可表里存teacher_user_id查询时 JOIN 出教师姓名效果一样代码更灵活。工程里如果要用也建议只加sc表那个唯一索引因为它承担的是业务约束别的都让 Service 层去控制。3.2 JDBC 连接 MySQL 8驱动类名、URL 参数与配置外置连不上数据库是这类项目里占比最高的报错而且基本都是同一个原因驱动版本和 MySQL 版本不匹配。MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.0 得改成com.mysql.cj.jdbc.Driver差一个cj就完全不认。驱动 jar 是从WEB-INF/lib里来的所以版本核对在导入工程时就要做掉。下面是最小可用的 DBUtilimport java.sql.*; public class DBUtil { // MySQL 8.x 必须用 com.mysql.cj.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/course_db ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD root; static { try { Class.forName(DRIVER); // 注册驱动静态块只执行一次 } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败检查 lib 下的 connector 版本, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL 里的三个参数是按 MySQL 8.0 装完后的标准配置写的。useSSLfalse是跳过 SSL 握手本地开发不配这条会在控制台刷一串警告偶尔还会卡住连接serverTimezoneAsia/Shanghai是必须的MySQL 8.0 对时区敏感不配报The server time zone value的异常这个属于装的 mysql 8.0 默认时区不是中国时区导致的characterEncodingutf8是中文不乱码的第一道保障。用户名密码按你本机的实际配置改。把连接参数硬编码在 Java 类里常见于课设源码能跑但不好维护。我一般会把它抽到db.properties文件里用Properties类加载这样换数据库只要改文件不改代码。演示项目用DriverManager完全够不用上连接池但结构上预留DataSource的替换位后面如果课程量大了想换 c3p0 或 druid只需要改getConnection()这一个方法DAO 层无感。3.3 DAO 层通用写法PreparedStatement 防注入与资源释放顺序DAO 层写的全是最简单的增删改查但就是这些简单 SQL最能看出代码习惯。以选课这个核心动作为例我给出一个兼顾事务和并发安全的写法public boolean addSc(int studentUserId, int courseId) throws Exception { // 条件式扣减只有剩余容量足够时才更新成功 String updateSql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; // 唯一索引兜底重复选课直接插入失败 String insertSql INSERT IGNORE INTO sc(student_user_id, course_id) VALUES(?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启手动事务两个操作要么都成要么都败 try (PreparedStatement ps1 conn.prepareStatement(updateSql)) { ps1.setInt(1, courseId); if (ps1.executeUpdate() 0) { conn.rollback(); return false; // 容量满了直接返回 } } int scRows; try (PreparedStatement ps2 conn.prepareStatement(insertSql)) { ps2.setInt(1, studentUserId); ps2.setInt(2, courseId); scRows ps2.executeUpdate(); } if (scRows 0) { conn.rollback(); // 唯一索引冲突回滚容量扣减 return false; } conn.commit(); return true; } }两个要点需要解释清楚。第一全部 SQL 用PreparedStatement的?占位符而不是字符串拼接。字符串拼接在传courseId时如果拼的是 or 11整条 SQL 语义就变了这就是 SQL 注入的由来。PreparedStatement先把 SQL 发给 MySQL 预编译参数单独传输从机制上断掉拼接这条路这也是面试被问到“怎么防 SQL 注入”时的标准答案。第二选课涉及两步写操作先给课程人数加一再插入选课记录。这两步必须放在一个事务里否则第一步成功第二步失败人数多了记录却没插上。setAutoCommit(false)之后所有 SQL 都在同一个事务里全部成功才commit()任何一步出问题就rollback()。很多课设源码的选课逻辑是先SELECT * FROM course判断容量再UPDATE再INSERT两个连接两段事务并发一上来就出问题这也是第 5 章要展开讲的超卖场景。资源释放的顺序从里到外先rs再stmt最后conn外层释放会自动关闭依赖它的内层资源反过来就不行。try-with-resources写法省去了手动 close 的大括号地狱但 DBUtil 里手动 close 的老代码在课设里依然大量存在两种写法都要认识。4. 多角色登录与权限拦截Session 存角色、Filter 管入口越权请求全挡在 Servlet 外面4.1 登录 Servlet 的实现校验、写 Session、按角色跳转登录接口是整个系统唯一不需要登录就能访问的业务接口它的逻辑不复杂但状态管理要严谨。登录成功只做一件事把用户对象和角色写进 Session。后面所有页面的“欢迎您xxx”、所有权限判断都从 Session 里取页面不直接访问数据库查当前用户。下面是核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); 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 userService.login(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setAttribute(role, user.getRole()); resp.sendRedirect(getTargetUrl(user.getRole(), req)); } private String getTargetUrl(String role, HttpServletRequest req) { String ctx req.getContextPath(); switch (role) { case admin: return ctx /admin/main.jsp; case teacher: return ctx /teacher/main.jsp; default: return ctx /student/main.jsp; } } }代码里有三个细节。WebServlet(/login)注解替代了 web.xml 里的 servlet-mapping 配置少写配置且更直观老工程如果还在用 web.xml 也兼容两者同时存在时以 web.xml 为准登录失败用forward而不是redirect因为 redirect 是浏览器重新发一次请求request 里的msg属性会丢用户看不到错误提示登录成功用redirect是为了避免刷新页面时浏览器弹“确认重新提交表单”这也是 Post/Redirect/Get 模式的标准应用。userService.login()的 SQL 一般是SELECT * FROM sys_user WHERE username? AND password?。课设项目密码常是明文能跑但答辩容易被追问我一般建议至少做一个 MD5 加密注册时DigestUtils.md5Hex(password)后再存库登录时把输入也 MD5 以后比对。这样不用改表结构只改 UserDao 里两条 SQL 的传参。4.2 登录拦截 Filter 的写法放行登录与静态资源拦截未登录和越权请求权限拦截用 Filter 是最成熟的方案它的原理是请求在到达 Servlet 之前会先经过过滤器链我们在链条里判断身份不合格的直接重定向根本不给 Servlet 执行的机会。一个 AuthFilter 即可完成登录态校验和角色校验两层任务WebFilter(/*) public class AuthFilter 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(); String ctx req.getContextPath(); String path uri.substring(ctx.length()); // 放行静态资源和登录入口否则会循环重定向 if (path.endsWith(.css) || path.endsWith(.js) || path.endsWith(.png) || path.endsWith(.jpg) || path.equals(/login.jsp) || path.equals(/) || path.equals(/login)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(ctx /login.jsp); // 未登录回登录页 return; } User user (User) session.getAttribute(loginUser); String role user.getRole(); if (path.startsWith(/admin/) !admin.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); // 越权403 return; } if (path.startsWith(/teacher/) !admin.equals(role) !teacher.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }path.equals(/login)的放行是最容易漏的。如果把登录接口也拦了Filter 发现用户没登录把它重定向到 login.jsplogin.jsp 提交后又回来循环往复浏览器提示“重定向次数过多”表面看是页面打不开实际是拦截逻辑把自己拦死了。同样的道理CSS、JS、图片这些资源也得放行否则页面的样式全部丢失只剩下赤裸裸的 HTML 表格。Admin 角色能访问教师页面这行判断也很重要——管理员虽然有自己的主页但查看教师开课情况是合理需求所以/teacher/前缀放了两个角色进去。如果业务上要求管理员只能走 admin 目录把admin.equals(role)去掉即可。这种前缀匹配的机制很简单但胜在直观每个角色能碰哪些目录一目了然比在 Servlet 里写一堆 if 判断更集中、更好维护。4.3 会话超时与退出登录session.invalidate 的正确用法会话超时这个细节课设源码里十个有八个没配。Tomcat 默认 session 有效时间是 30 分钟这个值是容器级的默认值但不同版本可能不同显式配置一次最稳。在 web.xml 里加session-config session-timeout30/session-timeout /session-config单位是分钟设置 0 或负数表示永不过期。项目里我一般设置成 30和默认值一致但写出来更明确也让答辩时有话可讲业务要求学生在课堂演示期间不被登出但又不能一直有效30 分钟是平衡点。退出登录比很多人想的简单核心就两行HttpSession session req.getSession(false); if (session ! null) { session.invalidate(); // 销毁整个会话loginUser 和 role 一起清掉 } resp.sendRedirect(req.getContextPath() /login.jsp);注意用getSession(false)而不是getSession()区别在于前者在没有会话时返回 null后者会新建一个空 session。一个已经退出的用户再点退出不应该莫名其妙多出一个新 session。invalidate 之后跳回登录页这时按浏览器的后退键回到刚才的页面页面能显示但一旦点击任何链接就会被 AuthFilter 拦住。这个行为是符合预期的但找一个页面刷新会带出缓存内容的情况需要加一句话在需要保密的页面加resp.setHeader(Cache-Control, no-cache, no-store)防止浏览器从本地缓存里恢复了已登出用户的页面。5. 选课系统避坑排查重复选课、超卖、乱码与连接泄漏的五段复盘5.1 重复选课双击按钮导致的脏数据用唯一约束和 INSERT IGNORE 兜底现象一个学生同一门课程在 sc 表里有两条记录选课列表里出现两个一模一样的条目。原因拆开看有三个层面。用户层学生点击“选课”按钮后页面没及时反馈手快又点了一下两个请求都到了后端代码层逻辑写的是“先查有没有已选记录没有就插入”查询和插入之间有时间差两个请求都查到了“未选课”于是都插入成功数据库层ower表没有任何约束限制同一个学生选同一门课只能有一条记录。解决要从数据库层兜底。执行下面这条 SQL 给选课记录表加联合唯一索引ALTER TABLE sc ADD UNIQUE KEY uk_stu_course (student_user_id, course_id);加了索引之后再插入相同组合会直接报Duplicate entry异常。配合代码里的INSERT IGNORE把报错降级为影响行数为 0Service 层判断scRows 0返回“你已经选过这门课了”体验顺畅。页面端的补救也要做选课按钮点击后立即document.getElementById(btn).disabled true减少无效请求。三层里数据库唯一索引是最后一道防线必须存在。5.2 课程容量超卖先查再改的并发问题一条条件 UPDATE 解决现象课程容量设置为 5选课名单里有 6 个学生。原因特别典型业务逻辑写的是“先 SELECT 查 capacity再判断 selected_count 是否小于 capacity小于就 UPDATE 加一”。两个学生同时发起选课都查到了还剩 1 个名额都通过了判断都执行了 UPDATE最终人数变成 6。这在单机课设里不容易复现但原理上就是经典的并发竞态。解决的关键是把“判断”和“扣减”合并成一条原子 SQL也就是第 3 章 DAO 里用到的写法UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity;这条 SQL 在 MySQL 里是行级原子操作selected_count capacity的判断发生在 UPDATE 执行时而不是 SELECT 读取时。两个并发请求到了数据库第一个执行成功后 selected_count 变成 6第二个再执行时条件6 5不成立影响行数为 0容量满的逻辑自然触发。退课时对应做反向操作UPDATE course SET selected_count selected_count - 1 WHERE id ?。这个方案不需要锁表、不需要事务隔离级别调整是课设项目里性价比最高的并发控制手段答辩时说一句“用条件式 UPDATE 做乐观控制”老师基本不会再追问。5.3 时间判断翻车DATETIME 当成字符串比较退课期限永远算不对现象退课截止时间明明还有三天页面却提示“已经超过退课期限”。原因十有八九是建表时把时间字段建成了 VARCHAR或者 Java 实体类里用的是 String。字符串比较时间和日期比较时间的规则完全不同字符串比较是从左到右逐字符比2024-9-1和2024-09-01一个是一位数月一个是两位数月排序结果就错了。退课判断写的是endTime.compareTo(nowTime) 0数据格式一旦不统一误差就会出现。解决分三层。数据库层时间字段一律用 DATETIME 类型不要用 VARCHARJava 对象层beginTime、endTime、selectTime全部声明为java.util.Date或LocalDateTime不要把字符串直接塞给实体类比较逻辑层SQL 里能用数据库时间就用数据库时间SELECT * FROM course WHERE begin_time NOW() AND end_time NOW();这条 SQL 直接让 MySQL 判断课程当前是否在可选期内避免把时间取到 Java 层再比较。如果需要在 Java 代码里判断用LocalDateTime.now().isBefore(course.getEndTime())这种自带语义比较的方法不要用 String 的compareTo。JSP 展示时间时用 JSTL 的fmt:formatDate格式化pattern 统一写成yyyy-MM-dd HH:mm避免页面输出一串没人看得懂的时间戳。5.4 中文乱码编码过滤器必须放第一位顺序错了没有后悔药现象页面显示正常但插入到数据库的中文变成问号或者反过来数据库正常页面上显示一串æå®¢之类的乱码。原因必然出在三个环节至少一个请求编码、响应编码、数据库连接编码。解决方式是三层全部统一为 UTF-8。数据库建库时已经指定了 utf8mb4JDBC URL 里也加了characterEncodingutf8剩下的请求和响应编码用一个 CharacterEncodingFilter 统一处理public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getCharacterEncoding() null) { request.setCharacterEncoding(UTF-8); } response.setCharacterEncoding(UTF-8); response.setContentType(text/html; charsetUTF-8); chain.doFilter(request, response); }这个 Filter 必须注册在所有业务 Filter 的最前面。原因是req.getParameter()一旦被调用编码就固定了再设置也来不及如果 AuthFilter 先执行并且内部调用了参数后面的编码 Filter 就成摆设这就是“顺序错了没有后悔药”的原因。用注解方式注册时过滤器执行顺序按类名字母序不可控所以我建议编码过滤器和权限拦截器都写进 web.xml 显式声明 mapping 顺序编码过滤器放在第一条。这里顺带说明 GET 请求的编码。POST 请求的参数在请求体里靠setCharacterEncoding解决GET 请求的参数拼在 URL 上由 Tomcat 决定解析方式Tomcat 8.5 之后默认按 UTF-8 解码之前的老版本需要连protocol和 connector 里的URIEncodingUTF-8都配上。这些零碎配置不统一经常改了一个环节乱码跑到另一个环节排查时建议三处同时检查。5.5 连接不释放Connection 泄漏把 MySQL 拖垮最隐蔽的翻车现场现象系统跑几分钟后所有页面都转圈后台日志报Connection is closed或者Too many connections重启 Tomcat 后恢复过一会儿又卡死。原因就是 JDBC 连接泄漏。DriverManager.getConnection()每次创建一个真实的数据库连接MySQL 默认最大连接数max_connections是 151而代码里getConnection()之后finally里没有关闭或关闭顺序不对连接就一直占着。用户一多连接池就耗尽后续请求全部排队等连接页面表现就是卡死。排查先看是不是连接问题SHOW PROCESSLIST; -- 看当前有多少连接、在干什么 SHOW STATUS LIKE Threads_connected; -- 看连接数走势如果Threads_connected在测试期间只涨不降基本坐实泄漏。解决是保证每条连接都在 finally 或 try-with-resources 里释放释放顺序从里到外ResultSet → Statement → Connection。第 3 章的 DBUtil 里的close()方法就是为此准备的Standard DAO 代码应该是Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); // 处理 rs } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); // 任何一步异常资源都要关 }如果项目升级用了连接池 c3p0 或 druid泄漏的对象变成了连接池里的连接现象更隐蔽只有连接池耗尽才崩溃但排查思路一样都是看连接数是否只增不减。习惯上从写第一行 JDBC 起就保持“连接用完立刻关”的肌肉记忆能省去后面无数血泪排查时间。6. 演示与答辩前的五项验证让“亲测可用”变成现场真能跑6.1 演示前过一遍的五项验证答辩现场最怕的不是功能少而是“在我电脑上明明是好的”这句台词。提前按下面这张表过一遍基本能覆盖评委大概率会点的操作验证项操作方式预期结果三角色登录分别用 admin、teacher、student 账号登录跳转到各自不同的主页管理员建账号在管理员页面新增一名学生再用新账号登录新账号能直接登录成功学生选课学生选一门课然后刷新课程列表已选人数 1再次点击提示已选容量满拦截把一门课容量改成 1第二个学生选它提示容量已满不允许选越权访问未登录访问 /admin/main.jsp学生访问 /teacher/ 页面分别跳登录页、返回 4036.2 让答辩多拿分的三个问法演示只是把代码跑给评委看真正拉分的是你能否讲清两个关键防御点。第一被问“怎么防止重复选课”时回答顺序从数据库到代码到页面UNIQUE KEY uk_stu_course兜底INSERT IGNORE降级页面按钮 disabled 拦截从里到外一层层说出来比只说“我做了个判断”清楚得多。第二被问“怎么处理并发超卖”时直接说那条条件式 UPDATE 在数据库层面是原子的把判断和扣减合并成一条 SQL不用锁也能拦住超卖。第三被问“怎么防 SQL 注入”时把 PreparedStatement 的?占位和预编译机制讲一遍。我第一次做这类系统时把选课逻辑全写在 student_addSc.jsp 的% %里重复选课和超卖两个问题改了两天都压不住后来老老实实按 MVC 分层数据库加唯一索引、DAO 里用事务和条件 UPDATE一个下午就全通了。这套经验后来做 Spring Boot 项目依然适用——框架会变但“约束放数据库、判断放原子 SQL、权限放过滤器”这三条底层规矩不会变。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价