☰
JSP+SQL Server购物系统:Web开发演进的实践标本
2026/10/10 6:24:30 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级网上购物系统完整实现方案,适用于课程设计、大作业及工程实训等实践场景,帮助初学者掌握JSP动态网页开发与SQL Server数据库协同应用的核心技能。资源包含108个文件,以37个JSP页面构成前端交互逻辑,53个GIF图像支撑界面展示,辅以6个HTML静态页、1个Java源文件、2个Class编译文件、1个XML配置文件及1个可运行的DB数据库文件,整体结构清晰体现MVC雏形;压缩包仅642KB,轻量易解压,适合作为教学案例快速部署验证。已有101人学习下载,读者可直接获取含登录注册、商品浏览、购物车管理、订单提交等核心功能的可运行源码,同步获得配套论文文档(.doc格式),涵盖需求分析、系统设计、数据库建模与实现总结,便于理解从理论到落地的全流程开发思路。

1. 为什么今天还要看 JSP + SQL Server 的购物系统?——它不是古董,而是理解 Web 开发演进的“活体标本”

如果你在技术社区刷到“基于JSP+SqlServer的网上购物系统的设计与实现(源代码+论文).zip”,第一反应可能是:这玩意儿还能跑?2000年代初的技术栈,连 Tomcat 都得卡在 6.x,SQL Server 还在用 2005 或 2008?但恰恰是这套看似过时的组合,在某高校软件工程课程设计、某职业院校毕业实训、某中小型企业早期内部采购平台中,仍被高频复用——不是因为怀旧,而是它把「请求-响应生命周期」「MVC雏形拆分」「数据库连接池本质」「JDBC事务边界」这些抽象概念,全压进一个可单机部署、无依赖、不需 Docker、不碰云服务的 ZIP 包里。它不教你怎么写 Spring Boot 自动装配,但它逼你手动写Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"),亲手配web.xml的<servlet-mapping>,在login.jsp里混写 HTML 和<% out.print(...) %>。这不是倒退,是降维打击式的学习锚点:当你能从零搭通用户登录 → 查询商品 → 提交订单 → SQL Server 写入四步链路,再回看现代框架的拦截器、ORM、事务传播,才真正明白“封装”二字沉在哪一层。适合三类人:刚学完 Java 基础想串起 Web 全链路的新手;需要带学生做课程设计、要可控、可调试、可逐行讲透的导师;或接手遗留系统维护,发现生产库仍是 SQL Server 2008、前端还是 JSP 片段的老项目工程师。


2. 从 ZIP 解压到首页可访问:本地环境搭建的最小闭环

2.1 环境选型:为什么必须锁定 Tomcat 6.0.37 + SQL Server 2008 R2 Express?

这不是怀旧执念,而是版本耦合的硬约束。该 ZIP 包内WEB-INF/lib/下的sqljdbc4.jar是微软为 Java 6 编译的驱动(javac -source 1.6 -target 1.6),若强行用 Tomcat 9+(要求 Java 8+)会直接抛UnsupportedClassVersionError;而web.xml头部声明的是<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN",Tomcat 7+ 默认拒绝加载 DTD 2.3 规范的部署描述符,报错web.xml is invalid。SQL Server 方面,包内db/目录下的.bak文件是 SQL Server 2008 R2 备份格式,用 2012+ 还原会提示The database was backed up on a server running version 10.50.1600(10.50 即 2008 R2 主版本号),且connectionString中integratedSecurity=true依赖 Windows 身份验证模式,2008 R2 Express 是最后一个默认启用该模式的免费版。常见做法是:在 Windows 10/11 上安装SQL Server 2008 R2 Express with Tools(注意选含 Management Studio 的版本),Tomcat 用apache-tomcat-6.0.37.zip(官网已下架,需从 Apache 存档库获取),JDK 锁定Java SE Development Kit 6u45(最后一个支持 Windows XP 的 JDK 6 更新版)。我一般会建一个独立目录D:\jsp-shop-env\,结构如下:

D:\jsp-shop-env\ ├── apache-tomcat-6.0.37\ ├── jdk1.6.0_45\ ├── SQLEXPRWT_x64_ENU.exe # SQL Server 2008 R2 Express 安装包 └── shopping-system.zip # 原始 ZIP 包

