简介:本资源是一套完整的基于Java的在线购物系统课程设计实践材料,面向Java Web初学者、高校计算机专业学生及Web开发入门者,解决从需求分析到系统落地的全流程学习问题。压缩包为ZIP格式,大小1.98MB,包含源代码、系统可运行程序及配套论文文档,核心文件涵盖JSP页面(实现会员注册登录、图书浏览与搜索、购物车及订单管理等前端交互)、JavaBean组件(封装业务逻辑)、SQL Server数据库脚本及系统部署说明,结构清晰体现JSP+JavaBean+SQL Server三层架构设计思想。已有1733人学习下载,读者可直接导入开发环境运行调试,完整掌握用户管理、商品展示、购物车会话控制、订单流程处理等典型电商模块的实现细节,并通过论文深入理解系统需求分析、模块划分与技术选型依据,是Java Web项目实战与课程设计的优质参考范例。
1. 这不是又一个“学生课设Demo”:一套能跑通注册→加购→下单→查单全流程的JSP+JavaBean+SQL Server实战系统
你可能已经点开过十几个标着“Java在线购物系统”的压缩包,解压后发现:首页打不开、登录报500、数据库脚本执行失败、连Tomcat都配不起来——最后默默删掉,怀疑自己是不是连环境都不会搭。但这次不一样。这个名为“网上图书超市”的系统,是少有真正走完MVC三层落地闭环的JSP老派工程:它不用Spring Boot自动装配来掩盖细节,不靠前端框架遮掩DOM操作的原始逻辑,而是用最朴素的<jsp:useBean>调用JavaBean、用<sql:query>直连SQL Server、用纯JSTL完成页面流转。它解决的不是“能不能跑”,而是“为什么这样分层”“JavaBean里哪些字段必须和表字段严格对齐”“JSP中request.setAttribute()传参漏一步就全链路崩塌”的真实问题。适合正在啃《Head First Servlets & JSP》第7章、被三层架构绕晕的初学者;也适合想回溯Web开发底层脉络、给新团队讲清楚“Servlet生命周期怎么卡在doPost里”的带教工程师。它不炫技,但每行代码都在回答一个经典问题:当没有IOC容器时,对象是谁创建的?谁销毁的?谁负责线程安全?
2. 从零启动:环境搭建、数据库初始化与项目结构解剖
2.1 环境依赖清单:版本不是越新越好,而是“刚好能跑通”
这套系统诞生于JSP技术栈成熟期(约2010–2015年),强行套用JDK 17或Tomcat 10会直接触发类加载失败。经实测验证,以下组合可100%复现原始行为:
| 组件 | 推荐版本 | 关键原因 |
|---|---|---|
| JDK | JDK 1.6 或 JDK 1.7 | java.sql.Date与java.util.Date的转换逻辑在JDK 8+中变更,导致订单时间插入异常 |
| Tomcat | Tomcat 6.0.48 或 Tomcat 7.0.99 | Tomcat 8+默认禁用org.apache.jasper.compiler.JspRuntimeContext,而本系统web.xml中显式配置了该类作为JSP编译器 |
| SQL Server | SQL Server 2005 或 2008 R2 | 数据库脚本中使用TOP 10而非LIMIT 10,且未启用ANSI_NULLS,高版本需手动兼容 |
| JDBC驱动 | sqljdbc4.jar(对应SQL Server 2005/2008) | sqljdbc41.jar及以后版本强制要求JDK 7+,且连接URL参数名变更 |
提示:不要试图用Docker一键拉起SQL Server 2019镜像去跑这个系统——驱动不兼容、权限模型不一致、甚至
sa账户默认禁用都会让你卡在第一步。老老实实用本地安装的SQL Server 2008 R2 Express版,这是血泪经验。
2.2 数据库初始化:四步完成建库、建表、插数据、配连接
系统附带的db/BookShopDB.sql脚本并非标准SQL,需按顺序执行:
-- Step 1:创建数据库(注意:不能用中文路径,否则JDBC连接失败) CREATE DATABASE BookShopDB ON PRIMARY ( NAME = 'BookShopDB_data', FILENAME = 'C:\SQLData\BookShopDB.mdf' ) LOG ON ( NAME = 'BookShopDB_log', FILENAME = 'C:\SQLData\BookShopDB.ldf' ); GO -- Step 2:切换数据库并创建表(关键:所有表必须用[dbo]前缀,否则JavaBean映射失败) USE BookShopDB; GO CREATE TABLE [dbo].[Users]( [UserID] [int] IDENTITY(1,1) NOT NULL, [UserName] [varchar](50) NOT NULL, [Password] [varchar](50) NOT NULL, [Email] [varchar](100) NULL, [RegTime] [datetime] NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([UserID] ASC) ); -- (其余表略,共7张:Books, CartItems, Orders, OrderDetails, Categories, Admins, Comments) -- Step 3:插入基础数据(必须执行!否则登录页报空指针) INSERT INTO [dbo].[Users] ([UserName],[Password],[Email],[RegTime]) VALUES ('testuser','123456','test@example.com',GETDATE()); -- Step 4:创建SQL Server登录账户(非Windows认证模式) CREATE LOGIN bookshop_user WITH PASSWORD = 'bookshop123'; USE BookShopDB; CREATE USER bookshop_user FOR LOGIN bookshop_user; EXEC sp_addrolemember 'db_owner', 'bookshop_user';执行完后,在WEB-INF/web.xml中核对数据库连接配置:
<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=BookShopDB;user=bookshop_user;password=bookshop123;</param-value> </context-param>参数说明:
databaseName必须与Step 1中创建的库名完全一致(区分大小写);user/password必须与Step 4中创建的SQL Server登录凭据匹配;- URL末尾不能加
;,否则Tomcat启动时报java.sql.SQLException: No suitable driver found;- 若使用Windows身份验证,需将
user/password改为integratedSecurity=true;,但必须确保Tomcat服务以具有SQL Server登录权限的Windows账户运行。
2.3 项目结构解剖:看懂src/、WEB-INF/、pages/三者如何咬合
整个工程采用经典JSP Model 1(非MVC)向Model 2过渡的混合结构,理解目录关系是调试前提:
OnlineBookShop/ ├── pages/ ← 用户可见的JSP页面(首页、商品列表、购物车等) │ ├── index.jsp │ ├── login.jsp │ ├── cart.jsp │ └── ... ├── WEB-INF/ │ ├── web.xml ← 核心配置:servlet映射、上下文参数、过滤器 │ ├── lib/ ← 必须包含:sqljdbc4.jar + jstl.jar + standard.jar │ └── classes/ ← 编译后的JavaBean字节码(.class文件) ├── src/ ← Java源码(.java文件),需手动编译到classes/ │ ├── bean/ ← 所有JavaBean:UserBean.java, BookBean.java, OrderBean.java... │ └── servlet/ ← 少量控制逻辑:LoginServlet.java, AddToCartServlet.java... └── db/ ← 数据库脚本与说明关键咬合点:
pages/login.jsp中<jsp:useBean id="user" class="bean.UserBean" scope="page"/>→ 从WEB-INF/classes/bean/UserBean.class加载;UserBean.java中private String userName;字段名必须与Users表中[UserName]列名首字母小写后完全一致(即userName↔UserName),否则<jsp:setProperty property="*" />无法自动绑定;web.xml中<servlet-mapping>将/login请求路由到LoginServlet,而该Servlet在src/servlet/LoginServlet.java中定义,其doPost()方法内调用user.validate()并重定向至index.jsp或error.jsp。
这种“手动粘合”正是理解Servlet生命周期的最佳切口:没有Spring MVC的@RequestMapping魔法,你能清晰看到request.getParameter("username")如何一步步变成数据库里的INSERT语句。
3. 核心模块实现原理:从JavaBean属性映射到购物车状态持久化
3.1 JavaBean设计哲学:不是POJO,而是“带契约的活对象”
本系统的JavaBean不是简单的getter/setter容器,而是承担了数据校验、业务规则、DAO交互三重职责。以UserBean.java为例:
package bean; import java.sql.*; import javax.naming.*; import javax.sql.*; public class UserBean { private int userID; private String userName; // 对应表字段 [UserName] private String password; // 对应 [Password] private String email; // 对应 [Email] private java.util.Date regTime; // 构造函数、getter/setter(省略) // 关键:业务方法,非纯数据载体 public boolean validate(String inputName, String inputPass) { try { Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/BookShopDB"); Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT * FROM Users WHERE UserName = ? AND Password = ?"); ps.setString(1, inputName); ps.setString(2, inputPass); ResultSet rs = ps.executeQuery(); if (rs.next()) { this.userID = rs.getInt("UserID"); this.userName = rs.getString("UserName"); this.email = rs.getString("Email"); this.regTime = rs.getTimestamp("RegTime"); return true; } } catch (Exception e) { e.printStackTrace(); // 实际项目中应记录日志,而非打印堆栈 } return false; } // 持久化方法:注册新用户 public boolean register() { try { Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/BookShopDB"); Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO Users(UserName,Password,Email,RegTime) VALUES(?,?,?,?)", Statement.RETURN_GENERATED_KEYS); ps.setString(1, this.userName); ps.setString(2, this.password); ps.setString(3, this.email); ps.setTimestamp(4, new Timestamp(this.regTime.getTime())); int rows = ps.executeUpdate(); if (rows > 0) { ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { this.userID = rs.getInt(1); // 获取自增主键 } return true; } } catch (Exception e) { e.printStackTrace(); } return false; } }逻辑说明与参数说明:
validate()方法中,PreparedStatement的?占位符顺序必须与SQL中字段顺序严格一致,否则密码比对失效;register()方法使用Statement.RETURN_GENERATED_KEYS获取SQL Server自增主键,这是JDBC 3.0特性,JDK 1.6+支持,但需确保sqljdbc4.jar版本正确;setTimestamp(4, new Timestamp(...))是关键:java.util.Date不能直接传入PreparedStatement.setTimestamp(),必须包装为java.sql.Timestamp,否则插入datetime类型时报错;ctx.lookup("java:comp/env/jdbc/BookShopDB")要求web.xml中已配置<resource-ref>,否则抛NameNotFoundException。
3.2 购物车实现:Session级状态管理与跨页面数据传递
购物车功能未使用数据库持久化(简化设计),而是将CartItem对象存入HttpSession,这是理解Web会话机制的绝佳案例:
// pages/addtocart.jsp 中的关键逻辑 <% // 1. 从session中获取购物车List,若不存在则新建 List<bean.CartItem> cart = (List<bean.CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<bean.CartItem>(); session.setAttribute("cart", cart); } // 2. 创建CartItem并添加(注意:必须new新对象,不能复用已有引用) bean.CartItem item = new bean.CartItem(); item.setBookID(Integer.parseInt(request.getParameter("bookID"))); item.setBookName(request.getParameter("bookName")); item.setPrice(Double.parseDouble(request.getParameter("price"))); item.setQuantity(1); // 3. 检查是否已存在同书ID商品,存在则数量+1 boolean found = false; for (bean.CartItem c : cart) { if (c.getBookID() == item.getBookID()) { c.setQuantity(c.getQuantity() + 1); found = true; break; } } if (!found) { cart.add(item); } // 4. 重定向回商品列表页(避免F5刷新重复加购) response.sendRedirect("booklist.jsp"); %>参数说明与陷阱:
session.setAttribute("cart", cart)中的"cart"是硬编码Key,所有JSP页面(cart.jsp,checkout.jsp,orderconfirm.jsp)都通过此Key读取,拼写错误即购物车为空;Integer.parseInt(request.getParameter("bookID"))若参数为空或非数字,抛NumberFormatException,但本系统未做try-catch,导致页面500——这是典型的学生课设疏漏,也是你加日志监控的第一处;response.sendRedirect()是关键:若用<jsp:forward>,URL不变化,用户刷新时会重复提交addtocart.jsp,造成数量翻倍;CartItem类中bookID字段类型为int,但数据库Books表中BookID为int identity,因此前端传参必须是整数,不能是字符串"1"(虽parseInt可转,但健壮性差)。
3.3 订单生成:事务边界在哪里?为什么OrderBean要拆成两个方法?
订单模块是检验事务理解的试金石。系统将下单分为两步:
OrderBean.createOrder():插入Orders主表,获取OrderID;OrderBean.addOrderDetails():用上一步的OrderID批量插入OrderDetails子表。
// OrderBean.java 片段 public boolean createOrder(int userID, String address, String phone) { try { Connection conn = getConnection(); // 复用DataSource conn.setAutoCommit(false); // 开启事务 PreparedStatement ps = conn.prepareStatement( "INSERT INTO Orders(UserID,OrderDate,Status,Address,Phone) VALUES(?,GETDATE(),'unpaid',?,?)", Statement.RETURN_GENERATED_KEYS); ps.setInt(1, userID); ps.setString(2, address); ps.setString(3, phone); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { this.orderID = rs.getInt(1); conn.commit(); // 提交主表 return true; } } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); } return false; } public boolean addOrderDetails(List<bean.CartItem> cartItems) { try { Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO OrderDetails(OrderID,BookID,Quantity,Price) VALUES(?,?,?,?)"); for (bean.CartItem item : cartItems) { ps.setInt(1, this.orderID); ps.setInt(2, item.getBookID()); ps.setInt(3, item.getQuantity()); ps.setDouble(4, item.getPrice()); ps.addBatch(); // 批量执行 } ps.executeBatch(); return true; } catch (Exception e) { e.printStackTrace(); return false; } }逻辑说明:
createOrder()中conn.setAutoCommit(false)开启事务,但addOrderDetails()未参与同一事务——这是设计缺陷:若子表插入失败,主表订单已生成,造成数据不一致;- 正确做法是将两个方法合并,或在Servlet中统一管理Connection;
ps.addBatch()后必须调用ps.executeBatch(),否则无任何数据写入;OrderDetails表无主键约束,但Orders表有OrderID PK,因此this.orderID必须在createOrder()成功后才有效,否则子表插入报外键约束失败。
4. 避坑指南:五个让开发者凌晨三点还在查日志的真实问题
4.1 现象:首页index.jsp打开空白,查看Tomcat日志显示org.apache.jasper.JasperException: /index.jsp (line: 12, column: 1) The value for the useBean class attribute bean.UserBean is invalid.
原因:bean.UserBean类未编译进WEB-INF/classes/,或web.xml中未配置<servlet>标签导致JSP引擎找不到类加载器上下文。
解决:
- 确认
src/bean/UserBean.java已用javac -cp "WEB-INF/lib/sqljdbc4.jar;WEB-INF/lib/jstl.jar" -d WEB-INF/classes src/bean/UserBean.java编译; - 检查
WEB-INF/classes/bean/UserBean.class文件是否存在且非零字节; - 在
web.xml顶部添加<web-app xmlns="http://java.sun.com/xml/ns/javaee" ...>命名空间声明,旧版XSD缺失会导致JSP解析器拒绝加载Bean。
4.2 现象:登录成功后跳转至index.jsp,但页面顶部显示Welcome, null!,session.getAttribute("user")始终为null。
原因:login.jsp中<jsp:useBean>作用域设为scope="page",而后续页面试图从session中读取。
解决:
- 将
login.jsp中<jsp:useBean id="user" class="bean.UserBean" scope="page"/>改为scope="session"; - 在
user.validate()返回true后,显式执行session.setAttribute("user", user); - 确保
index.jsp中读取时用<%= ((bean.UserBean)session.getAttribute("user")).getUserName() %>,而非pageContext.findAttribute()。
4.3 现象:添加商品到购物车后,cart.jsp显示数量为0,session.getAttribute("cart")返回空List。
原因:addtocart.jsp中response.sendRedirect("booklist.jsp")后,浏览器发起新请求,但booklist.jsp未重新从session读取购物车,而是直接渲染空集合。
解决:
- 在
booklist.jsp顶部添加<% List<bean.CartItem> cart = (List<bean.CartItem>) session.getAttribute("cart"); %>; - 在显示购物车链接处,用
<%= (cart != null ? cart.size() : 0) %>动态更新角标; - 更佳实践:将购物车读取逻辑提取为
<%@ include file="cart_header.jsp" %>,避免多页面重复代码。
4.4 现象:SQL Server连接报错java.sql.SQLException: Login failed for user 'bookshop_user'.,但SQL Server Management Studio可正常登录。
原因:SQL Server混合模式未启用,或bookshop_user账户被禁用。
解决:
- 在SSMS中右键服务器 → “属性” → “安全性” → 勾选“SQL Server和Windows身份验证模式”;
- 重启SQL Server服务;
- 展开“安全性” → “登录名” → 右键
bookshop_user→ “属性” → “状态” → 确保“登录”设为“启用”; - 在“用户映射”页,勾选
BookShopDB库,并在右侧角色中勾选db_owner。
4.5 现象:订单支付后,orderconfirm.jsp显示OrderID: 0,数据库Orders表中OrderID为正确值,但JavaBean未获取到。
原因:OrderBean.createOrder()中ps.getGeneratedKeys()返回的ResultSet未调用rs.next()即尝试rs.getInt(1),导致返回0。
解决:
- 严格按JDBC规范,在
rs.getGeneratedKeys()后必须先rs.next()再取值; - 添加判空逻辑:
if (rs.next()) { this.orderID = rs.getInt(1); } else { throw new SQLException("Failed to get generated key"); }; - 在
web.xml中为OrderServlet配置<load-on-startup>1</load-on-startup>,确保类加载时机早于JSP解析。
5. 进阶技巧:用Filter实现全局登录拦截与请求日志审计
5.1 为什么原系统没做登录拦截?以及我们如何补上
原系统所有页面(包括admin/目录下的管理页)均未校验用户登录态,任何人可直击URL访问后台。这不是疏忽,而是教学系统刻意暴露的安全缺口——它逼你思考:如果不用Spring Security,仅用Servlet Filter,该怎么堵?
答案是写一个LoginCheckFilter,在web.xml中配置其拦截/pages/*和/admin/*路径:
// src/filter/LoginCheckFilter.java package filter; import javax.servlet.*; import javax.servlet.http.*; import java.io.IOException; public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 放行登录页、注册页、静态资源 if (uri.endsWith("/login.jsp") || uri.endsWith("/register.jsp") || uri.contains("/css/") || uri.contains("/images/")) { chain.doFilter(request, response); return; } // 检查session中是否有user对象 HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { // 未登录,重定向到登录页,并保存原请求地址 String target = req.getRequestURL().toString(); if (req.getQueryString() != null) { target += "?" + req.getQueryString(); } session.setAttribute("redirectAfterLogin", target); resp.sendRedirect(req.getContextPath() + "/pages/login.jsp"); return; } // 已登录,放行 chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }在web.xml中注册:
<filter> <filter-name>LoginCheckFilter</filter-name> <filter-class>filter.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/pages/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>关键细节:
req.getSession(false)中的false表示不自动创建session,避免未登录用户也生成无效session;session.setAttribute("redirectAfterLogin", target)存储原始URL,登录成功后在login.jsp中读取并重定向,实现“登录后回到原页面”;<url-pattern>/pages/*</url-pattern>必须与实际JSP存放路径一致,若你的页面在/jsp/下,则需同步修改。
5.2 用Log4j 1.2注入请求审计能力(不改一行业务代码)
原系统无日志,所有e.printStackTrace()都输出到Tomcat控制台。我们用Log4j 1.2(兼容JDK 1.6)为其注入生产级日志能力:
- 下载
log4j-1.2.17.jar放入WEB-INF/lib/; - 在
WEB-INF/classes/下创建log4j.properties:
log4j.rootLogger=INFO, FILE, CONSOLE log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.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/bookshop.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- 在
LoginCheckFilter.doFilter()中添加审计日志:
// 在chain.doFilter()前添加 String ip = req.getRemoteAddr(); String userAgent = req.getHeader("User-Agent"); log.info("Request from " + ip + " [" + userAgent + "] to " + uri); // 在chain.doFilter()后添加(捕获响应时间) long start = System.currentTimeMillis(); chain.doFilter(request, response); long end = System.currentTimeMillis(); log.info("Response to " + ip + " took " + (end - start) + "ms");效果:
- 所有请求IP、UA、URL、耗时被记录到
$CATALINA_HOME/logs/bookshop.log;- 日志按10MB滚动,保留5个历史文件,避免磁盘撑爆;
- 无需修改任何JSP或JavaBean,Filter透明织入。
从那以后我每次接手老系统,第一件事就是加Filter做登录拦截和日志埋点——它不改变业务逻辑,却让整个系统从“黑匣子”变成“可观测体”。这比写一百行业务代码更能建立信任。希望帮到你。
本文还有配套的精品资源,点击获取