news 2026/9/4 2:07:30

Java JSP+Servlet+Tomcat构建农产品销售系统:从数据库设计到部署实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java JSP+Servlet+Tomcat构建农产品销售系统:从数据库设计到部署实战

简介:本资源是一套基于Java Web技术栈开发的农产品销售管理系统完整实现方案,面向计算机专业本科生、Java初学者及Web开发入门者,解决农产品流通环节中信息管理低效、订单跟踪困难、库存协同滞后等实际问题。压缩包共含全套可运行源码与配套文档,主要文件类型包括JSP前端页面、Servlet业务逻辑类、JavaBean数据模型、MySQL建表脚本及系统设计说明书,涵盖用户管理、商品发布、订单处理、库存统计等核心模块,整体大小为95.93MB,结构清晰、注释完整,便于理解MVC分层架构与BS模式开发流程。已有1438人学习下载,所有代码均经Tomcat环境实测验证,启动即用,附带详细部署说明与数据库配置指南,特别适合课程设计、毕业设计及小型农业电商项目快速原型开发。

1. 项目概述:一个典型的BS架构农产品销售系统

最近在整理过往项目时,翻到了一个挺有代表性的老项目——一个基于Java+JSP+Tomcat的农产品销售管理系统。这类系统在农业信息化、县域电商、合作社管理等领域其实非常普遍,核心目标就是把线下的农产品进销存、订单、客户管理搬到线上,实现流程化和数据化。虽然现在微服务、Spring Boot满天飞,但这种经典的JSP+Servlet+Tomcat的BS架构,对于理解Web应用从数据库到前端的完整链路,尤其是对于在校学生、初级开发者或者需要快速构建内部管理工具的小型团队来说,依然有很高的学习价值和实用意义。

这个系统本质上是一个B/S架构的管理后台。用户通过浏览器访问,请求发送到部署在Tomcat服务器上的Java Web应用,应用通过JDBC与后端的MySQL数据库交互,完成数据的增删改查,最后将动态生成的JSP页面返回给浏览器展示。整个技术栈非常“经典”:Java负责业务逻辑,JSP负责视图展示,Tomcat作为Servlet容器,MySQL存储所有业务数据。它要解决的核心问题,就是如何高效、清晰地将农产品从供应商录入、库存管理、订单处理到销售统计这一整套流程数字化。接下来,我会结合这个项目的设计与实现,拆解其中的关键技术和实操要点。

2. 系统整体设计与架构拆解

2.1 为什么选择JSP+Servlet+Tomcat这套“经典”组合?

在动手之前,技术选型是首要问题。现在可能很多人第一反应是Spring MVC甚至Spring Boot。但对于一个目标明确、业务相对固定的管理系统,尤其是作为学习或中小型项目原型,经典的JSP+Servlet模式有其独特的优势。

核心考量在于简单直接与学习成本。JSP允许我们在HTML中直接嵌入Java代码(虽然不推荐大量使用Scriptlet),对于前端展示逻辑简单的管理后台来说,开发速度很快。Servlet作为控制器,能清晰地处理HTTP请求和响应。Tomcat作为轻量级、开源且应用广泛的Servlet容器,部署和配置都非常简单。这套组合就像手动挡汽车,虽然不如自动挡(现代框架)便捷,但能让你透彻理解HTTP请求如何被接收、如何被分发到对应的Servlet、如何与数据库交互、以及最终如何渲染页面返回的每一个环节。这对于夯实Java Web开发基础至关重要。

架构模式上,我们采用了最经典的Model 1向Model 2过渡的架构。严格来说,没有使用完整的MVC框架,但我们在实践中进行了职责分离:

  • 模型层:由JavaBean(实体类,如Product,Order,User)和数据库访问对象(DAO,如ProductDaoImpl)组成,负责封装业务数据和数据持久化逻辑。
  • 视图层:由JSP页面担任,负责数据的展示。我们会尽量使用JSTL标签和EL表达式来替代<% ... %>脚本,让页面更清晰。
  • 控制器层:由Servlet担任。每个核心业务模块(如产品管理、订单管理)通常对应一个或多个Servlet,例如ProductServletOrderServlet。它们在web.xml中配置URL映射,负责接收请求、调用相应的DAO进行业务处理、将结果数据存入请求或会话作用域,最后转发到指定的JSP页面进行渲染。

