
简介这是一份基于 JSPServletJavaBean 架构的超市进销存管理系统项目面向 JavaWeb 课程设计、毕业设计及入门提升学习者适合需要完整可运行代码与配套配置说明的用户。系统围绕商品管理、分类维护、入库/出库记录、管理员与用户信息、数据导出等核心模块展开MVC 分层清晰Servlet 负责请求控制JavaBean 封装业务数据配合 MySQL 存储业务数据。资源整体为 zip 压缩包共 134 个文件大小约 6.94MB包含 9 个 JSP 页面、14 个 Java 源码、14 个 class 编译产物及 6 个 jar 依赖包前端由 CSS、JavaScript 和图片素材构成并附带工程配置文件按文档配置好环境后即可本地编译运行。项目难度适中代码结构较规整适合参考其登录控制、分页查询、数据导入导出等实现思路也方便在此基础上进行功能扩展或二次开发。当前已有 67 人学习下载有疑问也可向作者咨询。1. 这套 JSPServletJavaBean 进销存系统到底帮毕设解决了什么一个很现实的现象每年到三四月JavaWeb 方向的毕设需求里进销存管理系统的出现频率高得惊人。超市要管商品、供应商、进货单、销售单、库存余量这套业务边界清晰表结构不复杂正好卡在“课程设计往上够不着、纯管理系统又太简单”的中间地带。而 JSP Servlet JavaBean 的组合恰恰是 JavaWeb 最经典的三层分离写法——浏览器请求打到 ServletServlet 调 JavaBean业务和数据封装结果渲染回 JSP 页面。这篇笔记就是把“基于 JSPServletJavaBean 的超市进销存管理系统”这个压缩包项目拆开讲清楚它实际包含哪些模块代码怎么组织数据库表怎么建部署时哪些地方最容易翻车。适合三类人看正在做这类毕设、需要把系统讲明白好答辩的在校生刚接触 JavaWeb、想用一个小项目把三层架构从理论落成代码的初学者以及接手这类老项目、需要快速梳理结构和改 bug 的维护者。全程按“先立结构、再写代码、最后填坑”的顺序走照着做能跑出一个完整可演示的系统。2. 先把三层骨架切开JavaBean 管数据Servlet 当调度JSP 只做展示2.1 为什么要拆三层这是能答辩、也好改代码的最小结构JSP Servlet JavaBean 的组合本质是 MVC 在教学项目里的简化落地。Model 是 JavaBean负责封装商品、订单、供应商这类业务数据和简单的业务方法View 是 JSP只负责把数据展示出来Controller 是 Servlet接收请求、调业务方法、决定跳转到哪个页面。这个拆分最大的意义不是“优雅”而是出问题的时候你知道去哪一行改代码。举一个最容易碰到的场景商品列表价格显示多了两位小数或者是把“元”显示成了“分”。如果你把数据库查询和 HTML 渲染全混在同一个 JSP 里你得在几百行的% %里找那个字段的取值位置如果拆了层问题只可能出现在三个地方——JavaBean 里 price 字段的类型、Servlet 里查询数据时的封装方式、JSP 里格式化输出的表达式。定位范围一缩修复就快了。这套结构也很适合在答辩时讲老师问“你的代码怎么组织的”你就把三层各指一下再说一句“Servlet 不写 SQLJSP 不写业务逻辑JavaBean 封装数据和操作”这个回答比“我用的 MVC”要扎实得多因为你能指到具体的类和具体的落盘文件上。2.2 包结构设计先定好人名后面写代码不迷路常见做法是按“实体 数据访问 控制 工具”四块建包我一般用下面这套结构src/main/java ├── com.shop.entity // JavaBean对应数据库表 │ ├── Goods.java │ ├── Supplier.java │ └── User.java ├── com.shop.dao // 数据访问层负责 JDBC 操作 │ ├── GoodsDao.java │ ├── SupplierDao.java │ └── UserDao.java ├── com.shop.servlet // Servlet接收请求、调 DAO、跳页面 │ ├── LoginServlet.java │ ├── GoodsListServlet.java │ └── SaleServlet.java └── com.shop.util // 工具类数据库连接、字符编码过滤器 ├── DBUtil.java └── EncodingFilter.java有两点要建议你从一开始就遵守。一是命名宁长勿短GoodsListServlet比GoodsServlet的语义清楚因为后者既可能是列表也可能是新增二是不要出现Test1.java、Demo2.java这种名字这类文件在毕设答辩时一旦被老师点到解释成本极高。如果你用的开发环境是 Maven 项目src/main/webapp下面放 JSP 页面常见的分法是src/main/webapp ├── login.jsp ├── goods │ ├── list.jsp │ └── edit.jsp └── sale └── index.jsp这样按页面归属功能分目录比全部平铺在 webapp 根目录更清晰也方便后面配置 Servlet 的 url-pattern 时跟 JSP 路径对应起来。2.3 核心 JavaBean 怎么写字段类型先想清楚这里以商品Goods类为例子。JavaBean 有一套约定俗成的写法属性私有、提供 getter/setter、必须有一个无参构造方法。这三个特征几乎决定了它在 JSP 里能不能被 EL 表达式直接取到值也决定了 DAO 层能不能方便地封装它。看代码package com.shop.entity; import java.math.BigDecimal; import java.util.Date; public class Goods { private Integer id; // 商品ID主键自增 private String name; // 商品名称 private String category; // 分类饮料、食品、日用品 private BigDecimal price; // 单价用 BigDecimal 而不是 double private Integer stock; // 当前库存 private Integer supplierId; // 关联供应商ID private Date createDate; // 建档日期 public Goods() { } // getter 和 setter 省略需自行补全 // 典型写法public Integer getId() { return id; } // public void setId(Integer id) { this.id id; } }重点说price字段的选择。数据库里价格如果用DECIMAL(10,2)Java 这边对应的最佳类型就是BigDecimal。很多初学者习惯用double看着没毛病但超市商品一旦算总价、算折扣、算库存金额double的浮点误差会在第二个小数点以后露出来做演示时打出19.999999这类数非常尴尬。使用BigDecimal也算一个可以写进文档的技术点精度可控、进制安全。另一个容易忽略的字段是stock。进货和销售操作都会读它、改它、写回它这是一个典型的并发数据课程设计阶段可以不考虑多线程竞争但代码里要给这个字段留一个独立的更新方法后面验证库存闭环时会用到。2.4 Servlet 在哪个位置干活生命周期与请求路径的约定Servlet 是被容器Tomcat管理的 Java 类它的整个生命周期就三个关键方法init()、service()、destroy()。init()在第一次被请求或容器启动时调用一次适合做初始化工作service()每次请求都执行实际开发里一般不直接重写它而是重写doGet()和doPost()让框架替你把请求分发到对应方法destroy()在应用卸载时收尾。写一个登录 Servlet 看整个链条package com.shop.servlet; import com.shop.dao.UserDao; import com.shop.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 统一设置请求编码防止中文用户名乱码 request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /goods/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }这段代码包含了这个项目里最常用的三个 Servlet API 用法WebServlet(/login)用注解配置访问路径省掉 web.xml 里一大段servlet映射配置request.getSession()拿到当前会话response.sendRedirect()和forward()是两个跳转方式前者是浏览器重新发起请求地址栏会变后者是服务端内部转发地址栏不变。登录成功用重定向目的是防止用户按 F5 刷新时重复提交登录表单失败用转发因为需要把error这个错误提示带到 JSP 页面里。3. 从建表 SQL 到商品列表页一条能跑通的最小闭环3.1 四张业务表user、goods、supplier、stock_log 怎么建合理进销存的核心是“货进出有记录”只建一张商品表没法体现进货和销售的变化过程。课程设计规模下四张表足够覆盖核心业务用户表管登录商品表管当前库存供应商表管进货来源库存流水表管每一次数量变动。这样既能展示进货单和销售单也能回答“某个商品的库存是怎么变成现在这个数的”这类答辩追问。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, real_name VARCHAR(50) DEFAULT , PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE supplier ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact VARCHAR(50) DEFAULT , phone VARCHAR(20) DEFAULT , PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(50) DEFAULT , price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, supplier_id INT DEFAULT NULL, create_date DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_supplier (supplier_id), CONSTRAINT fk_goods_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_log ( id INT NOT NULL AUTO_INCREMENT, goods_id INT NOT NULL, change_type VARCHAR(10) NOT NULL COMMENT IN进货OUT销售, quantity INT NOT NULL COMMENT 变动数量正数, before_stock INT NOT NULL, after_stock INT NOT NULL, operator_id INT DEFAULT NULL, create_date DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有两个关键决策。一个是库存流水表记录了before_stock和after_stock不是为了好看而是为了对账——当库存数和流水算出来的结果不一致时能定位是哪一笔操作出了问题。另一个是商品表用外键关联供应商某些课设项目为了省事不建外键只保留一个 supplier_id我建议这个项目里把外键建上因为供应商模块本身就是进销存的组成部分外键能在数据库层面防止录入不存在的供应商 ID。MySQL 上选择 InnoDB 引擎是因为它支持外键和事务后者在扣减库存时非常关键。3.2 DAO 层写法加载驱动、获取连接、查询与封装的固定套路DAO 层的代码模式几乎是固定的获取连接、写 SQL、用 PreparedStatement 占位符、执行、遍历结果集、封装成 JavaBean、关闭资源。这里不为了“优雅”引入 MyBatis 或 JdbcTemplate因为 JSPServlet 的项目阶段手写 JDBC 是理解数据访问过程最直接的方式也方便答辩时解释每一行的作用。数据库连接的工具类建议单独放避免每个 DAO 里重复写加载驱动的代码package com.shop.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里值得注意的参数全部集中在 JDBC URL 上useUnicodetrue和characterEncodingutf8一起出现才能保证中文参数从 Java 这边进 MySQL 不乱码serverTimezone如果你用的 MySQL 8.x 驱动最好在 URL 里追加serverTimezoneAsia/Shanghai否则驱动校验时会报时区错误。看一个完整的商品 DAO 查询方法package com.shop.dao; import com.shop.entity.Goods; import com.shop.util.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; import java.util.List; public class GoodsDao { public ListGoods findAll() { ListGoods list new ArrayList(); String sql SELECT id, name, category, price, stock, supplier_id, create_date FROM goods; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); while (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setName(rs.getString(name)); g.setCategory(rs.getString(category)); g.setPrice(rs.getBigDecimal(price)); g.setStock(rs.getInt(stock)); g.setSupplierId(rs.getInt(supplier_id)); g.setCreateDate(rs.getTimestamp(create_date)); list.add(g); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; } }这里有个细节值得养成习惯SELECT后面显式列出字段名不用SELECT *。两者在这个查询里结果一样但显式列字段能让你清楚地看到 JavaBean 里每个属性对应着表的哪个列一旦表结构改了代码编译期就能暴露问题。rs.getBigDecimal(price)和rs.getTimestamp(create_date)是类型对应的关键如果 JavaBean 里是BigDecimal而这里用rs.getDouble()JSP 页面的金额格式就会乱。3.3 一个能显示商品列表的 Servlet JSP路径怎么对起来有了 DAO 之后把商品列表展示出来只需要一个 Servlet 和一个 JSP 页面。Servlet 负责查数据、把数据塞进 request、再转发给 JSPJSP 负责用 JSTL 和 EL 表达式把集合渲染成表格。注意 JSTL 标签库需要单独引入依赖这个在部署阶段是新手最常见的问题之一后面会专门讲。先写 Servletpackage com.shop.servlet; import com.shop.dao.GoodsDao; import com.shop.entity.Goods; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; WebServlet(/goods/list) public class GoodsListServlet extends HttpServlet { private GoodsDao goodsDao new GoodsDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListGoods goodsList goodsDao.findAll(); String keyword request.getParameter(keyword); // 如果带了 keyword 参数则做模糊过滤这个简单功能很加印象分 if (keyword ! null !keyword.trim().isEmpty()) { // 常见做法在 DAO 里加一个 findByNameLike 方法这里调用后替换 goodsList } request.setAttribute(goodsList, goodsList); request.getRequestDispatcher(/goods/list.jsp).forward(request, response); } }再写 JSP 的列表主体% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title商品列表/title /head body h2商品列表/h2 table border1 tr thID/th th名称/th th分类/th th单价/th th库存/th th操作/th /tr c:forEach items${goodsList} vargoods tr td${goods.id}/td td${goods.name}/td td${goods.category}/td td${goods.price}/td td${goods.stock}/td td a href${pageContext.request.contextPath}/goods/edit?id${goods.id}编辑/a a href${pageContext.request.contextPath}/sale?id${goods.id}销售/a /td /tr /c:forEach /table /body /html两个关键路径需要注意。Servlet 的访问路径是/goods/listJSP 展示用的request.getRequestDispatcher(/goods/list.jsp)是服务端内部转发路径它表示 webapp 目录下的真实文件位置“编辑”和“销售”链接里用${pageContext.request.contextPath}拼接项目上下文路径。如果你直接写/goods/edit?id1而不是${pageContext.request.contextPath}/goods/edit?id1部署时项目名一旦变化所有链接都会 404这就是俗称的路径“写死了”。3.4 登录与 Session让系统真正“认识”当前操作者进销存系统不能谁都能进来改数据登录功能是商品列表之外最先要做的模块。前面已经写了LoginServlet和UserDao的代码这里补一个关键设计登录成功后把用户对象放进 Session后续的进货、销售操作都从 Session 里取loginUser记录到流水表的operator_id字段。这正好呼应热搜词里的“servlet 生命周期”——Session 在 Servlet 容器里是独立于 Servlet 实例存在的Servlet 是单实例多线程处理请求但每个浏览器会话持有独立的 Session 对象。所以你不能把“当前登录用户”存在 Servlet 的成员变量里那会出现 A 登录后看到 B 的用户名这种串号事故。稍微想一想就能明白Servlet 的成员变量被所有请求共享而 Session 是每个浏览器私有的。// 在某个需要登录的 Servlet 里取当前用户 HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; }一个完整的系统通常会把这个校验逻辑写成过滤器Filter统一配置在需要保护的路径上而不是在每个 Servlet 里复制这段代码。但课程设计阶段先写在每个 Servlet 里也不丢人——运行起来逻辑清晰答辩时能讲明白 Session 是干什么的就行了。4. 四个最容易翻车的地方编码、部署、字段类型和 Session 陷阱4.1 现象页面中文全是问号乱码数据库里存的中文变成 ???原因是编码链路上某一环断了。一个请求从浏览器到 Tomcat、再到 JSP 显示至少经过四个环节页面本身的编码、请求参数编码、数据库连接编码、数据库表编码。常见问题是 JSP 文件没写pageEncoding、request.setCharacterEncoding(UTF-8)只写了 POST 路径而 GET 参数没处理、MySQL 表建成了latin1。解决方法是把编码一次性铺满JSP 文件头部写全 page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8%在web.xml里配置一个CharacterEncodingFilter拦截所有请求强制设成 UTF-8数据库连接 URL 带useUnicodetruecharacterEncodingutf8建表语句明确写CHARSETutf8mb4。这四处都检查过一遍中文乱码的问题就不会再出现。特别是表字符集改表的编码是很多人漏掉的最后一环。4.2 现象执行 SQL 时报“Data truncation: Out of range value for column”原因是字段类型和写入数据不匹配。一种常见场景是商品价格数据库里定义DECIMAL(10,2)应用程序里用字符串拼接 SQL 往里写一个很大的数或者用了INT存价格导致 12.5 直接被截断成 12。另一种常见场景是库存数量进货单填了 10000但表字段是TINYINT最大值只有 127写入直接报错。解决方法是先搞清楚每个字段的业务量级再定类型。价格统一用DECIMAL(10,2)Java 侧对应BigDecimal库存用INT在 Java 侧用Integer接收数量字段入库前做逻辑校验——进货数量不能为负数销售数量不能超过当前库存。这种校验最好放在 Servlet 层用简单判断拦截非法请求别指望数据库的报错信息反馈给用户。4.3 现象IDEA 里点运行一切正常打成 war 包部署到 Tomcat 就 404原因是路径感知差异。在 IDEA 里集成 Tomcat 运行时IDEA 会帮你自动处理 context path你能访问/goods/list不代表你部署到独立 Tomcat 的webapps目录下还能用同样的路径访问。如果 war 包名字是shop.war部署后所有 URL 都要加/shop前缀有的新手随手把 war 包改成ROOT.war虽然能去掉前缀但和容器默认路径之间的关系常常被忽略。解决方法是养成写相对路径的习惯重定向用response.sendRedirect(request.getContextPath() /goods/list)页面里的链接全部用${pageContext.request.contextPath}拼前缀。这样无论是部署成shop.war还是ROOT.war链接都会跟着上下文路径走不会再出现换了个部署方式就大面积 404 的情况。如果你负责维护老项目看到页面上有一堆手写的/shop/goods/list那基本就是踩了这个坑。4.4 现象销售一次商品刷新页面后库存扣了两次流水多了两条原因是用户按 F5 导致浏览器重新提交了上一个 POST 请求。销售操作在SaleServlet的doPost里扣减库存、写流水表用户付款后按刷新浏览器会重放这个 POST库存和流水就被重复记录了。解决方法是“Post/Redirect/Get”模式doPost里处理完业务后不要直接forward到成功页面而是用sendRedirect跳到一个 GET 地址比如response.sendRedirect(request.getContextPath() /sale/success)。这样浏览器最后一次请求是 GET刷新时只会重放 GET不会再重新执行一次扣库存逻辑。这也是前面 LoginServlet 里登录成功用sendRedirect而不是forward的原因。除了重定向之外销售扣减库存建议配合事务——扣库存和写流水两条 SQL 必须在一个事务里要么都成功要么都失败否则会出现库存扣了但流水没记的脏状态。手写 JDBC 时用conn.setAutoCommit(false)、最后conn.commit()已经足够应对这个场景。5. 从能用走到能答辩三个验证脚本和一个进阶思路5.1 先用 curl 把登录接口打一遍别急着开浏览器项目部署起来后第一件事不是点页面而是用 curl 验证最核心的接口链路。这样能把“前端页面问题”和“后端接口问题”分开排查——页面渲染可以后面慢慢调接口必须一次通。# 1. 登录保存 Cookie 到本地文件 curl -c /tmp/shop_cookie.txt -X POST \ -d usernameadminpassword123456 \ -w %{http_code} \ http://localhost:8080/shop/login # 2. 带 Cookie 请求商品列表接口 curl -b /tmp/shop_cookie.txt \ -w \n%{http_code} \ http://localhost:8080/shop/goods/list第一个命令返回302表示登录成功、重定向到列表页第二个命令能拉到商品列表的 HTML 就说明登录校验和列表查询都正常。如果第一个命令直接返回200且内容是登录页说明登录逻辑没走通此时检查UserDao的 SQL 和密码比对逻辑。5.2 验证库存闭环手动造一笔进货和销售看库存和流水对得上用一个 MySQL 客户端直接模拟数据库操作验证库存和流水的对应关系。这是最直接的业务正确性验证。-- 假设商品ID1当前库存为50 -- 模拟一笔进货进 20 件 INSERT INTO stock_log (goods_id, change_type, quantity, before_stock, after_stock, operator_id) VALUES (1, IN, 20, 50, 70, 1); UPDATE goods SET stock 70 WHERE id 1; -- 模拟一笔销售卖出 15 件库存 70 - 55 INSERT INTO stock_log (goods_id, change_type, quantity, before_stock, after_stock, operator_id) VALUES (1, OUT, 15, 70, 55, 1); UPDATE goods SET stock 55 WHERE id 1; -- 核对按流水累计变动的库存是否等于当前商品表的库存 SELECT g.stock AS current_stock, (SELECT before_stock FROM stock_log WHERE goods_id 1 ORDER BY id ASC LIMIT 1) SUM(CASE WHEN change_typeIN THEN quantity ELSE -quantity END) AS calc_stock FROM goods g WHERE g.id 1;这个验证技巧在答辩时特别加分你不仅做了功能还想到了“库存对账”。一旦两个数字对不上就能定位是 UPDATE 语句漏了还是流水记录写错了。平时开发时每次改完库存相关代码就跑一遍这个核对能提前拦住大部分隐藏 bug。5.3 从课程设计走向真实项目的三个扩展点如果你想在这个项目上继续往前走方向其实很明确。第一个是 JSP 这项技术的“终局”问题——搜索引擎里经常有人问“nginx 支持 jsp 吗”答案是不支持nginx 只能处理静态资源动态请求要通过proxy_pass反向代理到 Tomcat让 Tomcat 里的 JSP/Servlet 去处理这个知识点在部署生产环境时必然碰到。第二个是换成 Spring Boot 后 JSP 目录的安置问题——src/main/resources下放不了 JSPJSP 必须放在src/main/webapp且 Spring Boot 内置 Tomcat 只支持将 JSP 打包进 jar 的META-INF/resources目录实际项目里更多是用模板引擎替代 JSP第三个是 AdminLTE 或 Bootstrap 这类前端框架的引入——让你的系统页面不再像 2008 年的风格。这套项目的价值在于它用最少的依赖只有 Tomcat MySQL JDBC把 JavaWeb 底层的核心机制完整走了一遍。Servlet 的生命周期、Session 的会话管理、PreparedStatement 的参数绑定、JSP 的 EL 表达式这些知识换到任何 JavaWeb 框架里都还在。我做了这么多年项目见过太多上来就学 Spring Boot、结果连 HTTP 请求到方法调用这条链路都讲不清的人——先把这套老项目吃透后面的路反而好走得多。希望这份笔记能帮你的毕设少踩几个坑做得顺手一点。本文还有配套的精品资源点击获取