资讯动态

JSP+Servlet+JDBC 学生信息管理系统:MVC 分层与请求链路

发布时间:2026/10/23 16:23:09 来源:尧图企业网站定制
简介面向高校课程设计与Java Web入门练习的学生信息管理系统项目包围绕学生自助注册、教师集中管理学生资料等场景展开采用JSPServlet为核心并结合Layui、jQuery完成页面交互数据库使用MySQL开发运行环境涉及JDK7、Tomcat8与IntelliJ IDEA。资源共2个文件由1个项目压缩包和1个htm说明页组成整体约2.13MB说明页可用于了解项目结构、数据库文件与lib依赖包等信息源码包则便于直接导入IDE查看和调试。已有6437人学习下载适合需要完成课程设计、期末项目或Java Web综合练习的读者参考。项目涵盖注册登记、学生信息维护、教师管理、Servlet请求处理、JSP页面展示及MySQL数据存取等典型环节可帮助读者梳理分层结构、前后端协作与基础增删改查实现也便于在此基础上做功能扩展与界面调整。1. 一套不靠框架的学生信息管理系统JSP Servlet 到底怎么把请求跑完学生信息管理系统这个题目几乎每个学期的 Java Web 课程设计都会撞上一次。需求一句话能说完登录进去把学生记录增删改查。可真动手时翻车的点一个不少——JSP 提交的中文变问号、Servlet 里getParameter拿到 null、MySQL 8 报 Public Key Retrieval is not allowed、war 丢进 Tomcat 之后 404。这套基于 JSP Servlet 的简单实现走的是最朴素的 MVCServlet 收请求做控制DAO 走 JDBC 碰 MySQLJSP 只负责渲染不引入 Spring 全家桶mvn package出来的 war 丢进 Tomcat 就能访问。适合两类人要交课程设计、需要一份能把请求链路和分层讲清楚的完整代码的同学以及从框架里退回来、想搞明白 DispatcherServlet 之前那些活到底谁在干的开发者。2. 工程骨架先立住Maven 目录、依赖坐标与一条请求的完整链路很多人拿到一份 JSP Servlet 的代码第一反应是双击index.jsp用浏览器打开然后发现 EL 表达式原样输出、${}一片空白。Java Web 的调试方法里这是最容易让人怀疑人生的玄学之一它根本不是静态页面必须由容器编译执行。所以第一步不是看代码是把工程骨架和运行环境对齐。2.1 为什么不用框架也能自洽MVC 三分法与目录职责这套系统能读得懂靠的是一条硬规矩每一层只做自己那一层的事。Servlet 里不出现一行 SQLDAO 里不出现HttpServletRequestJSP 里不出现% %脚本片段。听起来像老生常谈但课程设计代码最常见的烂法就是从这条线开始塌的——一个StudentServlet里既prepareStatement又out.print(table)两百行下去没人敢改。目录按职责切不按文件类型切student-manager ├── pom.xml └── src/main ├── java/com/example/sms │ ├── controller # 控制器收参数、调 DAO、决定跳哪 │ │ ├── StudentServlet.java │ │ └── LoginServlet.java │ ├── dao # 数据访问只认 SQL 和实体 │ │ └── StudentDao.java │ ├── entity # 实体字段跟表一一对应 │ │ ├── Student.java │ │ └── SysUser.java │ ├── filter # 过滤器编码、登录拦截 │ │ ├── EncodingFilter.java │ │ └── LoginFilter.java │ └── util # 工具连接池、分页、密码 │ ├── DBUtil.java │ ├── Page.java │ └── PasswordUtil.java └── webapp ├── WEB-INF │ ├── web.xml │ └── views # 放在 WEB-INF 下浏览器访问不到 │ ├── list.jsp │ ├── edit.jsp │ └── login.jsp ├── css/ └── index.jsp # 只做一次重定向不写业务视图放WEB-INF/views是个小设计但很值外部无法直接拼 URL 访问list.jsp所有入口都被 Servlet 卡住登录拦截才有意义。否则你在LoginFilter里拦得再严别人敲一个/WEB-INF之外的老页面照样绕过去。层代表类该干的活不该出现的东西控制层StudentServlet取参数、校验、调 DAO、forwardSQL 语句、HTML 拼接数据层StudentDao拼 SQL、映射结果集request、session、response视图层list.jsp遍历数据、渲染表格Class.forName、DriverManager工具层DBUtil拿连接、还连接业务判断index.jsp只写一句response.sendRedirect(request.getContextPath() /student?actionlist);让首页就是列表页避免出现“首页空白”这种一看就废的第一印象。2.2 用 Maven 搭骨架pom、web.xml 与容器版本对齐依赖这块坑最多先把坐标写死。下面这份pom.xml只留必需项注意scope的用法——servlet-api和jsp-api必须是provided因为 Tomcat 自己带了一份你把它打进WEB-INF/lib就会出现同一个类被两个类加载器加载报错信息往往是一句毫无指向性的ClassCastException。properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- 容器自带必须 provided否则和 Tomcat 的类冲突 -- 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 !-- JSTL 容器不带必须打进 war -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesweb.xml用 Servlet 4.0 的 schema跟 Tomcat 9 对齐。虽然现在WebServlet注解很香但web.xml至少留着做welcome-file和 Filter 顺序声明两者不冲突。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namestudent-manager/display-name welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app容器版本必须和包名对齐这一条能省掉大半“编译通过但跑不起来”的时间Tomcat 版本Servlet 规范包名前缀JSTL 标签 uri 前缀8.53.1javax.servlethttp://java.sun.com/jsp/jstl/core9.04.0javax.servlethttp://java.sun.com/jsp/jstl/core10.05.0jakarta.servletjakarta.tags.core10.16.0jakarta.servletjakarta.tags.core我一般把 JDK 固定在 8Tomcat 固定在 9.0.x这一组是课程设计环境里最省心的组合javax命名空间、JSTL 1.2 直接可用、MySQL 8 驱动也兼容。上来就选 Tomcat 10 的话所有import javax.servlet.*全要改成jakarta.*JSTL 坐标也得换成jakarta.servlet.jsp.jstl那一套改到一半人容易崩。打包命令只有一句产物在target下mvn clean package -DskipTests # 产物target/student-manager.war直接丢进 TOMCAT_HOME/webapps/2.3 追一条请求从 index.jsp 到 list.jsp 的完整链路骨架立住之后最有价值的动作是把一条请求从头到尾追一遍。追通了后面加功能就是填空追不通写十遍 CRUD 还是糊涂的。浏览器敲http://localhost:8080/student-manager/student?actionlistpage2发生的事按顺序是Tomcat 按 context path/student-manager找到应用把 URL 剩余部分/student映射到StudentServlet。请求先过EncodingFilter/*匹配把request的字符集设成 UTF-8。再过LoginFilter从session里取loginUser取不到就redirect到登录页。StudentServlet.doGet()读到actionlist、page2调StudentDao.page()拿数据。DAO 从DBUtil借一条连接执行两条 SQL一条count算总数一条带LIMIT拿当页数据。Servlet 把结果塞进request.setAttribute(page, pageObj)然后forward到/WEB-INF/views/list.jsp。JSP 编译成 Servlet第一次访问会慢几百毫秒属于正常现象用 JSTL 遍历渲染表格输出 HTML。对应到代码Servlet 的最小骨架长这样WebServlet(/student) public class StudentServlet extends HttpServlet { private final StudentDao dao new StudentDao(); // 无状态可复用 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.isEmpty()) { action list; // 默认动作别让空 action 掉进 500 } switch (action) { case list: list(req, resp); break; case edit: edit(req, resp); break; // 跳到编辑页并回填 case delete: delete(req, resp); break; default: resp.sendError(HttpServletResponse.SC_BAD_REQUEST, unknown action); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String kw req.getParameter(kw); String cls req.getParameter(cls); int pageNo parsePage(req.getParameter(page)); // 非法值兜底成 1 PageStudent page dao.page(kw, cls, pageNo, 10); req.setAttribute(page, page); req.setAttribute(kw, kw); req.setAttribute(cls, cls); // forward同一个 request属性带得过去地址栏不变 req.getRequestDispatcher(/WEB-INF/views/list.jsp).forward(req, resp); } }这里有两个容易被忽略的点。第一parsePage必须对pageabc、page-1、page99999都做兜底前端手改 URL 是常态不兜底就是LIMIT -10,10这种 SQL 语法错误。第二增删改完成后要用sendRedirect而不是forward否则用户按 F5 会把删除操作再提交一遍——这是血泪经验演示时最容易出洋相的地方。3. 数据层落地MySQL 建表、Druid 连接池与分页 SQL 的写法数据层是这套系统里唯一“错一次、错一片”的部分。表结构定得随意后面每个页面都要打补丁连接忘了还跑几十次点击就卡死。这一章按建表、连接池、分页三块拆。3.1 建表字段、类型与索引怎么定学生表的字段不要贪多课程设计里够用就行但几个关键决策要有理由。CREATE DATABASE IF NOT EXISTS sms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE sms; CREATE TABLE student ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, sno VARCHAR(20) NOT NULL COMMENT 学号业务唯一, name VARCHAR(32) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, birthday DATE DEFAULT NULL, class_name VARCHAR(64) NOT NULL DEFAULT COMMENT 班级, phone VARCHAR(20) NOT NULL DEFAULT , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sno (sno), KEY idx_name (name), KEY idx_class (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password CHAR(64) NOT NULL COMMENT sha256(salt 明文), salt CHAR(16) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逐项说一下为什么这么定。字符集必须是utf8mb4utf8三字节存不下 emoji 和部分生僻字姓名里带生僻字的情况真的会碰到一存就报Incorrect string value。学号单独建唯一键uk_sno不要用学号当主键——学生转专业、退学复学改学号的场景虽然少见但业务键和物理主键分离是基本习惯外键关联也更好写。class_name建普通索引因为列表页按班级筛选是高频操作name建索引支持姓名模糊查询的前缀匹配虽然%张%这种左模糊用不上索引但数据量小的时候无所谓知道这个边界即可。gender用TINYINT不用CHAR(1)省空间且排序可控页面渲染时用 JSTL 的c:choose转成“男/女”别在数据库里存中文。create_time给DEFAULT CURRENT_TIMESTAMP插入语句就不用管这个字段少一处出错点。提示唯一键冲突要在 DAO 里捕获SQLIntegrityConstraintViolationException翻译成“学号已存在”返给页面。让异常直接冒到 Tomcat 的 500 页面用户看到的是堆栈体验直接掉档。插入语句显式写列名不要INSERT INTO student VALUES (...)INSERT INTO student (sno, name, gender, birthday, class_name, phone) VALUES (?, ?, ?, ?, ?, ?);显式列名的好处是以后加字段不用改这条 SQL顺序也不会因为表结构变动而错位。3.2 Druid 连接池与 JDBC 工具类课设代码里十份有八份在 DAO 里写DriverManager.getConnection(url, user, pwd)。能跑但每敲一次查询就新建一条 TCP 连接、做一次握手、退出时再断页面上点十次就开始明显卡顿。连接池不是“高级特性”在这里是刚需。配置文件放src/main/resources/druid.properties跟代码分离driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/sms?useUnicodetruecharacterEncodingutf8\ useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot passwordyour_password initialSize5 minIdle5 maxActive20 maxWait3000 validationQuerySELECT 1 testWhileIdletrue testOnBorrowfalse removeAbandonedtrue removeAbandonedTimeout60URL 上的参数一个都别省每一个都对应一类报错参数建议值不写的后果useUnicodecharacterEncodingtrue/utf8中文入库变问号useSSLfalse控制台刷 SSL 警告本地调试噪音serverTimezoneAsia/Shanghai报 The server time zone value is unrecognizedallowPublicKeyRetrievaltrueMySQL 8 默认认证插件下报 Public Key Retrieval is not allowedrewriteBatchedStatementstrue批量插入退化成逐条慢一个数量级maxWait3000是故意的等 3 秒拿不到连接就抛异常比默认无限等待好排查得多。默认值下连接泄漏的表现是“页面转圈转到天荒地老”日志里什么都没有纯黑匣子。工具类只暴露两个方法别让它长成万能类public final class DBUtil { private static final DataSource DS; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); DS DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { // 静态块里失败必须抛 Error否则类初始化半死不活后面全是空指针 throw new ExceptionInInitializerError(e); } } private DBUtil() {} public static Connection getConnection() throws SQLException { return DS.getConnection(); // 这里拿到的是池化代理对象 } }关键认知conn.close()在池化环境下不是断连是把连接还回池子。所以 DAO 里必须关而且要放在finally或者用 try-with-resources。用 try-with-resources 最省事编译器保证逆序关闭ResultSet→Statement→Connection顺序错了某些驱动会警告。3.3 DAO 的分页与动态条件 SQL分页要两条 SQL一条算总数一条拿当页数据。两条 SQL 的 where 条件必须完全一致这是最容易出错的地方——筛选条件只加在 list 上count 没加结果就是“总共 200 条翻到第 3 页后面全空”。我一般把条件拼装抽成一个方法两边共用。public class StudentDao { /** 拼 where 子句和参数count 与 list 共用杜绝条件不一致 */ private String buildWhere(String kw, String cls, ListObject args) { StringBuilder sb new StringBuilder( WHERE 11); if (kw ! null !kw.trim().isEmpty()) { sb.append( AND (sno LIKE ? OR name LIKE ?)); String like % kw.trim() %; args.add(like); args.add(like); } if (cls ! null !cls.trim().isEmpty()) { sb.append( AND class_name ?); args.add(cls.trim()); } return sb.toString(); } public PageStudent page(String kw, String cls, int pageNo, int size) throws SQLException { ListObject args new ArrayList(); String where buildWhere(kw, cls, args); long total; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT COUNT(*) FROM student where)) { bind(ps, args); try (ResultSet rs ps.executeQuery()) { rs.next(); total rs.getLong(1); } } // 页码越界时回头取最后一页避免前端改 URL 直接白屏 int pages (int) ((total size - 1) / size); if (pages 0 pageNo pages) pageNo pages; if (pageNo 1) pageNo 1; ListObject listArgs new ArrayList(args); listArgs.add((pageNo - 1) * size); // offset listArgs.add(size); // limit ListStudent rows new ArrayList(); String sql SELECT id, sno, name, gender, birthday, class_name, phone, create_time FROM student where ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { bind(ps, listArgs); try (ResultSet rs ps.executeQuery()) { while (rs.next()) rows.add(map(rs)); } } return new Page(rows, total, pageNo, size); } private void bind(PreparedStatement ps, ListObject args) throws SQLException { for (int i 0; i args.size(); i) { ps.setObject(i 1, args.get(i)); } } private Student map(ResultSet rs) throws SQLException { Student s new Student(); s.setId(rs.getInt(id)); s.setSno(rs.getString(sno)); s.setName(rs.getString(name)); s.setGender(rs.getInt(gender)); s.setBirthday(rs.getDate(birthday)); s.setClassName(rs.getString(class_name)); s.setPhone(rs.getString(phone)); s.setCreateTime(rs.getTimestamp(create_time)); return s; } }几个细节值得展开。第一SELECT显式列名而不是SELECT *字段顺序变动、加字段都不会让map里的列名对不上。第二全程PreparedStatement加?占位kw里塞 OR 11 --也不会出事字符串拼接 SQL 是这套代码里最不应该出现的写法。第三LIMIT ?, ?在 JDBC 里可以用占位符MySQL 驱动会把它们当整数绑定不用担心被转成字符串。第四ORDER BY id DESC保证翻页顺序稳定缺了这行 MySQL 的返回顺序不保证同一页可能重复出现某条记录看起来像数据错乱其实是排序没定死。深分页的问题也要知道边界LIMIT 100000, 10会先扫过前十万行再丢掉数据量上百万时会明显变慢。课设数据量在几千条以内不用管真要做大改成基于id的游标翻页WHERE id 上一页最后一条的 id ORDER BY id DESC LIMIT 10这是另一套东西了。4. Servlet 与 JSP 的对接action 分发、乱码处理与 JSTL 渲染分层写对之后剩下最容易出问题的就是“Servlet 收到的东西不对”和“JSP 显示的东西不对”。这两个问题的根源都在参数和字符集上属于看一眼就懂、不写就忘的类型。4.1 一个 Servlet 管五个动作action 分发怎么写不烂学生模块的增删改查总共五六个入口一个StudentServlet全管用action参数区分。这样路由集中、日志好打也不用在web.xml里注册一堆。代价是方法会变长所以要拆私有方法每个方法只做一件事。Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); // 表单提交走 POST复用同一套分发 } private void delete(HttpServletRequest req, HttpServletResponse resp) throws IOException { String idStr req.getParameter(id); if (idStr null || !idStr.matches(\\d)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, id 非法); return; } try { dao.deleteById(Integer.parseInt(idStr)); } catch (SQLException e) { throw new ServletException(删除失败, e); // 保留堆栈别吞异常 } // PRGPost-Redirect-Get刷新不会重复提交 resp.sendRedirect(req.getContextPath() /student?actionlist); }doGet里调doPost、doPost里调doGet都行选一个方向固定下来就好最怕两个方法各写一套逻辑改一处漏一处。action分发时一定要有default分支并返回 400否则拼错一个字母就静默失败页面白屏、日志空白排查成本极高。还有一个常被忽视的线程安全问题Servlet 在容器里是单实例多线程StudentServlet只会被 new 一次所有请求共用一个对象。所以成员变量只能是dao这种无状态的东西Connection、HttpServletRequest、当前登录用户这些绝对不能存成成员变量一存就会串数据——A 同学刷新页面看到 B 同学的信息这种 bug 演示时出现一次就凉了。4.2 中文乱码与参数绑定Filter 与 BeanUtils乱码是 Java Web 的头号经典问题但它的成因非常确定字符集在解码那一刻就已经确定了之后改不回来。request.setCharacterEncoding(UTF-8)必须写在任何一次getParameter之前晚一行都无效。用 Filter 统一处理别在每个 Servlet 里抄一遍WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); // 只影响 POST 请求体 resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); // 放行后续 getParameter 才按 UTF-8 解 } }GET 请求的参数在 URL 上走的是容器配置不受这行影响。Tomcat 8 之后Connector的URIEncoding默认就是 UTF-8一般不用动如果跑在更老的容器上得去conf/server.xml里给Connector补URIEncodingUTF-8。这也是为什么有些人本地好好的换台机器就乱码——代码没问题容器配置不一样。多个 Filter 时顺序由web.xml里的filter-mapping决定WebFilter的顺序是不保证的。所以登录拦截要读到参数的话把编码过滤器在web.xml里显式排在前面更稳。参数封装建议手写别急着上反射工具private Student readForm(HttpServletRequest req) { Student s new Student(); s.setSno(req.getParameter(sno).trim()); s.setName(req.getParameter(name).trim()); s.setGender(parseInt(req.getParameter(gender), 0)); String birthday req.getParameter(birthday); // yyyy-MM-dd if (birthday ! null !birthday.isEmpty()) { s.setBirthday(java.sql.Date.valueOf(birthday)); // 格式不对会抛 IllegalArgumentException } s.setClassName(req.getParameter(class_name).trim()); s.setPhone(req.getParameter(phone).trim()); return s; }BeanUtils.populate那种写法确实少几行但String到java.sql.Date的类型转换要自己注册Converter表单字段名还必须和实体属性名一模一样改个名字就静默丢值。课设量级下手写反而更好调试出问题能直接看到是哪一行。校验也别全靠前端required后端至少检查学号非空、姓名非空、学号格式前端校验只是体验后端校验才是数据。4.3 JSP 渲染与 JSTL把 Java 从页面里赶出去视图层的目标很简单JSP 里不出现% %。用 JSTL 加 EL 就能做到页面长这样% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html headmeta charsetUTF-8title学生列表/title/head body form action${pageContext.request.contextPath}/student methodget input typehidden nameaction valuelist input namekw value${kw} placeholder学号或姓名 input namecls value${cls} placeholder班级 button typesubmit查询/button /form table border1 cellpadding6 trth学号/thth姓名/thth性别/thth生日/th th班级/thth电话/thth操作/th/tr c:forEach vars items${page.rows} tr tdc:out value${s.sno}//td tdc:out value${s.name}//td td c:choose c:when test${s.gender 1}男/c:when c:when test${s.gender 2}女/c:when c:otherwise未知/c:otherwise /c:choose /td tdfmt:formatDate value${s.birthday} patternyyyy-MM-dd//td tdc:out value${s.className}//td tdc:out value${s.phone}//td td a href${pageContext.request.contextPath}/student?actioneditid${s.id}编辑/a a href${pageContext.request.contextPath}/student?actiondeleteid${s.id} onclickreturn confirm(确认删除)删除/a /td /tr /c:forEach /table p共 ${page.total} 条第 ${page.pageNo}/${page.pages} 页/p /body /html这里有三个实践要点。第一输出用户数据一律用c:out它会转义 HTML 标签姓名里填个script也不会执行这是最省事的一层 XSS 防护。第二链接用${pageContext.request.contextPath}拼前缀不要写死/student-manager/...否则改个 war 包名全站链接失效这种问题在换环境部署时必现。第三删除链接加confirm只是体验优化真正的防误删还是靠 PRG 加后端二次校验前端弹窗挡不住手快的。分页条也放在同一个 JSP 里用c:url拼参数可以自动做 URL 编码c:url varprevUrl value/student c:param nameaction valuelist/ c:param namepage value${page.pageNo - 1}/ c:param namekw value${kw}/ c:param namecls value${cls}/ /c:url c:if test${page.pageNo 1} a href${prevUrl}上一页/a /c:ifc:url加c:param会自动处理中文参数和转义手拼字符串很容易在kw张page2上翻车。5. 避坑与排查五个让人返工的翻车现场下面这五条基本都是我在拆这类项目时真实踩过或者帮别人定位过的现象看着各不相同根因集中在“环境版本”“字符集”“资源释放”三件事上。按现象 → 原因 → 解决写遇到类似的直接对号。5.1 容器、依赖与连接层的三处坑坑一换了 Tomcat 10所有 Servlet 都 404。现象工程编译无报错war 部署成功控制台没有异常访问任何路径都是 404web.xml里的配置看着也没问题。原因Tomcat 10 起把javax.servlet.*整体迁到了jakarta.servlet.*命名空间换了。用javax编译出来的 class容器里根本没有对应的父类WebServlet注解也不会被扫描到所以你注册的路由一条都不存在表现就是全站 404。解决二选一别混。要么把 Tomcat 降到 9.0.x 继续用javax要么把pom.xml里的 servlet-api 换成jakarta.servlet:jakarta.servlet-api:5.0.0代码里所有import javax.servlet.*改成import jakarta.servlet.*JSTL 换成 jakarta 坐标标签 uri 改成jakarta.tags.core。改完记得mvn cleantarget里的旧 class 不清会骗人。坑二MySQL 8 连不上报 Public Key Retrieval is not allowed。现象Navicat 能连Java 代码连不上或者第一次能连重启服务后报The server time zone value 中国标准时间 is unrecognized。原因MySQL 8 默认认证插件是caching_sha2_password首次连接需要客户端主动索取服务端公钥而 JDBC 驱动默认关掉了这个行为时区那条是因为驱动拿到的服务端时区名它不认识。解决URL 上补allowPublicKeyRetrievaltrueuseSSLfalseserverTimezoneAsia/Shanghai。生产环境不要关 SSL本地调试为了方便可以关。补完还是连不上就去看驱动版本5.1.x 的驱动配 MySQL 8 会有各种奇怪问题统一升到 8.0.x。坑三点几次之后页面卡死日志里什么都没有。现象刚启动正常连续操作十来次以后所有请求都挂住maxWait到了抛GetConnectionTimeoutException。原因DAO 里拿到了Connection但没关连接池的 20 条连接被借空。池化环境下close()是还池但前提是你得调。也有人把Connection存成了 Servlet 的成员变量多线程下不仅泄漏还会串事务。解决所有 JDBC 资源一律 try-with-resourcesConnection只在方法内局部变量里存在顺手把removeAbandonedtrue、removeAbandonedTimeout60打开让池子自己回收跑飞的连接这是一层后悔药。排查时打开 Druid 的监控页StatViewServlet看ActiveCount是不是只涨不跌一眼定位。5.2 页面与编码层的两处坑坑四JSP 页面把c:forEach原样输出到浏览器。现象页面能看到表格表头但数据区显示的是一串标签文本或者直接 500 报According to TLD or attribute directive in tag file, attribute items does not accept any expressions。原因JSTL 的 jar 没打进 war或者taglib的 uri 跟容器版本不匹配。WEB-INF/lib里没有jstl-1.2.jar容器就把c:forEach当普通 HTML 标签输出uri 写错则直接报 TLD 解析失败。另一个隐蔽原因是scope写成provided本地 IDE 跑得动依赖在 classpath 里打成 war 就丢了。解决确认jstl依赖没有providedmvn dependency:tree看一眼有没有被别的依赖顶掉版本解压 war 检查WEB-INF/lib/jstl-1.2.jar是否存在。Tomcat 10 环境下 uri 必须换成 jakarta 那套用旧 uri 一样报错。坑五中文一会儿正常一会儿乱码尤其是搜索框输入中文时。现象新增能存进去中文列表页也显示正常但在搜索框输入“张”点查询结果为空或者变成å¼。原因POST 请求体由request.setCharacterEncoding决定Filter 生效就没问题GET 请求的参数在 URL 上由容器的URIEncoding决定。很多人只在doPost里设了编码doGet没设或者 Filter 的url-pattern写成了/student漏掉了其他路径。还有一种情况是response.setContentType忘了带charsetUTF-8浏览器按 ISO-8859-1 猜页面整体花掉。解决Filter 映射写/*request和response的字符集都在里面设确认容器URIEncoding是 UTF-8JSP 页头同时写contentType和pageEncoding两个都要数据库和表的字符集确认是utf8mb4连接串带上characterEncodingutf8。乱码问题要一层层排除从浏览器 → 容器 → 应用 → 驱动 → 表别跳步猜。6. 上线前的最后一段会话拦截、密码存储与部署自检清单代码能跑通只是及格线能不能拿去演示、能不能讲清楚取决于最后这几步收尾做得扎不扎实。登录这块LoginServlet校验通过后只做一件事req.getSession().setAttribute(loginUser, user)。拦截交给LoginFilter在doFilter里判断session.getAttribute(loginUser) null就重定向到登录页同时放行/login、/css/*、/js/*这几类路径否则登录页自己被拦死循环。密码别存明文也别只做一次 MD5——彩虹表早就把常见密码打穿了。存salt加sha256(salt 明文)盐每次注册随机生成 16 位十六进制public static String hash(String raw, String salt) { try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] bs md.digest((salt raw).getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bs) sb.append(String.format(%02x, b)); return sb.toString(); // 64 位十六进制对应 password CHAR(64) } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } }部署前我会照着下面这张清单走一遍少一项都可能在演示现场翻车检查项判断标准常见失败表现容器与包名Tomcat 9 javax或 Tomcat 10 jakarta不混用全站 404依赖 scopeservlet-api/jsp-api是providedJSTL 不是本地正常部署 500数据库连接URL 带时区和allowPublicKeyRetrieval启动即报连接异常字符集库、表、连接串、JSP、Filter 五处都是 UTF-8中文变问号资源释放无Connection泄漏ActiveCount稳定点十几次后卡死路由入口index.jsp能跳列表未登录跳登录页打开是一片空白从那以后我每次交付这类项目都会先在干净的 Tomcat 里解压 war、删掉work目录缓存、用新库跑一遍完整流程再打开监控页看一眼连接数曲线。这几分钟能挡掉八成“在我机器上是好的”的尴尬。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