ARTICLE DETAIL

资讯详情

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

Java+JSP+MySQL学生信息管理系统:分层设计到部署实战

Java+JSP+MySQL学生信息管理系统:分层设计到部署实战 简介基于JavaJSPMySQL实现的Web学生信息管理系统项目是一份面向Java Web初学者、课程设计或毕业设计人群的完整项目包。系统围绕学生信息管理场景覆盖用户登录与权限管理、学生信息录入、信息查询、报表统计等典型功能模块前后端交互与业务分层清晰可以直接借鉴其中的ServletJSPMySQL开发思路。项目压缩包共75个文件其中JSP页面28个、Java源码11个、Class编译文件22个另有XML配置、SQL脚本、依赖Jar包及IDEA工程配置整体仅3.92MB目录结构规整便于导入IDE运行调试。SQL脚本可直接建表并填充示例数据Java源码便于按需修改业务逻辑JSP页面则可对照学习动态数据渲染与表单提交处理。该资料已有8992人学习下载代码量适中可帮助已有Java基础但缺少完整项目经验的学习者梳理Web开发全流程也可直接用作课程设计或毕业设计的参考起点。1. “JavaJSPMysql实现Web学生信息管理系统”这个组合为什么还有人在用“JavaJSPMysql实现Web学生信息管理系统”这个标题放在 Spring Boot 普及的今天看确实有点旧但它背后对应着大量真实存在的项目学校教务处的校内工具、学生的毕业设计、外包团队手里那些改需求改到烦的老系统。学生信息系统的业务其实很收敛——管班级、管学生档案、管成绩用户几十人并发压力不大需求却总在变今天加个字段明天导一次 Excel后天要一个按年级统计的报表。用 JSPServletMysql 这种组合页面和逻辑直接联动改完刷新就能看效果比搭一整套微服务骨架省太多环节。这套方案适合三类人接手老系统需要快速上手的工程师做毕设或课设的在校生以及只想低成本维护内部工具的小团队。你不需要会 Spring Boot 全家桶但得把 JDBC 连接、Servlet 生命周期、Mysql 建表和 Tomcat 部署这些底子吃透。下面我按从骨架到落地的顺序把项目分包、库表设计、增删改查和部署排障整条路径讲一遍。2. 项目骨架怎么搭ServletJSPDAO 三层与 war 包的依赖边界开始动手前先把分层的边界想清楚否则后面每个页面都会长成一大坨。标题里的三个词各管一段JSP 负责展示Servlet 负责接请求Mysql 负责存数据。它们中间还要夹一层 DAO 来做数据库访问这一层不单独拎出来SQL 就会散落到各个 Servlet 甚至 JSP 里等需求一多就收拾不动了。2.1 JSP 在项目里的真实位置它是视图不是业务层JSP 页面里可以直接使用 request、response、session 这些内置对象是因为 JSP 在运行期会被 Tomcat 编译成 Servlet这些对象就存在于编译后生成的 doGet/doPost 方法里。但能用不等于该用。我见过最乱的维护现场是列表页顶部先连数据库查班级表中间写权限判断底部再用脚本拼接字符串。这种写法在页面少的时候确实能跑但一旦要加功能改错一行 SQL 都可能让整个页面白屏。常见的工程做法是 ServletJSPDAO 三层分工JSP 只做展示从 request 里取数据渲染Servlet 接收请求、调用 DAO、把结果放进 request 再 forward 到页面DAO 封装 JDBC 和 SQL实体类对应表结构。这样换数据库只改 DAO加字段只加实体和页面做登录拦截只动 Filter。改页面时不用碰 Java 文件这是我做维护项目攒下的血泪经验一个页面超过十段 scriptlet 之后改起来会非常折磨。如果只是几十人用的内部工具也不建议硬上 Spring、MyBatisServletDAO 足够支撑框架的价值在小系统里会被它引入的复杂度抵消。2.2 war 包的目录结构与依赖Maven 和非 Maven 两种摆法无论用不用 Mavenwar 包的内部结构是一致的。一个能跑起来的学生信息管理系统代码目录大致长这样src/main/java/com/student/ ├─ entity/ Student.java ClassInfo.java Score.java ├─ dao/ StudentDao.java BaseDao.java ├─ servlet/ StudentListServlet.java StudentSaveServlet.java LoginServlet.java ├─ filter/ LoginFilter.java └─ util/ DBUtil.java src/main/webapp/ ├─ WEB-INF/ web.xml lib/ ├─ css/ js/ ├─ login.jsp ├─ list.jsp ├─ add.jsp └─ edit.jsp如果接手的是别人拷过来的 WebContent 目录没必要硬改成 Maven把 mysql 驱动和 jstl 的 jar 复制进 WEB-INF/lib整个目录给 Tomcat 就能跑。如果是新起项目用 Maven 管理依赖会省很多事至少换驱动版本不用手动翻 lib。两种方式的取舍可以看这张表对比项普通 Web 工程Maven war 工程依赖处理手动复制 jar 到 WEB-INF/libpom 声明自动下载构建产物整个项目目录拷给 Tomcattarget 下的 war 包上手成本低中等适合场景接手旧项目、临时改需求新起项目、长期维护用 Maven 时pom 里要声明成 war 打包依赖边界靠 scope 控制groupIdcom.example/groupId artifactIdstudent-info-web/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies build finalNamestudent/finalName /buildservlet-api 和 jsp-api 必须声明为 provided因为它们由 Tomcat 提供混进 war 反而可能在启动时出现 NoSuchMethodError。mysql 驱动和 jstl 则要打进 war。这里还有一个常被忽略的版本边界JDK8 配 Tomcat9 用的是 javax.servlet 命名空间Tomcat10 开始换成 jakarta.servlet老代码里的 import javax.servlet.* 直接编译不过或启动即报 ClassNotFoundError。所以我一般建议新项目用 Tomcat9 JDK8 这套组合跑 JSP最省事。2.3 一个功能页面对应一个 Servlet路由约定让改页面不迷路为了让路由可预测我习惯定一个约定一个功能页面由一个 XxxServlet 加一个同名 JSP 组成。列表是 StudentListServlet 对 list.jsp新增保存是 StudentSaveServlet 对 add.jsp路径统一写成 /student/xxx。Servlet3.0 以上用 WebServlet 注解注册不用在 web.xml 里逐个写映射WebServlet(/student/list) public class StudentListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 查询逻辑在第4章补全这里先演示路由 req.getRequestDispatcher(/list.jsp).forward(req, resp); } }这个约定的价值是看到请求路径能定位到代码文件看到 JSP 文件名就知道它属于哪个功能。新成员接手时不用问“这个列表是谁转发过来的”。对一个字段级改动频繁的学生信息管理系统来说这种可预测性比省几行配置重要得多。接下来我把库表设计定下来再往里填代码。3. 库表设计班级、学生、成绩三张表与建表编码纪律学生信息管理系统最容易翻车的地方不在 Servlet而在建表阶段。业务代码可以后面补表结构一旦上线再改要连带处理历史数据和所有 SQL。核心就是三张表班级、学生、成绩。表不要贪多字段不要贪全够用和留好扩展点就行。3.1 三张表的字段取舍为什么班级信息不能直接塞进学生表先看最终设计再解释每个字段为什么这么定表核心字段说明t_classid, class_name, grade, create_time年级如“2024级”班级名如“计算机1班”t_studentid, student_no, name, gender, birthday, phone, email, class_id, status, create_timestatus 表示在校/离校t_scoreid, student_id, course_name, score, exam_time一个学生可有多条成绩记录第一眼可能有人问学生表里直接存一个“班级名”字段不就行了查询还少一次 JOIN实际做下去会发现这条捷径会坑自己。学生换班时要改多少人按年级统计时怎么过滤班级改个名字要不要全校更新把班级 ID 作为逻辑外键存进学生表这些情况都只需要改一行。学号要单独加唯一索引因为导入导出 Excel 时业务上按学号判重数据库层不挡一下重复学号会静默插进去。status 字段是逻辑删除学生误删时还有后悔药物理 DELETE 一发就真的找不回来了。3.2 建表 SQL 与初始化数据从 utf8mb4 到索引有了字段设计直接把 SQL 落到 Mysql 里。字符集统一用 utf8mb4别用 utf8因为备注里可能出现生僻字甚至 Emojiutf8 存进去会直接报错或变问号。CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE t_class ( id INT NOT NULL AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL COMMENT 班级名称, grade VARCHAR(20) NOT NULL COMMENT 年级如2024级, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_student ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男, birthday DATE DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, class_id INT NOT NULL COMMENT 班级ID, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在校 0离校, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_class_id (class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_score ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL, course_name VARCHAR(50) NOT NULL, score DECIMAL(5,1) DEFAULT NULL, exam_time DATE DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student_course (student_id, course_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表格后面跟着两条测试数据INSERT INTO t_class (class_name, grade) VALUES (计算机1班, 2024级); INSERT INTO t_student (student_no, name, gender, birthday, phone, email, class_id) VALUES (20240001, 张三, 男, 2006-03-15, 13800000000, zhangsanexample.com, 1);几个参数值得解释ENGINEInnoDB 保证事务和行级锁MyISAM 在并发写的时候容易锁表DECIMAL(5,1) 支持 0.0 到 999.9 的成绩比 FLOAT 精确不会出现 89.99999 这种尴尬值status 用 TINYINT 而不是 CHAR后面做统计筛选更快。学号的 UNIQUE KEY 一定要建应用层校验再严谨也挡不住绕过页面直接操作数据库的情况。3.3 设计阶段就要定的三个边界时间类型、物理外键和冗余字段时间字段统一用 DATETIME。create_time 用 DATETIME DEFAULT CURRENT_TIMESTAMP 最省事birthday 用 DATE。不推荐 TIMESTAMP 的原因有两个它受 Mysql 服务器时区影响跨时区部署时显示会偏其次它有 2038 年问题存毕业时间这种远期日期不是不行但没必要。DATETIME 不随系统时区变存的是什么读出来就是什么。物理外键这个坑很多人是上线后才踩到。建表时给 class_id、student_id 加普通索引但不要加 FOREIGN KEY 约束。物理外键的问题在于删除班级时如果学生表里还有关联数据Mysql 会直接拒绝删除你想批量导入学生数据时外键校验还会拖慢速度。常见做法是外键约束放到业务层校验删除班级前先查学生表有没有人有就提示先处理学生。逻辑外键加普通索引查询性能不差灵活性高很多。冗余字段要控制住。不要在 t_score 里存学生姓名和班级名称一份成绩单 JOIN 一下就能拼出来更不要在 t_student 里存班级名字符串。报表要的是实时关系冗余字段会变成“改一处漏多处”的定时炸弹。等需要导出 Excel 时现场 JOIN 出来性能和代码可维护性都能接受。4. 把增删改查跑通DBUtil、分页列表、表单回显与学号校验骨架和表结构定了下面就是代码层的最小闭环。这一章围绕增删改查展开顺带把分页、搜索和表单回显这些必然遇到的需求一起做完。代码可以直接抄但每个参数的含义要弄明白。4.1 DBUtil 连接 Mysql三个必调参数和一个习惯连接管理是这套系统的地基先写一个最简工具类public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(找不到Mysql驱动类, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL 里这几个参数不是网上随便抄的每个都有对应的报错场景。serverTimezoneAsia/Shanghai 不写Mysql 8.0 会直接抛异常说服务器时区无法识别allowPublicKeyRetrievaltrue 是配合 Mysql8 默认的 caching_sha2_password 认证方式不加会报 Public Key Retrieval is not alloweduseSSLfalse 是因为本地开发没有证书硬要开 SSL 会连不上。驱动类名也要注意mysql-connector-java 8.x 是 com.mysql.cj.jdbc.Driver5.x 是 com.mysql.jdbc.Driver用反了会 ClassNotFoundException。提示密码不要硬编码成明文。演示项目可以这么写维护项目建议把数据库配置放到 properties 文件里读取。生产环境再往配置中心切别在一开始就给后面挖坑。这个工具类每次调用都新建连接适合几十人的小系统。如果用户量上来或者报表查询变多再换连接池连接池要接的话从 HikariCP 起步就行。我见过太多人一上来就上连接池结果配置比业务代码还复杂没必要。4.2 StudentDao 的分页与关键词查询占位符别拼字符串DAO 层要同时支持分页和关键词搜索SQL 长这样public ListStudent list(String keyword, int page, int pageSize) { StringBuilder sql new StringBuilder( SELECT s.id, s.student_no, s.name, s.gender, s.birthday, s.phone, s.email, c.class_name FROM t_student s LEFT JOIN t_class c ON s.class_id c.id WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (s.name LIKE ? OR c.class_name LIKE ?) ); params.add(% keyword.trim() %); params.add(% keyword.trim() %); } sql.append(LIMIT ? OFFSET ?); params.add(pageSize); params.add((page - 1) * pageSize); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { ListStudent list new ArrayList(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setClassName(rs.getString(class_name)); list.add(s); } return list; } } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } }WHERE 11 不是炫技是为了后面条件拼接不用判断是不是第一条。所有参数都用 ? 占位符% 通配符拼进参数值里而不是拼 SQL 字符串这是防止注入的基本功。JOIN 用 LEFT JOIN 而不是 INNER JOIN原因是如果某个学生的班级被误删了INNER JOIN 会让这条学生记录整个从列表消失LEFT JOIN 至少还能把学生显示出来。分页的总页数需要另一条 count 查询逻辑放 Servlet 层int total dao.countTotal(keyword); int totalPage (total pageSize - 1) / pageSize;countTotal 的 SQL 和 list 的 WHERE 条件保持一致只是把 SELECT 字段换成 COUNT(*)关键词过滤逻辑完全一样。totalPage 这个公式要记牢很多人栽在边界上total 正好是 pageSize 的整数倍时这个公式不会多出空白页。4.3 Servlet 转发数据JSP 只管渲染forward 和 JSTL 的分工Servlet 负责把数据装进 request再转发给 JSP 渲染WebServlet(/student/list) public class StudentListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int page 1; int pageSize 10; if (req.getParameter(page) ! null) { page Integer.parseInt(req.getParameter(page)); } String keyword req.getParameter(keyword); StudentDao dao new StudentDao(); int total dao.countTotal(keyword); int totalPage (total pageSize - 1) / pageSize; req.setAttribute(list, dao.list(keyword, page, pageSize)); req.setAttribute(page, page); req.setAttribute(totalPage, totalPage); req.getRequestDispatcher(/list.jsp).forward(req, resp); } }页面这边用 JSTL 渲染不要在 JSP 里写 %% 循环% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 tr th学号/thth姓名/thth性别/thth班级/thth操作/th /tr c:forEach items${list} vars tr td${s.studentNo}/td td${s.name}/td td${s.gender}/td td${s.className}/td tda hrefedit?id${s.id}编辑/a/td /tr /c:forEach /table c:forEach 里的 ${s.studentNo} 实际调用的是 Student 类的 getStudentNo() 方法所以实体类属性名和 getter 必须对应。Servlet 这里用 forward 而不是 sendRedirect是因为 forward 是服务器内部跳转request 里放的 list、page 这些数据页面能直接拿到sendRedirect 会让浏览器重新发一次请求属性全丢。 提示上面 Servlet 里 Integer.parseInt 简化写没做异常处理。真实项目里 page 参数可能被人传成乱字符建议捕获 NumberFormatException 后回退到 page1否则一个 URL 就能打崩列表页。 ### 4.4 新增与编辑共用一个保存逻辑回显、日期转换和学号去重 新增和编辑共用一个 StudentSaveServlet区别在于表单里有没有隐藏的 id 字段 java WebServlet(/student/save) public class StudentSaveServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String idStr req.getParameter(id); String studentNo req.getParameter(studentNo); String name req.getParameter(name); String gender req.getParameter(gender); String birthdayStr req.getParameter(birthday); String phone req.getParameter(phone); int classId Integer.parseInt(req.getParameter(classId)); StudentDao dao new StudentDao(); Student student new Student(); student.setStudentNo(studentNo); student.setName(name); student.setGender(gender); student.setPhone(phone); student.setClassId(classId); // 学号去重编辑时要排除自己 Student exist dao.findByNo(studentNo); if (exist ! null !String.valueOf(exist.getId()).equals(idStr)) { req.setAttribute(msg, 学号已存在); req.getRequestDispatcher(/add.jsp).forward(req, resp); return; } // 日期转换空字符串要特殊处理 SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); if (birthdayStr ! null !birthdayStr.isEmpty()) { try { student.setBirthday(sdf.parse(birthdayStr)); } catch (ParseException e) { req.setAttribute(msg, 日期格式应为yyyy-MM-dd); req.getRequestDispatcher(/add.jsp).forward(req, resp); return; } } if (idStr null || idStr.isEmpty()) { dao.insert(student); } else { student.setId(Integer.parseInt(idStr)); dao.update(student); } resp.sendRedirect(req.getContextPath() /student/list); } }这段代码把学生信息管理系统最常见的三个问题一起处理了。日期字段在 HTML 表单里是可选项不填会提交空字符串直接 SimpleDateFormat.parse() 会抛异常所以要先判空。学号去重必须排除自己否则编辑时没改学号也会提示“学号已存在”这个逻辑一漏整个编辑功能就废了。新增和编辑的区分用隐藏字段 id新增时 id 为空编辑时 JSP 在表单里输出。保存成功用 sendRedirect 跳回列表避免用户刷新页面时表单重复提交。5. 部署与常见问题排查Tomcat 启动、中文乱码和 Mysql 连接失败代码能跑和能部署是两回事。我刚开始做这类系统时一半时间都耗在这几个问题上。JSPMysql 这条技术路线最大的好处是问题可复现报错直接解决有套路。下面这几类问题基本覆盖了从打包上线到日常维护的绝大多数踩坑现场。5.1 部署 war 与访问路径从 Maven 打包到 Tomcat 启动Maven 工程的部署命令很简单cd student-info-web mvn clean package cp target/student.war /opt/tomcat9/webapps/ /opt/tomcat9/bin/startup.sh tail -f /opt/tomcat9/logs/catalina.out访问路径由 war 包名决定。pom.xml 里 finalName 配的是 student所以访问地址是 http://localhost:8080/student/list。如果 finalName 改成了别的路径跟着变。不用 Maven 的项目更直接把项目目录改名为 student整个丢进 webapps 下启动 Tomcat 一样能访问。这里有一个新手很容易踩的更新陷阱只覆盖 war 不删旧目录。Tomcat 启动时会自动解压 war第二次部署时旧解压目录还在新 war 覆盖进去老的 class 文件不会自动清掉结果就是改了代码没生效。规范操作是先 shutdown Tomcat删掉 webapps 下的 student 目录和 student.war重新放新 war 再启动。日志别只看 startup.sh 的输出真正的报错在 logs/catalina.out 里全量搜索 Exception 比在控制台翻屏可靠。5.2 中文乱码排查JSP、Servlet、JDBC 与 Mysql 四端对齐中文乱码是这个项目里出现频率最高的问题现象分两种页面输出乱码或者数据库存进去变成 ???。原因基本是编码没对齐排查顺序固定为四端检查。第一端是 JSP 页面开头必须写完整page contentTypetext/html;charsetUTF-8这一行不写浏览器会用默认编码解析页面。第二端是 Servlet代码第一行 req.setCharacterEncoding(UTF-8) 必须放在读取任何参数之前位置放错等于没设。第三端是 JDBC URL必须带 useUnicodetruecharacterEncodingutf8这决定驱动往 Mysql 发数据时用什么编码。第四端是数据库建库建表时已经指定 utf8mb4如果还乱码就用 SHOW CREATE TABLE t_student 确认一下表字符集。GET 和 POST 的乱码机制还不一样。Tomcat8 以后 GET 请求默认 URIEncodingUTF-8URL 里的中文名字基本安全POST 表单如果不设 setCharacterEncodingTomcat 会按 ISO-8859-1 解析中文必然乱。所以排查时先分清是哪种请求出的问题再决定动哪一端不用四端一起瞎改。5.3 连接失败三类高频误用驱动缺失、时区参数和权限问题现象一启动或访问时报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是 mysql 驱动 jar 没进 WEB-INF/lib。解决Maven 项目检查依赖 scope 是不是 compile普通 Web 工程直接确认 lib 目录下有驱动文件并重新构建 war。现象二报 Communications link failure 或 Connection refused。原因是 Mysql 没启动、端口不是 3306、或者 URL 里 host 写错。解决先本地用 mysql 命令连一下排除数据库服务问题再确认 URL 里的端口。还有一个 Linux 上的细节URL 写 localhost 时驱动走 Mysql Socket写 127.0.0.1 才走 TCP两者行为可能不一样排查时统一用 127.0.0.1 或机器 IP。现象三报 Public Key Retrieval is not allowed。原因是 Mysql8 默认 caching_sha2_password 认证第一次连接需要拉取服务端公钥驱动出于安全考虑默认不自动获取。解决JDBC URL 加 allowPublicKeyRetrievaltrue。如果项目用的是 Mysql 5.7驱动类名要改成 com.mysql.jdbc.Driver同时可以去掉这个参数。5.4 连接没关导致 Too many connections用 try-with-resources 收好现象系统跑几天后页面突然变慢Mysql 报 Too many connections。原因基本不用怀疑——代码里 Connection 开了没关。Java 的 Connection 对象在超出作用域后会等 GC 回收但 GC 什么时候跑不由你说了算连接池和 Mysql 服务端有超时时间两边一错位就堆满。现在写 DAO 有个很省心的写法直接用 try-with-resources连接、预编译语句、结果集全自动关闭try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 处理结果集 } catch (SQLException e) { throw new RuntimeException(数据库操作失败, e); }提示这个写法比在 finally 里一个个判断 null 再 close 干净得多。代码审查时看到手动 close 的旧代码建议逐步换成这种写法。排查连接数问题时不要急着重启 Tomcat先看 Mysql 这边mysql -uroot -p -e SHOW PROCESSLIST;看到 Sleep 状态的连接大量堆积基本就是应用侧没释放。把这些进程 KILL 掉然后回头改 DAO 代码改完再观察连接数曲线是否平稳。这类问题在演示环境很难复现但一到真实使用就现原形属于典型的“初看是玄学排查全是账”。6. 上线前验收登录过滤器加 curl 冒烟测试代码写完不是结束还要验证“未登录不能进列表页”这类边界。学生信息管理系统通常只有管理员在用但登录态过滤不能省否则任何人拿 URL 直接访问 /student/list 都能看到全量学生数据。这里给出一个最简过滤器再用 curl 跑一遍冒烟测试。6.1 登录过滤器三行拦截逻辑挡掉未登录请求WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录接口和静态资源其余全部拦截 if (uri.endsWith(/login.jsp) || uri.endsWith(/LoginServlet) || uri.contains(/css/)) { chain.doFilter(req, resp); return; } // 未登录直接跳回登录页 if (request.getSession().getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }放行 /css/ 是让登录页的样式能正常加载不然过滤器把 CSS 也拦了登录页会变成纯文本。loginUser 是登录成功后塞进 Session 的标记值是什么不重要重要的是它在不在。6.2 用 curl 验收接口看 302、看 200、看页面渲染部署完成后用三条命令就能把小系统的核心链路验一遍# 未登录访问列表页应该返回302而不是200 curl -i http://localhost:8080/student/list | head -n 3 # 登录把Session Cookie保存到cookies.txt curl -c cookies.txt -d usernameadminpassword123456 http://localhost:8080/LoginServlet # 带Cookie访问列表页应该返回200 curl -b cookies.txt http://localhost:8080/student/list -o list.html # 确认页面真的渲染出了数据 grep 张三 list.html302 说明过滤器拦截生效200 说明登录态通过grep 到学生姓名说明页面渲染正常。这一套验证在 Windows 下用 Git Bash 或 Postman 都能做习惯先看状态码再看页面内容排查问题会快很多。以前我验收这种信息系统只看 Tomcat 控制台没有红色报错就算完事真正上线后才发现未登录能直接访问列表页这种低级问题。后来我养成固定习惯每次改完先跑一遍 302 和 200 的状态码检查再确认页面渲染顺序不能乱。这种老技术栈没有黑匣子问题都是可复现的照着这个步骤走能省下不少来回沟通的成本希望帮到你。本文还有配套的精品资源点击获取
返回列表