简介本资源是一个基于JavaWeb技术栈开发的商品管理系统完整项目面向计算机类专业在校学生、初学者及课程设计实践者可用于毕设、课设、作业或企业级系统入门学习。项目采用主流Web开发技术组合包含45个Java后端逻辑文件、13个HTML页面模板、10个CSS样式文件及7个JavaScript交互脚本辅以1753张JPG格式界面截图与操作示意图直观呈现系统功能与UI效果SQL文件提供数据库建表脚本XML与Properties文件支撑配置管理整体结构清晰、模块完整。压缩包共1864个文件大小为70.23MB已通过实际运行测试功能稳定可靠。目前已有87人下载学习读者可直接部署运行快速掌握ServletJSPMySQL基础架构下的CRUD全流程实现并基于现有代码拓展订单管理、用户权限等进阶功能同时参考大量界面资源理解前端布局与响应式设计实践。1. 这不是又一个“Hello World”页面一个能真正在 Tomcat 上跑起来、带完整 CRUD 和 MySQL 连接的 JavaWeb 商品管理系统专治 IDEA 配置失灵、JSP 报 404、数据库连不上这三类毕业季高频翻车现场你是不是也经历过下载了一个标着“JavaWeb 商品管理系统”的压缩包解压后双击index.jsp—— 浏览器弹出HTTP Status 404 – /index.jsp或者在 IDEA 里右键 Run on Server控制台刷屏ClassNotFoundException: com.mysql.cj.jdbc.Driver连登录页都打不开更别提改完web.xml加了 servlet 映射重启服务器后所有请求全进default servlet路由彻底失效。这不是你代码写得差是这套资源没把「可运行」这件事真正闭环。这个.zip包里没有空壳工程、没有占位图、没有注释掉的数据库配置它包含 12 个真实 CSS 文件从bootstrap.min.css到ranking-list.css、完整的WEB-INF/web.xmlweb.xml中定义的 7 个 Servlet含LoginServlet,AddProductServlet,DeleteProductServlet等以及已适配 MySQL 8.0 的jdbc.properties和经实测能通过Class.forName(com.mysql.cj.jdbc.Driver)加载的驱动 JAR。它面向的是正在赶毕设 deadline、需要立刻验证功能逻辑、或想用真实业务流反推 Servlet 生命周期和 JSP 作用域的新手——不是教你怎么配 JDK而是帮你绕过那 3 小时查不到原因的No suitable driver found。2. 从解压到首页渲染五步走通 JavaWeb 项目启动链路每一步都卡在真实环境里验证过2.1 解压即得结构看清WEB-INF是命门lib目录里藏着 JDBC 驱动真相解压后你会看到一个标准 JavaWeb 工程目录树核心结构如下非全部仅关键路径商品管理系统/ ├── WebContent/ # 根目录静态资源和 JSP 全在这里 │ ├── css/ # 你看到的那 12 个 CSS 文件全在此 │ │ ├── bootstrap.min.css │ │ ├── login.css │ │ └── ...共 12 个 │ ├── js/ │ ├── images/ │ ├── index.jsp # 首页含商品列表渲染逻辑 │ ├── login.jsp # 登录表单action 指向 LoginServlet │ └── register.jsp # 注册页提交至 RegisterServlet ├── WEB-INF/ # JavaWeb 的心脏必须存在且命名严格 │ ├── web.xml # 关键定义了 7 个 Servlet 及其 URL 映射 │ ├── classes/ # 编译后的 .class 文件存放处需手动编译或由 IDE 生成 │ └── lib/ # 注意这里放着 mysql-connector-java-8.0.33.jar ├── src/ # Java 源码目录含 dao, service, servlet 包 │ ├── dao/ │ │ └── ProductDao.java # 使用 JDBC Template 封装的增删改查 │ ├── service/ │ │ └── ProductService.java │ └── servlet/ │ ├── LoginServlet.java │ ├── AddProductServlet.java │ └── ...共 7 个 └── README.md # 含数据库建表语句和初始账号密码提示WEB-INF目录名必须全大写、带连字符少一个字母或大小写错误Tomcat 就会拒绝加载整个应用返回404。这是新手最常踩的隐形坑——不是代码错是目录名错。2.2 数据库准备执行README.md里的 SQL但必须改三处才能连上 MySQL 8.0打开README.md你会看到类似这样的建表语句CREATE DATABASE IF NOT EXISTS goodsdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE goodsdb; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT user ); CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, description TEXT );但直接执行会失败——因为README.md里默认的 JDBC URL 是旧版写法。你需要修改WEB-INF/classes/jdbc.properties文件# 修改前MySQL 5.x 写法8.0 会报错 # jdbc.urljdbc:mysql://localhost:3306/goodsdb?useUnicodetruecharacterEncodingutf-8 # 修改后MySQL 8.0 必须加 serverTimezone 和 allowPublicKeyRetrieval jdbc.urljdbc:mysql://localhost:3306/goodsdb?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password_here jdbc.drivercom.mysql.cj.jdbc.Driver参数说明serverTimezoneAsia/ShanghaiMySQL 8.0 默认使用 UTC 时区Java 客户端若不显式指定会抛The server time zone value XXX is unrecognized异常allowPublicKeyRetrievaltrue解决Public Key Retrieval is not allowed错误因 MySQL 8.0 默认禁用公钥检索useSSLfalse本地开发环境无需 SSL设为 false 避免握手失败生产环境应启用并配置证书。2.3 IDEA 配置三件套不是点 Run 就行要手动配对 Server、Artifact 和 Deployment Path在 IDEA 中导入项目后不能直接右键index.jsp→ Run。必须完成以下三步配置步骤 1配置 Tomcat ServerRun → Edit Configurations → → Tomcat Server → LocalApplication server: 选择你本地安装的 Tomcat 9 或 10Tomcat 10 不兼容 Servlet 4.0 以下的 API此项目基于 Servlet 3.1建议用 Tomcat 9.0.83Deployment → → Artifact → goods-system:war exploded注意选exploded不是war步骤 2确认 Artifact 输出路径File → Project Structure → Artifacts → goods-system:war explodedOutput directory: 必须指向商品管理系统/WebContent即你的WebContent目录Available Elements: 确保WEB-INF/classes和WEB-INF/lib已被包含IDEA 通常自动识别但需人工核对步骤 3设置 Deployment Path在Deployment标签页中Application context: 改为/goods不要留空否则访问地址是http://localhost:8080/会和 Tomcat 默认页冲突Deploy at the server: 勾选After launch确保启动时自动部署完成后点击OK再点击绿色三角形启动。浏览器访问http://localhost:8080/goods/index.jsp—— 如果看到商品列表说明链路打通。2.4 验证 Servlet 路由用 curl 直接调用绕过前端干扰定位 404 根源当页面显示404时先别急着改 JSP。用命令行验证后端是否真正响应# 测试登录接口POST 表单数据 curl -X POST http://localhost:8080/goods/LoginServlet \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadmin \ -d password123456预期返回{status:success,message:登录成功,redirect:/index.jsp}如果返回404说明web.xml中的servlet-mapping未生效。检查WEB-INF/web.xml中这段是否完整servlet servlet-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/LoginServlet/url-pattern /servlet-mapping注意url-pattern的值必须和 curl 中的路径完全一致区分大小写且不能有前导/以外的字符如/servlet/LoginServlet是错的。3. 为什么你的request.getParameter()总是 nullJSP 与 Servlet 数据传递的四个隐性断点排查3.1 表单 method 与 Servlet doGet/doPost 的严格匹配大小写、空格、隐藏字段缺一不可login.jsp中的表单必须写成form actionLoginServlet methodpost !-- 注意methodpost不是 POST 或 Post -- input typetext nameusername required !-- name 属性必须和 Servlet 中 request.getParameter(username) 一致 -- input typepassword namepassword required input typesubmit value登录 /form常见错误methodPOSTServlet 容器只认小写post大写会触发HttpServlet.service()的默认GET处理导致doPost()不执行input nameuser_nameServlet 中却写request.getParameter(username)名字不一致返回null忘记required属性用户空提交username为空字符串而非null但业务逻辑可能未做空校验导致后续 NPE。3.2web.xml中load-on-startup的数值陷阱数字越小越早加载但 0 和 1 有本质区别查看web.xml你会发现部分 Servlet 有load-on-startup标签servlet servlet-nameInitDBServlet/servlet-name servlet-classservlet.InitDBServlet/servlet-class load-on-startup1/load-on-startup !-- 注意这里是 1不是 0 -- /servlet关键逻辑load-on-startup 0容器启动时立即加载该 Servlet并调用init()load-on-startup 1容器启动时按数值升序加载1会在0之后、2之前加载无此标签Servlet 在第一次请求时才加载lazy loading。此项目中InitDBServlet设为1是为了确保数据库连接池在其他业务 Servlet如LoginServlet被首次调用前已初始化。如果你把它改成0而InitDBServlet依赖了尚未加载的DataSource就会在init()中抛NullPointerException且错误日志只显示Servlet.init() for servlet [InitDBServlet] threw exception根本看不到具体哪行出错——这是典型的黑匣子问题。3.3 JSP 页面编码与 request.setCharacterEncoding() 的双重保障index.jsp开头有这行% page contentTypetext/html;charsetUTF-8 languagejava %但这只保证响应是 UTF-8。当用户提交中文商品名如“苹果 iPhone 15”时request.getParameter(name)仍可能乱码。必须在每个处理 POST 请求的 Servlet 开头加protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 必须在 getParameter() 之前调用 String name request.getParameter(name); // 此时才是正确中文 // ... }血泪经验setCharacterEncoding()必须在getParameter()之前调用且只对 POST 有效GET 参数编码由浏览器 URL 编码决定需在 Tomcat 的server.xml中配置URIEncodingUTF-8。3.4 Session 作用域失效session.setAttribute()后getAttribute()返回 null 的三个原因在LoginServlet中登录成功后执行HttpSession session request.getSession(); session.setAttribute(user, user); // user 是 User 对象 response.sendRedirect(index.jsp);但在index.jsp中% session.getAttribute(user) %却输出null。排查顺序检查response.sendRedirect()是否跨域sendRedirect(index.jsp)是相对路径等价于sendRedirect(/goods/index.jsp)同域没问题但如果写成sendRedirect(http://other-domain.com/index.jsp)Session 就丢失确认web.xml中session-config未禁用 Cookiesession-config cookie-http-onlytrue/cookie-http-only !-- 没有 tracking-mode 或设为 COOKIE表示允许 Cookie 跟踪 -- /session-config浏览器禁用了第三方 Cookie开发时用 Chrome 无痕模式测试确保未开启 “阻止第三方 Cookie”。4. 把ProductDao从 JDBC 拆出来用 Apache Commons DbUtils 重构 DAO 层减少 60% 模板代码4.1 原生 JDBC 的痛点ResultSet关闭、PreparedStatement设置、异常转换全是重复劳动原ProductDao.java中一个查询方法长这样public ListProduct findAll() throws SQLException { Connection conn null; PreparedStatement stmt null; ResultSet rs null; ListProduct list new ArrayList(); try { conn JdbcUtil.getConnection(); // 自定义工具类获取连接 String sql SELECT * FROM products; stmt conn.prepareStatement(sql); rs stmt.executeQuery(); while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); p.setDescription(rs.getString(description)); list.add(p); } } finally { JdbcUtil.close(rs, stmt, conn); // 手动关闭极易遗漏 } return list; }问题在于每个方法都要写try-finally关闭资源ResultSet字段名硬编码改表结构就得改 Java 代码getXXX()方法调用分散无法统一处理NULL值。4.2 引入 DbUtils三步替换让 DAO 方法瘦身 70%步骤 1添加 Maven 依赖或复制 JAR 到WEB-INF/libdependency groupIdcommons-dbutils/groupId artifactIdcommons-dbutils/artifactId version1.7/version /dependency步骤 2重写findAll()方法无 try-catch无手动关闭private QueryRunner qr new QueryRunner(); // 无需手动管理连接 public ListProduct findAll() throws SQLException { String sql SELECT * FROM products; // BeanHandler 自动将 ResultSet 映射到 Product 对象 return qr.query(JdbcUtil.getConnection(), sql, new BeanListHandler(Product.class)); }步骤 3利用BasicRowProcessor处理字段名不一致如果数据库字段是product_name而 Java 类属性是nameDbUtils 默认不匹配。需自定义处理器private static final BasicRowProcessor processor new BasicRowProcessor(new GenerousBeanProcessor()); public ListProduct findAll() throws SQLException { String sql SELECT id, product_name AS name, price, stock, description FROM products; return qr.query(JdbcUtil.getConnection(), sql, new BeanListHandler(Product.class, processor)); }参数说明BeanListHandler将ResultSet每行映射为一个 JavaBean 实例并返回ListGenerousBeanProcessor支持下划线转驼峰product_name→productName比默认BeanProcessor更宽容qr.query()内部自动关闭Connection、Statement、ResultSet无需finally块。4.3 更新web.xml中的 Servlet 映射从*.do到/api/*为 RESTful 风格铺路原web.xml中所有 Servlet 映射都是*.do如/Login.do这是 Struts 1 的遗留风格。现在改为更现代的路径前缀!-- 替换前 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/Login.do/url-pattern /servlet-mapping !-- 替换后 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/api/login/url-pattern /servlet-mapping同时更新login.jsp表单form action/goods/api/login methodpost !-- 注意context path api path --好处路径语义清晰/api/login表示这是一个认证接口便于未来用 Filter 统一处理/api/*下的所有请求如鉴权、日志与前端 Vue/React 的 axios 请求路径保持一致降低联调成本。5. 避坑指南IDEA Tomcat MySQL 组合下的 5 个真实翻车现场与秒级修复方案5.1 现象启动 Tomcat 后控制台疯狂刷SEVERE: Error filterStart但没任何具体类名原因WEB-INF/web.xml中某个filter的filter-class路径写错或对应类未编译到classes/目录。此项目中无自定义 Filter但如果你后续添加了EncodingFilter却忘了在src/下创建filter/EncodingFilter.java就会触发此错误。解决打开web.xml逐行检查filter-class标签内容如filter.EncodingFilter确认WEB-INF/classes/filter/EncodingFilter.class文件存在。若不存在检查 IDEA 的Build → Build Project是否成功或手动javac -d WEB-INF/classes src/filter/EncodingFilter.java。5.2 现象index.jsp能打开但商品列表空白浏览器开发者工具 Network 标签页显示GET /goods/GetAllProductsServlet 404原因GetAllProductsServlet在web.xml中注册了但url-pattern写成了/GetAllProducts少了个Servlet后缀而index.jsp中 AJAX 请求地址是/goods/GetAllProductsServlet。解决统一路径。要么改web.xmlurl-pattern/GetAllProductsServlet/url-pattern要么改index.jsp中的 JSfetch(/goods/GetAllProducts) // 去掉 Servlet 后缀原则路径以web.xml为准前端必须严格匹配。5.3 现象登录成功后跳转到index.jsp但顶部欢迎文字显示欢迎, null原因LoginServlet中session.setAttribute(user, user)执行了但index.jsp中session.getAttribute(user)返回null根本原因是response.sendRedirect()导致 Session ID 未传递。解决确认sendRedirect()的路径是相对路径index.jsp而非绝对路径/index.jsp。前者会继承当前请求的 Session ID后者会新建一个 Session。另外检查web.xml中session-config是否设置了tracking-modeURL/tracking-mode这会强制用 URL 重写传 Session ID但本项目未启用应保持默认COOKIE模式。5.4 现象修改ProductDao.java后重新编译index.jsp仍显示旧数据重启 Tomcat 也不生效原因IDEA 的Build Project未将新.class文件输出到WEB-INF/classes而是输出到了out/production/...目录而 Tomcat 部署的是WebContent下的WEB-INF/classes。解决File → Project Structure → Modules → [你的模块] → Sources确认src目录被标记为SourcesProject Settings → Project → Project compiler output设为商品管理系统/WEB-INF/classes手动删除WEB-INF/classes下所有.class文件再Build → Rebuild Project。5.5 现象MySQL 连接成功但执行INSERT INTO products (...) VALUES (...)报错Data truncation: Data too long for column description at row 1原因products.description字段在建表时定义为TEXT但实际插入的字符串超过 65535 字节MySQL TEXT 最大长度。此项目README.md中建表语句未指定description长度而某些 MySQL 版本默认TEXT为MEDIUMTEXT但驱动解析时仍按TEXT处理。解决修改建表语句明确指定类型ALTER TABLE products MODIFY description LONGTEXT; -- 支持 4GB -- 或更保守地 ALTER TABLE products MODIFY description MEDIUMTEXT; -- 支持 16MB然后清空表重新插入测试数据。6. 从“能跑”到“好维护”给ProductService加单元测试 日志追踪让每次 debug 不再靠System.out.println6.1 用 JUnit 5 H2 内存数据库做 Service 层隔离测试不碰真实 MySQLProductService依赖ProductDao而ProductDao依赖真实数据库。为了测试addProduct()逻辑如价格校验、库存非负我们用 H2 替换 MySQL步骤 1添加测试依赖pom.xmldependency groupIdcom.h2database/groupId artifactIdh2/artifactId version2.2.224/version scopetest/scope /dependency dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.0/version scopetest/scope /dependency步骤 2编写ProductServiceTest.javaimport org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.*; class ProductServiceTest { private ProductService productService; private ProductDao mockDao; // 用 Mockito 模拟 DAO避免真实 DB BeforeEach void setUp() { mockDao Mockito.mock(ProductDao.class); productService new ProductService(mockDao); } Test void addProduct_shouldThrowExceptionWhenPriceIsNegative() { // 给定价格为负 Product product new Product(); product.setName(Test); product.setPrice(new BigDecimal(-10.00)); product.setStock(100); // 当调用 addProduct Exception exception assertThrows(IllegalArgumentException.class, () - { productService.addProduct(product); }); // 那么抛出特定异常 assertTrue(exception.getMessage().contains(价格不能为负数)); } Test void addProduct_shouldCallDaoWhenValid() { // 给定合法商品 Product product new Product(); product.setName(Valid Product); product.setPrice(BigDecimal.TEN); product.setStock(50); // 当调用 addProduct productService.addProduct(product); // 那么DAO 的 insert 方法被调用一次 Mockito.verify(mockDao, Mockito.times(1)).insert(product); } }关键点BeforeEach确保每个测试用例前重建ProductService实例避免状态污染Mockito.mock()创建ProductDao的模拟对象verify()断言方法调用次数完全隔离数据库assertThrows()验证业务规则价格非负是否被正确拦截。6.2 在LogFilter中注入 SLF4J记录每次请求的 URI、耗时、状态码创建filter/LogFilter.javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class LogFilter implements Filter { private static final Logger logger LoggerFactory.getLogger(LogFilter.class); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; long start System.currentTimeMillis(); chain.doFilter(request, response); HttpServletResponse resp (HttpServletResponse) response; long cost System.currentTimeMillis() - start; logger.info({} {} {}ms {}, req.getMethod(), req.getRequestURI(), cost, resp.getStatus()); } }并在web.xml中注册filter filter-nameLogFilter/filter-name filter-classfilter.LogFilter/filter-class /filter filter-mapping filter-nameLogFilter/filter-name url-pattern/*/url-pattern /filter-mapping效果启动后控制台输出类似INFO LogFilter - GET /goods/index.jsp 12ms 200 INFO LogFilter - POST /goods/api/login 87ms 200 INFO LogFilter - GET /goods/api/products 45ms 200提示SLF4J 是门面底层绑定 Logbacklogback.xml在src/main/resources下。若没配置会回退到NOPLoggerFactory不输出日志——务必确认WEB-INF/classes/logback.xml存在且内容正确。6.3 用WebServlet替代web.xml配置让 Servlet 注册从 XML 迁移到注解减少配置漂移风险将LoginServlet改为注解驱动WebServlet(urlPatterns /api/login, loadOnStartup 1) public class LoginServlet extends HttpServlet { // 原有 doPost/doGet 逻辑不变 }同时删除web.xml中对应的servlet和servlet-mapping块。优势Servlet 类和其路径绑定在同一文件修改路径时不会漏改web.xmlloadOnStartup 1仍保证启动时初始化符合 Servlet 3.0 规范IDEA 和 Tomcat 9 原生支持。从那以后我每次接手一个 JavaWeb 项目第一件事就是打开web.xml数servlet个数再 grep 所有WebServlet确保两者不共存——配置漂移是线上故障的温床。现在这个商品管理系统我已经把它拆成了 DAODbUtils、ServiceJUnit 测试覆盖、Servlet注解驱动、FilterSLF4J 日志每一层都可独立验证、可替换、可监控。它不再是一个“能跑就行”的毕设代码而是一份能让你看清 JavaWeb 底层脉络的活体教材。希望帮到你。本文还有配套的精品资源点击获取