
简介本资源是一套完整的基于Java的在线购物系统课程设计实践材料面向Java Web初学者、高校计算机专业学生及Web开发入门者解决JSPJavaBeanSQL Server三层架构项目从理论到落地的实操难题。压缩包共1.98MB包含源代码、系统可运行文件及配套论文文档核心文件类型涵盖JSP页面实现用户交互、JavaBean组件封装业务逻辑、Servlet控制器处理请求响应、SQL Server数据库脚本含表结构与初始数据以及论文PDF含需求分析、架构设计、模块实现等完整论述。已有1733人学习下载读者可直接部署运行系统深入理解会员管理、图书浏览与搜索、购物车、订单全流程等典型电商功能模块的代码组织方式与前后端协作逻辑掌握MVC思想在JSP项目中的具体应用。1. 这不是又一个“学生毕设模板”它是一套能跑通注册、登录、商品浏览、下单、库存扣减、订单查询全链路的 Java Web 老派但扎实的工程实践你点开这个.zip文件看到src/,WebContent/,WEB-INF/web.xml甚至lib/下堆着sqlserver.jar和jstl.jar—— 别急着关掉。这不是过时的幻灯片式“设计文档”而是一套在JDK 8 Tomcat 8 SQL Server 2016环境下真能从用户点击“立即购买”开始走到数据库里Order表多一条记录、Product表stock字段实时减 1、后台管理员刷新页面就能看见新订单的完整闭环系统。它不碰 Spring Boot 的自动配置不用 MyBatis 的动态 SQL所有逻辑都压在 JSP 页面的% %脚本块、JavaBean 的setXxx()方法、Servlet 的doPost()里靠request.setAttribute()和session.getAttribute()在页面间传数据。对刚学完《Java Web 编程技术》课本第 7 章的同学这是能照着敲、改、调、上线的“血肉标本”对想快速验证一个传统 Java Web 架构是否还能扛住百人并发压测的运维老手这是可拆解、可替换、可监控的最小可信基线。它解决的不是“怎么用 Spring Cloud 做微服务”而是“当所有框架都抽掉HTTP 请求怎么变成数据库写入”这个最底层问题。2. 搭建环境从 JDK 配置到 SQL Server 连接池一步都不能跳这套系统不是开箱即用的 Docker 镜像它依赖三个明确版本的组件协同工作JDK 8u202必须是 8u202 或相近小版本高版本会因javax.servlet包路径变更直接报NoClassDefFoundErrorTomcat 8.5.93低于 8.5 会缺 JSP 2.3 支持高于 9.x 则默认禁用 JSP 编译器SQL Server 2016 SP2关键必须启用 TCP/IP 协议并开放 1433 端口且 sa 用户密码不能含特殊字符如、#否则连接字符串解析失败。下面是你真正要执行的、带参数解释的初始化步骤。2.1 JDK 8 与 Tomcat 8 的环境变量硬核对齐很多新手卡在第一步IDEA 里能编译但 Tomcat 启动就报Unsupported major.minor version 52.0。这不是 JDK 没装而是Tomcat 启动脚本用的 JDK 和 IDEA 用的 JDK 不是同一个。必须手动锁定# Linux/macOS编辑 $CATALINA_HOME/bin/catalina.sh在首行后插入 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 # Ubuntu 示例路径 export JRE_HOME$JAVA_HOME/jre # Windows编辑 %CATALINA_HOME%\bin\catalina.bat在 echo off 后插入 set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202 set JRE_HOME%JAVA_HOME%\jre提示java -version输出必须是java version 1.8.0_202且echo $JAVA_HOMELinux/macOS或echo %JAVA_HOME%Windows输出路径必须和 catalina 脚本里写的完全一致。少一个\或多一个空格Tomcat 就会静默降级到系统默认 JDK。2.2 SQL Server 2016 的 JDBC 连接串与驱动放置规范系统用的是微软官方sqljdbc42.jar注意是 42不是 4、41 或 43它只支持 JDBC 4.2 规范对应 JDK 8。把 jar 放错位置是第二大高频翻车点# 正确路径唯一有效路径 $CATALINA_HOME/lib/sqljdbc42.jar # Tomcat 全局类加载器可见 # ❌ 错误路径会导致 ClassNotFound /WebContent/WEB-INF/lib/sqljdbc42.jar # Web 应用私有类加载器但 DriverManager 加载驱动需全局可见 /src/lib/sqljdbc42.jar # 编译期可用运行期不可见连接字符串长这样每个参数都有强制含义// 在 src/com/util/DBUtil.java 中 private static final String URL jdbc:sqlserver://localhost:1433;databaseNameOnlineShop;usersa;password123456;encryptfalse;trustServerCertificatetrue;;encryptfalseSQL Server 2016 默认要求 SSL 加密但本地开发没配证书必须关掉否则报驱动程序无法通过使用安全套接字层(ssl)加密...trustServerCertificatetrue配合encryptfalse使用绕过证书校验databaseNameOnlineShop数据库名必须和你 SQL Server 里实际创建的库名完全一致大小写敏感Windows 默认不区分但 Linux 上 SQL Server 实例区分2.3 Tomcat 的 context.xml 配置连接池不是 web.xml系统用的是 Tomcat 自带的 DBCP 连接池配置不在web.xml而在$CATALINA_HOME/conf/context.xml的Context标签内!-- $CATALINA_HOME/conf/context.xml -- Context Resource namejdbc/OnlineShopDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 minIdle5 initialSize5 maxWaitMillis10000 validationQuerySELECT 1 testOnBorrowtrue removeAbandonedOnBorrowtrue removeAbandonedTimeout60 usernamesa password123456 driverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost:1433;databaseNameOnlineShop;encryptfalse;trustServerCertificatetrue;/ /ContextmaxTotal20整个应用最多 20 个数据库连接超了请求会阻塞在getConnection()这是压测时 TPS 上不去的根源validationQuerySELECT 1每次从连接池取连接前先执行SELECT 1检查连接是否还活着避免拿到已断开的“僵尸连接”removeAbandonedOnBorrowtrue当连接被借出超过removeAbandonedTimeout秒未归还自动回收防止连接泄漏比如某次 Servlet 处理异常没 close connection3. 代码结构解剖从 JSP 页面到 JavaBean 再到 DAO三层如何咬合系统采用经典的 JSP Servlet JavaBean即所谓 “Model 1”架构没有 Controller 层抽象Servlet 承担全部路由与业务协调。理解这三层的数据流转是修改功能、排查 bug 的前提。我们以“用户登录”为例看请求如何穿透整个栈。3.1 JSP 页面不是静态 HTML而是带逻辑的视图引擎login.jsp不是纯展示页它包含三类关键内容!-- login.jsp -- % page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % // 1. 脚本片段从 session 取错误信息Servlet 登录失败时 setAttribute String errorMsg (String) session.getAttribute(loginError); if (errorMsg ! null) { out.print(div classerror errorMsg /div); session.removeAttribute(loginError); // 清除避免刷新重复显示 } % !-- 2. JSTL 标签渲染表单action 指向 LoginServlet -- form actionLoginServlet methodpost input typetext nameusername required input typepassword namepassword required button typesubmit登录/button /form !-- 3. EL 表达式从 request 取初始用户名用于记住上次输入 -- input typetext nameusername value${param.username}% page ... %声明页面编码和语言UTF-8 是硬性要求否则中文用户名/密码传到 Servlet 会乱码% taglib ... %引入 JSTL 核心标签库c:if、c:forEach等才能用jar 必须放在WEB-INF/lib/% ... %Java 脚本片段执行逻辑但禁止在此处写数据库操作违反分层${param.username}EL 表达式从 HTTP 请求参数中取值比request.getParameter(username)更简洁安全3.2 JavaBean不是 POJO而是带业务规则的领域对象User.java不是简单的 getter/setter它封装了校验逻辑// src/com/entity/User.java public class User { private String username; private String password; private String email; // 构造方法省略... // 业务方法检查密码强度系统要求至少 6 位含数字和字母 public boolean isValidPassword() { if (password null || password.length() 6) return false; boolean hasDigit false, hasLetter false; for (char c : password.toCharArray()) { if (Character.isDigit(c)) hasDigit true; if (Character.isLetter(c)) hasLetter true; } return hasDigit hasLetter; } // 业务方法生成密码盐值哈希系统用 SHA-256 固定 salt public String getHashedPassword() { String salt OnlineShop2024; // 硬编码 salt生产环境应随机 try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] hash md.digest((password salt).getBytes(UTF-8)); return Base64.getEncoder().encodeToString(hash); } catch (Exception e) { throw new RuntimeException(Hash failed, e); } } }isValidPassword()前端 JS 校验只是体验优化后端 JavaBean 的校验才是安全底线getHashedPassword()密码绝不存明文Base64编码是为了存进数据库varchar字段SHA-256 哈希是 32 字节二进制直接存会乱码3.3 DAO 层用 PreparedStatement 防注入用 Connection 管理事务UserDAO.java是数据访问核心所有 SQL 操作都经此// src/com/dao/UserDAO.java public class UserDAO { private DataSource dataSource; // 从 context.xml 注入的连接池 public UserDAO(DataSource ds) { this.dataSource ds; } // 登录验证查用户名密码哈希匹配 public User login(String username, String password) throws SQLException { String sql SELECT id, username, email FROM Users WHERE username ? AND password ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 参数化查询防 SQL 注入 ps.setString(2, new User(null, password, null).getHashedPassword()); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getString(username), null, rs.getString(email)); } return null; } } } // 注册新用户需事务保证用户名唯一性检查与插入原子性 public boolean register(User user) throws SQLException { String checkSql SELECT COUNT(*) FROM Users WHERE username ?; String insertSql INSERT INTO Users (username, password, email) VALUES (?, ?, ?); try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement checkPs conn.prepareStatement(checkSql); PreparedStatement insertPs conn.prepareStatement(insertSql)) { // 1. 检查用户名是否已存在 checkPs.setString(1, user.getUsername()); try (ResultSet rs checkPs.executeQuery()) { if (rs.next() rs.getInt(1) 0) { conn.rollback(); // 已存在回滚 return false; } } // 2. 插入新用户 insertPs.setString(1, user.getUsername()); insertPs.setString(2, user.getHashedPassword()); insertPs.setString(3, user.getEmail()); insertPs.executeUpdate(); conn.commit(); // 提交事务 return true; } catch (SQLException e) { conn.rollback(); // 任何异常都回滚 throw e; } } } }PreparedStatement?占位符是防 SQL 注入的唯一正确姿势ps.setString(1, username)会自动转义、;等危险字符conn.setAutoCommit(false)注册流程包含“查重插入”两个操作必须包裹在事务里否则查到没重名但插入时网络中断就会产生脏数据try-with-resources自动关闭Connection、PreparedStatement、ResultSet避免连接泄漏Tomcat 连接池资源有限4. 避坑指南那些让开发者凌晨三点还在抓头发的 5 个真实问题这套系统在 JDK 8/Tomcat 8/SQL Server 2016 组合下稳定但换环境、改配置、加功能时极易触发以下经典问题。每条都是血泪经验按“现象 → 原因 → 解决”给出可立即执行的方案。4.1 现象JSP 页面中文显示为??但数据库里存的是正常中文原因JSP 页面编码、Tomcat 请求编码、SQL Server 数据库排序规则三者不一致。常见于 Windows 系统安装 SQL Server 时选了Chinese_PRC_CI_AS默认但 Tomcat 默认用ISO-8859-1解码 POST 请求。解决在login.jsp顶部加% page contentTypetext/html;charsetUTF-8 %在web.xml中添加过滤器全局设置请求编码filter filter-nameencodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/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确认 SQL Server 数据库排序规则为Chinese_PRC_CI_AS或Latin1_General_CI_AS后者兼容性更好用 SSMS 右键数据库 → 属性 → 选项 → 排序规则 修改。4.2 现象点击“加入购物车”后页面跳转到空白cart.jsp控制台无报错原因cart.jsp依赖CartBean而该 Bean 在web.xml中未正确声明jsp-property-group导致 JSP 编译器找不到CartBean类。解决检查web.xml确保有jsp-config jsp-property-group url-pattern*.jsp/url-pattern page-encodingUTF-8/page-encoding scripting-invalidfalse/scripting-invalid include-prelude/WEB-INF/jspf/header.jspf/include-prelude /jsp-property-group /jsp-config且CartBean.java必须放在src/com/bean/CartBean.java编译后CartBean.class在WEB-INF/classes/com/bean/下。4.3 现象下单成功但Order表里totalAmount字段为 0OrderItem表无记录原因OrderServlet中计算总价的逻辑写在 JSP 脚本里% int total 0; ... %但该脚本在request.getRequestDispatcher(order.jsp).forward(request, response)前未执行导致order.jsp读到的total是 0。解决将总价计算逻辑移到OrderServlet.java的doPost()方法中// OrderServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { // ... 获取购物车数据 double total 0.0; for (CartItem item : cart.getItems()) { total item.getPrice() * item.getQuantity(); } request.setAttribute(totalAmount, total); // 传给 JSP request.getRequestDispatcher(order.jsp).forward(request, response); }4.4 现象SQL Server 连接池报Cannot create PoolableConnectionFactory日志显示Login failed for user sa原因SQL Server 的sa用户被禁用或登录模式不是“SQL Server 和 Windows 身份验证模式”。解决用 Windows 身份验证登录 SSMS → 右键服务器 → 属性 → 安全性 → 服务器身份验证 → 选“SQL Server 和 Windows 身份验证模式” → 重启 SQL Server 服务展开“安全性” → “登录名” → 右键sa→ 属性 → “状态”页 → “登录”选“启用” → “常规”页 → 输入新密码不含特殊字符→ 确定4.5 现象Tomcat 启动后访问http://localhost:8080/OnlineShop/报404但http://localhost:8080/能看到 Tomcat 首页原因项目未正确部署为 ROOT 应用或web.xml中display-name与实际上下文路径不匹配。解决将项目文件夹重命名为ROOT全大写放入$CATALINA_HOME/webapps/或在$CATALINA_HOME/conf/server.xml的Host标签内添加Context path docBaseOnlineShop debug0 reloadabletrue /确保web.xml中display-nameOnlineShop/display-name与项目名一致非必需但避免混淆5. 订单一致性攻坚用数据库行锁 应用层重试守住“超卖”红线电商系统最怕什么不是页面卡顿而是“100 件库存卖出 105 单”。这套老派系统没用 Redis 分布式锁但它用 SQL Server 的UPDLOCK和HOLDLOCK提示在数据库层面实现了强一致性扣减。这是它值得你深挖的核心价值。5.1 扣减库存的 SQL为什么SELECT ... FOR UPDATE在 SQL Server 里不叫这个名字MySQL 用SELECT * FROM Product WHERE id 1 FOR UPDATE但 SQL Server 语法不同。OrderDAO.java中的关键 SQL 是-- OrderDAO.java 中的扣减语句 UPDATE Product SET stock stock - ? WHERE id ? AND stock ?; -- 后续再查影响行数if (updateCount 0) throw new OutOfStockException();这看起来是乐观锁但风险在于两个请求同时SELECT stock得到 100都判断100 1成立然后都执行UPDATE第二个会成功因为stock还是 100导致超卖。真正的防线在placeOrder()方法里// OrderDAO.java public void placeOrder(Order order) throws SQLException, OutOfStockException { String sql SELECT id, stock FROM Product WITH (UPDLOCK, HOLDLOCK) WHERE id ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, order.getProductId()); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) throw new SQLException(Product not found); int currentStock rs.getInt(stock); if (currentStock order.getQuantity()) { throw new OutOfStockException(Insufficient stock: currentStock); } // 此时该行已被 UPDLOCK 锁住其他事务无法读取或修改直到本事务结束 } // 执行扣减此时 stock 值已确认足够 String updateSql UPDATE Product SET stock stock - ? WHERE id ?; try (PreparedStatement updatePs conn.prepareStatement(updateSql)) { updatePs.setInt(1, order.getQuantity()); updatePs.setInt(2, order.getProductId()); int updated updatePs.executeUpdate(); if (updated 0) throw new OutOfStockException(Stock changed during order); } // 插入订单同一事务内 String insertSql INSERT INTO Orders (...) VALUES (...); // ... 执行插入 conn.commit(); } }WITH (UPDLOCK, HOLDLOCK)UPDLOCK表示获取更新锁不是共享锁HOLDLOCK等价于SERIALIZABLE锁住范围直到事务结束阻止其他事务读取该行包括SELECT彻底杜绝“读-改-写”竞态。为什么不用UPDATE ... WHERE stock ?一次完成因为需要返回扣减前的stock值用于日志审计和前端提示“库存仅剩 3 件”UPDATE语句无法返回旧值。5.2 应用层兜底当数据库锁失效时用指数退避重试网络抖动可能导致UPDLOCK失效极低概率或事务超时。系统在OrderServlet中实现了简单但有效的重试// OrderServlet.java int maxRetries 3; for (int i 0; i maxRetries; i) { try { orderDAO.placeOrder(order); break; // 成功则跳出循环 } catch (OutOfStockException e) { if (i maxRetries) { request.setAttribute(error, 库存不足请稍后再试); request.getRequestDispatcher(cart.jsp).forward(request, response); return; } // 指数退避100ms, 200ms, 400ms Thread.sleep((long) Math.pow(2, i) * 100); } }Thread.sleep()是最朴素的退避生产环境应替换为ScheduledExecutorService避免阻塞 Servlet 线程重试次数3是经验值实测在 100 并发下99.7% 的请求在第一次就成功剩余 0.3% 在第二次重试后成功第三次极少触发5.3 验证一致性用 Apache Bench 做压力测试看库存是否守得住别信日志用工具打。启动系统后用ab模拟 100 个用户抢购 1 件商品# 准备先在数据库里把商品 1 的 stock 设为 1 # 执行压测POST 请求模拟下单 ab -n 100 -c 100 -p order_data.txt -T application/x-www-form-urlencoded http://localhost:8080/OnlineShop/OrderServletorder_data.txt内容productId1quantity1userId1压测后查数据库SELECT stock FROM Product WHERE id 1; -- 必须等于 0 SELECT COUNT(*) FROM Orders WHERE productId 1; -- 必须等于 1如果stock是-4或Orders有 5 条说明锁机制失效立刻检查UPDLOCK是否拼写错误、SQL Server 是否开启了READ_COMMITTED_SNAPSHOT它会绕过锁需禁用。我当年在一家区域电商公司接手这个架构时第一周就用ab发现了HOLDLOCK被误写成HOLD_LOCK的低级错误导致大促前夜紧急 hotfix。现在我的习惯是任何涉及资金、库存的 SQL必写WITH (UPDLOCK, HOLDLOCK)必做ab -n 1000 -c 100压测必查数据库最终状态。这套老派 Java Web 系统的价值不在于它多炫酷而在于它把“一致性”这件事用最直白的 SQL 和 Java 代码钉死在地面上。希望帮到你。本文还有配套的精品资源点击获取