☰
农产品销售管理系统实战:JSP+Tomcat+MySQL从环境搭建到部署排错
2026/10/7 19:02:18 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生与Java Web初学者的一套农产品销售管理系统完整项目,基于Java、JSP与Tomcat技术栈,配合MySQL数据库,采用B/S架构实现,适合用作课程设计、毕业设计或相关项目实战参考。压缩包整体约95.93MB,内含项目全套源码与完整文档,源码均经过测试校正,可保证百分百成功运行,方便读者直接部署与二次开发。系统围绕农产品销售业务展开,涵盖商品管理、订单处理、用户权限等典型模块,能帮助读者理解JSP页面与后端逻辑的协作方式、数据库表结构设计以及Tomcat部署流程。目前已有1440人浏览学习,说明该方案在同类项目中具有一定参考价值。对于需要快速搭建Web项目、梳理B/S架构开发思路或准备答辩材料的读者,这份源码加文档的组合能提供从代码实现到设计说明的完整支撑,便于对照学习与查漏补缺。

1. 农产品销售管理系统:从JSP+Tomcat到MySQL的完整落地路径

很多做Java Web课设或毕设的开发者,第一次拿到“农产品销售管理系统”这个题目时,脑子里想的都是功能列表:用户注册、商品展示、购物车、订单管理。但真正动手才发现,卡住你的从来不是业务逻辑,而是Tomcat启动报错、JSP页面中文乱码、MySQL连接池配置这些“玄学”问题。这套基于Java+JSP+Tomcat+MySQL的技术栈,虽然看起来是十几年前的老组合,但在教学、课设、小型企业内部工具场景里依然是最稳的选择——不需要Spring Boot的自动配置黑盒,每一层都看得见摸得着,出了问题能顺着调用链一路排查到底。这篇文章面向的是需要从零搭建一套可运行农产品销售系统的开发者,不管你是刚学完Java基础的新手,还是想快速复现一套BS架构项目的熟手,下面会从环境搭建、数据库设计、核心功能实现到部署排错,把每个环节的参数和坑都讲清楚。

2. 环境搭建:JDK、Tomcat、MySQL三件套的版本匹配与配置

2.1 为什么这套技术栈对版本匹配如此敏感

JSP+Servlet+Tomcat的组合有一个特点:Tomcat的版本直接决定了你能用哪个版本的Servlet规范和JSP规范,而JDK版本又决定了Tomcat能不能跑起来。常见做法是锁定JDK 8 + Tomcat 8.5或9.0 + MySQL 5.7或8.0这个组合,原因很简单——JDK 8是Tomcat 8.5/9.0官方支持最充分的版本,MySQL 5.7和8.0的JDBC驱动在JDK 8下兼容性最好。如果你用JDK 17去跑Tomcat 8.5,大概率会遇到UnsupportedClassVersionError,因为Tomcat 8.5的部分类是用JDK 7编译的,反过来JDK 8跑Tomcat 10也会出问题,因为Tomcat 10把javax.servlet包名改成了jakarta.servlet,你项目里所有import javax.servlet.*的代码全部要改。

我一般会建议在项目根目录放一个环境版本.txt,把三个版本号写死,避免换机器后重新踩一遍版本坑。

2.2 JDK安装与环境变量配置

JDK 8的安装本身不复杂,但环境变量配错会导致java命令能用而javac不能用,或者Tomcat启动时找不到JAVA_HOME。下面是在Windows 10上的标准配置步骤:

# 假设JDK安装在 D:\dev\jdk1.8.0_301 # 新建系统变量 JAVA_HOME = D:\dev\jdk1.8.0_301 CLASSPATH = .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar # 编辑Path变量,追加 %JAVA_HOME%\bin %JAVA_HOME%\jre\bin

配置完成后打开新的cmd窗口验证:

java -version # 应输出 java version "1.8.0_301" javac -version # 应输出 javac 1.8.0_301 echo %JAVA_HOME% # 应输出 D:\dev\jdk1.8.0_301

这里有个细节:CLASSPATH最前面的.不能省,它代表当前目录,否则你编译一个.java文件后运行.class会提示找不到主类。另外修改环境变量后必须开新窗口,旧窗口读的还是旧值。