这种设计虽然不如Spring MVC那样通过注解自动映射优雅,但结构清晰,特别适合初学者理解Web请求的生命周期。

2.2 数据库设计与核心表结构解析

任何管理系统的核心都是数据。农产品销售系统的数据库设计需要紧扣业务流。这里我给出几个最核心的表结构设计,并解释其关联和字段设计的考量。

1. 产品信息表这是系统的基石,存储所有农产品的详细信息。

CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '产品ID', `name` varchar(100) NOT NULL COMMENT '产品名称', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `supplier_id` int(11) DEFAULT NULL COMMENT '供应商ID', `specification` varchar(200) DEFAULT NULL COMMENT '规格(如:500g/盒)', `price` decimal(10,2) NOT NULL COMMENT '单价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量', `description` text COMMENT '产品描述', `image_url` varchar(500) DEFAULT NULL COMMENT '产品图片路径', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_supplier` (`supplier_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品信息表';

设计要点:

  • price字段使用DECIMAL(10,2),确保金额计算的精确性,避免浮点数误差。
  • stock库存是关键字段,所有出入库操作都必须原子性地更新它,后续在高并发场景下需要考虑锁或乐观锁机制。
  • image_url只存储图片在服务器上的相对路径,而不是将图片以BLOB形式存入数据库,这是为了提升数据库性能和便于CDN分发。
  • 建立了分类和供应商的外键索引,加速关联查询。

2. 订单主表与订单明细表这是典型的“主-子”表结构,一个订单头对应多个订单项。

