ARTICLE DETAIL

建站实战干货

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

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

2026/9/4 2:07:38 拓冰建站 浏览量
Java JSP+Servlet+Tomcat构建农产品销售系统:从数据库设计到部署实战 简介本资源是一套基于Java Web技术栈开发的农产品销售管理系统完整实现方案面向计算机专业本科生、Java初学者及Web开发入门者解决农产品流通环节中信息管理低效、订单跟踪困难、库存协同滞后等实际问题。压缩包共含全套可运行源码与配套文档主要文件类型包括JSP前端页面、Servlet业务逻辑类、JavaBean数据模型、MySQL建表脚本及系统设计说明书涵盖用户管理、商品发布、订单处理、库存统计等核心模块整体大小为95.93MB结构清晰、注释完整便于理解MVC分层架构与BS模式开发流程。已有1438人学习下载所有代码均经Tomcat环境实测验证启动即用附带详细部署说明与数据库配置指南特别适合课程设计、毕业设计及小型农业电商项目快速原型开发。1. 项目概述一个典型的BS架构农产品销售系统最近在整理过往项目时翻到了一个挺有代表性的老项目——一个基于JavaJSPTomcat的农产品销售管理系统。这类系统在农业信息化、县域电商、合作社管理等领域其实非常普遍核心目标就是把线下的农产品进销存、订单、客户管理搬到线上实现流程化和数据化。虽然现在微服务、Spring Boot满天飞但这种经典的JSPServletTomcat的BS架构对于理解Web应用从数据库到前端的完整链路尤其是对于在校学生、初级开发者或者需要快速构建内部管理工具的小型团队来说依然有很高的学习价值和实用意义。这个系统本质上是一个B/S架构的管理后台。用户通过浏览器访问请求发送到部署在Tomcat服务器上的Java Web应用应用通过JDBC与后端的MySQL数据库交互完成数据的增删改查最后将动态生成的JSP页面返回给浏览器展示。整个技术栈非常“经典”Java负责业务逻辑JSP负责视图展示Tomcat作为Servlet容器MySQL存储所有业务数据。它要解决的核心问题就是如何高效、清晰地将农产品从供应商录入、库存管理、订单处理到销售统计这一整套流程数字化。接下来我会结合这个项目的设计与实现拆解其中的关键技术和实操要点。2. 系统整体设计与架构拆解2.1 为什么选择JSPServletTomcat这套“经典”组合在动手之前技术选型是首要问题。现在可能很多人第一反应是Spring MVC甚至Spring Boot。但对于一个目标明确、业务相对固定的管理系统尤其是作为学习或中小型项目原型经典的JSPServlet模式有其独特的优势。核心考量在于简单直接与学习成本。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例如ProductServlet、OrderServlet。它们在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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;设计要点user表的password字段必须存储加密后的字符串绝对禁止明文存储。通常使用BCrypt或PBKDF2等强哈希算法。inventory_log表是系统的“账本”。任何库存变动无论是采购、销售还是盘点调整都必须先更新product表的stock字段然后同步插入一条流水记录。这个“双写”操作必须在同一个数据库事务中完成以保证数据一致性。通过这个表可以追溯任意时间点的库存变动情况对账和排查问题非常有用。注意数据库设计时字段注释一定要写清楚。utf8mb4字符集可以支持完整的UTF-8包括表情符号。为经常用于查询条件的字段如外键、状态字段建立索引但索引不是越多越好会影响写性能。3. 核心模块实现与关键技术点3.1 使用Servlet处理核心业务请求Servlet是整个系统的交通枢纽。以处理产品列表查询和新增产品的ProductServlet为例我们通常通过doGet和doPost方法来区分请求。// 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查询数据 ListProduct 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?actionlistsave success); } }实操要点与避坑指南URL设计采用类似/product?actionlist或RESTful风格/products需额外过滤器的方式。前者简单直观后者更现代但需要更多配置。中文乱码这是新手必坑点。对于POST请求必须在doPost方法第一行设置request.setCharacterEncoding(UTF-8)。对于GET请求Tomcat 8.5以上版本默认URI编码是UTF-8如果遇到乱码可能需要修改Tomcat的server.xml中Connector的URIEncodingUTF-8属性。响应编码通常在JSP页面头部通过% page contentTypetext/html;charsetUTF-8 languagejava %设置。请求转发与重定向forward是服务器内部跳转地址栏不变请求域数据可共享。sendRedirect是客户端跳转地址栏变化相当于一次新请求。保存数据后务必使用重定向否则用户刷新页面会导致表单重复提交。DAO层注入上述代码直接在Servlet中new了DAO对象在简单项目中可以但不利于测试和解耦。更好的做法是使用一个简单的“服务定位器”或工厂模式来获取DAO实例为将来引入Spring等IoC容器做准备。3.2 JSP页面与JSTL/EL的最佳实践JSP页面负责展示。我们要避免在JSP中编写大量Java代码Scriptlet而是使用EL表达式和JSTL标签库使页面更清晰、更易于维护。首先需要在JSP头部引入JSTL核心库% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %产品列表页示例!-- product/list.jsp -- !DOCTYPE html html head title产品管理/title link relstylesheet href${pageContext.request.contextPath}/assets/css/bootstrap.css /head body div classcontainer mt-4 h2产品列表/h2 a href${pageContext.request.contextPath}/product?actionedit classbtn btn-primary mb-3新增产品/a !-- 使用JSTL c:if进行条件判断 -- c:if test${not empty param.save param.save true} div classalert alert-success保存成功/div /c:if table classtable table-bordered table-striped thead tr thID/th th产品名称/th th规格/th th单价/th th库存/th th操作/th /tr /thead tbody !-- 使用JSTL c:forEach遍历产品列表 -- c:forEach varproduct items${productList} tr td${product.id}/td td${product.name}/td td${product.specification}/td !-- 使用JSTL fmt:formatNumber格式化金额 -- tdfmt:formatNumber value${product.price} typecurrency currencySymbol¥//td td span classbadge ${product.stock 10 ? bg-success : bg-warning} ${product.stock} /span /td td a href${pageContext.request.contextPath}/product?actioneditid${product.id} classbtn btn-sm btn-outline-primary编辑/a a href# onclickconfirmDelete(${product.id}) classbtn btn-sm btn-outline-danger删除/a /td /tr /c:forEach !-- 使用JSTL c:if处理空列表 -- c:if test${empty productList} trtd colspan6 classtext-center暂无产品数据/td/tr /c:if /tbody /table /div script function confirmDelete(id) { if (confirm(确定要删除这个产品吗)) { window.location.href ${pageContext.request.contextPath}/product?actiondeleteid id; } } /script /body /html最佳实践与技巧绝对路径所有静态资源CSS, JS, 图片和链接地址务必使用${pageContext.request.contextPath}作为前缀。这能确保应用部署在任何上下文路径下都能正确访问资源避免404错误。避免Scriptlet如非必要极简单的逻辑不要使用% ... %。EL表达式${}和JSTL标签足以完成99%的展示逻辑。格式化输出使用fmt:formatNumber和fmt:formatDate标签来格式化金额、日期提升用户体验。前端框架引入Bootstrap等前端UI框架可以极大加快页面开发速度并保证基本的响应式和美观。将CSS/JS文件放在WebContent/assets/目录下统一管理。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-namedbUrl/param-name param-valuejdbc:mysql://localhost:3306/agriculture_sales?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai/param-value /context-param context-param param-namedbUser/param-name param-valueyour_username/param-value /context-param context-param param-namedbPassword/param-name param-valueyour_password/param-value /context-param listener listener-classcom.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 { ListProduct 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 ListProduct findAll(String keyword) { ListProduct 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; } // ... 其他方法实现 }关键技术与避坑指南使用PreparedStatement这是防止SQL注入攻击的铁律。绝对不要用字符串拼接来构造SQL语句。Try-with-Resources从Java 7开始强烈推荐使用此语法自动关闭Connection,Statement,ResultSet等资源避免内存泄漏。连接归还注意从连接池getConnection()获得的连接在调用close()方法时并不是真的关闭TCP连接而是将其归还到连接池中。因此务必确保在finally块或try-with-resources中关闭连接。事务处理对于涉及多个表更新的操作如创建订单扣库存、生成订单主表、插入订单明细必须使用事务。在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部署开发环境JDK安装JDK 8或11LTS版本配置JAVA_HOME环境变量。IDE推荐使用IntelliJ IDEA或Eclipse for Enterprise Java Developers。IDEA对Web开发支持更友好。Tomcat从官网下载Tomcat 9.x或8.5.x版本。在IDEA中配置本地Tomcat服务器Run - Edit Configurations - Add New Configuration - Tomcat Server - Local指定Tomcat安装目录。MySQL安装MySQL 5.7或8.0使用root用户创建数据库agriculture_sales并执行项目中的SQL脚本建表。项目部署到独立Tomcat打包在IDEA中使用Build - Build Artifacts...生成一个WAR包例如agriculture_sales.war。部署将WAR包复制到Tomcat的webapps目录下。启动运行Tomcat的bin/startup.batWindows或bin/startup.shLinux/Mac。访问打开浏览器访问http://localhost:8080/agriculture_sales假设你的WAR包名为agriculture_sales.war。注意生产环境部署时务必修改Tomcat的server.xml配置将Connector的port从8080改为80如果需要并考虑配置HTTPS。同时数据库连接信息应使用JNDI数据源配置在Tomcat中而不是硬编码在web.xml里这样更安全、更易于管理。4.2 性能优化与安全加固建议一个可用的系统和一个健壮的系统之间就差在优化和安全上。性能优化数据库层面索引优化确保WHERE、ORDER BY、JOIN条件上的字段有索引。但避免过度索引。查询优化避免SELECT *只取需要的字段。复杂查询使用EXPLAIN分析。连接池调优根据实际并发量调整maxTotal、maxIdle等参数。应用层面页面静态化对于不常变动的页面如商品详情页可以考虑生成静态HTML减轻数据库和JSP编译压力。前端缓存合理设置HTTP缓存头对CSS、JS、图片等静态资源进行缓存。分页查询所有列表接口必须支持分页使用LIMIT offset, size避免一次性加载海量数据。安全加固SQL注入坚持使用PreparedStatement已解决绝大部分问题。XSS跨站脚本攻击在JSP中使用JSTL的c:out value${userInput}/或EL表达式默认会对HTML进行转义。如果需要在页面中显示富文本必须进行严格的白名单过滤。CSRF跨站请求伪造为关键操作如修改、删除、支付的表单添加CSRF Token。可以在用户登录时生成一个Token存入Session在表单中作为一个隐藏域提交服务端进行校验。会话安全用户登录后密码等敏感信息不要存入Session。Session应有合理的超时时间。可以考虑对Session ID进行轮换。密码存储使用BCryptPasswordEncoder等强哈希算法加盐存储密码绝对禁止明文或弱加密如MD5。文件上传如果系统支持图片上传必须限制文件类型白名单、检查文件头、重命名文件、并将文件存储在Web应用目录之外通过Servlet提供访问。4.3 开发与部署中的常见问题及解决方案在实际开发和部署中你几乎一定会遇到下面这些问题问题现象可能原因解决方案启动Tomcat时报ClassNotFoundException或NoClassDefFoundError项目依赖的JAR包如MySQL驱动、DBCP2没有被打包到WEB-INF/lib下或Tomcat的lib目录下缺少相应JAR。1. 在IDE中确保依赖库被添加到项目的Deployment Assembly或Artifact中。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 pageEncodingUTF-8%和contentType中的charsetUTF-8。2. Servlet的doPost方法开头加request.setCharacterEncoding(UTF-8)。3. 修改Tomcatserver.xml的Connector添加URIEncodingUTF-8。4. JDBC URL中添加characterEncodingUTF-8。插入数据库时中文变成问号数据库、表、字段的字符集不是UTF-8或连接未指定字符集。1. 创建数据库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。2. 检查并修改已有表和字段的字符集。3. 确保JDBC URL中包含useUnicodetruecharacterEncodingUTF-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件”用户体验和运维效率都提升了很多。最后即使是这样一个小型项目从第一天起就坚持写清晰、格式统一的日志在出问题时真的能救命。本文还有配套的精品资源点击获取