资讯动态

JavaWeb商品管理系统实战:Servlet+JSP+JDBC增删改查与避坑指南

发布时间:2026/10/14 5:29:37 来源:尧图企业网站定制
简介一款基于JavaWeb技术构建的商品管理系统源码包适合作高校软件技术、计算机相关专业毕业设计或课程实践参考。系统围绕商品信息管理场景实现用户注册登录与角色权限区分、商品增删改查、分类管理、库存自动更新与提醒、购物车订单流转及销售数据统计等核心模块并包含品牌管理、条件分页查询等典型业务逻辑实现覆盖电商后台管理常见业务链路。压缩包共163个文件内含52个class编译文件、26个java源码、34个xml配置以及html、css、js等前端资源另有sql数据库脚本与jar依赖库整体体积约3.29MB结构清晰便于按源码、配置、页面分类检索。已有1124人学习下载。完整增删改查代码可直接运行部署并附带数据库初始化脚本适合需要快速掌握JavaWeb完整开发流程的读者进行二次开发与学习。1. JavaWeb商品管理系统到底是什么选题老但基本功检验一点也不含糊JavaWeb商品管理系统这类选题几乎每到课程设计、期末大作业就会被翻出来一次。很多人第一反应是“这东西早该被 Spring Boot 淘汰了”可真把需求拿到手就会发现要分层、要处理表单、要连数据库、要管会话、要打包部署每一环都是躲不开的硬功夫。它不是一个能让你炫耀技术的项目恰恰是检验 JavaWeb 请求-处理-响应这条主链路有没有真正入门的试金石。这套系统能解决的问题很实在理解一个 Web 请求从浏览器出发经过容器、经过 Servlet 代码、落到数据库再原路返回的全过程理解为什么商品价格要用定点数、为什么删除操作不该用 GET、为什么新增后要重定向而不是转发。适合的人群也清晰正在选课设题目的学生、准备 JavaWeb 阶段考核的开发者以及需要快速交付内部小工具但不想先背一整套框架的从业者。这篇文章不介绍某个开源包的用法而是把“JavaWeb 商品管理系统”背后最常见的做法、参数和坑完整拆开。2. 先把底子打好技术栈选型、数据库设计与项目骨架一上来就写代码很容易翻车没有方向代码也会白写。这一章先解决三个问题用什么组合做、商品表怎么建、项目怎么搭。2.1 为什么推荐 Servlet JSP JDBC在现代框架面前怎么自处JavaWeb 商品管理系统只是一个泛化题目不同人会用不同方式交掉单体框架、SSM、甚至前后端分离都有人做。但题目里既然写着 JavaWeb最正统、最适合当训练场的组合还是 Servlet JSP JDBC。这个组合没有自动配置、没有 ORM 的二级缓存每一步请求在哪个方法里、每一条 SQL 在哪个类里都看得见出了问题可以直接定位到一行代码。方案适合场景需要注意的代价Servlet JSP JDBC课程设计、基础考核、快速小工具代码量偏大但链路透明、便于自查单体框架注解版快速交付、前后端不分离的内部系统自动配置多出问题要靠框架日志排查前后端分离作品集、团队协作型题目前端工程化成本高单人不划算我一般会根据一个标准决定选型如果这个系统的答辩问题只会是“你的请求从哪里进、SQL 在哪里执行、为什么用了 INNER JOIN”这类那 Servlet JSP JDBC 就是最合适的答案。它不会帮你隐藏任何步骤自然也不会在答辩时让你无话可说。反过来如果你已经有扎实的框架经验想在这个标题上体现工程化能力追求更高的架构也没问题只是这个标题的常规解法仍然以传统三层为主。顺带说一句SSM 和 Spring Boot 底层还是 Servlet 规范那一套只是中间加了一层封装。把这里的 CRUD 手写一遍再看框架源码里 DispatcherServlet 的核心流程会比直接上手框架顺很多。2.2 商品表怎么设计才算合格字段、类型与三条设计原则商品管理系统第一张表就是商品主表。先建库建表用常见的数据库语法来写CREATE DATABASE IF NOT EXISTS product_db DEFAULT CHARACTER SET utf8mb4; USE product_db; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, name VARCHAR(100) NOT NULL COMMENT 商品名称, category VARCHAR(50) DEFAULT 未分类 COMMENT 商品分类, price DECIMAL(10,2) NOT NULL COMMENT 商品单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, description TEXT COMMENT 商品描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;这段 SQL 里有几个关键选择。id 用 BIGINT 而不是 INT是为了以后数据量上来后主键不够用的风险name 加 NOT NULL空名称商品没有任何管理意义。price 用 DECIMAL(10,2) 而不是 FLOAT 或 DOUBLE这是金额字段的铁律浮点数存金额会出现 0.1 0.2 不等于 0.3 的精度问题而 DECIMAL 是定点数精度可控。stock 用 INT业务上限可以设一个小一点的数数据库层再加 CHECK 约束防负数应用层也要校验一次。接下来插入几条测试数据后面所有功能调试都靠它INSERT INTO product(name, category, price, stock, description) VALUES (机械键盘, 外设, 399.00, 50, 入门级机械键盘), (无线鼠标, 外设, 129.00, 120, 办公无线鼠标), (27寸显示器, 显示设备, 1299.00, 20, 2K分辨率显示器);字段设计遵循三条原则。第一业务数值精度优先涉及金额一律 DECIMAL第二可以为空、可以为 0但要避免无意义数据description 允许 NULL 是因为不是所有商品都需要描述第三create_time 和 update_time 交给数据库维护默认值应用层插入数据时不用手动传时间避免各台服务器时间不一致。如果后面要支持多级分类可以把 category 拆成一张独立的分类表用外键关联单表单表打底先跑通再说。2.3 一个能跑起来的骨架目录结构、依赖与配置项目用 Maven 管理依赖和打包这是当前最通用的方式。目录结构规划成下面这样每个类职责单一product-manage/ ├── pom.xml └── src/main/ ├── java/com/example/product/ │ ├── entity/Product.java │ ├── dao/ProductDao.java │ ├── servlet/ │ │ ├── ProductListServlet.java │ │ ├── ProductAddServlet.java │ │ ├── ProductUpdateServlet.java │ │ └── ProductDeleteServlet.java │ └── util/DBUtil.java ├── resources/db.properties └── webapp/ ├── WEB-INF/web.xml └── jsp/ ├── list.jsp └── product_form.jsppom.xml 里的依赖要特别注意 scope。下面是精简后的完整配置project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdproduct-manage/artifactId version1.0/version packagingwar/packaging dependencies !-- Servlet API版本跟随本地容器 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.x/version scopeprovided/scope /dependency !-- JSP API -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.x/version scopeprovided/scope /dependency !-- JSTL 标准标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- 数据库驱动版本按数据库版本选 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version /dependency /dependencies build finalNameproduct-manage/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.x/version configuration source1.8/source target1.8/target /configuration /plugin /plugins /build /projectservlet-api 和 jsp-api 用 provided 作用域意思是编译时需要、但打包时不要带进 war因为运行容器本身已经提供了这些类。如果把容器自带的类打进 war启动时会出现 ClassCastException 之类的冲突。数据库驱动和 JSTL 保持默认的 compile 作用域会打进 war部署到没有这些 jar 的机器上也能跑。JDBC 连接参数放在 db.properties和代码分离jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/product_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的数据库密码这里三个参数是血泪经验的结晶。characterEncodingutf8 告诉驱动用 UTF-8 写库注意是 utf8 不是 utf-8useSSLfalse 是为了避免本地环境没有证书时疯狂告警serverTimezoneAsia/Shanghai 是 MySQL 8.0 的硬要求不加会直接报时区错误。JDK 编译级别设 1.8是绝大多数课程环境和老容器都能接受的版本。提示数据库密码不要提交到公开仓库。这是配置文件建议加入版本忽略列表。3. 商品增删改查的完整链路DAO、Servlet 与 JSP 怎么配合骨架搭好后开始填最核心的业务商品的新增、展示、编辑、删除。这一章按三层来写每一层都贴可直接用的代码。3.1 实体类与数据库工具类先解决连接和手写 JDBC 的重复代码实体类 Product 对应 product 表字段类型要和数据库严格对应。BIGINT 对应 LongDECIMAL 对应 BigDecimalINT 对应 IntegerDATETIME 先用 String 接收即可代码量最小package com.example.product.entity; import java.math.BigDecimal; public class Product { private Long id; private String name; private String category; private BigDecimal price; private Integer stock; private String description; private String createTime; private String updateTime; // getter / setter 由 IDE 生成这里省略 }价格字段如果用了 Double后面做金额计算、排序、报表都会遇到精度问题所以这里直接上 BigDecimal。时间字段用 String 适合展示场景如果后面要做“最近 30 天上架的商品”这类查询建议换成 LocalDateTime。数据库连接不能每个方法都重复写一遍加载驱动、取连接的代码抽一个工具类package com.example.product.util; import java.sql.*; public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/product_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }静态块在类加载时执行一次把驱动注册好close 方法按 ResultSet、Statement、Connection 的顺序关闭资源。JDBC 规范里连接用完必须关闭否则连接数会被耗尽页面会出现卡死。初学阶段不建议直接用连接池先自己管连接理解生命周期后再换连接池不迟。3.2 DAO 层把增删改查写清楚顺便躲开 SQL 注入ProductDao 是唯一直接写 SQL 的地方。为了让代码可读先把 ResultSet 到 Product 的映射抽成私有方法package com.example.product.dao; import com.example.product.entity.Product; import com.example.product.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDao { private Product mapRow(ResultSet rs) throws SQLException { Product p new Product(); p.setId(rs.getLong(id)); p.setName(rs.getString(name)); p.setCategory(rs.getString(category)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); p.setDescription(rs.getString(description)); return p; } public ListProduct findAll() { String sql SELECT id, name, category, price, stock, description FROM product ORDER BY id DESC; ListProduct list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } catch (SQLException e) { throw new RuntimeException(查询商品列表失败, e); } return list; } public Product findById(Long id) { String sql SELECT id, name, category, price, stock, description FROM product WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { throw new RuntimeException(按ID查询商品失败, e); } return null; } public boolean add(Product p) { String sql INSERT INTO product(name, category, price, stock, description) VALUES(?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getName()); ps.setString(2, p.getCategory()); ps.setBigDecimal(3, p.getPrice()); ps.setInt(4, p.getStock()); ps.setString(5, p.getDescription()); return ps.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(新增商品失败, e); } } public boolean update(Product p) { String sql UPDATE product SET name?, category?, price?, stock?, description? WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getName()); ps.setString(2, p.getCategory()); ps.setBigDecimal(3, p.getPrice()); ps.setInt(4, p.getStock()); ps.setString(5, p.getDescription()); ps.setLong(6, p.getId()); return ps.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(更新商品失败, e); } } public boolean delete(Long id) { String sql DELETE FROM product WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); return ps.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(删除商品失败, e); } } }这里全部使用 PreparedStatement而不是把参数直接拼进 SQL 字符串。开发时常见的危险写法是 SELECT ... WHERE name keyword 用户输入一个 OR 11 就能把所有商品拖出来这就是教科书级的 SQL 注入。PreparedStatement 把参数当作纯数据处理从机制上挡住这一招。参数说明setString、setBigDecimal、setLong 这些方法的第一个参数是占位符下标从 1 开始不是 0。查询用 executeQuery返回结果集增删改用 executeUpdate返回受影响行数。try-with-resources 会自动关闭连接但要注意它关闭的顺序和手工 close 一样是逆序的不用担心。3.3 Servlet 层接收参数、调用 DAO、决定去哪个页面Servlet 负责接请求、取参数、调 DAO、跳页面。列表 Servlet 是入口package com.example.product.servlet; import com.example.product.dao.ProductDao; import com.example.product.entity.Product; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/product/list) public class ProductListServlet extends HttpServlet { private final ProductDao productDao new ProductDao(); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); ListProduct list productDao.findAll(); req.setAttribute(products, list); req.getRequestDispatcher(/jsp/list.jsp).forward(req, resp); } }doGet 只负责读和展示不做数据变更。req.setAttribute 把数据塞进 request 作用域forward 把请求转发给 JSP。注意 forward 和 sendRedirect 的区别forward 是服务器内部转发地址栏不变request 里的属性能带到目标页面sendRedirect 是浏览器重新请求地址栏变化数据要重新查一遍。新增商品的 Servlet 是一个完整的 POST 处理链路package com.example.product.servlet; import com.example.product.dao.ProductDao; import com.example.product.entity.Product; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; WebServlet(/product/add) public class ProductAddServlet extends HttpServlet { private final ProductDao productDao new ProductDao(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); Product p new Product(); p.setName(req.getParameter(name)); p.setCategory(req.getParameter(category)); p.setPrice(new BigDecimal(req.getParameter(price))); p.setStock(Integer.parseInt(req.getParameter(stock))); p.setDescription(req.getParameter(description)); productDao.add(p); resp.sendRedirect(req.getContextPath() /product/list); } }价格和库存从请求参数里拿到的是字符串必须手动转换成 BigDecimal 和 Integer转换失败会抛 NumberFormatException页面变 500。所以 JSP 表单里要做前端校验Servlet 里再包一层 try-catch 或者用工具类做参数校验才算完整。删除和更新的 Servlet 结构完全同构。删除这里特别强调不要用a href.../delete?id1这种 GET 链接直接删。爬虫、浏览器预取、用户误触都可能触发删除。常见做法是删除用 POST 表单提交或者在页面上加 confirm 确认弹窗但服务器的兜底判断不能省。3.4 JSP 层用 JSTL 渲染列表和表单列表页用 JSTL 的 c:forEach 循环渲染比在 Java 里拼接 HTML 干净得多% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle商品管理/title/head body h2商品列表/h2 pa href${pageContext.request.contextPath}/jsp/product_form.jsp新增商品/a/p table border1 cellpadding6 tr thID/thth名称/thth分类/th th价格/thth库存/thth操作/th /tr c:forEach items${products} varp tr td${p.id}/td td${p.name}/td td${p.category}/td td${p.price}/td td${p.stock}/td td a href${pageContext.request.contextPath}/product/edit?id${p.id}编辑/a a href${pageContext.request.contextPath}/product/delete?id${p.id} onclickreturn confirm(确定删除该商品);删除/a /td /tr /c:forEach /table /body /html${p.name} 是 p.getName() 的 EL 表达式简写EL 输出 null 值时会变成空白不会报错。${pageContext.request.contextPath} 是当前应用的上下文路径所有链接和表单 action 都要带上它否则部署后项目不在根路径时链接会 404。新增和编辑共用一个表单页 product_form.jspform action${pageContext.request.contextPath}/product/add methodpost 名称input typetext namename requiredbr 分类input typetext namecategorybr 价格input typenumber step0.01 nameprice requiredbr 库存input typenumber namestock value0br 描述textarea namedescription/textareabr button typesubmit保存/button /forminput 的 name 属性必须和 Servlet 里 getParameter(name) 的参数名一致这是新手最常对不上的位置。required 是浏览器端校验防止空值直接提交但它只能挡正常人挡不住手动构造的请求所以服务端校验不能省。到这里“列表-新增-编辑-删除”已经能完整跑通。接下来是真正耗时间的地方各种看起来莫名其妙的问题。4. JavaWeb商品管理系统避坑实录4 个高频翻车现场下面四条是按出现频率排的每一条都是真实调试中反复遇到的。按“现象 → 原因 → 解决”的顺序说清楚。4.1 新增后刷新导致重复提交一个老掉牙但必踩的坑现象在新增商品表单里点击保存列表页正常出现这条数据。然后按 F5 刷新又冒出一条一模一样的商品数据库里多了两条记录。原因表单以 POST 提交到 /product/addServlet 处理完如果用的是 forward 跳回列表浏览器地址栏始终停留在 /product/add。此时按 F5浏览器会告诉你“要重新发送表单”点确认后同一个 POST 请求又执行了一次数据自然翻倍。这是 Web 开发里的经典问题表单重复提交。解决所有改变数据库状态的请求处理完后一律用重定向不用转发。在 ProductAddServlet 的 doPost 末尾resp.sendRedirect(req.getContextPath() /product/list);sendRedirect 返回 302浏览器收到后重新向 /product/list 发起 GET。地址栏变成了列表地址此时按 F5 只是重复查询列表不会再写库。req.getContextPath() 是应用上下文路径假设部署后访问地址是 http://localhost:8080/product-manage/contextPath 就是 /product-manage。少了它重定向地址会找不到页面。新增、修改、删除三个 Servlet 都要遵守这个约定这是 PRGPost-Redirect-Get模式最基础的应用。4.2 中文乱码请求、响应、数据库三层对不上现象列表页标题和按钮显示正常但从数据库读出的商品名显示成一堆问号或者新增一个“机械键盘”保存后变成了“??? ???”。更隐蔽的情况是 Windows 开发环境正常部署到 Linux 服务器后乱码。原因乱码不是一处的问题而是三层编码不一致。第一层请求体编码POST 参数默认按容器的默认编码解析如果页面是 UTF-8 而容器按 ISO-8859-1 解析中文进来已经是乱码第二层响应编码response 输出页面时没指定 charset浏览器可能按平台默认编码去解码第三层数据库连接JDBC URL 没带 characterEncodingutf8写入数据库时又错了一次。解决项目里加一个全局过滤器统一处理请求和响应编码package com.example.product.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }req.setCharacterEncoding 必须在第一次调用 getParameter 之前执行在 Filter 里统一处理可以保证这一点。resp.setCharacterEncoding 和 setContentType 都写也没问题后者会把 charset 一并写进响应头。之后即使在 Servlet 里忘了设置编码页面也不会乱。排错顺序先看页面输出源码的 meta 或响应头确认页面编码再看 JDBC URL 是否带 characterEncodingutf8最后看数据库表字符集。三层统一成 UTF-8乱码问题基本绝迹。4.3 换台机器就连接失败驱动与时区问题现象本地开发一切正常把 war 包部署到另一台服务器访问商品列表一直转圈最后页面 500日志里出现 ClassNotFoundException 或 Communications link failure。原因通常是两类问题。第一数据库驱动 jar 没有打包进 war。如果 pom 里把驱动依赖的 scope 标成了 provided或者手动拷贝 jar 时漏了war 包里就没有驱动类运行环境自然找不到。第二新版数据库对 JDBC URL 要求更严格没写 serverTimezone 会直接报时区错误SSL 参数没写会引发握手异常。解决先确认驱动到底有没有进 war 包用 jar 命令查看jar tf product-manage.war | grep mysql如果输出为空说明驱动没进包。去 pom.xml 里把 mysql-connector-java 的 scope 改回默认的 compile或者直接去掉 scope 行重新打包。grep 的是 mysql 而不是某个具体类名因为驱动类路径通常是 com/mysql/ 开头搜 mysql 能覆盖。时区问题统一改 JDBC URLjdbc.urljdbc:mysql://localhost:3306/product_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai 是让驱动知道数据库在哪个时区避免日期时间换算错乱。如果应用服务器和数据库服务器跨时区部署宁可统一用 UTC也不要在代码里手工做时区加减。4.4 上传的图片重启后全部失效路径写死的代价现象商品管理里加了一个图片上传功能当时在本机能正常显示。重启容器或者把项目部署到另一台机器图片全部 404只剩文字商品信息。原因开发时图省事把上传文件写进了项目的部署目录比如 target/product-manage/upload 或者 webapp/upload。容器重启时会重新解压 war 包旧文件全部被覆盖。如果再把路径硬编码成 D:/upload 或 /root/upload换机器自然找不到。解决把上传目录放到项目外部由外部配置维护数据库只存相对路径。上传代码的目录取值改成从配置读取String uploadBaseDir /data/product-manage/upload; // 从配置文件读取 File dest new File(uploadBaseDir, fileName);展示图片时不要直接拼绝对路径输出写一个独立的资源 Servlet 把文件流写回浏览器String relativePath product.getImageUrl(); // 数据库里存的是 /upload/2025/xxx.jpg File file new File(uploadBaseDir, relativePath); resp.setContentType(image/jpeg); // 用文件输入流把 data 写给 response 输出流数据库只存相对路径的好处是以后换域名、换存储位置、上对象存储只改前缀商品数据表不用动。如果题目本身不需要图片这个功能可以整体跳过但路径设计的原则要记住。5. 从能跑到能交差登录鉴权、分页与自测清单5.1 给后台加一道登录过滤器商品管理页面不应该裸奔。简单做法是登录成功后往 session 里放一个标记再用 Filter 拦截受保护路径WebFilter(/product/*) 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; Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }注意一个细节login.jsp 要放在 /product/ 之外比如 webapp 根目录下否则过滤器会把登录页也拦下来自己进不了登录页。登录成功后 session.setAttribute(loginUser, 用户名)退出时 invalidate session。5.2 分页两个参数搞定商品一多列表页要分页。给 DAO 加一个按偏移量查询的方法public ListProduct findByPage(int page, int pageSize) { String sql SELECT id, name, category, price, stock FROM product ORDER BY id DESC LIMIT ?, ?; // page 从 1 开始offset (page - 1) * pageSize ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); }LIMIT 后第一个参数是偏移量跳过的行数第二个是返回条数。page 参数从列表页链接传进来没传默认 1pageSize 固定 10 或 20。分页链接要带上当前页码上一页、下一页页码做加减即可这个改动不涉及业务代码只动 DAO 和列表页。5.3 交付前的自测清单与值不值得模块操作预期结果新增保存一件正常商品列表出现新记录刷新不重复新增价格填非数字页面拦截不提交编辑修改库存数量列表值同步更新删除点击删除出现确认提示确定后记录消失中文新增“机械键盘”列表和数据库均正常显示权限未登录直接访问列表自动跳转登录页部署打包后换机器驱动、时区、图片路径均正常我见过不少开发者把这个项目做完后被问“新增和编辑为什么要分开两个 Servlet”都说不清楚也见过答辩时被问“删除为什么不用 POST”当场卡壳。JavaWeb 商品管理系统的真正价值是逼你想清楚每一个 HTTP 动作背后的约定。别看它是老题目能把这套链路讲明白的人换框架也只是换一层皮。血泪经验是别急着套用万能脚手架先手写一遍 CRUD 的每一步再谈优化。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