ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JSP+Servlet+JDBC手写JavaWeb库存管理系统:架构拆解与避坑指南

2026/10/7 16:05:41 拓冰建站 浏览量
JSP+Servlet+JDBC手写JavaWeb库存管理系统:架构拆解与避坑指南 简介一个基于JSP与Servlet的库存商品管理系统配合数据库完成数据持久化专门面向正在学习Java Web开发的高校学生与初级开发者也可用于课程设计或毕业设计参考。系统覆盖管理员登录、库存列表展示、商品记录的新增、修改与删除、删除前确认、库存少于十件自动标红、新加商品跳转与表单字段校验、重复商品入库自动累加以及操作成功后返回列表刷新等典型需求贴近实际业务场景。压缩包共38个文件其中包含9个Java源文件、6个JSP页面、2个SQL脚本以及工程配置文件、样式表、使用说明文档等辅助内容整体大小仅148KB便于快速下载与本地部署。项目还附带使用说明文档和数据库初始化脚本能够帮助使用者从零搭建运行环境理清Servlet处理请求、JSP渲染页面、数据库访问的完整调用链并掌握数据校验与列表刷新等实现细节。目前已有1488人学习下载代码结构清晰适合用来巩固Java Web知识并练习完整的增删改查开发流程。1. 别嫌 JSP 老这个库存管理系统包为什么还值得下先说个反直觉的结论JSP Servlet 数据库这套组合在 2025 年的企业开发里确实不多见了但在课设、毕设和 JavaWeb 入门这个层面它依然是出现频率最高的技术栈。原因不是它多先进而是它足够“裸”没有 Spring 帮你包办一切请求怎么进来、参数怎么取、SQL 怎么拼、事务怎么控制每一步都得自己写。把这个库存商品管理系统跑通了你对 JavaWeb 的整个请求链路才算真正有手感。这个压缩包里是一个完整的简易库存商品管理系统管理员登录、商品分类维护、商品增删改查、库存的入库出库操作都是 JSP Servlet 原生 JDBC MySQL 实现的。它适合两类人一是正在做 JavaWeb 课设、需要一份能跑通且能讲清楚原理的参考代码的人二是想搞明白 Servlet 生命周期、Session 会话、数据库增删改查在真实项目里怎么串起来的人。接下来我会按“这套代码怎么组织 → 怎么在本地跑起来 → 核心功能怎么实现 → 有哪些坑”的顺序把它拆给你看。2. JSP、Servlet、JDBC 的分工这套库存系统为什么这么搭2.1 三个技术各自管什么别混着用很多初学者容易把 JSP 和 Servlet 搞混觉得两个东西好像都能写页面。实际上在这个项目里分工非常明确Servlet 负责接收请求、调用数据库操作、决定跳转到哪个页面JSP 负责呈现数据也就是把商品列表、库存数量这些东西用 HTML 的形式展示给浏览器。JDBC 则是 Java 连接数据库的那座桥所有 SQL 语句都通过它发到 MySQL 去执行。这套库存管理系统的请求链路是标准的 MVC 雏形。用户在前端页面点一下“查询商品”浏览器就把请求发到对应的 ServletServlet 拿到参数后调用 DAO 层的方法DAO 层用 JDBC 去查数据库查完的结果封装成 Java 对象Servlet 把对象存到 request 或 session 作用域里再 forward 到 JSP 页面JSP 用 JSTL 或脚本片段把数据渲染成表格。整个过程没有框架介入每一步你都能在代码里找到对应的那一行这就是它适合学习的原因。2.2 项目里的包结构一眼看出这个系统有哪些功能解压之后先别急着启动花两分钟看一下 src 目录下的包结构。在这个项目里包名是按功能划分的常见做法是分成servlet、dao、entity、filter、util这几个包。entity 里放的是商品、分类、管理员这些实体类对应数据库表结构dao 里放的是对数据库的增删改查操作servlet 里是各个功能入口filter 里有登录拦截器util 里是数据库连接工具类。src/ ├── com.example.entity 商品、分类、管理员实体 ├── com.example.dao 商品DAO、分类DAO、管理员DAO ├── com.example.servlet 登录、商品管理、库存操作等Servlet ├── com.example.filter Session登录拦截过滤器 └── com.example.util DBUtil数据库连接工具这套分层不是随便分的。它保证了一个底线原则Servlet 里不写 SQLDAO 里不写 HTML。如果你拿到代码后发现某个 Servlet 里直接拼了 SQL那说明这个代码的质量是打折扣的后面维护起来会很痛苦。2.3 核心表结构商品、分类、管理员三张表整个系统的业务逻辑比较简单数据库里一般就三张表管理员表、分类表、商品表。管理员表负责登录认证分类表和商品表通过外键关联。这里定义表结构的时候有一个值得注意的细节商品表里的库存字段建议用int而不是double因为库存操作只涉及整数加减用浮点类型反而会埋下精度隐患。字段名类型说明idint 主键自增商品IDproduct_namevarchar(50)商品名称category_idint所属分类ID关联分类表pricedecimal(10,2)单价stockint当前库存unitvarchar(10)单位如“件”create_timedatetime录入时间分类表就简单多了分类ID、分类名称、分类描述。管理员表通常只有 id、username、password、real_name 四个字段。注意 password 在绝大多数课设项目里是明文存的这个项目大概率也是这样生产环境不能这么干但学习阶段可以先不管。3. 本地跑通这个项目环境、建库、部署一步不落3.1 环境版本怎么搭配直接抄作业这个项目用的是相对传统但非常稳定的组合JDK 8 Tomcat 8.5 MySQL 5.7 IDEA。为什么推荐这套而不是新版因为 JSP 项目在 Tomcat 9 之后需要额外引入依赖而且 JDK 8 对 Servlet 3.0 的支持非常成熟网上能查到的排错方案几乎都是针对这个组合的。你如果非要上 JDK 17 Tomcat 10大概率会遇到javax.servlet包名变更的编译错误那就变成给自己找事了。MySQL 5.7 需要注意一点安装时选择 utf8 字符集。如果你用的是 MySQL 8.0 也可以项目一般兼容但要确认驱动版本是mysql-connector-java5.1.49 或以上并且连接 URL 里加上useSSLfalseserverTimezoneAsia/Shanghai两个参数否则启动时报时区错误。3.2 建库建表执行初始化脚本项目压缩包里的sql文件夹通常自带建库脚本文件名一般叫stock_manager.sql或者inventory_db.sql。拿到数据库客户端先执行这段建库语句CREATE DATABASE IF NOT EXISTS stock_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE stock_manager; -- 管理员表 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, real_name VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, cate_name VARCHAR(50) NOT NULL, cate_desc VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(50) NOT NULL, category_id INT, price DECIMAL(10,2), stock INT DEFAULT 0, unit VARCHAR(10), create_time DATETIME, CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_admin(username, password, real_name) VALUES (admin, 123456, 系统管理员);这段 SQL 里有几个参数值得说明。utf8mb4比utf8多支持一些特殊字符比如部分生僻字和 emoji现在建库默认用它基本不会错。外键fk_category的作用是保证商品表里的 category_id 必须在分类表里存在这能防止录入商品时随手写一个不存在的分类编号。默认管理员账号 admin、密码 123456你第一次登录成功后可别忘了改掉。3.3 IDEA 里导入项目并配置 Tomcat导入步骤不复杂但很多新手在最后一步启动时报错。先说正确流程IDEA 里选择File - New - Project from Existing Sources选中解压后的项目目录如果是 Maven 结构就选导入 Maven 项目如果是传统 Web 项目就选Web Application。随后要确认项目的 JDK 版本统一Project Structure - Project里选 JDK 8Modules里 Dependencies 选Module SDK为 8。Tomcat 的配置在Run - Edit Configurations点加号选择Tomcat Server - Local在Deployment标签页把项目以war exploded方式部署修改Application context为/stock。这一步的坑在于单独配置了 Tomcat 但 deployment 里没加 artifact启动时会提示“没有可部署的 artifact”页面直接打不开。正确配置后启动日志里能看到information: Deploying web application这时候就可以打开http://localhost:8080/stock访问了。4. 核心功能拆解登录、商品管理、库存操作怎么实现的4.1 登录模块Servlet 校验 Session 会话 Filter 拦截登录功能是这个系统的门面。它不是一个简单的“密码比对成功就放行”而是一套完整的会话控制逻辑。用户提交用户名和密码后请求会到LoginServlet的doPost方法代码逻辑大致是这样的protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); Admin admin adminDao.findByUsernameAndPassword(username, password); if (admin ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, admin); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(request.getContextPath() /product/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }这段代码最关键的是两个点一是request.setCharacterEncoding(utf-8)必须放在读取参数之前否则中文用户名会乱码二是登录成功后把admin对象存进 session后续所有页面判断 “是否登录” 就看 session 里有没有这个对象。setMaxInactiveInterval(30 * 60)设置的是会话超时时间单位是秒这里设置了 30 分钟也就是说用户 30 分钟不操作就需要重新登录。有了 Session 还不够如果用户绕过登录页直接访问product/list.jsp这个资源就暴露了。所以项目里一般会有一个LoginFilter把这种绕过路径堵住public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); }过滤器的工作原理是在请求到达 JSP 或 Servlet 之前先做一道检查session.getSession(false)表示如果当前没有 session 就直接返回 null而不是新建一个。如果 session 不存在或没有 loginUser 属性就重定向到登录页。这个 Filter 在web.xml里配置拦截路径通常配置为拦截/index.jsp、/product/*、/category/*这些需要登录才能访问的路径而login.jsp、LoginServlet本身要排除在外不然会形成“无法登录”的死循环。4.2 商品管理和分页查询Servlet 传参到 JSP 的完整链路商品管理是整个系统的重头戏它把数据库最基本的增删改查CRUD全部覆盖了。以商品列表页为例页面会发起GET /product/list?pageNum1pageSize10这样的请求ProductListServlet里会做这些事接收页码参数、计算 offset、查询当前页数据、查询总记录数、封装成分页对象、转发给 JSP。int pageNum 1; int pageSize 10; String pageNumStr request.getParameter(pageNum); if (pageNumStr ! null !pageNumStr.isEmpty()) { pageNum Integer.parseInt(pageNumStr); } ListProduct productList productDao.findByPage(pageNum, pageSize); int totalCount productDao.countAll(); int totalPages (totalCount pageSize - 1) / pageSize; request.setAttribute(productList, productList); request.setAttribute(pageNum, pageNum); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/product/list.jsp).forward(request, response);分页参数的设计有一点要说清楚pageNum是当前页码从 1 开始pageSize是每页条数这里的findByPage(pageNum, pageSize)在 DAO 层执行的是SELECT * FROM t_product LIMIT ?, ?第一个参数是偏移量offset需要程序里换算成(pageNum - 1) * pageSize。totalPages的计算公式(totalCount pageSize - 1) / pageSize是一个经典写法避免小数页。比如总记录数 21 条、每页 10 条按这个公式算出来是 3 页而不会出现 2.1 页这种情况。向 JSP 转发用的是forward而不是sendRedirect这个区别很关键。forward 是服务器内部跳转请求地址栏不变request 里的productList属性在 JSP 中可以直接用 EL 表达式\${productList}拿到sendRedirect 是浏览器重新发起请求此时 request 是新的放进去的属性就丢了。4.3 库存操作入库和出库必须要做事务控制库存操作是这个系统跟普通 CRUD 练习最大的不同点。入库和出库不只是改一个数字那么简单它们涉及数据的准确性。假设你要做“入库”操作输入商品 ID 和入库数量系统需要把商品库存增加对应数量。但如果业务要记录每次操作的流水操作时间、操作人、变动数量那就会涉及两张表的同时更新库存表加数 流水表插入记录。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 String updateSql UPDATE t_product SET stock stock ? WHERE id ?; PreparedStatement ps1 conn.prepareStatement(updateSql); ps1.setInt(1, quantity); ps1.setInt(2, productId); ps1.executeUpdate(); String insertSql INSERT INTO t_stock_log(product_id, change_qty, operate_time, remark) VALUES (?, ?, NOW(), ?); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setInt(1, productId); ps2.setInt(2, quantity); ps2.setString(3, 入库); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } e.printStackTrace(); } finally { DBUtil.closeAll(conn, null, null); }这段代码里最重要的就是第 3 行的setAutoCommit(false)。JDBC 默认每执行一条 SQL 自动提交一次如果你不做这个设置很可能出现“库存数量加了但流水没记上”的情况事后根本无从查起。commit()表示把两步操作作为一个整体提交任一步失败都会执行rollback()回滚库存和流水保持一致。这里的PreparedStatement是预编译的 SQL 语句用?占位符传参有三个好处避免拼字符串导致 SQL 注入参数类型自动处理不需要手工加引号同样的 SQL 在数据库端有缓存重复执行效率更高。出库操作逻辑相同只是 SQL 变成了stock stock - ?但要注意出库前要查出当前库存判断够不够扣不够要直接抛异常或返回失败信息不能让库存变成负数。这是个业务校验问题写在代码里的优先级要高于数据库约束。5. 避坑指南这个项目最常见的五类翻车点5.1 页面中文全部变成问号现象浏览器里商品名称、分类名称全是???或者数据库里存进去的是乱码。原因有三处字符集不一致。第一处是数据库表的字符集建表时没指定 utf8mb4默认用了 latin1第二处是 JDBC 连接 URL 没带characterEncodingutf-8第三处是 JSP 页面头部没有设置pageEncoding。解决建表时不放心就用DEFAULT CHARSETutf8mb4连接 URL 写成jdbc:mysql://localhost:3306/stock_manager?useUnicodetruecharacterEncodingutf-8JSP 文件第一行加上% page contentTypetext/html;charsetUTF-8 languagejava %。这三处都对了乱码基本就能消失。5.2 Tomcat 启动报 404页面打不开现象Tomcat 启动成功但访问http://localhost:8080/stock出现 404 错误页。原因90% 的情况是 IDEA 的 Deployment 配置没做。项目没有被打进 Tomcat 的部署目录或者Application context设置的路径不对Tomcat 根本找不到这个 Web 应用。解决打开Run - Edit Configurations在 Tomcat Server 配置里的Deployment标签下点加号添加Artifact选择项目名对应的war exploded然后在Application context里填/stock。重新启动后Tomcat 启动日志里会出现Deploying web application字样说明部署成功了。5.3 连接数据库报 ClassNotFoundException 或连接超时现象启动项目后访问任何涉及数据库的页面浏览器报 500Tomcat 控制台出现java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者Communications link failure。原因ClassNotFoundException是 MySQL 驱动 JAR 包没放进WEB-INF/lib目录Communications link failure是数据库没启动或者连接 URL 里的 IP、端口、数据库名写错了也可能本地 MySQL 服务没开。解决把mysql-connector-java-5.1.49.jar复制到项目的web/WEB-INF/lib下IDEA 中右键选择 Add as Library 也行并在 Artifact 配置里确认这个 JAR 被打进部署包。连接超时的问题先去任务管理器确认mysqld.exe进程在运行再检查 URL 里的localhost:3306是否和 MySQL 实际端口一致。5.4 登录成功后按刷新用户被弹回登录页现象登录后正常操作没问题但浏览器按 F5 刷新某个页面或者打开新标签页又跳回了登录页。原因这是 Session 超时或者会话被重新创建导致的。session.getSession(false)这个写法有个隐藏行为如果 session 不存在返回 null过滤器就会认为是未登录。另外如果你在 JSP 里用了response.sendRedirect跳转而不是forward也会丢失 request 作用域但这跟登录无关是数据传递问题。解决先把MaxInactiveInterval调长一点比如session.setMaxInactiveInterval(60 * 60)然后检查过滤器里getSession(false)是不是被误写成了getSession()。后者在请求进来时不管有没有 session 都会新建一个导致即使你登录了也已经有一个旧的过期 session 在会话里过滤器判断逻辑就会判定为未登录。5.5 入库操作重复扣库存或者库存为负数现象用户连续点击两次“入库”按钮库存数量被加了两次或者出库时没做校验库存数量变成了负数。原因第一是页面按钮没做防重复提交用户在等待响应时又点了一次第二是出库的 Servlet 里只执行了stock stock - quantity的 SQL没有先查当前库存做比较。解决前端可以在点击后禁用按钮onclickthis.disabledtrue后端在出库逻辑里先执行一次查询判断stock quantity再更新条件不满足就抛业务异常并回滚。更稳妥的做法是使用乐观锁在商品表里加一个 version 字段更新时WHERE id ? AND version ?如果更新影响行数为 0说明数据被别人改过了需要重新读取再操作。这个方案对课设而言有点超纲但面试时能讲出来就是加分项。6. 从数据库反推页面一套完整的项目验收检查清单这个项目拿到手怎么快速验证它是不是真的没问题、是不是真的完整我的习惯是打开数据库客户端从数据层面去验证页面的每一次操作。这比在页面上点点点要可靠得多因为页面可能粉饰问题但数据库不会撒谎。第一步验证建表是否成功。连接数据库后执行SHOW TABLES确认能看到三张表以上再执行DESC t_product查看商品表字段。如果发现stock字段是double类型那你就要警惕后面库存计算时可能出现浮点误差建议改成int并重启项目。第二步打开商品列表页面记录页面上第一行商品的 ID然后在数据库执行SELECT stock FROM t_product WHERE id 1两者对比确认页面显示的数据和数据库一致。第三步做一次入库操作做完后立刻执行SELECT * FROM t_stock_log检查流水表有没有生成对应的记录同时再看 t_product 里的 stock 字段有没有同步增加。如果流水有记录但库存没变就是事务没控制好回滚失败如果库存变了但流水是空的那连事务都没开启。第四步最容易被忽略验证过滤器。在浏览器直接输入http://localhost:8080/stock/product/list.jsp的地址看是否会跳转到登录页。如果直接打开了商品列表说明登录拦截没生效你的项目在公网被人扫到地址就等于裸奔。第五步清理浏览器缓存后重新走一遍完整流程登录 → 新增分类 → 新增商品 → 入库 → 出库 → 删除商品每一步操作完都刷新数据库里的表数据确认页面结果和数据库状态完全对得上。这套检查流程走完项目有没有隐藏问题基本就暴露干净了。写代码时我也养成了一个同等重要的习惯不信任任何一次“看似成功”的操作。页面弹出“操作成功”的提示框时我会条件反射地打开数据库确认一遍。这个习惯帮我抓出过不少真相——有时候提示框是写死的SQL 根本没执行。从那以后我每次拿到一个 JSP Servlet 的项目不管是不是自己写的都强制走一遍这个数据反查流程确认没问题才敢往下改代码。这篇拆解里提到的坑大多就是在这套流程里抓出来的。希望帮到你。本文还有配套的精品资源点击获取