2.3 Tomcat下载、安装与IDEA集成

Tomcat不需要安装,解压即用。下载地址去Apache官网找apache-tomcat-9.0.x-windows-x64.zip,解压到D:\dev\apache-tomcat-9.0.xx。目录结构里需要关注的是conf/server.xml(端口配置)、webapps/(部署目录)、logs/(排错看这里)。

在IDEA里配置Tomcat的步骤:

  1. Run→Edit Configurations→+→Tomcat Server→Local
  2. Application server处点Configure...,选择Tomcat解压目录
  3. Deployment标签页 →+→Artifact→ 选择xxx:war exploded
  4. Application context设为/farm(这样访问路径就是http://localhost:8080/farm/)

注意:如果IDEA 2026版本里找不到Tomcat Server选项,需要先在Settings→Plugins里确认Smart Tomcat或Tomcat and TomEE Integration插件已启用。新版IDEA把Tomcat集成拆成了独立插件。

2.4 MySQL 8.0安装与农产品销售系统数据库初始化

MySQL 8.0在Windows上的安装推荐用MSI安装包,自定义安装时记得勾选MySQL Server和MySQL Workbench。安装过程中设置的root密码要记牢,后面JDBC连接串里要用。

安装完成后验证服务:

net start mysql # 如果提示服务名无效,说明MySQL服务没注册,需要手动初始化 # 进入MySQL的bin目录执行: mysqld --initialize-insecure --user=mysql mysqld --install MySQL net start MySQL

数据库初始化脚本的核心是建库、建表、插入测试数据。农产品销售系统至少需要这几张表:用户表、农产品分类表、农产品信息表、订单表、订单明细表。下面给出建库和核心表的DDL:

CREATE DATABASE farm_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE farm_sale; -- 用户表:区分普通用户和管理员 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 农产品表 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image_url VARCHAR(255), description TEXT, status TINYINT DEFAULT 1 COMMENT '1-上架 0-下架' ) ENGINE=InnoDB; -- 订单表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT '0-待付款 1-已付款 2-已发货 3-已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;

字符集用utf8mb4而不是utf8,因为农产品名称里可能出现生僻字或emoji,utf8在MySQL里实际是utf8mb3,只支持3字节字符。JDBC连接串里也要对应写characterEncoding=utf8,驱动会自动协商。

3. 农产品销售系统核心功能:从JSP页面到Servlet的请求链路

3.1 项目目录结构与MVC分层

一个能跑通的JSP项目,目录结构必须清晰,否则后面加功能会乱成一锅粥。我一般用这样的结构:

farm-sale/ ├── src/ │ └── com/farm/ │ ├── dao/ # 数据库操作层 │ ├── entity/ # 实体类 │ ├── servlet/ # 控制层 │ └── util/ # 工具类(DBUtil等) ├── web/ │ ├── css/ │ ├── js/ │ ├── images/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ # 放mysql-connector-java.jar │ ├── index.jsp │ ├── product_list.jsp │ └── cart.jsp

WEB-INF目录下的文件不能被浏览器直接访问,所以web.xml和第三方jar放这里最安全。JSP页面放外面,Servlet通过request.getRequestDispatcher()转发。

3.2 数据库连接工具类:避免每次请求都新建连接

新手最容易犯的错是在每个Servlet里写DriverManager.getConnection(),请求一多数据库就扛不住。正确做法是写一个DBUtil,用静态代码块加载驱动,并提供获取和关闭连接的方法:

package com.farm.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/farm_sale?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { 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(); } } }

连接串里的serverTimezone=Asia/Shanghai必须加,否则MySQL 8.0会报The server time zone value '?D1ú±ê×?ê±??' is unrecognized。useSSL=false是关掉SSL警告,本地开发不需要加密连接。驱动类名从MySQL 8.0开始是com.mysql.cj.jdbc.Driver,旧版的com.mysql.jdbc.Driver已经废弃。

3.3 农产品列表页:JSP如何从Servlet拿数据并渲染