CREATE TABLE `order` ( `order_id` varchar(32) NOT NULL COMMENT '订单号(可自定义规则生成)', `user_id` int(11) NOT NULL COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1待付款 2待发货 3已发货 4已完成 5已取消', `payment_method` varchar(20) DEFAULT NULL COMMENT '支付方式', `shipping_address` text COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`order_id`), KEY `idx_user` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; CREATE TABLE `order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` varchar(32) NOT NULL COMMENT '订单号', `product_id` int(11) NOT NULL COMMENT '产品ID', `quantity` int(11) NOT NULL COMMENT '购买数量', `unit_price` decimal(10,2) NOT NULL COMMENT '下单时单价', `subtotal` decimal(10,2) NOT NULL COMMENT '小计金额', PRIMARY KEY (`id`), KEY `idx_order` (`order_id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

设计要点:

  • order_id没有使用自增ID,而是使用自定义规则生成的字符串(如时间戳+随机数),这样更便于业务展示和沟通,也避免了自增ID可能暴露的业务量信息。
  • status字段使用枚举值,清晰定义订单生命周期。所有状态变更必须有严格的业务逻辑控制。
  • order_item表中存储了unit_price,这是非常重要的“快照”设计。产品价格可能会变动,但订单历史价格必须定格在交易发生时,不能随着产品主表价格改变而改变。
  • subtotal是冗余字段,由quantity * unit_price计算得出。虽然可以通过计算得到,但存储它有利于提高统计查询效率,是一种典型的“空间换时间”策略。

3. 用户表与库存流水表用户表相对标准,库存流水表则是保证库存准确性的关键审计表。

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL DEFAULT 'CUSTOMER' COMMENT '角色:ADMIN, SUPPLIER, CUSTOMER', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `inventory_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '产品ID', `change_quantity` int(11) NOT NULL COMMENT '变动数量(正为入库,负为出库)', `current_stock` int(11) NOT NULL COMMENT '变动后实时库存', `type` varchar(20) NOT NULL COMMENT '类型:PURCHASE(采购入库), SALES(销售出库), ADJUST(手动调整)', `order_id` varchar(32) DEFAULT NULL COMMENT '关联订单号(如果是销售出库)', `operator_id` int(11) NOT NULL COMMENT '操作员ID', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_product` (`product_id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';

设计要点:

  • user表的password字段必须存储加密后的字符串,绝对禁止明文存储。通常使用BCrypt或PBKDF2等强哈希算法。
  • inventory_log表是系统的“账本”。任何库存变动,无论是采购、销售还是盘点调整,都必须先更新product表的stock字段,然后同步插入一条流水记录。这个“双写”操作必须在同一个数据库事务中完成,以保证数据一致性。通过这个表,可以追溯任意时间点的库存变动情况,对账和排查问题非常有用。

注意:数据库设计时,字段注释一定要写清楚。utf8mb4字符集可以支持完整的UTF-8,包括表情符号。为经常用于查询条件的字段(如外键、状态字段)建立索引,但索引不是越多越好,会影响写性能。

3. 核心模块实现与关键技术点

3.1 使用Servlet处理核心业务请求

Servlet是整个系统的交通枢纽。以处理产品列表查询和新增产品的ProductServlet为例,我们通常通过doGetdoPost方法来区分请求。

// ProductServlet.java @WebServlet("/product") // 使用注解配置URL映射,替代web.xml配置 public class ProductServlet extends HttpServlet { private ProductDao productDao = new ProductDaoImpl(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { listProducts(request, response); } else if ("edit".equals(action)) { showEditForm(request, response); } else { // 默认展示列表 listProducts(request, response); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); // 关键!解决POST中文乱码 String action = request.getParameter("action"); if ("save".equals(action)) { saveProduct(request, response); } else if ("delete".equals(action)) { deleteProduct(request, response); } } private void listProducts(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取查询参数(如分类、关键词) String keyword = request.getParameter("keyword"); // 2. 调用DAO查询数据 List<Product> productList = productDao.findAll(keyword); // 3. 将数据存入请求作用域 request.setAttribute("productList", productList); // 4. 转发到JSP页面 RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/product/list.jsp"); dispatcher.forward(request, response); } private void saveProduct(HttpServletRequest request, HttpServletResponse response) throws IOException { // 1. 获取表单参数 String name = request.getParameter("name"); String priceStr = request.getParameter("price"); // 2. 数据校验(非常重要!) if (name == null || name.trim().isEmpty()) { response.sendRedirect(request.getContextPath() + "/product?error=名称不能为空"); return; } // 3. 封装对象 Product product = new Product(); product.setName(name); product.setPrice(new BigDecimal(priceStr)); // ... 设置其他字段 // 4. 调用DAO保存 boolean success = productDao.save(product); // 5. 重定向到列表页(防止表单重复提交) response.sendRedirect(request.getContextPath() + "/product?action=list&save=" + success); } }

实操要点与避坑指南:

  1. URL设计:采用类似/product?action=list或RESTful风格/products(需额外过滤器)的方式。前者简单直观,后者更现代但需要更多配置。
  2. 中文乱码:这是新手必坑点。对于POST请求,必须在doPost方法第一行设置request.setCharacterEncoding("UTF-8")。对于GET请求,Tomcat 8.5以上版本默认URI编码是UTF-8,如果遇到乱码,可能需要修改Tomcat的server.xml<Connector>URIEncoding="UTF-8"属性。响应编码通常在JSP页面头部通过<%@ page contentType="text/html;charset=UTF-8" language="java" %>设置。
  3. 请求转发与重定向:forward是服务器内部跳转,地址栏不变,请求域数据可共享。sendRedirect是客户端跳转,地址栏变化,相当于一次新请求。保存数据后务必使用重定向,否则用户刷新页面会导致表单重复提交。
  4. DAO层注入:上述代码直接在Servlet中new了DAO对象,在简单项目中可以,但不利于测试和解耦。更好的做法是使用一个简单的“服务定位器”或工厂模式来获取DAO实例,为将来引入Spring等IoC容器做准备。

3.2 JSP页面与JSTL/EL的最佳实践

JSP页面负责展示。我们要避免在JSP中编写大量Java代码(Scriptlet),而是使用EL表达式和JSTL标签库,使页面更清晰、更易于维护。

首先,需要在JSP头部引入JSTL核心库:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

产品列表页示例:

<!-- product/list.jsp --> <!DOCTYPE html> <html> <head> <title>产品管理</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/assets/css/bootstrap.css"> </head> <body> <div class="container mt-4"> <h2>产品列表</h2> <a href="${pageContext.request.contextPath}/product?action=edit" class="btn btn-primary mb-3">新增产品</a> <!-- 使用JSTL c:if进行条件判断 --> <c:if test="${not empty param.save && param.save == 'true'}"> <div class="alert alert-success">保存成功!</div> </c:if> <table class="table table-bordered table-striped"> <thead> <tr> <th>ID</th> <th>产品名称</th> <th>规格</th> <th>单价</th> <th>库存</th> <th>操作</th> </tr> </thead> <tbody> <!-- 使用JSTL c:forEach遍历产品列表 --> <c:forEach var="product" items="${productList}"> <tr> <td>${product.id}</td> <td>${product.name}</td> <td>${product.specification}</td> <!-- 使用JSTL fmt:formatNumber格式化金额 --> <td><fmt:formatNumber value="${product.price}" type="currency" currencySymbol="¥"/></td> <td> <span class="badge ${product.stock > 10 ? 'bg-success' : 'bg-warning'}"> ${product.stock} </span> </td> <td> <a href="${pageContext.request.contextPath}/product?action=edit&id=${product.id}" class="btn btn-sm btn-outline-primary">编辑</a> <a href="#" onclick="confirmDelete(${product.id})" class="btn btn-sm btn-outline-danger">删除</a> </td> </tr> </c:forEach> <!-- 使用JSTL c:if处理空列表 --> <c:if test="${empty productList}"> <tr><td colspan="6" class="text-center">暂无产品数据</td></tr> </c:if> </tbody> </table> </div> <script> function confirmDelete(id) { if (confirm('确定要删除这个产品吗?')) { window.location.href = '${pageContext.request.contextPath}/product?action=delete&id=' + id; } } </script> </body> </html>

最佳实践与技巧:

  1. 绝对路径:所有静态资源(CSS, JS, 图片)和链接地址,务必使用${pageContext.request.contextPath}作为前缀。这能确保应用部署在任何上下文路径下都能正确访问资源,避免404错误。
  2. 避免Scriptlet:如非必要(极简单的逻辑),不要使用<% ... %>。EL表达式${}和JSTL标签足以完成99%的展示逻辑。
  3. 格式化输出:使用<fmt:formatNumber><fmt:formatDate>标签来格式化金额、日期,提升用户体验。
  4. 前端框架:引入Bootstrap等前端UI框架可以极大加快页面开发速度,并保证基本的响应式和美观。将CSS/JS文件放在WebContent/assets/目录下统一管理。
  5. JSP放置位置:建议将所有的JSP文件放在/WEB-INF/views/目录下。因为WEB-INF下的内容不能被客户端直接访问,必须通过Servlet转发,这增加了一层安全控制,防止用户直接输入JSP地址访问。

3.3 数据库连接池与DAO模式实现

直接在每个Servlet中创建和关闭数据库连接是灾难性的,性能极差且容易耗尽连接。我们必须使用数据库连接池。这里以Apache DBCP2为例,结合DAO模式进行实现。

1. 配置数据库连接池WEB-INF/web.xml中配置一个监听器,用于初始化和销毁连接池。

<!-- web.xml 配置 --> <context-param> <param-name>dbUrl</param-name> <param-value>jdbc:mysql://localhost:3306/agriculture_sales?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai</param-value> </context-param> <context-param> <param-name>dbUser</param-name> <param-value>your_username</param-value> </context-param> <context-param> <param-name>dbPassword</param-name> <param-value>your_password</param-value> </context-param> <listener> <listener-class>com.yourproject.util.DBCPInitListener</listener-class> </listener>

然后实现这个监听器:

// DBCPInitListener.java public class DBCPInitListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { try { String url = sce.getServletContext().getInitParameter("dbUrl"); String user = sce.getServletContext().getInitParameter("dbUser"); String password = sce.getServletContext().getInitParameter("dbPassword"); BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl(url); dataSource.setUsername(user); dataSource.setPassword(password); // 连接池配置 dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxIdle(10); dataSource.setMinIdle(5); dataSource.setMaxWaitMillis(10000); // 获取连接超时时间 sce.getServletContext().setAttribute("dataSource", dataSource); System.out.println("数据库连接池初始化成功!"); } catch (Exception e) { throw new RuntimeException("初始化数据库连接池失败", e); } } @Override public void contextDestroyed(ServletContextEvent sce) { BasicDataSource ds = (BasicDataSource) sce.getServletContext().getAttribute("dataSource"); if (ds != null) { try { ds.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

2. 实现基础的DAO工具类创建一个工具类来统一处理连接的获取和释放,以及基本的CRUD模板。

// DBUtil.java public class DBUtil { public static Connection getConnection() throws SQLException { // 从ServletContext中获取连接池(需在Servlet中获取ServletContext) // 更优雅的方式是使用ThreadLocal或依赖注入,这里为简化,假设通过一个静态方法获取上下文 // 实际项目中,可以通过一个BaseServlet或过滤器来设置 ServletContext context = ...; // 如何获取ServletContext是关键,通常通过请求对象 DataSource dataSource = (DataSource) context.getAttribute("dataSource"); return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } // 这里是归还连接到连接池,不是真正关闭 catch (SQLException e) { e.printStackTrace(); } } }

重要提示:上述DBUtil.getConnection()方法是一个简化示例,直接静态获取连接在实际Web应用中存在问题,因为ServletContext无法在静态方法中轻易获得。更常见的做法是:在每个Servlet的init()方法中获取一次DataSource并保存为实例变量,或者使用一个ServiceLocator模式。为了更清晰,下面展示一个改进的、基于ThreadLocal的简单连接管理思路(伪代码),但请注意,在生产环境中,通常会使用框架管理事务。

3. 实现具体的DAO类

// ProductDao.java (接口) public interface ProductDao { List<Product> findAll(String keyword); Product findById(int id); boolean save(Product product); boolean update(Product product); boolean delete(int id); } // ProductDaoImpl.java (实现类) public class ProductDaoImpl implements ProductDao { // 假设通过某种方式获得了DataSource,例如构造函数注入 private DataSource dataSource; public ProductDaoImpl(DataSource dataSource) { this.dataSource = dataSource; } @Override public List<Product> findAll(String keyword) { List<Product> list = new ArrayList<>(); String sql = "SELECT * FROM product WHERE name LIKE ? ORDER BY id DESC"; // 使用try-with-resources自动关闭资源,Java 7+ try (Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, "%" + (keyword == null ? "" : keyword) + "%"); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { Product product = new Product(); product.setId(rs.getInt("id")); product.setName(rs.getString("name")); product.setPrice(rs.getBigDecimal("price")); product.setStock(rs.getInt("stock")); // ... 设置其他字段 list.add(product); } } } catch (SQLException e) { e.printStackTrace(); // 实际应记录日志或抛出自定义异常 } return list; } @Override public boolean save(Product product) { String sql = "INSERT INTO product (name, price, stock, specification) VALUES (?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, product.getName()); pstmt.setBigDecimal(2, product.getPrice()); pstmt.setInt(3, product.getStock()); pstmt.setString(4, product.getSpecification()); int affectedRows = pstmt.executeUpdate(); if (affectedRows > 0) { try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { if (generatedKeys.next()) { product.setId(generatedKeys.getInt(1)); } } return true; } } catch (SQLException e) { e.printStackTrace(); } return false; } // ... 其他方法实现 }

关键技术与避坑指南:

  1. 使用PreparedStatement:这是防止SQL注入攻击的铁律。绝对不要用字符串拼接来构造SQL语句。
  2. Try-with-Resources:从Java 7开始,强烈推荐使用此语法自动关闭Connection,Statement,ResultSet等资源,避免内存泄漏。
  3. 连接归还:注意,从连接池getConnection()获得的连接,在调用close()方法时,并不是真的关闭TCP连接,而是将其归还到连接池中。因此,务必确保在finally块或try-with-resources中关闭连接。
  4. 事务处理:对于涉及多个表更新的操作(如创建订单:扣库存、生成订单主表、插入订单明细),必须使用事务。在Servlet中,可以这样处理:
    Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 更新库存 // 2. 插入订单主表 // 3. 插入订单明细 conn.commit(); // 提交事务 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); // 处理异常,返回错误信息给用户 } finally { DBUtil.close(conn, null, null); }

4. 项目部署、优化与常见问题排查

4.1 本地开发环境搭建与Tomcat部署

开发环境:

  1. JDK:安装JDK 8或11(LTS版本),配置JAVA_HOME环境变量。
  2. IDE:推荐使用IntelliJ IDEA或Eclipse for Enterprise Java Developers。IDEA对Web开发支持更友好。
  3. Tomcat:从官网下载Tomcat 9.x或8.5.x版本。在IDEA中配置本地Tomcat服务器:Run -> Edit Configurations -> Add New Configuration -> Tomcat Server -> Local,指定Tomcat安装目录。
  4. MySQL:安装MySQL 5.7或8.0,使用root用户创建数据库agriculture_sales,并执行项目中的SQL脚本建表。

项目部署到独立Tomcat:

  1. 打包:在IDEA中,使用Build -> Build Artifacts...生成一个WAR包(例如agriculture_sales.war)。
  2. 部署:将WAR包复制到Tomcat的webapps目录下。
  3. 启动:运行Tomcat的bin/startup.bat(Windows)或bin/startup.sh(Linux/Mac)。
  4. 访问:打开浏览器,访问http://localhost:8080/agriculture_sales(假设你的WAR包名为agriculture_sales.war)。

注意:生产环境部署时,务必修改Tomcat的server.xml配置,将<Connector>port从8080改为80(如果需要),并考虑配置HTTPS。同时,数据库连接信息应使用JNDI数据源配置在Tomcat中,而不是硬编码在web.xml里,这样更安全、更易于管理。

4.2 性能优化与安全加固建议

一个可用的系统和一个健壮的系统之间,就差在优化和安全上。

性能优化:

  1. 数据库层面:
    • 索引优化:确保WHEREORDER BYJOIN条件上的字段有索引。但避免过度索引。
    • 查询优化:避免SELECT *,只取需要的字段。复杂查询使用EXPLAIN分析。
    • 连接池调优:根据实际并发量调整maxTotalmaxIdle等参数。
  2. 应用层面:
    • 页面静态化:对于不常变动的页面(如商品详情页),可以考虑生成静态HTML,减轻数据库和JSP编译压力。
    • 前端缓存:合理设置HTTP缓存头,对CSS、JS、图片等静态资源进行缓存。
    • 分页查询:所有列表接口必须支持分页,使用LIMIT offset, size,避免一次性加载海量数据。

安全加固:

  1. SQL注入:坚持使用PreparedStatement,已解决绝大部分问题。
  2. XSS跨站脚本攻击:在JSP中,使用JSTL的<c:out value="${userInput}"/>或EL表达式默认会对HTML进行转义。如果需要在页面中显示富文本,必须进行严格的白名单过滤。
  3. CSRF跨站请求伪造:为关键操作(如修改、删除、支付)的表单添加CSRF Token。可以在用户登录时生成一个Token存入Session,在表单中作为一个隐藏域提交,服务端进行校验。
  4. 会话安全:用户登录后,密码等敏感信息不要存入Session。Session应有合理的超时时间。可以考虑对Session ID进行轮换。
  5. 密码存储:使用BCryptPasswordEncoder等强哈希算法加盐存储密码,绝对禁止明文或弱加密(如MD5)。
  6. 文件上传:如果系统支持图片上传,必须限制文件类型(白名单)、检查文件头、重命名文件、并将文件存储在Web应用目录之外,通过Servlet提供访问。

4.3 开发与部署中的常见问题及解决方案

在实际开发和部署中,你几乎一定会遇到下面这些问题:

问题现象可能原因解决方案
启动Tomcat时报ClassNotFoundExceptionNoClassDefFoundError项目依赖的JAR包(如MySQL驱动、DBCP2)没有被打包到WEB-INF/lib下,或Tomcat的lib目录下缺少相应JAR。1. 在IDE中,确保依赖库被添加到项目的Deployment AssemblyArtifact中。
2. 手动将所需JAR包复制到WEB-INF/lib目录。
访问JSP页面出现HTTP 404错误1. URL路径错误。
2. JSP文件没有放在Web应用可访问的路径下(如放在了WEB-INF里但未通过Servlet转发)。
3. 应用上下文路径配置错误。
1. 检查浏览器地址栏URL。
2. 确认JSP访问路径,WEB-INF下的需通过Servlet转发。
3. 检查Tomcat中应用的上下文路径(context path)。
JSP页面中文显示乱码1. JSP页面编码未设置为UTF-8。
2. Servlet未处理POST请求编码。
3. Tomcat对GET请求URI解码编码不是UTF-8。
4. 数据库连接字符串未指定字符集。
1. JSP页面顶部加<%@ page pageEncoding="UTF-8"%>contentType中的charset=UTF-8
2. Servlet的doPost方法开头加request.setCharacterEncoding("UTF-8")
3. 修改Tomcatserver.xml<Connector>,添加URIEncoding="UTF-8"
4. JDBC URL中添加characterEncoding=UTF-8
插入数据库时中文变成问号数据库、表、字段的字符集不是UTF-8,或连接未指定字符集。1. 创建数据库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
2. 检查并修改已有表和字段的字符集。
3. 确保JDBC URL中包含useUnicode=true&characterEncoding=UTF-8
Tomcat启动成功,但访问应用时报500错误,日志中有java.lang.ClassNotFoundException: javax.servlet.Servlet项目使用的是Servlet API,但Tomcat提供的servlet-api.jar未正确引入,或与项目使用的版本冲突。1. 确保在项目构建路径中,Servlet API的Scope是Provided(由Tomcat运行时提供)。
2. 不要将servlet-api.jar打包到最终的WAR包中。
静态资源(CSS/JS/图片)加载404资源路径错误。在JSP/HTML中使用了相对路径,当页面路径变化时找不到资源。统一使用绝对路径:在路径前加上${pageContext.request.contextPath},如<link href="${pageContext.request.contextPath}/css/style.css">
数据库连接池耗尽,应用变慢或报错1. 连接池配置过小。
2. 代码中存在连接泄漏(获取连接后未关闭)。
1. 适当调大maxTotal
2.务必在finally块或使用try-with-resources确保连接被关闭(归还)。
3. 使用监控工具(如Druid的连接池监控)查看连接使用情况。

我个人在实际开发这个系统时,最深的一个体会是:在看似简单的增删改查背后,数据一致性和事务的边界是设计中最需要花心思的地方。比如“下单”这个操作,它不仅仅是插入一条订单记录,还涉及到库存的扣减、可能还有优惠券的使用、积分的变化。这些操作必须在一个数据库事务中完成,要么全部成功,要么全部回滚。在设计初期,就要把这些关联操作梳理清楚,确定好事务的边界。另一个教训是关于异常处理,早期我经常简单地在catch块里e.printStackTrace()然后就返回了,这在前端用户体验和问题排查上都很糟糕。后来我建立了一个统一的异常处理机制,将系统异常(如数据库连接失败)和业务异常(如库存不足)区分开,系统异常记录详细日志并给用户友好的“系统繁忙”提示,业务异常则明确告知用户原因(如“库存仅剩X件”),用户体验和运维效率都提升了很多。最后,即使是这样一个小型项目,从第一天起就坚持写清晰、格式统一的日志,在出问题时真的能救命。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 2:06:15

Spring Boot+Vue校园奶茶店点单系统:全栈开发实战与期末项目指南

简介&#xff1a;这是一套面向计算机专业本科生的期末大作业级实战项目&#xff0c;基于Spring Boot后端与Vue 3前端构建的校园奶茶店线上点单系统&#xff0c;适用于课程设计、毕业设计选题及Java全栈开发入门练习。资源包含完整可运行源码、MySQL数据库脚本、详细设计报告文档…

作者头像 李华
网站建设 2026/9/4 2:05:30

基于Godot引擎的2.5D格斗游戏开发:从Hyper DBZ开源项目学习核心技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 2:01:44

基于HTML/CSS/JS的海贼王角色图鉴系统:前端课程设计实战

简介&#xff1a;这是一份面向前端初学者的课程设计实践资源&#xff0c;以热门动漫《海贼王》为主题&#xff0c;完整呈现HTML结构搭建、CSS样式美化与JavaScript交互实现的全流程开发案例。资源聚焦基础技能整合应用&#xff0c;帮助学习者在真实项目中掌握语义化标签使用、响…

作者头像 李华
网站建设 2026/9/4 2:01:44

企业目标执行落地的三大关键问题与解决方案

本文分析了企业目标执行过程中常见的脱节现象&#xff0c;提出通过明确任务关联性、建立统一执行视图和强化责任管理来实现目标-计划-任务闭环。为什么目标难以落地企业在制定目标后&#xff0c;往往在执行过程中出现脱节现象。目标停留在规划或汇报阶段&#xff0c;缺乏实际执…

作者头像 李华
网站建设 2026/9/4 2:01:08

空中飞鸟检测数据集:多格式标签与YOLOv8小目标检测实战

简介&#xff1a;本资源是面向计算机视觉算法工程师、高校课程设计与竞赛参赛者、机场智能安防系统开发者的一套高质量空中飞鸟检测数据集&#xff0c;专为飞鸟入侵预警、驱赶控制、识别计数等实际工业场景建模需求而构建。数据集共3362张高清实景图像&#xff08;jpg&#xff…

作者头像 李华
网站建设 2026/9/4 1:59:37

机载仪表盘:让低成本机器人开发从“看不见”到实时可控

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华