提示:不要试图用 WAMP/XAMPP 替代——它们默认集成 PHP/Apache,对 JSP 支持极弱;也不要尝试用 SQL Server LocalDB,它不支持integratedSecurity=true的 Windows 身份验证连接方式。

2.2 数据库还原四步法:从 .bak 到可用 connection

解压 ZIP 后,进入db/目录,你会看到shoppingDB.bak和restore_db.sql。后者是辅助脚本,但实际还原必须走 SSMS 图形界面,因为restore_db.sql中的RESTORE DATABASE ... FROM DISK = 'C:\path\shoppingDB.bak'路径是绝对路径,且未处理文件移动(.mdf/.ldf默认还原到 C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA\,而你的 SQL Server 实例名可能不同)。正确步骤:

  1. 打开 SQL Server Management Studio,用 Windows 身份验证登录;
  2. 右键【数据库】→【还原数据库】→ 在【源】选项卡选择【设备】→ 点击右侧省略号 → 添加shoppingDB.bak文件;
  3. 在【目标】选项卡确认数据库名为shoppingDB(不能改,否则后续 JDBC 连接字符串会失败);
  4. 切换到【选项】选项卡 → 勾选【覆盖现有数据库】→ 在【将数据库文件还原为】列表中,手动修改两行路径:
    • shoppingDB_Data.mdf→ 改为C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA\shoppingDB.mdf
    • shoppingDB_Log.ldf→ 改为C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA\shoppingDB_log.ldf
      (路径中的MSSQL10_50.SQLEXPRESS是默认实例名,若你安装时自定义了实例名,需替换为实际名称,如MSSQL12.MYINSTANCE)
-- 验证还原成功:执行此查询应返回 5 行(用户、商品、订单等基础表) SELECT TABLE_NAME FROM shoppingDB.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE';

逻辑说明:SQL Server 还原时,.bak文件包含完整的数据库结构和数据,但物理文件路径由当前实例配置决定。不手动指定.mdf/.ldf路径会导致还原失败或文件写入错误位置;勾选【覆盖现有数据库】是必须的,否则遇到同名数据库会报错Database 'shoppingDB' already exists。

2.3 Tomcat 部署:WEB-INF/web.xml 的三个致命配置点

解压 ZIP 后,src/目录是 Java 源码,WebContent/是 Web 根目录。将WebContent/重命名为shopping,整个文件夹复制到apache-tomcat-6.0.37\webapps\下。启动 Tomcat 前,必须检查shopping\WEB-INF\web.xml:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <!-- 1. servlet 映射必须显式声明,Tomcat 6 不支持注解扫描 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.shopping.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <!-- 2. welcome-file-list 必须指向 index.jsp,而非 index.html --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 3. context-param 定义数据库连接参数,此处不能留空 --> <context-param> <param-name>driver</param-name> <param-value>com.microsoft.sqlserver.jdbc.SQLServerDriver</param-value> </context-param> <context-param> <param-name>url</param-name> <param-value>jdbc:sqlserver://localhost:1433;databaseName=shoppingDB;integratedSecurity=true;</param-value> </context-param> </web-app>

参数说明:

  • version="2.4"是 Tomcat 6.0.37 支持的最高 web.xml 版本,若写成2.5会启动失败;
  • <servlet-mapping>是核心,JSP 页面里的表单<form action="login" method="post">依赖此映射,否则提交后 404;
  • integratedSecurity=true表示使用 Windows 当前登录用户身份连接 SQL Server,无需用户名密码,但要求 Tomcat 进程以该用户权限运行(见 2.4 节);
  • databaseName=shoppingDB必须与还原的数据库名完全一致,大小写敏感。

3. JDBC 连接池与事务控制:手写 DBUtil 的三个关键补丁

3.1 为什么不能直接 new Connection()?——连接泄漏的雪球效应

原始 ZIP 包中,com.shopping.util.DBUtil.java往往是这样写的:

public class DBUtil { public static Connection getConnection() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); return DriverManager.getConnection( "jdbc:sqlserver://localhost:1433;databaseName=shoppingDB;integratedSecurity=true;"); } }