以农产品列表为例,请求链路是:浏览器访问/product/list→ProductListServlet处理 → 调用ProductDao查数据库 → 把List存到request → 转发到product_list.jsp→ JSP用JSTL遍历渲染。

Servlet代码:

package com.farm.servlet; import com.farm.dao.ProductDao; import com.farm.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 ProductDao productDao = new ProductDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置编码,防止中文参数乱码 req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); String keyword = req.getParameter("keyword"); List<Product> products; if (keyword != null && !keyword.trim().isEmpty()) { products = productDao.searchByName(keyword.trim()); } else { products = productDao.findAllOnSale(); } req.setAttribute("products", products); req.getRequestDispatcher("/product_list.jsp").forward(req, resp); } }

JSP页面用JSTL的c:forEach渲染:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>农产品列表</title></head> <body> <h2>农产品列表</h2> <form action="${pageContext.request.contextPath}/product/list" method="get"> <input type="text" name="keyword" placeholder="搜索农产品"> <button type="submit">搜索</button> </form> <table border="1"> <tr><th>名称</th><th>价格</th><th>库存</th><th>操作</th></tr> <c:forEach items="${products}" var="p"> <tr> <td>${p.name}</td> <td>${p.price}</td> <td>${p.stock}</td> <td><a href="${pageContext.request.contextPath}/cart/add?id=${p.id}">加入购物车</a></td> </tr> </c:forEach> </table> </body> </html>

${pageContext.request.contextPath}是获取项目上下文路径,这样部署到不同路径下链接都不会失效。JSTL的jar包(jstl.jar和standard.jar)要放到WEB-INF/lib下,否则c:forEach标签不生效。

3.4 购物车与订单:Session存储与事务处理

购物车用Session存最方便,结构是Map<Integer, CartItem>,key是商品ID,value是商品信息和数量。加入购物车的Servlet:

@WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int productId = Integer.parseInt(req.getParameter("id")); HttpSession session = req.getSession(); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } if (cart.containsKey(productId)) { cart.get(productId).setQuantity(cart.get(productId).getQuantity() + 1); } else { Product p = new ProductDao().findById(productId); cart.put(productId, new CartItem(p, 1)); } resp.sendRedirect(req.getContextPath() + "/cart.jsp"); } }

下单时要把购物车数据写入t_order和t_order_item两张表,必须用事务保证一致性:

public boolean createOrder(int userId, Map<Integer, CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表 String orderSql = "INSERT INTO t_order(user_id, total_price, status) VALUES(?,?,0)"; PreparedStatement ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, userId); ps.setBigDecimal(2, calcTotal(cart)); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); rs.next(); int orderId = rs.getInt(1); // 2. 插入订单明细 String itemSql = "INSERT INTO t_order_item(order_id, product_id, quantity, price) VALUES(?,?,?,?)"; ps = conn.prepareStatement(itemSql); for (CartItem item : cart.values()) { ps.setInt(1, orderId); ps.setInt(2, item.getProduct().getId()); ps.setInt(3, item.getQuantity()); ps.setBigDecimal(4, item.getProduct().getPrice()); ps.addBatch(); } ps.executeBatch(); // 3. 扣减库存 String stockSql = "UPDATE t_product SET stock = stock - ? WHERE id = ? AND stock >= ?"; ps = conn.prepareStatement(stockSql); for (CartItem item : cart.values()) { ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getProduct().getId()); ps.setInt(3, item.getQuantity()); ps.addBatch(); } int[] results = ps.executeBatch(); for (int r : results) { if (r == 0) throw new SQLException("库存不足"); } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn != null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

扣库存的SQL里AND stock >= ?是关键,它保证了并发下不会超卖——如果库存不够,executeBatch返回的影响行数为0,直接抛异常回滚。这个写法比先查再扣更安全,因为查和扣之间可能有其他请求插进来。

4. 避坑与排查:Tomcat、JSP、MySQL的高频翻车现场

4.1 Tomcat启动报错Port 8080 already in use

现象:IDEA里启动Tomcat,控制台红字提示Address already in use: JVM_Bind。

原因:8080端口被其他进程占用了,常见的是之前没关干净的Tomcat进程,或者装了Oracle数据库(Oracle的XDB服务占8080)。

