ARTICLE DETAIL

资讯详情

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

基于JSP+JavaEE的学生信息管理系统实战与避坑指南

基于JSP+JavaEE的学生信息管理系统实战与避坑指南 我第一次用 JSP 做学生信息管理系统还是大学那会儿的课程设计。当时只觉得这东西又老又麻烦页面里嵌 Java 代码运行还得配一个 Tomcat跟现在前端框架那一套完全不在一个世界。结果工作之后发现很多学校、中小型企业的内部系统还在用这套技术栈有些老项目维护起来更是绕不开 JSP。后来带新人、帮人改课设我才意识到JSP JavaEE 这套经典组合虽然年代久远但它的分层思想、请求响应模型、会话管理机制恰恰是理解后面 Spring MVC、Shiro、前后端分离这些框架的基石。这篇内容围绕的就是一个典型的 JSP JavaEE 学生信息管理系统从项目搭建、环境配置、架构分层到每个功能模块的实现细节再到乱码、数据库时区、WAR 包部署这些必踩的坑一次性讲清楚。如果你正在做类似的课程设计或者刚接触 JavaEE 想找一个能练手的完整项目又或者要接手一个 JSP 老项目但心里没底这篇文章应该能帮你省下不少折腾的时间。1. 都什么年代了为什么还要折腾 JSP 学生管理系统聊这个项目之前得先解决一个疑问JSP 到底过时了没有我的答案是技术栈过时了但思想没有过时。放在今天纯 JSP 做前后端交互确实不够高效Vue、React 这类框架在复杂交互上完胜。但 JSP 这套东西有一个其他技术栈替代不了的价值——它足够简单、足够直白把一个 Web 请求从浏览器到服务器再到数据库再返回页面的完整链路赤裸裸地摆在你面前。你不需要理解虚拟 DOM不需要搞明白代理转发一个 form 表单提交Servlet 里 getParameter 接收JDBC 查数据库request.setAttribute 回传JSP 页面再 el 表达式取值渲染。每一步都看得见摸得着。也正因为如此JSP Servlet JavaBean 这种模式至今仍然是很多高校 JavaWeb 课程的标准教学内容。学生管理信息管理系统这个题目更是经典中的经典它几乎覆盖了 JavaEE 入门必须掌握的所有知识点JSP 的九大内置对象、四大作用域、Servlet 生命周期、JDBC 操作数据库、Session 会话管理、请求转发与重定向的区别、过滤器 Filter 的使用。把这些东西吃透了后面学 SSM、SpringBoot 会顺畅很多因为框架只是帮你把底层重复劳动封装了核心的请求处理逻辑并没有变。从实际应用场景看学生信息管理系统的受众也不止是学生。很多培训机构、驾校、小型私立学校至今还在用这类系统管理学员档案哪怕界面朴素、交互简单但胜在稳定、好维护、改起来成本低。有些传统 JSP 项目甚至还在正常运行部署在 Tomcat 上打个 WAR 包就能跑连 Docker 都不需要。所以学这套东西短期看是为了交作业、过考试长期看是在给自己积累一套接手老系统不慌的底气。我个人的建议是如果你只是想快速交一个课设别过度设计老老实实把登录、增删改查、分页这几个功能做扎实就够了。如果你是想借这个项目练手、理解 JavaWeb 底层原理那就在基础功能之上自己把连接池、过滤器、监听器这些东西也加上去。这篇文章后半部分会讲两者的通用部分也会提到如何从基础版往工程化方向走。2. 项目搭建从 IDEA 新建 JavaEE 项目到跑通第一个页面很多人第一步就卡在环境上。JSP 项目跑不起来八成不是代码问题而是 IDEA 创建的项目类型不对或者 Tomcat 没有正确关联。这块我单独拿出来讲因为后面所有功能都要建立在能跑起来这个前提之上。2.1 环境选型JDK 8 Tomcat 9 MySQL 5.7 的稳妥组合一开始选型别追求新。JDK 用 8 就好绝大多数老项目、教材、兼容性测试都是基于 JDK 8 的JSP 标准也是在这个版本上最稳妥。Tomcat 9 对应 Servlet 4.0支持 JSP 2.3够用了。如果你想用更高版本的 Tomcat 10注意包名会有变化——原来javax.servlet会变成jakarta.servlet很多老代码直接复制过来是编译不过的。数据库方面MySQL 5.7 和 8.0 都能用但如果你是第一次配置建议直接用 5.7。原因有两个一是 5.7 的字符集、认证插件相关坑少二是市面上大多数教程、课设代码默认都是 5.7 环境遇到问题更容易搜到解决方案。数据库连接驱动用 5.1.49 版本的mysql-connector-java跟 MySQL 5.7 是经典搭配跟 8.0 也能兼容。远程连接 MySQL 时记得检查两个地方用户的 host 权限localhost只允许本机访问要改成%MySQL 8.0 默认的 caching_sha2_password 认证方式在旧驱动下可能报错5.7 不存在这个问题2.2 IDEA 里创建项目的正确姿势IDEA 新建 JSP 项目有两个入口容易搞混。一种是直接New Project里选 Java然后手动添加 Web Application 支持。这种适合你已经建好了普通 Java 项目需要右键项目名,选择Add Framework Support勾选Web Application注意勾选 Create web.xml。另一种更直接的方式是在New Project的时候选择Java Enterprise或者Jakarta EE模板勾选Web Application然后指定 Application Server 为你的 Tomcat 路径。IDEA 会自动帮你生成web/WEB-INF/web.xml和index.jsp项目结构也会带上src/main/java、src/main/resources这些目录。创建完之后第一件事是确认输出目录和依赖。老版本 IDEA 生成的 Web 项目编译输出路径可能不在WEB-INF/classes下运行时会报 ClassNotFoundException。解决办法是用File Project Structure Artifacts看一下 Output Layout确认WEB-INF/classes下面有 Copy 的编译输出。如果用的是 Maven 管理的 JavaEE 项目直接依赖maven-war-plugin打包即可这块后面讲 WAR 部署时会提到。Tomcat 关联也容易踩坑。在Run Edit Configurations里点左上角加号找到 Tomcat Server Local在 Application server 里选择你的 Tomcat 安装目录。然后切到 Deployment 标签页点加号把 Artifact 加进去Application context 通常填根路径/或者项目名比如/student_manager。这一步漏了的话启动 Tomcat 会报 No artifacts marked for deployment。2.3 第一个能跑通的页面长什么样环境配好之后我习惯先写一个极简的 JSP 页面验证整个链路通不通而不是一上来就写一堆业务代码。比如在web/index.jsp里写% page contentTypetext/html;charsetUTF-8 languagejava % html head title学生管理系统-首页/title /head body p当前时间${requestTime}/p /body /html然后在src/main/java下建一个HelloServletWebServlet(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setAttribute(requestTime, new java.util.Date().toString()); req.getRequestDispatcher(/index.jsp).forward(req, resp); } }启动 Tomcat浏览器访问http://localhost:8080/hello页面上能显示当前时间说明从 Servlet 到 JSP 的转发链是通的。这一步验证完再去写学生管理的业务代码心里才有底。这里有一个新手最容易混乱的点访问index.jsp和访问/hello有什么区别直接访问 JSP 文件Tomcat 会把它当成一个动态页面解析执行JSP 里的 HTML 原样输出但不会执行里面的 Servlet 逻辑。访问/hello则会先走 Servlet 的逻辑setAttribute 设值再 forward 到 JSP 渲染。所以进入系统的入口应该先走一个 Servlet 做初始化再去 forward 页面。很多 JSP 项目习惯用/index.jsp直接作为入口然后在 JSP 里调用业务逻辑这也是能跑的但不够规范后面维护起来页面和业务逻辑纠缠在一起会很痛苦。3. 核心架构拆解一个学生信息管理系统的完整分层跑通第一个页面只是开胃菜真正的重头戏是理清系统的分层结构。学生信息管理系统虽然简单但五脏俱全用到的分层方式和真实企业项目是一致的。我见过很多课设代码把所有东西堆在一个 JSP 页面里数据库连接、业务逻辑、HTML 全混在一起能跑但一坨。这里我会给出一个比较标准的分层方式也解释每一层为什么要那么做。3.1 实体类、DAO、Servlet、JSP 分别该干什么一个典型的 JavaEE 项目大致分成四层各管各的事层级职责示例实体类对应数据库表结构承载一条条记录的数据Student类字段对应student表DAO 层直接操作数据库封装 JDBC 代码StudentDao提供findAll()、add()、delete()Servlet 层接收请求、调用 DAO、控制页面跳转StudentServlet处理doPost、doGetJSP 层把数据渲染到页面上负责展示list.jsp、add.jsp、edit.jsp实体类不需要花里胡哨属性跟表字段一一对应即可public class Student { private Integer id; private String studentNo; // 学号 private String name; private String gender; private Integer age; private String className; // getter/setter 省略 }DAO 层是这个系统的核心。传统写法就是自己管理 Connection写 PreparedStatement然后遍历 ResultSet 封装成实体类。别嫌代码繁琐这一步是为了让你理解后面所有 ORM 框架到底帮你干了什么。public ListStudent findAll() { ListStudent list new ArrayList(); String sql SELECT * FROM t_student ORDER BY id; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student student new Student(); student.setId(rs.getInt(id)); student.setStudentNo(rs.getString(student_no)); student.setName(rs.getString(name)); student.setGender(rs.getString(gender)); student.setAge(rs.getInt(age)); student.setClassName(rs.getString(class_name)); list.add(student); } } catch (Exception e) { e.printStackTrace(); } return list; }注意上面这个写法用了 try-with-resourcesJava 7 之后的特性流和连接都能自动关闭。如果你用老写法记得在 finally 里手动关 Connection、Statement、ResultSet否则连接不释放多跑几次数据库连接就耗尽了。Servlet 层做的事情比较机械接收参数、调 DAO、存数据到 request 或 session、跳转。跳转方式有两种只有一字之差但行为天差地别request.getRequestDispatcher(/list.jsp).forward(...)是请求转发URL 地址栏不变request 里的数据能带到 JSP 页面resp.sendRedirect(list)是重定向浏览器重新发起一次新请求URL 变了request 里的数据也丢失了查询列表一般用 forward因为需要携带数据到页面新增、修改、删除之后的跳转则用 redirect否则浏览器一刷新就会重复提交上一次的表单操作。JSP 层的原则是尽量不写 Java 代码。用 EL 表达式${student.name}取值用 JSTL 标签c:forEach循环遍历。JSP 里直接嵌% %脚本片段不是说不行而是很容易让页面变得没法维护。早期 JSP 项目很多都是页面里if (...)、for (...)到处飞改样式时一不小心就把逻辑改坏了。后来 Sun 公司自己都看不下去才推出了 EL 和 JSTL就是为了让你彻底摆脱脚本片段。3.2 数据库设计与 JDBC 连接配置学生信息管理系统的数据库设计不需要太复杂单表就够了。我通常建一张t_student表外加一张t_user表做登录CREATE TABLE t_student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender VARCHAR(10), age INT, class_name VARCHAR(50) ) DEFAULT CHARSETutf8mb4; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) DEFAULT CHARSETutf8mb4;JDBC 连接信息一般放到一个工具类DBUtil里集中管理public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD your_password; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }写死用户名密码只适合课设和本地开发部署到服务器上至少要改成读取配置文件后面优化部分我会再说。数据库驱动的 jar 包要记得放到WEB-INF/lib目录下或者通过 Maven 依赖引入这一步漏了会报ClassNotFoundException: com.mysql.jdbc.Driver。3.3 登录认证与 Session 会话控制学生管理系统虽然是一个内部小系统但谁在用、有没有登录这个问题必须处理。最简单的做法是登录成功后把用户信息塞进 SessionUser user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(student/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); }之后每个需要登录才能访问的页面在 Servlet 里做一次拦截判断HttpSession session req.getSession(false); User loginUser (session null) ? null : (User) session.getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; }这个手动判断是一种做法。更规范的做法是用 Filter 做一个统一拦截写一个LoginFilter在web.xml里配置拦截规则比如/student/*都需要登录才能访问/login.jsp、/loginServlet放行。Filter 可以让你不在每个 Servlet 里重复写登录校验老项目里如果看到每个操作都要先判断 session 的基本是没写过滤器属于可以优化的地方。Session 还有一个隐藏坑默认空闲超时时间是 30 分钟可配置。用户登录后长时间不动再操作时就发现Session 失效了这时候后端会判定未登录跳回登录页。这是正常现象不用慌。4. 每个功能模块的实现细节与页面数据流转分层理清楚之后功能模块就是按部就班地往框架里填。学生信息管理系统最核心的是五个功能列表查询、新增、修改、删除、登录。其中登录在上一节已经讲了这一节重点说增删改查四个操作以及页面数据流转的具体实现。4.1 列表展示从数据库到页面的完整渲染列表查询的逻辑上节已经写过Servlet 里拿到ListStudent之后req.setAttribute(studentList, studentDao.findAll()); req.getRequestDispatcher(/list.jsp).forward(req, resp);然后list.jsp里用 JSTL 遍历输出% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table thead tr th学号/thth姓名/thth性别/thth年龄/thth班级/thth操作/th /tr /thead tbody c:forEach items${studentList} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.className}/td td a hrefedit?id${stu.id}编辑/a a hrefdelete?id${stu.id} onclickreturn confirm(确定删除吗?);删除/a /td /tr /c:forEach /tbody /table这里有几个细节值得新手注意。第一如果列表是空的${studentList}在页面上显示为空不会报错但用户看到的就是一张空表体验不好可以加一个c:if test${empty studentList}提示暂时没有学生数据。第二删除操作用链接a hrefdelete?id${stu.id}实现虽然简单但有一个问题搜索引擎的爬虫或者浏览器预取可能会自动请求这个链接导致数据被意外删除。生产环境一般会改成 form 表单 POST 提交或者至少加一个确认弹窗。课设阶段一个onclickreturn confirm(...)也就够了。第三列表数据量大的时候要考虑分页。分页最简单的实现是接收两个参数pageNum当前页和pageSize每页条数SQL 里用LIMIT offset, pageSize实现。JSP 页面上渲染上一页、下一页的数据需要知道总页数。总页数的计算是(totalCount pageSize - 1) / pageSize这个公式用整数除法也可以直接算出来注意别把边界情况丢掉。分页是 JavaWeb 面试特别喜欢问的点能自己手写一遍是最好的。4.2 新增学生表单提交、数据校验与重定向新增学生的流程很直接add.jsp页面展示表单用户填写提交到add这个 ServletServlet 解析参数调 DAO 的insert成功之后重定向回列表页。表单页面有一个地方很容易漏表单的methodpost一定要显式写上。如果你省略了浏览器默认走 GET中文参数会拼在 URL 上一是 URL 长度有限制二是参数直接暴露了三是乱码问题更难控制。POST 之后Servlet 接收req.setCharacterEncoding(UTF-8); String studentNo req.getParameter(studentNo); String name req.getParameter(name); String gender req.getParameter(gender); int age Integer.parseInt(req.getParameter(age)); String className req.getParameter(className);这行req.setCharacterEncoding(UTF-8)必须放在取任何参数之前。之前遇到过一个特别隐蔽的问题参数取完之后再调setCharacterEncoding结果完全无效因为 request 的编码在第一次读取参数时就已经确定了。服务端校验也是容易省略的一个环节。表单页面可以加required属性做前端校验但前端校验很容易被绕过服务端一定要再校验一遍。比如年龄字段用户通过开发者工具把ageabc传给服务器Integer.parseInt就会直接抛NumberFormatException。建议的做法是用 try-catch 接住异常再返回错误提示int age 0; try { age Integer.parseInt(req.getParameter(age)); } catch (NumberFormatException e) { req.setAttribute(error, 年龄必须是一个整数); req.getRequestDispatcher(/add.jsp).forward(req, resp); return; }新增成功后的跳转一定要用sendRedirect如果用了forward用户刷新页面时表单会重新提交数据库里就会多出一条一模一样的记录。这个说大不大说小不小但绝对是 JavaWeb 新手最爱踩的坑之一。4.3 修改与删除隐藏域传参和 URL 传参的差异编辑和删除的页面跳转逻辑很类似都要先拿到当前记录的 id再根据 id 查出完整数据回显到表单。编辑页面的设计有一个常见的选择要不要把表单的提交地址区分开。我推荐的做法是一个edit.jsp同时承担新增和修改两个功能。新增时表单里没有 id 字段修改时通过隐藏域在表单里携带 idform actionsave methodpost input typehidden nameid value${student.id} input typetext namename value${student.name} ... /formServlet 里判断 id 是否为空决定是 update 还是 insertString id req.getParameter(id); if (id null || id.isEmpty()) { studentDao.insert(student); } else { student.setId(Integer.parseInt(id)); studentDao.update(student); }这里隐藏域传参和 URL 传参的差别就出来了。URL 传参比如之前的删除链接delete?id1有缓存风险但好处是参数能直接被 JavaScript 读取、能被用户收藏分享。隐藏域传参则更适合表单场景用户不可见、POST 提交安全、不会出现在地址栏和浏览器历史记录里。编辑修改适合用隐藏域删除操作适合用带确认的 POST 表单如果你要改造现成的项目这两个方向的调整性价比是最高的。4.4 页面加载完成后的刷新控制热词里有个jsp页面让加载完后刷新一次这个场景在 JavaEE 项目里其实很常见。比如用户新增学生后跳转到列表页列表页已经通过重定向重新请求了但某些老版本浏览器或者特定场景下可能缓存了上一次的页面内容。想要在页面加载完成后强制刷新一次有几种办法。最简单的一种是在 JSP 的head里加下面这段script window.addEventListener(pageshow, function (event) { if (event.persisted || window.performance.getEntriesByType(navigation)[0].type back_forward) { window.location.reload(); } }); /scriptpageshow事件在页面每次显示时都会触发event.persisted为 true 说明页面是从bfcache浏览器往返缓存里直接恢复的这时调用reload()强制刷新保证数据是全新的。另一种场景是在 Session 或 Cookie 里打标记判断是否刚完成过一次写操作是的话就在页面里自动触发一次刷新刷新完把标记清掉避免死循环。这里需要提醒一下能不做自动刷新就别做。刷新是非常干扰用户操作的重定向本来已经能解决很多缓存问题。如果非要刷新优先排查是不是 Servlet 里用了 forward 而不是 redirect从根上解决而不是靠页面端reload兜底。5. 踩坑合集乱码、时区、WAR 包、重复提交这一章节是真正的干货区。JSP 项目原理简单但细节极其琐碎很多问题查了半天资料才发现是一个小配置没做对。我把这几年遇到最多的四个坑集中起来讲每个都附完整的排查思路。5.1 乱码问题的三处设置从请求到响应的完整链路JavaWeb 的中文乱码重灾区可以拆成三个环节数据库存储、请求参数解码、响应页面展示。数据库层面建表时如果没指定DEFAULT CHARSET或者指定成了latin1中文存进去就是问号或者乱码。解决方式是建表时统一用utf8mb4如果你已经建好表了可以用ALTER TABLE t_student CONVERT TO CHARACTER SET utf8mb4;转换。请求参数层面分 GET 和 POST 两种。POST 请求在入口处加req.setCharacterEncoding(UTF-8)即可。GET 请求的编码跟 Tomcat 默认解码方式有关老版本 Tomcat 8.0 之前默认是ISO-8859-1中文参数会乱码需要改conf/server.xml里Connector的 URIEncoding 为 UTF-8或者useBodyEncodingForURItrue。Tomcat 8.0 之后默认 URI 编码已经改成了 UTF-8新版本一般不需要动。响应页面层面JSP 页面本身通过pageEncoding告诉容器生成 Servlet 时用什么编码再通过contentType告诉浏览器怎么解码% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %要同时满足三个层面都统一最稳妥的做法是所有涉及字符集的地方一律 UTF-8JSP 文件本身存成 UTF-8 格式数据库连接串带characterEncodingutf8参数Tomcat 连接器也保持 UTF-8。不要试图混用 GBK 和 UTF-8哪怕当前某一个环节能用换个部署环境大概率出问题。排查乱码的技巧是从数据库往页面反向排查。先把数据库里存进去的值通过 SQL 客户端查出来看看原始数据是否正确。如果数据库里已经是乱码那就是连接串或者写入环节的问题如果数据库里是正常的再用浏览器开发者工具看接口返回的响应头里的Content-Type有没有charsetUTF-8。这样一步步缩小范围比盲目改配置高效得多。5.2 数据库连接串的时区参数这个坑在 MySQL 8.0 版本上非常典型。项目在本地能跑部署到服务器之后所有跟时间相关的查询报错错误信息类似于The server time zone value ???í is unrecognized。原因是 MySQL 8.0 的默认时区设置改成了系统时区而中国服务器上系统时区往往是 CSTChina Standard Time在部分 MySQL 客户端驱动里解析异常。解决方案有两种。第一种是改 MySQL 全局时区SET GLOBAL time_zone 08:00;第二种是改连接串在 JDBC URL 后面加时区参数jdbc:mysql://localhost:3306/student_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai我推荐第二种因为改连接串只影响当前应用不会动数据库全局配置在共享数据库环境里更安全。如果你的应用使用了连接池改完连接串之后记得重启应用部分连接池会缓存旧的连接配置不重启可能不会生效。5.3 传统 JSP 项目如何正确打包成 WAR 并部署传统 JSP 项目脱离 IDEA 部署一般要打成 WAR 包。这里有一个概念区分WAR 是 Web Application Archive专门为 Web 应用设计的一种打包格式内部结构有规范要求必须包含WEB-INF/web.xml除非是 Servlet 3.0 以上的注解配置、WEB-INF/classes编译后的 class 文件、WEB-INF/lib依赖 jar 包。如果你是 Maven 项目打包就是一条命令mvn clean package执行完会在target目录下生成xxx.war。如果没有用 Maven传统做法是在 IDEA 里Build Build Artifacts Build它会按照你在 Project Structure 里配置的 Artifact 打包方式生成 WAR。拿到 WAR 包之后把它复制到 Tomcat 的webapps目录下启动 Tomcat容器会自动解压部署。访问路径默认是http://localhost:8080/项目名/也就是 WAR 包的文件名去掉.war后缀就是访问路径。如果想直接用根路径访问可以把 WAR 包重命名为ROOT.warTomcat 会把ROOT.war部署为根应用访问http://localhost:8080/直接进入系统。部署中最容易犯的一个错误是本地 ID 环境能正常启动但部署到服务器的 Tomcat 报ClassNotFoundException。原因往往是乱码类的问题——Maven 打包时没有把某些资源文件拷贝到target/classes。检查pom.xml里的build配置确认resources包含了src/main/resources目录同时把 JDBC 驱动的依赖确认一遍。另一个原因是 Tomcat 版本不一致比如本地用 9 开发服务器上装的是 Tomcat 7Servlet 版本对不上直接报错。部署前先检查服务器上的 Tomcat 版本这是很多老手也会犯的糊涂。5.4 刷新按钮导致的重复提交问题重复提交是 JavaWeb 项目里非常高发的问题。场景很典型用户填了一个新增学生表单点击提交成功了。用户下意识按下 F5 刷新页面浏览器弹窗是否重新提交表单点确认数据库里就多了一条一模一样的记录。这个问题在面试中经常被问到标准的解法有几种从简单到复杂排第一种是提交后重定向也就是 PRG 模式Post/Redirect/Get。表单提交后Servlet 处理完业务不直接 forward 回去而是返回一个 302 重定向让浏览器 GET 请求列表页。这样刷新的是 GET 请求的列表页不会触发重复提交。这是 JavaWeb 里最推荐的做法也是我在前面反复提到新增、修改、删除之后用 redirect不要用 forward的根本原因。第二种是前端按钮置灰。提交按钮在点击后立刻禁用同时显示正在提交避免用户连续点击。这个方案逻辑简单但只能防手抖防不了 F5 刷新。第三种是Token 令牌机制。在表单页面生成一个随机 token 存到 Session 里表单提交时带上隐藏的 token。Servlet 处理前先比较 Session 里的 token 和请求里的 token 是否一致一致才处理处理完立刻删除 Session 里的 token。这样第二次提交时Session 里的 token 已经没了校验不通过重复提交被拦截。这个方案可以有效防掉所有重复提交但要理解一次性 token的思想才能在代码里写对。课程设计阶段能做好第一种就够了。后续工作中如果遇到资金、订单这类敏感数据的重复提交问题再上 Token 机制。6. 从课程作业到工程化这套系统的优化方向如果你不只是为了交作业而是想把这个学生管理系统当成一个练手项目那做完基础功能后还有很大的优化空间。优化的方向非常多我挑几个性价比高、又能真正提升代码能力的思路展开。6.1 替换手写 JDBC从 DBUtils 到连接池手写 JDBC 在学习阶段非常必要但到了真实项目里里面的样板代码太浪费了。最简单的替换是用 Apache Commons DbUtils它提供QueryRunner一行代码就能执行 SQL 并自动映射结果集QueryRunner runner new QueryRunner(DataSourceUtils.getDataSource()); ListStudent list runner.query(SELECT * FROM t_student, new BeanListHandler(Student.class)); String sql INSERT INTO t_student(student_no, name, gender, age, class_name) VALUES (?,?,?,?,?); runner.update(sql, studentNo, name, gender, age, className);再进一步使用连接池比如 Druid 或者 HikariCP通过读取配置文件初始化连接池Properties props new Properties(); props.load(new FileInputStream(druid.properties)); DataSource ds DruidDataSourceFactory.createDataSource(props);这样做的好处一是复用数据库连接性能大幅提升二是连接池自带连接有效性检测和回收机制比手动DriverManager.getConnection稳定得多。把这两步做完你的项目代码已经和一个普通企业级老项目差距不大了。6.2 用 Filter 和 Listener 完善系统骨架除了登录校验的 Filter还可以用 Filter 处理全站编码。写一个EncodingFilter在doFilter里统一调用request.setCharacterEncoding(UTF-8)就不用每个 Servlet 里都重复写了。Listener 方面可以在应用启动时用ServletContextListener读取全局配置或者用HttpSessionListener做在线用户统计。这些代码量都不算大但能显著提升项目在面试官眼中的完整度。Filter 还有一个常规用途是统一权限校验。除了登录拦截还可以限制某些角色才能访问管理功能。学生信息管理系统里通常是管理员和普通用户两类可以在用户实体上增加角色字段Filter 里判断角色属性再放行或拒绝。6.3 什么时候该考虑前后端分离JSP 项目的天花板比较低。如果页面交互越来越复杂比如要做实时搜索、动态表格、图表展示你会发现 JSP 加 Ajax 混着写特别别扭——El 表达式渲染的是服务端拿到的数据而 Ajax 回来的是 JSON两种渲染方式并存会让代码非常混乱。到这个时候就可以考虑前后端分离了。后端把原来的 Servlet 逻辑改造成返回 JSON 的接口前端用 Vue 或 React 单独开发公共接口走 RESTful 风格。但这里要提醒一句不要为了潮流而盲目拆。如果团队只有你一个人又是课设或者内部小系统前后端分离带来的构建工具、跨域、部署链路复杂度很可能比 JSP 直出的方案多出三倍工作量。我见过很多半途而废的项目都是因为前期高估了自己的维护能力。我的建议是以你当前的目标为准。目标是学 JavaEE 基础老实用 JSP Servlet目标是学 Spring 全家桶和前端工程化那直接用 Spring Boot Vue 做前后端分离别再套 JSP。最怕的就是两边都想要最后两边都没做好。目前 6 章内容已经基本讲完一个 JSP 学生信息管理系统的完整链路。最后再多说一句所有框架、模式、最佳实践本质上都是在解决某类特定问题而 JSP 这套老技术正好把最底层的问题暴露得一清二楚。如果你能老老实实把登录、增删改查、分页、乱码这些基础功能吃透比会背十几个框架的名字有用得多。我在实际项目里修改过一个十年前的老系统光是把里面的类名、接口名理清楚就已经能解释不少现代框架设计的初衷了。希望这篇文章能帮你少踩一些我当年踩过的坑。
返回列表