简介:这是一份面向JavaWeb初学者与高校实训学生的在线商城项目实战资源,基于JSP+Servlet+MySQL+JDBC技术栈实现,覆盖用户注册登录、商品浏览、购物车管理、订单提交等核心电商功能,适合课程设计、期末实训及Web开发入门实践。压缩包共189个文件,包含18个JSP页面(前端交互)、11个Java类(业务逻辑与控制器)、22个Class字节码(编译结果)、17个Jar包(数据库驱动与工具依赖)、16个CSS与14个JS文件(页面样式与交互),以及大量图片资源(42个JPG+18个PNG+2个WEBP),整体体积23.58MB,结构完整、模块清晰。已有2034人学习下载,资源附带可直接部署的WAR包、基础数据库脚本及README说明,预览可见RegisterController、DetailController、AddCarController等典型MVC分层代码,便于理解请求流转与数据处理逻辑,是掌握JavaWeb前后端协同开发流程的实用参考范例。
1. 这不是一个“仿小米”的UI套壳项目,而是一套可跑通的JavaWeb分层实践骨架
很多初学者下载“JavaWeb仿小米商城”压缩包后,第一眼看到Register.class、DetailController.class这些编译文件就懵了:没有源码怎么学?其实这恰恰是本项目最值得拆解的地方——它用最朴素的Servlet+JSP+JDBC组合,把用户注册、商品详情、购物车添加等核心链路,压缩在6个可执行的Class文件里,不依赖Maven、不包装Spring Boot、不抽象DAO接口,所有逻辑直击HTTP请求生命周期。它解决的不是“如何做出高颜值电商首页”,而是“当浏览器发来一个/addCar?pid=1024&count=2请求时,Java Web容器内部到底发生了什么”。适合大三刚学完JDBC和Servlet生命周期、正卡在“写完代码却不知道请求怎么进来的”阶段的同学;也适合想快速验证自己对HttpServletRequest.getParameter()、HttpSession.setAttribute()、JDBCUtils.getConnection()三者协作关系理解是否准确的开发者。项目虽小,但每个.class文件背后都对应着一次完整的MVC流程切片。
2. 从字节码反推分层结构:解析RegisterController.class与Register.class的职责边界
2.1 RegisterController.class:Servlet层的请求路由中枢
RegisterController.class是典型的HttpServlet子类编译产物。通过javap -c RegisterController.class反编译其字节码,可确认其doPost()方法中必然包含以下关键指令序列:
// 伪代码还原(实际字节码指令对应此逻辑) String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); Register registerBean = new Register(); // 注意:此处调用的是Register.class的无参构造 registerBean.setUsername(username); registerBean.setPassword(password); registerBean.setEmail(email); int result = JDBCUtils.executeUpdate( "INSERT INTO users(username,password,email) VALUES(?,?,?)", username, password, email ); if (result > 0) { response.sendRedirect("login.jsp"); } else { request.setAttribute("msg", "注册失败"); request.getRequestDispatcher("register.jsp").forward(request, response); }提示:
RegisterController.class本身不处理数据库连接,它只做三件事——接收参数、组装数据对象、调用工具类执行SQL。这种剥离符合Servlet规范:Controller不持有Connection,Connection由JDBCUtils.class统一管理。
2.2 Register.class:纯粹的数据载体(POJO),无业务逻辑
反编译Register.class会发现其仅含私有字段、getter/setter及无参构造方法,典型JavaBean结构:
public class Register { private String username; private String password; private String email; public Register() {} // 必须存在,否则new Register()会抛NoSuchMethodError public void setUsername(String username) { this.username = username; } public String getUsername() { return username; } // ... 其他getter/setter }2.2.1 为什么不用Map<String, Object>替代Register?
虽然request.getParameterMap()能直接获取键值对,但使用Register类有三个硬性优势:
- 类型安全:
setPassword(String)强制密码为字符串,避免getParameter("password")返回null时未判空导致NPE; - IDE支持:在
RegisterController.doPost()中敲registerBean.即可自动补全字段名,减少拼写错误; - 扩展预留:未来若需对密码加密,只需在
setPassword()中插入BCrypt.hashpw(...),无需修改Controller层。
2.3 JDBCUtils.class:连接池缺失下的务实方案
项目未使用Druid或HikariCP,JDBCUtils.class采用最简DriverManager.getConnection()实现:
public class JDBCUtils { private static final String URL = "jdbc:mysql://localhost:3306/shoppingmall?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); // 每次新建连接,性能低但代码极简 } public static int executeUpdate(String sql, Object... params) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); // 防止SQL注入的核心:预编译参数化 } return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }2.3.1 关键参数说明与可修改项
| 参数 | 当前值 | 修改建议 | 说明 |
|---|---|---|---|
useSSL=false | 强制关闭SSL | 生产环境必须改为true并配置证书 | MySQL 8.0+默认要求SSL,本地开发可关闭 |
serverTimezone=UTC | 时区设为UTC | 建议改为Asia/Shanghai | 避免NOW()函数返回时间与系统时区不一致 |
USER/PASSWORD | root/123456 | 必须按实际数据库账号修改 | 硬编码密码是严重安全隐患,应抽到properties文件 |
注意:
executeUpdate()方法中try-with-resources自动关闭Connection/PreparedStatement,这是JDK 7+语法糖,确保资源不泄漏。若运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,需将mysql-connector-java-8.0.33.jar放入WEB-INF/lib/目录。
3. 购物车功能闭环:AddCarController.class与Session状态管理实战
3.1 AddCarController.class的请求处理链路
该类处理形如/addCar?pid=1024&count=2的GET请求,核心逻辑在于将商品ID与数量存入HttpSession,而非直接操作数据库:
public class AddCarController extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pidStr = request.getParameter("pid"); String countStr = request.getParameter("count"); // 1. 参数校验(生产环境必须加) if (pidStr == null || countStr == null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少pid或count参数"); return; } int pid = Integer.parseInt(pidStr); int count = Integer.parseInt(countStr); // 2. 从Session获取购物车Map,若不存在则创建新HashMap Map<Integer, Integer> cart = (Map<Integer, Integer>) request.getSession().getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); request.getSession().setAttribute("cart", cart); } // 3. 更新购物车:累加数量(非覆盖) cart.put(pid, cart.getOrDefault(pid, 0) + count); // 4. 重定向到购物车页面,避免重复提交 response.sendRedirect("cart.jsp"); } }3.1.1 为什么用request.getSession().getAttribute("cart")而不是request.getAttribute("cart")?
request.getAttribute()作用域仅限本次请求,跳转到cart.jsp后数据丢失;HttpSession在用户浏览器关闭前持续存在,cart作为Session属性可跨多个请求访问;cart.jsp中通过<%= session.getAttribute("cart") %>即可获取该Map,再用JSTL<c:forEach>遍历渲染。
3.2 DetailController.class:商品详情页的动态数据加载
该类负责根据URL参数pid查询商品详情并转发至detail.jsp:
public class DetailController extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pidStr = request.getParameter("pid"); if (pidStr == null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } int pid = Integer.parseInt(pidStr); // 查询SQL:SELECT * FROM products WHERE id = ? Product product = JDBCUtils.executeQuery( "SELECT * FROM products WHERE id = ?", rs -> { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setImgUrl(rs.getString("img_url")); return p; }, pid ); if (product != null) { request.setAttribute("product", product); // 绑定到request作用域 request.getRequestDispatcher("detail.jsp").forward(request, response); } else { response.sendError(HttpServletResponse.SC_NOT_FOUND, "商品不存在"); } } }3.2.1 JDBCUtils.executeQuery()的泛型回调设计
此方法采用Function<ResultSet, T>回调,将结果集映射逻辑与数据库操作分离:
public static <T> T executeQuery(String sql, Function<ResultSet, T> mapper, Object... params) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { return rs.next() ? mapper.apply(rs) : null; // 只取第一条记录 } } catch (SQLException e) { e.printStackTrace(); return null; } }提示:
Product类需与数据库products表字段严格对应(如img_url映射到setImgUrl()),否则rs.getString("img_url")返回null。若字段名不一致,应在SQL中用AS别名,例如SELECT id AS productId, name AS productName FROM products。
4. 数据库建模与前端交互:MySQL表结构与jQuery异步提交验证
4.1 shoppingmall数据库核心表设计
项目依赖的MySQL数据库需包含以下三张表,建表语句必须与Java代码中的SQL完全匹配:
-- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存储BCrypt加密后的密文 email VARCHAR(100) NOT NULL ); -- 商品表 CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, img_url VARCHAR(200), description TEXT ); -- 订单表(项目未实现下单,但预留结构) CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) );4.1.1 字段类型选择依据
| 字段 | 类型 | 选择理由 |
|---|---|---|
price | DECIMAL(10,2) | 避免浮点数精度丢失(如19.99存储为19.990000000000002),金融计算必须用DECIMAL |
password | VARCHAR(100) | BCrypt加密后密文长度约60字符,留足余量 |
create_time | DATETIME | MySQL原生时间类型,比BIGINT时间戳更易读、支持NOW()函数 |
4.2 register.jsp中的jQuery表单验证
前端register.jsp使用jQuery对注册表单做客户端校验,关键代码如下:
<script src="js/jquery-3.6.0.min.js"></script> <script> $(function() { $("#registerForm").submit(function(e) { e.preventDefault(); // 阻止默认提交 var username = $("#username").val().trim(); var password = $("#password").val(); var email = $("#email").val().trim(); // 1. 用户名非空且长度2-20 if (!username || username.length < 2 || username.length > 20) { alert("用户名长度必须为2-20个字符"); return false; } // 2. 密码非空且至少6位 if (!password || password.length < 6) { alert("密码长度不能少于6位"); return false; } // 3. 邮箱格式验证(简单正则) var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { alert("请输入有效的邮箱地址"); return false; } // 4. 提交表单(触发RegisterController) this.submit(); }); }); </script>4.2.1 为什么客户端验证不能替代服务端验证?
- 客户端JS可被禁用或绕过(如直接curl发送请求);
RegisterController.class中仍需二次校验username != null && !username.isEmpty();- 安全原则:客户端验证只为提升用户体验,服务端验证才是安全底线。
5. 部署调试与常见故障排查:Tomcat日志定位与JSP编译错误修复
5.1 Tomcat启动失败的三大高频原因
当将项目部署到Tomcat后访问http://localhost:8080/ShoppingMall/register.jsp报404或500错误,按以下顺序排查:
| 故障现象 | 日志关键词 | 解决方案 |
|---|---|---|
ClassNotFoundException: com.mysql.cj.jdbc.Driver | SEVERE: Servlet.service() for servlet [RegisterController] in context with path [/ShoppingMall] threw exception | 将mysql-connector-java-8.0.33.jar复制到$CATALINA_HOME/lib/或ShoppingMall/WEB-INF/lib/ |
SQLException: Access denied for user 'root'@'localhost' | Caused by: java.sql.SQLException: Access denied for user | 修改JDBCUtils.class中的USER/PASSWORD为真实数据库账号,或在MySQL中执行CREATE USER 'shopuser'@'localhost' IDENTIFIED BY 'pwd123'; GRANT ALL ON shoppingmall.* TO 'shopuser'@'localhost'; |
org.apache.jasper.JasperException: /register.jsp (line: 12, column: 1) The method setAttribute(String, Object) is undefined for the type HttpServletRequest | org.apache.jasper.compiler.DefaultErrorHandler.javacError | 检查register.jsp顶部是否有<%@ page import="javax.servlet.http.HttpServletRequest" %>,JSP中request对象已内置,无需import,此错误通常因JSP语法错误(如多写了<%=)导致编译器误判 |
5.2 JSP页面中文乱码终极解决方案
若register.jsp中显示“用户名”变成“ç”乱码,需同步修改三处:
JSP页面声明(
register.jsp首行):<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>Tomcat配置(
$CATALINA_HOME/conf/server.xml中Connector节点):<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 必须添加此属性 -->数据库连接URL(
JDBCUtils.class中):private static final String URL = "jdbc:mysql://localhost:3306/shoppingmall?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8";
提示:
URIEncoding="UTF-8"解决GET请求参数乱码(如?username=张三),characterEncoding=UTF-8解决数据库存取乱码,二者缺一不可。
5.3 使用Chrome DevTools验证Session购物车状态
在AddCarController添加商品后,可通过浏览器开发者工具验证Session是否生效:
- 打开Chrome DevTools(F12)→ Application → Storage → Cookies;
- 查看
http://localhost:8080域名下的Cookie,确认存在JSESSIONID(Tomcat生成的Session ID); - 在Console中执行:
若返回HTML中包含// 模拟向cart.jsp发起请求,查看Session中cart内容 fetch('cart.jsp').then(r => r.text()).then(console.log);<c:forEach items="${sessionScope.cart}"且渲染出商品,则证明Session写入成功。
最终,这个看似简单的ShoppingMall.zip包,本质是一份可执行的JavaWeb教学切片——它用最原始的Servlet代替Spring MVC的@RequestMapping,用JDBCUtils代替MyBatis的SqlSession,用HttpSession代替Redis缓存。当你能手动反编译出AddCarController.class的字节码,并在Tomcat日志里精准定位到NullPointerException发生在哪一行request.getParameter()时,你就真正穿过了JavaWeb的第一道门。
本文还有配套的精品资源,点击获取