ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet拍卖管理系统实战:从环境搭建到并发出价与WAR部署

JSP+Servlet拍卖管理系统实战:从环境搭建到并发出价与WAR部署 简介这是一套面向高校计算机专业毕业设计场景的拍卖管理系统完整源码包采用JSPServlet技术栈前端使用jQuery后端基于Servlet与JDBC实现区分管理员与用户两类角色。系统集成商品竞拍、分类管理、商品管理、订单管理、留言互动等核心模块用户端支持注册登录、商品检索与竞拍、个人中心记录查询管理端覆盖账号、分类、商品、竞拍、订单及系统配置等后台功能适合作为课程设计或毕设参考方案。资源包共438个文件包含79个jsp页面、126个png与66个gif图像素材、42个css与42个js前端资源、17个jar依赖及1个sql数据库脚本压缩包约23.19MB目录结构清晰便于按模块查阅与二次开发。目前已有26人浏览学习。整体提供从数据库脚本到前后端页面的完整实现可帮助读者快速理解竞拍业务逻辑、掌握Servlet分层开发思路并完成环境部署与功能扩展。1. 拍卖管理系统为什么还在用 JSPServlet 这套老古董打开招聘网站搜 Java 后端满屏都是 Spring Boot、微服务、云原生JSP 和 Servlet 似乎早就该进博物馆了。但如果你正在做课程设计、毕业设计或者接手了一个跑了七八年的企业内部系统你会发现 JSPServlet 依然是绕不开的存在。拍卖管理系统就是这类场景的典型代表用户注册登录、发布拍品、出价竞拍、倒计时截拍、订单生成业务链条完整但不至于复杂到必须上框架用 JSPServlet 加 MySQL 刚好能把整个 MVC 流程跑通代码量可控调试直观答辩的时候每一层都能讲清楚。这套方案适合谁一是计算机相关专业做课程设计或毕设的学生需要一套能跑起来、能讲明白、能自己改的完整系统二是刚转 Java Web 的新人想通过一个真实业务把 Servlet 生命周期、JSP 九大内置对象、Session 管理、JDBC 连接池这些基础打牢。它不适合高并发生产环境但作为理解 Java Web 底层运作的载体比直接上 Spring Boot 更能看清请求从浏览器到数据库再回到页面的完整链路。下面从环境搭建开始一步步把这套系统跑起来。2. 环境搭建与项目结构从零把骨架立起来2.1 JDK、Tomcat、MySQL 的版本选择与安装要点这套技术栈对版本不挑剔但版本组合选错了会浪费大量时间在环境问题上。我一般推荐 JDK 8 或 JDK 11Tomcat 8.5 或 9.0MySQL 5.7 或 8.0。JDK 8 和 Tomcat 8.5 是经典搭配网上资料最多遇到问题容易搜到答案。MySQL 8.0 在连接 URL 上需要额外参数后面会讲。安装 JDK 后配置JAVA_HOME和PATH命令行执行java -version确认输出。Tomcat 解压即用关键是CATALINA_HOME指向解压目录启动脚本在bin目录下。MySQL 安装时记住 root 密码建库时字符集选utf8mb4排序规则选utf8mb4_general_ci避免中文乱码。# 验证 JDK 安装 java -version # 输出类似java version 1.8.0_381 # 验证 Tomcat 能否启动Linux/Mac cd $CATALINA_HOME/bin ./startup.sh # 浏览器访问 http://localhost:8080 看到 Tomcat 欢迎页即成功 # 验证 MySQL 连接 mysql -u root -p # 输入密码后进入 mysql 提示符这三步验证缺一不可。很多人跳过 Tomcat 独立启动验证直接部署项目结果分不清是 Tomcat 本身有问题还是项目配置有问题。先把容器跑通再往里放东西排查路径清晰得多。2.2 拍卖管理系统的目录结构与 web.xml 配置一个标准的 JSPServlet 项目目录结构如下auction-system/ ├── src/ │ └── com/auction/ │ ├── dao/ # 数据库访问层 │ ├── service/ # 业务逻辑层 │ ├── servlet/ # 控制层 │ ├── entity/ # 实体类 │ └── util/ # 工具类DBUtil 等 ├── WebContent/ 或 webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # 第三方 jar 包 │ ├── css/ │ ├── js/ │ ├── jsp/ # JSP 页面 │ └── index.jsp └── database/ └── auction.sql # 建表与初始数据web.xml是 Servlet 3.0 之前的核心配置文件Servlet 3.0 之后可以用注解替代但拍卖管理系统这类教学项目通常还是保留web.xml因为配置集中、结构清晰。关键配置包括 Servlet 映射、Session 超时、欢迎页、过滤器。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 display-nameAuctionSystem/display-name !-- Session 30 分钟超时 -- session-config session-timeout30/session-timeout /session-config !-- 欢迎页 -- welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 字符编码过滤器解决中文乱码 -- filter filter-nameEncodingFilter/filter-name filter-classcom.auction.util.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-appsession-config里的 30 表示 30 分钟无操作后 Session 失效拍卖场景下这个值可以适当调大因为用户可能长时间停留在某个拍品页面等待出价。EncodingFilter必须放在所有过滤器最前面否则后续过滤器拿到的参数已经是乱码了。url-pattern用/*拦截所有请求包括静态资源确保编码统一。2.3 数据库建表用户表、拍品表、出价记录表、订单表拍卖管理系统的核心数据模型围绕四张表展开。用户表存账号信息拍品表存商品和起拍价、当前价、截止时间出价记录表存每一次出价订单表存成交结果。CREATE DATABASE auction_db DEFAULT CHARACTER SET utf8mb4; USE auction_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 0-普通用户 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, description TEXT, start_price DECIMAL(10,2) NOT NULL, current_price DECIMAL(10,2) NOT NULL, image_url VARCHAR(500), seller_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待审核 1-竞拍中 2-已结束 3-已成交, FOREIGN KEY (seller_id) REFERENCES t_user(id) ); CREATE TABLE t_bid ( id INT PRIMARY KEY AUTO_INCREMENT, item_id INT NOT NULL, user_id INT NOT NULL, bid_price DECIMAL(10,2) NOT NULL, bid_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (item_id) REFERENCES t_item(id), FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, item_id INT NOT NULL, buyer_id INT NOT NULL, final_price DECIMAL(10,2) NOT NULL, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已取消, FOREIGN KEY (item_id) REFERENCES t_item(id), FOREIGN KEY (buyer_id) REFERENCES t_user(id) );t_item表的current_price字段是竞拍的核心状态每次有效出价都要更新它。status字段控制拍品生命周期从待审核到竞拍中再到已结束状态流转要在 Service 层做校验不能只靠前端按钮控制。t_bid表记录所有出价历史方便回溯和展示出价记录。外键约束在开发阶段建议加上能帮你提前发现数据一致性问题上线前如果性能敏感可以去掉。3. 核心功能实现Servlet 生命周期与 JSP 页面协作3.1 用户登录注册Session 管理与密码处理登录功能是理解 Servlet 生命周期的好入口。用户提交表单到LoginServletServlet 在doPost方法中调用 Service 层查询数据库验证通过后将用户信息存入HttpSession然后重定向到拍品列表页。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取参数编码已由 Filter 统一处理 String username req.getParameter(username); String password req.getParameter(password); // 2. 调用 Service 验证 User user userService.login(username, password); if (user ! null) { // 3. 登录成功写入 Session HttpSession session req.getSession(); session.setAttribute(currentUser, user); session.setMaxInactiveInterval(60 * 60); // 1 小时 resp.sendRedirect(req.getContextPath() /item/list); } else { // 4. 登录失败回传错误信息 req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); } } }req.getSession()默认会创建新 Session如果只想获取已有 Session 用req.getSession(false)。setMaxInactiveInterval单位是秒这里设 1 小时比web.xml里的 30 分钟优先级更高。登录失败用forward而不是redirect因为需要把错误信息通过request域传到 JSP 页面。密码存储方面教学项目常用 MD5 加盐生产环境应该用 BCrypt但核心逻辑是数据库里永远不存明文密码。JSP 登录页面用 EL 表达式回显错误信息form action${pageContext.request.contextPath}/login methodpost 用户名input typetext nameusername requiredbr 密码input typepassword namepassword requiredbr span stylecolor:red${error}/spanbr input typesubmit value登录 /form${pageContext.request.contextPath}动态获取项目上下文路径避免硬编码项目名导致部署路径变化后 404。${error}对应 Servlet 中req.setAttribute(error, ...)设置的值EL 表达式会自动从 request 域中查找。3.2 拍品发布与列表展示JSP 个人信息展示页面的数据绑定拍品发布涉及表单提交和文件上传拍品图片列表展示则要把数据库中的拍品数据渲染到 JSP 页面。这里重点讲数据绑定因为 JSP 页面展示数据的方式直接决定了代码的可维护性。Servlet 端查询拍品列表并存入 request 域WebServlet(/item/list) public class ItemListServlet extends HttpServlet { private ItemService itemService new ItemServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 分页参数 int page 1; int pageSize 12; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } // 查询竞拍中的拍品 ListItem items itemService.getItemsByStatus(1, page, pageSize); int totalCount itemService.countByStatus(1); int totalPages (totalCount pageSize - 1) / pageSize; req.setAttribute(items, items); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/jsp/item_list.jsp).forward(req, resp); } }JSP 页面用 JSTL 的c:forEach遍历% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classitem-grid c:forEach items${items} varitem div classitem-card img src${pageContext.request.contextPath}/upload/${item.imageUrl} alt${item.name} h3${item.name}/h3 p当前价fmt:formatNumber value${item.currentPrice} pattern#,##0.00/ 元/p p截止fmt:formatDate value${item.endTime} patternyyyy-MM-dd HH:mm//p a href${pageContext.request.contextPath}/item/detail?id${item.id}查看详情/a /div /c:forEach /div !-- 分页 -- div classpagination c:if test${currentPage 1} a href?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} / ${totalPages} 页/span c:if test${currentPage totalPages} a href?page${currentPage 1}下一页/a /c:if /divfmt:formatNumber处理金额千分位fmt:formatDate处理日期格式比在 Java 代码里格式化再传字符串更灵活。分页逻辑放在 Servlet 里算好总页数JSP 只负责展示不要把计算逻辑写进 JSP 脚本片段。${item.imageUrl}只存文件名完整路径由${pageContext.request.contextPath}/upload/拼接这样项目部署路径变化时图片不会失效。3.3 出价竞拍并发控制与事务边界出价是拍卖系统最核心也最容易出问题的环节。多个用户同时对同一拍品出价时必须保证出价必须高于当前价、出价后当前价立即更新、出价记录写入历史表。这三步必须在一个数据库事务中完成否则会出现出价成功但当前价没变或者两个人同时出价都以为自己领先的情况。public class BidServiceImpl implements BidService { Override public synchronized BidResult placeBid(int itemId, int userId, BigDecimal bidPrice) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查询拍品当前状态加行锁 String sql SELECT current_price, end_time, status FROM t_item WHERE id ? FOR UPDATE; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, itemId); ResultSet rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return BidResult.fail(拍品不存在); } BigDecimal currentPrice rs.getBigDecimal(current_price); Timestamp endTime rs.getTimestamp(end_time); int status rs.getInt(status); // 2. 校验 if (status ! 1) { conn.rollback(); return BidResult.fail(拍品不在竞拍中); } if (System.currentTimeMillis() endTime.getTime()) { conn.rollback(); return BidResult.fail(竞拍已结束); } if (bidPrice.compareTo(currentPrice) 0) { conn.rollback(); return BidResult.fail(出价必须高于当前价); } // 3. 更新当前价 String updateSql UPDATE t_item SET current_price ? WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setBigDecimal(1, bidPrice); updatePs.setInt(2, itemId); updatePs.executeUpdate(); // 4. 写入出价记录 String insertSql INSERT INTO t_bid (item_id, user_id, bid_price) VALUES (?, ?, ?); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, itemId); insertPs.setInt(2, userId); insertPs.setBigDecimal(3, bidPrice); insertPs.executeUpdate(); conn.commit(); return BidResult.success(出价成功); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return BidResult.fail(系统异常请重试); } finally { DBUtil.close(conn); } } }SELECT ... FOR UPDATE是关键它在事务中对该行加排他锁其他事务的相同查询会阻塞直到当前事务提交从而避免两个线程同时读到相同的current_price然后都认为自己出价有效。synchronized关键字在单机部署时能进一步保证同一 JVM 内的串行执行但分布式部署时无效那时候需要靠数据库锁或 Redis 分布式锁。事务边界要覆盖查询-校验-更新-插入全过程任何一步失败都回滚。4. 避坑与排查JSPServlet 项目最容易翻车的五个地方4.1 中文乱码POST 和 GET 处理方式不同现象表单提交后数据库里存的是????或者页面显示乱码。原因POST 请求的乱码可以通过request.setCharacterEncoding(UTF-8)解决但 GET 请求的参数在 URL 中Tomcat 8.5 之前默认用 ISO-8859-1 解码setCharacterEncoding对 GET 无效。解决POST 用 EncodingFilter 统一设置GET 乱码要么改 Tomcat 的server.xml在 Connector 上加URIEncodingUTF-8要么在代码里手动转码new String(name.getBytes(ISO-8859-1), UTF-8)。最省事的办法是统一用 POST 提交或者在 Tomcat 8.5 上确认默认 URI 编码已经是 UTF-8。4.2 MySQL 8.0 连接报错时区和驱动类名现象java.sql.SQLException: The server time zone value ?D1ú±ê×?ê±?? is unrecognized或者Loading class com.mysql.jdbc.Driver deprecated。原因MySQL 8.0 需要显式设置时区且驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver。解决连接 URL 改为jdbc:mysql://localhost:3306/auction_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse驱动类名用com.mysql.cj.jdbc.Driver。useSSLfalse在开发环境可以关掉避免证书警告。4.3 Session 失效导致出价时用户信息丢失现象用户明明登录了出价时却提示请先登录。原因Session 超时时间设置过短或者浏览器禁用了 Cookie 导致 JSESSIONID 无法传递。解决检查web.xml和代码中setMaxInactiveInterval的值拍卖场景建议不低于 30 分钟。如果浏览器禁用 Cookie需要用 URL 重写response.encodeURL()来传递 Session ID但更推荐引导用户开启 Cookie。另外注意session.invalidate()会清空所有属性退出登录时调用它但不要在普通页面跳转时误调。4.4 出价并发导致价格覆盖现象两个用户几乎同时出价后出价的人价格更低但系统显示他领先。原因没有加锁两个线程同时读到相同的current_price都通过了校验然后先后更新后更新的覆盖了先更新的。解决如 3.3 节所示用SELECT ... FOR UPDATE在事务中加行锁或者用乐观锁版本号UPDATE t_item SET current_price ?, version version 1 WHERE id ? AND version ?根据executeUpdate返回的影响行数判断是否更新成功。悲观锁适合并发量不高的教学项目乐观锁适合读多写少的场景。4.5 JSP 页面直接访问数据库导致连接泄漏现象系统运行一段时间后报Too many connections。原因在 JSP 脚本片段中直接DriverManager.getConnection()而没有关闭或者 DAO 层关闭连接写在 try 块里而不是 finally 块里异常时连接无法释放。解决所有数据库操作必须在 finally 块中关闭 Connection、Statement、ResultSet或者用 try-with-resources。更好的做法是引入连接池如 Druid 或 HikariCP由池管理连接的创建和回收。教学项目可以用简单的DBUtil封装但一定要确保close方法在 finally 中调用。5. 从能跑到好用连接池替换与 WAR 打包部署5.1 用 Druid 连接池替换 DriverManagerDriverManager每次请求都创建新连接开销大且无法控制连接数。替换成 Druid 连接池只需要改DBUtil和加一个配置文件。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/auction_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn) { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }initialSize是启动时创建的连接数maxActive是最大连接数maxWait是获取连接的超时毫秒数。这三个参数根据实际并发调整教学项目 5 到 20 足够了。注意conn.close()在连接池模式下是归还连接而不是真正关闭所以 finally 中照常调用即可。5.2 传统 JSP 项目打包 WAR 并部署到 Tomcat开发阶段在 IDE 里跑没问题但交付或部署到服务器时需要打成 WAR 包。# 在项目根目录执行Maven 项目 mvn clean package # 生成的 WAR 包在 target/ 目录下复制到 Tomcat cp target/auction-system.war $CATALINA_HOME/webapps/ # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # 访问 # http://localhost:8080/auction-system/WAR 包名决定了访问路径的上下文auction-system.war部署后访问路径就是/auction-system/。如果不想带这个前缀可以把 WAR 包重命名为ROOT.war但一个 Tomcat 只能有一个 ROOT 应用。部署后检查$CATALINA_HOME/logs/catalina.out看有没有启动异常常见问题是数据库连接失败或 jar 包冲突。5.3 一个验证系统是否真正跑通的小技巧部署完成后不要只点几下页面就算完。我一般会做三个验证第一注册两个用户分别登录确认 Session 互不干扰第二用两个浏览器同时打开同一个拍品交替出价确认价格正确递增且出价记录完整第三等拍品截止时间过后再出价确认系统拒绝并提示竞拍已结束。这三个场景覆盖了用户隔离、并发控制和状态校验能跑通说明核心链路没问题。这套 JSPServlet 拍卖管理系统看起来技术栈老旧但把 Servlet 生命周期、Session 管理、JDBC 事务、JSP 数据绑定这些基础打牢之后再学 Spring Boot 会发现很多概念只是换了个封装形式。我自己的习惯是每接手一个新框架都会回想它在 Servlet 层面对应的是什么这样不容易被框架的黑匣子迷惑。希望帮到你。本文还有配套的精品资源点击获取
返回列表