ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+JSP+MySQL实现Web学生信息管理系统:从JDBC到分页避坑

Java+JSP+MySQL实现Web学生信息管理系统:从JDBC到分页避坑 简介面向Java Web初学者与课程设计开发者该学生信息管理系统基于JavaJSPMySQL构建覆盖登录权限、学生信息增删改查、条件查询、报表统计等典型功能可快速掌握B/S架构下的前后端协作与数据库操作。压缩包共75个文件含28个JSP页面、11个Java源码、22个class文件、10个XML配置及1个SQL脚本整体仅3.92MBJSP负责页面展示Java处理业务逻辑SQL脚本可直接初始化数据库便于导入IDE运行调试。系统采用经典ServletJSPMySQL组合包含用户登录与角色权限管理、学生信息录入与编辑、多条件查询以及数据备份恢复等模块适合课程设计、毕业设计或自学参考。目前已有8992人学习从中可收获完整的项目目录结构与代码注释思路有助于理解Web应用从需求分析、数据库设计到编码部署的完整流程。1. JavaJSPMySQL实现Web学生信息管理系统为什么这套组合在轻量项目里仍是快路径如果你正面临课程设计、毕业设计或者只想给一个小部门搭个能用的内网管理系统用 Java JSP MySQL 实现 Web 学生信息管理系统依然是当下最不折腾的选择。它不需要把 Spring Boot、MyBatis 那套初始化流程背熟数据库表结构也足够直观改一个 JSP 页面刷新就能看到结果这对刚接触 Java Web 的人是条能快速跑通的路径。本文就按“环境与建库 → JDBC 连接层 → 增删改查与分页 → 避坑排查”的顺序展开目标是让你照着做两天内交付一套可以演示、可以提交的完整页面。2. 环境组合与数据库设计JDK、Tomcat、MySQL 的版本搭配和建表脚本2.1 版本搭配为什么首选 JDK 8 Tomcat 9 MySQL 5.7/8.0这套项目最大的坑往往不在代码而在版本组合。你如果直接装最新的 Tomcat 10会发现在网上复制的 JSP 教程代码大面积编译报错原因是 Tomcat 10 把javax.servlet包改成了jakarta.servlet所有涉及import javax.servlet.*的旧代码都需要改动。这不是你代码的问题是生态断代的问题。所以做学生信息管理系统这类依赖大量现成资料的 Web 项目老老实实回到 Tomcat 9.0.x配合 JDK 8 是最稳的。JDK 8 看起来老但它对 JSP、Servlet 标签库的支持最完整Tomcat 9 官方也明确兼容 JDK 8。MySQL 可以选 5.7 或 8.0两者在业务功能上对这个项目没有本质区别只是连接驱动和 URL 参数不同。我一般建议直接上 MySQL 8.0因为新装环境没必要选旧版本后面真遇到问题也好搜网上的解决方案多数都是针对 8.0 写的。版本组合参考下表。组件推荐版本选型理由常见坑JDK1.8与 Tomcat 9 兼容老 JSP 代码无需改动不要一开始就上 JDK 17反射和模块化限制多Tomcat9.0.x包名仍是 javax.*教程和标签库资料都匹配Tomcat 10 改成 jakarta.*旧代码编译报错MySQL8.0功能新驱动多版本兼容好连接需要额外加 allowPublicKeyRetrieval 参数数据库驱动mysql-connector-j 8.0.x同时兼容 MySQL 5.7 和 8.0驱动类名是 com.mysql.cj.jdbc.Driver别写旧的顺便说下依赖管理。这个项目真正的外部依赖只有 MySQL 驱动一个 jar 包Servlet 和 JSP 的 API 是 Tomcat 自带提供的所以我建议先不要上 Maven直接把 jar 放进WEB-INF/lib目录Tomcat 启动时会自动加载。这样项目结构清楚也方便在 IDE 里直接部署调试。等你以后项目依赖超过五六个再切换 Maven 不迟。提示如果团队已经统一用 Maven就把它加进 pom.xml 当普通依赖处理本文后面代码按手动放 jar 的目录结构演示两者在 Java 代码层没有区别。2.2 MySQL 安装后的三件事字符集、专用账号与连接参数网上关于 MySQL 安装教程的内容很多从 Windows 10 上的图形化安装到 zip 解压都有人写这里不重复安装步骤只说装完之后必须做的三件事否则后面学生信息入库时中文变成问号你排查半天也找不到原因。第一件是确认字符集。安装 MySQL 8.0 时可以在安装向导里直接选 utf8mb4如果已经装完才发现是默认 latin1也不用重新装改配置文件重启即可。Windows 上一般改my.ini里的character-set-serverutf8mb4重启 MySQL 服务后生效。第二件事是给应用建专用账号不要在 DAO 层用 root。root 权限太大一旦 JSP 页面被注入整库都可能被删掉。用专用账号还能避免误连到其他业务库。初始化 SQL 如下。CREATE USER student_applocalhost IDENTIFIED BY s123456; GRANT SELECT, INSERT, UPDATE, DELETE ON student_ms.* TO student_applocalhost; FLUSH PRIVILEGES;这段 SQL 的意思是只允许student_app从本机登录并且只在student_ms这个库里拥有增删改查权限。参数说明localhost限定来源地址开发环境够用密码s123456只是示例部署时换强密码。这样即使代码里账号泄露影响范围也被限制在一个库里。第三件事是准备好连接 URL 参数。这一步的细节在第 3 章展开但建库阶段就要记着一个结论MySQL 8.0 的驱动在连接时如果没带useSSLfalse和allowPublicKeyRetrievaltrue会抛出公钥获取失败的连接异常届时不是网络问题就是这几个参数没写对。2.3 建库建表脚本学生表、班级表、用户表的最小设计学生信息管理系统的核心数据就是学生信息但只建一张学生表是远远不够的。班级信息需要单独拆一张表否则多个学生重复写一个班级名改个班名要连带改几十条记录登录功能也需要一张用户表否则管理系统没有任何访问限制。三张表的最小设计如下。CREATE DATABASE IF NOT EXISTS student_ms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_ms; CREATE TABLE t_class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE, grade_year VARCHAR(20) DEFAULT NULL ) ENGINEInnoDB; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE, stu_name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT 男, birthday DATE DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, class_id INT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_class_id (class_id) ) ENGINEInnoDB; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, login_name VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) DEFAULT NULL, role TINYINT DEFAULT 1 ) ENGINEInnoDB; INSERT INTO t_class (class_name, grade_year) VALUES (软件2101, 2021), (软件2102, 2021); INSERT INTO t_student (stu_no, stu_name, gender, birthday, phone, class_id) VALUES (2021001, 张伟, 男, 2003-05-12, 13800000001, 1), (2021002, 李娜, 女, 2003-08-22, 13800000002, 1); INSERT INTO t_user (login_name, password, real_name, role) VALUES (admin, 202cb962ac59075b964b07152d234b70, 管理员, 1);字段设计上有几个细节值得注意。学号stu_no设置了 UNIQUE 唯一约束这是业务上的硬要求不能用主键 id 代替学号因为学号是业务编号主键只是自增代理键。性别用CHAR(1)就够了不要用 Varchar(10) 浪费空间。create_time用DEFAULT CURRENT_TIMESTAMP插入时不用显式赋值排查数据问题时会好用很多。我没有在这三张表之间建立任何物理外键约束。学生表的class_id在逻辑上引用班级表主键但物理外键会造成删除顺序的麻烦比如先删班级后删学生就会被约束拦住。课程设计和团队开发里逻辑外键加程序校验已经足够这也是业务系统的常见做法。密码字段里存的是 MD5 值202cb962ac59075b964b07152d234b70对应明文 123演示够用正式项目要换成 BCrypt 这类加盐哈希这里先不展开。3. 连接层JDBC 驱动、DAO 封装与中文乱码的根源3.1 jar 包放哪、类名怎么写驱动引入的两种方式写 Java JSP MySQL 的 Web 项目第一步不是写查询代码而是确认驱动能被 Tomcat 加载。很多新手把mysql-connector-java的 jar 扔进 IDE 的 External Libraries本地运行依赖能找到一部署到 Tomcat 就报ClassNotFoundException。原因是 Tomcat 加载 Web 应用时只扫描每个应用WEB-INF/lib目录下的 jar不会管 Eclipse 或 IDEA 帮你引的库。这个目录结构长这样。student_ms/ ├── src/ │ └── com/example/ │ ├── bean/ │ │ └── Student.java │ └── dao/ │ └── StudentDao.java └── web/ ├── WEB-INF/ │ ├── web.xml │ └── lib/ │ └── mysql-connector-j-8.0.x.jar ├── css/ └── list_student.jsp把驱动 jar 复制到web/WEB-INF/lib/后在 Java 代码里加载驱动时类名要和你用的驱动版本匹配。MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver以前 5.x 时代写的com.mysql.jdbc.Driver在 8.x 版本里已经过时但要区分情况。最稳妥的写法是在 DAO 类的静态块里只写一次加载逻辑后面所有连接都复用。static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } }这段代码说明两点一是Class.forName的作用是把驱动类加载进 JVM驱动类自己会在静态初始化时向DriverManager注册二是放进静态块的目的是保证只执行一次避免每次建立连接都反射加载类。如果这里的类名和你 jar 包实际版本不一致应用启动后第一次访问数据库就会抛出异常这也是 5.2 里要重点排查的翻车现场。3.2 DAO 封装把 JDBC 写成查一次少一次的代码连接层最忌讳在每个 JSP 页面里重复写Connection、PreparedStatement、ResultSet三件套。正确做法是抽一个BaseDao把获取连接、创建预编译语句、关闭资源全部封装成公共方法子类只写 SQL 和参数。下面是一个最小可用的父类。package com.example.dao; import java.sql.*; public class BaseDao { private static final String URL jdbc:mysql://localhost:3306/student_ms ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER student_app; private static final String PASSWORD s123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } protected Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } protected PreparedStatement prepare(Connection conn, String sql, Object... args) throws SQLException { PreparedStatement ps conn.prepareStatement(sql); for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } return ps; } protected void close(AutoCloseable... closes) { for (AutoCloseable c : closes) { if (c ! null) { try { c.close(); } catch (Exception ignored) { } } } } }这个类的设计核心是参数绑定。所有 SQL 都通过PreparedStatement执行而不是用字符串拼接 SQL。字符串拼接最怕学生姓名里带一个单引号比如“张伟S”直接就把语句截断变成语法错误更严重的是这个位置可以注入 OR 11之类的片段绕过登录校验。用setObject绑定参数MySQL 驱动会自动做类型转换和转义这属于面向对象封装里最基础也最值得花时间写对的一层。close方法用可变参数接收各种可关闭对象调用时一行就能关掉ResultSet、PreparedStatement、Connection。注意关闭顺序是先开的后关也就是 ResultSet 先关、Connection 最后关这个细节很多教程不强调但漏了很容易在并发场景下耗尽数据库连接。连接耗尽的表现是运行一段时间后页面卡住日志里出现Too many connections。3.3 中文乱码到底是谁的锅页面、请求、连接三处对齐学生管理系统里中文乱码是出现频率最高的问题。它不只是一个配置项的问题而是页面编码、请求编码、数据库编码三处必须对齐。任何一个环节不一致最终展示出来的就是一篇问号。第一处是 JSP 页面本身的字符集声明。每个页面第一行应该写% page contentTypetext/html;charsetUTF-8 languagejava %这同时声明了响应内容的类型和编码。缺了它浏览器可能用 ISO-8859-1 去解码 HTML中文直接裂开。第二处是接收请求参数之前的编码设置。POST 表单提交时Tomcat 默认用 ISO-8859-1 解码请求体所以在你调用request.getParameter之前必须先执行request.setCharacterEncoding(UTF-8)并且要放在第一个 getParameter 调用之前才有效。GET 请求的乱码是另一套机制由 Tomcat 连接器URIEncoding控制一般开发环境在server.xml的连接器上加上URIEncodingUTF-8即可。第三处就是上一章已经强调过的数据库字符集。连库 URL 里必须带characterEncodingutf8而建库用utf8mb4两边配合才不会在写入阶段就丢字符。下面这个字符编码过滤器可以作为全站统一解决方案比在每个 JSP 里手动设置更省事。package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }WebFilter(/*)是 Servlet 3.0 提供的注解方式表示拦截所有请求。这个过滤器对所有 POST 请求都生效就不用在每个页面里重复写request.setCharacterEncoding。有了它中文乱码的“请求”这一环基本就堵住了。但这个过滤器只处理请求和响应编码数据库连接 URL 和页面声明仍然要对齐三处缺一不可。4. 增删改查与分页落地从学生列表到可提交的表单页4.1 学生列表页JSP 直接调用 DAO 渲染数据列表页是这个系统的门面学生信息的展示都在这一页完成。课程设计阶段为了方便理解和演示可以直接在 JSP 中用脚本代码实例化 DAO 并调用查询方法Servlet 那层封装等后面项目复杂了再补也不迟。先定义StudentDao里的一个列表查询方法。public ListStudent listAll() { String sql SELECT s.id, s.stu_no, s.stu_name, s.gender, s.phone, c.class_name FROM t_student s LEFT JOIN t_class c ON s.class_id c.id ORDER BY s.id; ListStudent list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn getConnection(); ps prepare(conn, sql); rs ps.executeQuery(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setStuName(rs.getString(stu_name)); s.setGender(rs.getString(gender)); s.setPhone(rs.getString(phone)); s.setClassName(rs.getString(class_name)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } finally { close(rs, ps, conn); } return list; }这里用LEFT JOIN把学生表和班级表关联起来目的是一次查询就把班级名也带出来避免在 JSP 里循环时再为每条记录查一次班级。参数说明ORDER BY s.id保证列表输出顺序稳定这是分页和数据核对的基础。JSP 页面里拿到这个 List用循环渲染表格即可。% page contentTypetext/html;charsetUTF-8 languagejava % % page importcom.example.dao.StudentDao, com.example.bean.Student, java.util.List % % StudentDao dao new StudentDao(); ListStudent list dao.listAll(); % !DOCTYPE html html head meta charsetUTF-8 title学生列表/title /head body table border1 tr th学号/th th姓名/th th性别/th th班级/th th操作/th /tr % for (Student stu : list) { % tr td% stu.getStuNo() %/td td% stu.getStuName() %/td td% stu.getGender() %/td td% stu.getClassName() %/td td a hrefedit_student.jsp?id% stu.getId() %编辑/a a hrefdelete_student.jsp?id% stu.getId() % onclickreturn confirm(确定删除学号为 % stu.getStuNo() % 的学生吗)删除/a /td /tr % } % /table a hrefedit_student.jsp新增学生/a /body /html这段 JSP 的核心是% %脚本代码和% %输出表达式的配合。它读起来像 HTML 里插了 Java这也是 JSP 最初的设计哲学。需要注意stu.getStuName()直接输出到 HTML 时没有任何转义如果学生姓名里包含script标签会构成 XSS 漏洞。生产系统建议改用 EL 表达式配合 JSTL 的c:out标签自带转义能力课程设计阶段可以先知道这个风险点。删除链接的onclick加了 confirm 确认框防止误删算是低成本保护。4.2 新增与编辑共用一个表单页自提交、回显与防重复提交新增和编辑的表单字段几乎一样完全可以共用一个edit_student.jsp。区别只在于进入页面时是否带上id参数以及表单提交后执行插入还是更新。这种“一个页面同时处理展示和提交”的模式在 JSP 项目里很常见也符合目录最简原则。% page contentTypetext/html;charsetUTF-8 languagejava % % page importcom.example.dao.StudentDao, com.example.bean.Student % % request.setCharacterEncoding(UTF-8); String action request.getParameter(action); Integer id null; String idParam request.getParameter(id); if (idParam ! null !idParam.isEmpty()) { id Integer.valueOf(idParam); } String msg ; if (save.equals(action)) { String stuNo request.getParameter(stuNo); String stuName request.getParameter(stuName); String gender request.getParameter(gender); String phone request.getParameter(phone); if (stuNo null || stuNo.trim().isEmpty() || stuName null || stuName.trim().isEmpty()) { msg 学号和姓名不能为空; } else { Student s new Student(); s.setStuNo(stuNo.trim()); s.setStuName(stuName.trim()); s.setGender(gender); s.setPhone(phone); StudentDao dao new StudentDao(); int result (id null) ? dao.insert(s) : dao.update(id, s); if (result 0) { response.sendRedirect(list_student.jsp); return; } else { msg 保存失败请检查学号是否重复; } } } Student student null; if (id ! null) { student new StudentDao().findById(id); if (student null) { out.println(该学生不存在或已被删除); return; } } % html head meta charsetUTF-8 title编辑学生/title /head body h2% id null ? 新增学生 : 编辑学生 %/h2 p stylecolor:red;% msg %/p form methodpost actionedit_student.jsp % if (id ! null) { % input typehidden nameid value% id % % } % input typehidden nameaction valuesave 学号input typetext namestuNo value% student ! null ? student.getStuNo() : %br 姓名input typetext namestuName value% student ! null ? student.getStuName() : %br 性别select namegender option value男 % student ! null 男.equals(student.getGender()) ? selected : %男/option option value女 % student ! null 女.equals(student.getGender()) ? selected : %女/option /selectbr 电话input typetext namephone value% student ! null ? student.getPhone() : %br input typesubmit value保存 a hreflist_student.jsp返回列表/a /form /body /html这个页面的处理流程分成三段。第一段先取action参数如果等于save说明是表单提交就把表单字段封装进Student对象根据id是否存在决定调用insert还是update。第二段是msg变量用来做服务端简单校验的提示比如学号或姓名为空、学号重复导致保存失败。第三段是页面初始化逻辑只在非提交状态下把已有学生数据加载并回显到表单控件里。student ! null ? student.getStuNo() : 这种三元表达式保证了新增时输入框是空的编辑时显示数据库里的旧值。保存成功后这里用的是response.sendRedirect(list_student.jsp)而不是forward。这背后是 PRG 模式即 POST-Redirect-GET。先让浏览器 POST 提交到当前页成功后 302 重定向到列表页再由浏览器 GET 列表页。好处是刷新列表页时不会重复提交表单这是 5.4 节要展开的经典问题。4.3 分页与排序LIMIT 参数绑定、页码越界与排序白名单学生数据一多一次查出来全部渲染就不合适了。MySQL 的分页语法是LIMIT offset, count配合 ORDER BY 排序可以稳定输出每页数据。先看 DAO 的分页查询方法。public ListStudent page(int page, int pageSize, String orderBy) { String orderCol safeOrder(orderBy); String sql SELECT s.id, s.stu_no, s.stu_name, s.gender, s.phone, c.class_name FROM t_student s LEFT JOIN t_class c ON s.class_id c.id ORDER BY orderCol LIMIT ?, ?; ListStudent list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn getConnection(); ps prepare(conn, sql, (page - 1) * pageSize, pageSize); rs ps.executeQuery(); while (rs.next()) { // 与 listAll() 中相同的结果集映射代码 } } catch (SQLException e) { e.printStackTrace(); } finally { close(rs, ps, conn); } return list; } private String safeOrder(String key) { if (stu_no.equals(key) || stu_name.equals(key) || create_time.equals(key)) { return key; } return id; }LIMIT ?, ?的第一个参数是偏移量第二个是每页条数。(page - 1) * pageSize的用意是第一页从 0 开始取第二页跳过第一条数据。这两个参数都用setObject绑定不会注入。真正危险的是 ORDER BY 后面接的字段名不能参数化如果直接把用户传来的排序字段拼进 SQL比如?orderByid;DROP TABLE t_studentMySQL 驱动会如实拼进语句里。safeOrder做一个白名单映射不在白名单里的都回退到id这就是排序字段防注入的常见解法。JSP 页面的分页栏需要算清楚三个值总记录数、总页数、当前页。当前页越界是个隐蔽问题删除最后一条数据后当前页可能比总页数还大此时查询返回空列表。% int page 1; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { page Integer.parseInt(pageParam); } int pageSize 10; StudentDao dao new StudentDao(); int total dao.count(); int totalPages (total pageSize - 1) / pageSize; if (page 1) { page 1; } if (totalPages 0 page totalPages) { page totalPages; } ListStudent list dao.page(page, pageSize, request.getParameter(orderBy)); %(total pageSize - 1) / pageSize是向上取整的标准写法避免整数除法把未满一页的余数丢掉。page totalPages的兜底赋值解决了删除数据后的越界问题。对 MySQL 排序而言数据量大时OFFSET偏移太大会越来越慢因为数据库要跳过前面所有行才能取到目标数据这是 LIMIT 分页的固有代价。在这个学生管理系统的数据规模下完全够用如果以后真做到数十万条再考虑WHERE id lastId的游标分页方案。5. 避坑手册这套 JSP 项目最常见的 5 个翻车现场与排查路径这类项目业务逻辑不复杂真正让人熬夜的翻车点集中在三个方向环境加载、字符编码、请求跳转。与其等问题出现再盲目改配置不如把这些高频坑按现象对号入座一张张排查表记在心里。5.1 ClassNotFoundException驱动类加载不到现象Tomcat 启动成功后访问任意涉及数据库的 JSP页面报 500 错误堆栈里有java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因驱动 jar 没有进入 Tomcat 的类加载路径。最常见的是在 IDE 里用 Build Path 引了 jar但项目里WEB-INF/lib目录下没有这个文件或者驱动放进了WEB-INF/classes下面那个位置只能放 .class 文件不能放 jar。解决把mysql-connector-j-8.0.x.jar复制到web/WEB-INF/lib/目录然后在 IDE 里执行 Clean 和重新发布最后重启 Tomcat。如果重启后仍报错检查 jar 是否损坏可以把它解压后确认里面真的有com/mysql/cj/jdbc/Driver.class这个类。这一步最常见的误操作是只改了 IDE 里的依赖列表没有同步到实际的部署目录。5.2 中文全部变成问号先查库再查连接最后查页面现象表单提交的学生姓名写入数据库后变成???或者页面上显示ç¨å这类乱码。原因三个环节至少有一个不是 UTF-8。数据库当前连接用的字符集不对连接 URL 缺characterEncodingutf8页面头缺少charsetUTF-8。这个问题最迷惑的地方在于有时候数据库里是对的但页面乱有时候页面是对的但库是乱的让人以为是改了一处就能解决。解决按固定顺序排查。第一步连上 MySQL 执行SHOW VARIABLES LIKE character_set_database;确认结果是utf8mb4或utf8。第二步看连接 URL 是否包含useUnicodetruecharacterEncodingutf8注意characterEncoding要写 utf8 而不是 utf8mb4驱动层会自己映射。第三步检查 JSP 页面第一行是否写了charsetUTF-8。按这个顺序查基本十分钟内能定位到问题环节。5.3 MySQL 8 连接报 Public Key Retrieval is not allowed现象Tomcat 日志出现SQLNonTransientConnectionException: Public Key Retrieval is not allowed或者有时第一次连接报错过一会儿反而好了。原因MySQL 8.0 默认的认证插件是caching_sha2_password客户端第一次建立连接时需要从服务端获取公钥来加密密码传输而驱动默认不允许自动获取公钥。这算是 MySQL 8 最常见的连接层坑纯粹是连接参数问题不是账号密码错误也不是网络不通。解决在连接 URL 上追加两个参数useSSLfalseallowPublicKeyRetrievaltrue。useSSLfalse是本地开发环境的标准配置避免 SSL 握手带来的额外复杂度和性能损耗allowPublicKeyRetrievaltrue允许客户端自动获取服务端公钥。如果你不方便改代码里的 URL也可以把数据库账号的认证方式改成旧版执行ALTER USER student_applocalhost IDENTIFIED WITH mysql_native_password BY s123456;但新项目没必要为了兼容而用旧插件。5.4 改完数据显示旧值重定向与转发混用的后果现象点击保存后成功跳回了列表页但页面上显示的仍是修改前的名字手动刷新一次才变成新值或者保存按钮被连点两次数据库里出现了两条相同记录。原因保存逻辑里用了request.getRequestDispatcher(list_student.jsp).forward(...)转发而不是重定向。转发发生在服务端内部浏览器地址栏 URL 没有变化此时刷新等于重新提交了上次的表单重复插入数据。另一种情况是列表页设置了响应缓存浏览器直接用了缓存里的旧列表没回源请求。解决统一采用 PRG 模式保存成功后用response.sendRedirect(list_student.jsp)重定向。重定向会告诉浏览器重新 GET 列表页杜绝重复提交。同时可以在列表页顶部加上response.setHeader(Cache-Control, no-cache);强制浏览器每次回源。这两个改动都做上旧值问题基本绝迹。5.5 浏览器把 JSP 当文件下载部署结构与 web.xml 的问题现象在浏览器地址栏输入list_student.jsp页面没有渲染反而把 JSP 源码当文本文件展示或者直接弹出下载框。原因Tomcat 内置的 JspServlet 没有接管*.jsp的请求。这通常是应用部署结构不对比如 JSP 文件被放到了WEB-INF目录里面Tomcat 出于安全考虑不把 WEB-INF 下的资源当动态页面渲染或者是web.xml里被误配置了静态资源映射把*.jsp映射到了 DefaultServlet。解决先确认 JSP 文件在web根目录下不要放在WEB-INF里。然后打开 Tomcat 的conf/web.xml检查全局配置确认有*.jsp到org.apache.jasper.servlet.JspServlet的映射。项目自己的web.xml如果有servlet-mapping看是否把.jsp后缀覆盖掉了。最后清理 Tomcatwork/Catalina目录下的临时编译文件再重启让 JSP 重新完成编译。6. 进阶技巧排序白名单、Excel 导出与部署前最后检查6.1 排序白名单的另一种写法参数名映射到列名上一章提到的safeOrder是最朴素的字符串判断如果排序字段多用 Map 映射维护更清晰。把前端传来的参数名作为 key把真实列名作为 value未命中的一律回退到默认排序。这样前端改动参数名不影响 SQL也杜绝了任何拼接注入的可能。private static final MapString, String ORDER_MAP new HashMap(); static { ORDER_MAP.put(no, stu_no); ORDER_MAP.put(name, stu_name); ORDER_MAP.put(time, create_time); } private String safeOrder(String key) { String col ORDER_MAP.get(key); return col ! null ? col : id; }6.2 用 Apache POI 导出学生列表列表页直接打印往往不符合交付要求最常见的需求是导出成 Excel。Apache POI 是 Java 生态里最常用的 Excel 处理库导出一个表格只需三件套创建工作簿、创建行、写入单元格。// 伪代码示意导出流程 // XSSFWorkbook workbook new XSSFWorkbook(); // Sheet sheet workbook.createSheet(学生信息); // Row row sheet.createRow(0); // row.createCell(0).setCellValue(学号); // 遍历 list逐行写入学生数据 // workbook.write(response.getOutputStream()); // workbook.close();导出接口里必须手动设置响应头Content-Disposition: attachment; filenamestudents.xlsx这个头告诉浏览器这是要下载的文件而不是网页。文件名如果包含中文最好用 URLEncoder 编码一下否则不同浏览器对文件名解析不一样经常出现乱码或下载失败。6.3 上线前最后检查编码过滤器加上清理清单部署前我会把CharacterEncodingFilter确认存在并生效然后逐页搜索遗留的调试输出例如System.out.println和临时测试按钮。这些输出平时看不出问题但访问量一大Tomcat 日志会被刷满排查真实错误时只能翻到几千行之前才能找到问题。另外记得在 Tomcat 的conf/web.xml里禁用目录浏览防止学生访问http://ip:8080/student_ms/时看到整个目录的文件清单。我自己每次接手这类 JSP 项目第一件事永远是验证三处编码对齐再检查有没有把连接公用的工具类抽出来。这个习惯帮我避开了无数次“本来能用换个环境就崩”的尴尬。这些经验是从多次翻车里换来的希望你这次的路走得顺一点能帮到你就好。本文还有配套的精品资源点击获取
返回列表