问题在于:每次调用getConnection()都新建物理连接,而 SQL Server Express 最大连接数仅 10 个。当 10 个用户并发访问商品列表页(每个页面执行 3 次查询),瞬间耗尽连接池,后续请求全部卡死在getConnection(),Tomcat 日志出现java.sql.SQLException: No more data to read from socket。这不是代码 bug,是架构缺失。必须引入连接池,但 Tomcat 6 不支持context.xml中的<Resource>配置(那是 Tomcat 7+ 的特性),所以得手写轻量级池。

3.2 手写简易连接池:BlockingQueue + 固定大小策略

在com.shopping.util包下新建DBConnectionPool.java,核心逻辑是用BlockingQueue<Connection>管理连接:

package com.shopping.util; import java.sql.Connection; import java.sql.DriverManager; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class DBConnectionPool { private static final int MAX_POOL_SIZE = 5; // SQL Server Express 限制,设为 5 更安全 private static BlockingQueue<Connection> pool = new LinkedBlockingQueue<>(MAX_POOL_SIZE); static { // 初始化时预创建 3 个连接 for (int i = 0; i < 3; i++) { try { pool.offer(createConnection()); } catch (Exception e) { e.printStackTrace(); } } } private static Connection createConnection() throws Exception { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); return DriverManager.getConnection( "jdbc:sqlserver://localhost:1433;databaseName=shoppingDB;integratedSecurity=true;"); } public static Connection getConnection() throws Exception { Connection conn = pool.poll(); // 先尝试取空闲连接 if (conn == null || conn.isClosed()) { conn = createConnection(); // 没有空闲则新建 } return conn; } public static void releaseConnection(Connection conn) { if (conn != null && !pool.isFull()) { try { if (!conn.getAutoCommit()) conn.rollback(); // 归还前回滚未提交事务 pool.offer(conn); // 放回队列 } catch (Exception e) { e.printStackTrace(); } } } }

逻辑说明:

  • MAX_POOL_SIZE = 5是硬性限制,超过 SQL Server Express 的 10 连接上限一半,留出空间给系统进程;
  • pool.poll()是非阻塞获取,避免线程长时间等待;
  • releaseConnection()中强制rollback()是关键:若某个 Servlet 执行了conn.setAutoCommit(false)但未commit()就归还,该连接下次被取出时仍处于事务中,导致SELECT查询被锁表;
  • isFull()判断防止队列溢出,因LinkedBlockingQueue构造时已设容量上限。

3.3 订单提交事务:try-catch-finally 的黄金三角

购物车提交订单涉及三张表:orders(主单)、order_items(明细)、products(扣减库存)。原始代码常把三步 SQL 写在同一个executeUpdate()里,一旦第二步失败,第一步已提交无法回滚。正确写法必须用 JDBC 事务:

public class OrderService { public boolean createOrder(Order order) { Connection conn = null; PreparedStatement ps1 = null, ps2 = null, ps3 = null; try { conn = DBConnectionPool.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入主单 ps1 = conn.prepareStatement("INSERT INTO orders (user_id, total_price, create_time) VALUES (?, ?, ?)"); ps1.setInt(1, order.getUserId()); ps1.setDouble(2, order.getTotalPrice()); ps1.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps1.executeUpdate(); // 2. 插入明细(循环) for (OrderItem item : order.getItems()) { ps2 = conn.prepareStatement("INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)"); ps2.setInt(1, getLastOrderId(conn)); // 需另写方法查刚插入的 order_id ps2.setInt(2, item.getProductId()); ps2.setInt(3, item.getQuantity()); ps2.setDouble(4, item.getPrice()); ps2.executeUpdate(); } // 3. 扣减库存 for (OrderItem item : order.getItems()) { ps3 = conn.prepareStatement("UPDATE products SET stock = stock - ? WHERE id = ?"); ps3.setInt(1, item.getQuantity()); ps3.setInt(2, item.getProductId()); ps3.executeUpdate(); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { // 必须关闭所有 Statement,否则连接无法归还池 closeQuietly(ps1); closeQuietly(ps2); closeQuietly(ps3); DBConnectionPool.releaseConnection(conn); } } private void closeQuietly(PreparedStatement ps) { if (ps != null) try { ps.close(); } catch (Exception e) {} } }

参数说明:

  • setAutoCommit(false)是事务起点,commit()是终点,中间任何一步异常都触发rollback();
  • closeQuietly()是防御性编程,避免ps.close()抛异常导致releaseConnection()不被执行;
  • getLastOrderId(conn)需用SELECT SCOPE_IDENTITY()实现,不能用SELECT MAX(id),因并发时可能取错。

4. 避坑:五个让新手卡住 3 小时以上的典型问题

4.1 现象:首页打开空白,Tomcat 日志报java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver

原因:sqljdbc4.jar未放入shopping\WEB-INF\lib\目录,或放入了但 jar 包损坏(ZIP 包解压时部分文件被杀毒软件拦截)。SQL Server 2008 R2 对应的驱动必须是sqljdbc4.jar(非sqljdbc.jar或mssql-jdbc-9.4.jar),后者是 Java 8+ 驱动。
解决:从微软官方下载 Microsoft SQL Server JDBC Driver 4.0 ,解压后取sqljdbc_4.0\chs\sqljdbc4.jar,覆盖WEB-INF\lib\下同名文件;用jar -tf sqljdbc4.jar | head -5验证 jar 包可读。

4.2 现象:登录时跳转到error.jsp,日志显示java.sql.SQLException: Login failed for user ''

原因:web.xml中integratedSecurity=true要求 Tomcat 进程以 Windows 用户身份运行,但默认双击startup.bat启动时,Tomcat 运行在 SYSTEM 权限,无法继承当前用户的 Windows 凭据。
解决:以当前用户身份启动命令行,再执行D:\jsp-shop-env\apache-tomcat-6.0.37\bin\startup.bat;或在 Windows 服务中将 Tomcat 服务的“登录身份”改为当前用户(控制面板 → 管理工具 → 服务 → Apache Tomcat → 属性 → 登录)。

4.3 现象:商品图片不显示,路径为/shopping/images/1.jpg,但浏览器请求返回 404

原因:原始 ZIP 包中images/目录在WebContent/下,但解压后未保持相对路径。index.jsp中<img src="images/${product.id}.jpg">依赖images/与 JSP 同级。
解决:确认shopping\images\目录存在,且内含1.jpg,2.jpg等文件;若图片存于shopping\resources\images\,需统一修改所有 JSP 中的src路径为resources/images/...。

4.4 现象:添加商品到购物车后,页面刷新,购物车为空

原因:CartServlet中使用HttpSession session = request.getSession(false);,当false参数表示不创建新 session,而用户首次访问未触发getSession(true),导致 session 为 null,后续session.setAttribute("cart", cart)失败。
解决:将getSession(false)改为getSession(true),确保 session 存在;或在web.xml中添加<session-config><session-timeout>30</session-timeout></session-config>防止 session 过期。

4.5 现象:订单提交成功,但products表库存未扣减

原因:UPDATE products SET stock = stock - ? WHERE id = ?中?参数顺序写反,或WHERE id = ?的?传入了order_id而非product_id。
解决:在OrderService.createOrder()中加日志System.out.println("Updating product: " + item.getProductId() + ", qty: " + item.getQuantity());;用 SQL Server Profiler 抓取实际执行的 SQL,比对参数值。


5. 从功能可用到生产就绪:三个必须做的加固动作

5.1 SQL 注入防护:JSP 页面里的 EL 表达式不是银弹

原始代码中,搜索商品常写为:

<!-- 危险!直接拼接用户输入 --> <% String keyword = request.getParameter("keyword"); String sql = "SELECT * FROM products WHERE name LIKE '%" + keyword + "%'"; %>

即使你用了PreparedStatement,这种写法仍存在风险——因为keyword可能含%、_等通配符,导致意外匹配。更严重的是,若某处漏掉PreparedStatement,' OR '1'='1就能绕过登录。正确做法是:所有用户输入必须经PreparedStatement绑定,且 JSP 中禁止任何+拼接 SQL 字符串。对搜索场景,应统一用?占位符:

// 在 ProductDao.java 中 public List<Product> searchProducts(String keyword) { String sql = "SELECT * FROM products WHERE name LIKE ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); // 关键:通配符由 Java 拼,不交给 SQL // ... executeQuery } }

注意:LIKE中的%必须由 Java 字符串拼接,不能写成WHERE name LIKE '%?%',否则?会被当作文本字面量,而非参数占位符。

5.2 XSS 过滤:JSP 输出时的fn:escapeXml()不是摆设

商品详情页常有product.getDescription(),若描述字段存了<script>alert(1)</script>,直接<%= product.getDescription() %>会执行脚本。原始 ZIP 包几乎都不做过滤。解决方案有两个层级:

  • 视图层:在所有 JSP 输出用户数据处,用 JSTL 的fn:escapeXml():
    <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <div class="desc">${fn:escapeXml(product.description)}</div>
  • 服务层:在ProductService.saveProduct()中,对description字段做预处理:
    // 使用 Apache Commons Text 的 StringEscapeUtils product.setDescription(StringEscapeUtils.escapeHtml4(product.getDescription()));

二者选其一即可,推荐视图层,因过滤逻辑与展示强相关,且不污染业务数据。

5.3 日志与监控:用 log4j 1.2.17 替代System.out.println

原始代码满屏System.out.println("Login success"),线上环境无法分级、无法滚动、无法定位。Tomcat 6 兼容 log4j 1.2.x,需三步接入:

  1. 下载log4j-1.2.17.jar放入WEB-INF/lib/;
  2. 在WEB-INF/classes/下新建log4j.properties:
log4j.rootLogger=INFO, stdout, file log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=${catalina.home}/logs/shopping.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5 log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n
  1. 在 Java 类中替换输出:
// 旧写法 System.out.println("User login: " + username); // 新写法 private static final Logger logger = Logger.getLogger(LoginServlet.class); logger.info("User login: " + username);

参数说明:MaxFileSize=10MB防止日志撑爆磁盘;MaxBackupIndex=5保留最近 5 个归档日志;%c{1}只输出类名(如LoginServlet),不输出完整包路径,日志更简洁。


6. 一个血泪经验:如何用这个老系统练出“框架感”

我带过几届学生做这个项目,最常听到的抱怨是:“代码太散,找不到入口”。确实,没有@SpringBootApplication,没有main()方法,一切始于index.jsp的<form action="login">。但正是这种“散”,逼你画出真正的调用链:

  • 用户点击“登录” → 浏览器发 POST 到/login→ Tomcat 查web.xml映射 → 找到LoginServlet→doPost()执行 → 调用UserService.login()→DBUtil.getConnection()→ JDBC 执行 SQL → 返回User对象 →request.setAttribute("user", user)→request.getRequestDispatcher("success.jsp").forward(request, response)。

这条链路上,每一步都是现代框架的“黑匣子”:@RequestMapping就是web.xml的<servlet-mapping>;@Autowired UserService就是new UserService();@Transactional就是conn.setAutoCommit(false)+conn.commit()。我让学生做完后,强制做一件事:用笔在纸上画出从index.jsp到数据库写入的完整箭头图,标出每个类、每个方法、每个配置文件位置。画完三次,再看 Spring MVC 的 DispatcherServlet 源码,突然就懂了什么叫“前端控制器模式”。

另一个技巧是“逆向注入”:把LoginServlet中的UserService userService = new UserService();改成静态变量private static UserService userService;,然后在ServletContextListener.contextInitialized()中初始化它。这就模拟了 Spring 的 IOC 容器初始化过程——虽然简陋,但你能亲手控制对象生命周期。这种“用原始材料搭乐高”的过程,比直接跑通一个 Spring Boot Demo,更能建立对分层架构的肌肉记忆。

最后说个玄学但真实的经验:永远先跑通index.jsp→login.jsp→success.jsp三页,再碰数据库。很多同学一上来就改DBUtil,结果连首页都打不开,陷入“是环境问题?是代码问题?是配置问题?”的无限循环。而三页纯静态流程能快速验证 Tomcat、JSP 引擎、URL 映射是否正常,这是最可靠的“后悔药”。希望帮到你。

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

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

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

立即咨询