解决:先找到占用端口的进程:

netstat -ano | findstr :8080 # 输出最后一列是PID,比如 12345 taskkill /F /PID 12345

如果不想杀进程,就改Tomcat端口。打开conf/server.xml,找到<Connector port="8080",改成8081或别的空闲端口。改完在IDEA里重新部署,访问路径也要跟着改。

4.2 JSP页面中文乱码的三种场景

现象:页面显示??????或者测试。

原因分三种:一是JSP文件本身的编码不是UTF-8;二是Servlet没有设置response.setContentType("text/html;charset=UTF-8");三是GET请求的参数编码没处理。

解决:JSP文件头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;Servlet里doGet和doPost第一行都加req.setCharacterEncoding("UTF-8")和resp.setContentType("text/html;charset=UTF-8");如果是Tomcat 8.5以下,GET请求的中文参数还需要在server.xml的Connector里加URIEncoding="UTF-8"。Tomcat 8.5及以上默认就是UTF-8,不用改。

4.3 MySQL 8.0驱动ClassNotFoundException

现象:启动项目后访问数据库相关页面,报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。

原因:mysql-connector-java的jar包没放到WEB-INF/lib目录下,或者放的是旧版驱动但代码里写的新类名。

解决:确认WEB-INF/lib下有mysql-connector-java-8.0.xx.jar。如果用的是Maven,在pom.xml里加依赖:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

注意scope不要写provided,否则打包时不会带进war。另外IDEA的Project Structure→Artifacts里要确认WEB-INF/lib下确实包含了这个jar,有时候Maven依赖更新了但Artifact没重新构建。

4.4 JSP中图片路径404的排查思路

现象:农产品列表页的图片全部显示裂图,F12看Network里图片请求是404。

原因:图片放在web/images/下,但JSP里写的路径是相对路径images/xxx.jpg,而当前页面URL可能是/farm/product/list,浏览器会去/farm/product/images/xxx.jpg找,实际路径不对。

解决:统一用${pageContext.request.contextPath}/images/xxx.jpg拼绝对路径。如果图片是用户上传的,不要传到项目目录里,因为重新部署war包会覆盖掉。正确做法是配一个外部目录,在Tomcat的conf/server.xml里加<Context docBase="D:/farm_upload" path="/upload"/>,JSP里用/upload/xxx.jpg访问。

4.5 事务未回滚导致的数据不一致

现象:下单时订单主表插入了,但明细表没插入,或者库存扣了但订单没生成。

原因:Connection没有关掉自动提交,或者异常被catch了但没有rollback。

解决:严格按照3.4节的模板写,conn.setAutoCommit(false)之后,所有数据库操作放在try块里,catch块里第一件事就是conn.rollback()。另外注意PreparedStatement和ResultSet的关闭顺序,先关ResultSet再关PreparedStatement最后关Connection,否则可能报Operation not allowed after ResultSet closed。

5. 进阶技巧:用Filter统一编码与登录拦截,少写重复代码

项目写到后面你会发现,每个Servlet里都写一遍req.setCharacterEncoding("UTF-8")和登录检查,代码重复且容易漏。用Filter可以一次性解决。

编码Filter:

@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }

登录拦截Filter:

@WebFilter("/admin/*") public class AdminAuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

@WebFilter("/*")表示拦截所有请求,@WebFilter("/admin/*")只拦截后台路径。多个Filter的执行顺序在web.xml里按<filter-mapping>的声明顺序,用注解的话顺序不确定,所以如果编码Filter和登录Filter有依赖关系,建议在web.xml里显式配置。

验证Filter是否生效的方法:在doFilter里加一行System.out.println("Filter拦截: " + request.getRequestURI()),启动后访问页面看控制台输出。如果没输出,检查web.xml的metadata-complete属性是不是true,这个属性为true时注解不生效。

最后说一个我自己的习惯:每次改完数据库连接串或者Tomcat配置,先重启Tomcat再测,不要热部署。JSP热部署有时候会缓存旧的编译结果,导致你改了代码但页面没变,白白浪费半小时排查一个根本不存在的bug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询