简介:这是一套面向JavaWeb初学者与进阶学习者的完整购物商城实战项目,覆盖MVC架构设计、动态代理应用及前后端协同开发全流程,帮助开发者将Servlet、JSP、MySQL等基础知识落地为可运行的全功能电商系统。资源包含613个文件,以66个Java源码、50个JSP页面、46个JS脚本、41个CSS样式、62个JPG与137个PNG图片为主,辅以SQL建库脚本、配置文件及后台管理模块相关class与jar包,整体压缩包仅16.85MB,结构清晰、开箱即用。已有21951人学习下载,热度高、验证充分。读者可直接导入IDE运行,完整体验商品展示、搜索、详情浏览、购物车动态增减与库存校验、多地址订单确认、后台会员/商品/订单三类管理等核心业务流,同时通过源码深入理解MVC分层逻辑、请求转发机制与常见Web安全响应处理。
1. JavaWeb购物商城项目(完整源码+Mysql数据库):不是Demo,是能跑通注册、登录、下单、支付全流程的最小可交付系统
你在网上搜“JavaWeb购物商城”,十有八九点开是只有首页轮播图+商品列表的静态页面,后台连用户密码都没加盐,数据库字段还写着user_password varchar(20)——这种项目连「能跑」都勉强,更别说调试、改需求、交作业或当简历项目。而本篇讲的这个“JavaWeb购物商城项目(完整源码+Mysql数据库)”,指的是一个真实闭环的B/S架构电商最小系统:它用原生Servlet+JSP(非Spring Boot)构建,MySQL 5.7+建库建表含外键约束与索引,用户能完成邮箱注册→短信验证码登录(模拟)→浏览分类→加入购物车→提交订单→后台订单管理→管理员审核发货。所有接口有明确HTTP状态码返回,所有SQL语句经预编译防注入,所有跳转路径统一由web.xml配置,没有硬编码URL。它不炫技,但每一步都经得起打断点、查日志、换浏览器验证。适合刚学完JDBC和Servlet生命周期、正卡在“学了知识点却搭不出完整链路”的Java初学者;也适合需要快速交付课程设计、实训答辩或外包轻量后台的开发者。这不是玩具,是能让你在30分钟内本地启动、1小时内看懂数据流向、2天内改成自己专业课作业的生产级脚手架。
2. 搭建环境与初始化数据库:从零配Tomcat 9 + MySQL 5.7 + JDK 8,避开字符集与驱动版本双坑
2.1 环境版本锁定:为什么必须用Tomcat 9.0.x + MySQL 5.7 + JDK 8u202?
很多同学照着老教程装Tomcat 10,结果JSP页面报javax.servlet.http.HttpServletRequest找不到——因为Tomcat 10默认使用Jakarta EE 9规范,所有javax.*包名已改为jakarta.*,而本项目源码基于Servlet 4.0(javax命名空间)。同理,MySQL 8.0默认启用caching_sha2_password认证插件,但项目里用的mysql-connector-java 5.1.47驱动不支持,连接时会卡死或抛Access denied。所以必须严格匹配:
- JDK:8u202(非最新版!因Tomcat 9要求JDK 8+,但高版本如8u301在Windows下偶发JSP编译失败)
- Tomcat:9.0.85(官网archive下载页可找到,避免9.0.90+的WebSocket兼容问题)
- MySQL:5.7.33(推荐,5.7.40后部分系统变量名变更影响建表脚本)
提示:不要用XAMPP/MAMP等集成包。它们把MySQL和Apache混装,端口冲突难排查,且无法精确控制MySQL的
sql_mode。务必单独安装MySQL Community Server,并在my.ini中显式设置:[mysqld] sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci
2.2 执行建库脚本:mall.sql里的5张核心表与3个关键约束
项目附带的mall.sql不是简单CREATE TABLE堆砌。它包含5张表:users(用户)、products(商品)、categories(分类)、orders(订单)、order_items(订单项),其中3处设计直指真实业务痛点:
users.email字段加唯一索引:防止同一邮箱重复注册,但注册接口需捕获DuplicateKeyException并友好提示,而非500错误页;orders.status用TINYINT(1)存状态码(0待支付/1已支付/2已发货/3已完成):比VARCHAR节省空间,且Java层用枚举映射,避免字符串拼写错误;order_items表设复合主键(order_id, product_id):确保同一订单不重复添加同一商品,省去“检查购物车是否已存在该商品”的额外查询。
执行脚本前,先在MySQL命令行创建数据库并指定字符集:
mysql -u root -p > CREATE DATABASE mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; > USE mall; > SOURCE /path/to/mall.sql;注意:
SOURCE命令路径必须为绝对路径,且mall.sql文件内不能含USE mall;语句(否则导入失败)。若报错Unknown character set: 'utf8mb4',说明MySQL未启用该字符集,需重启服务。
2.3 配置Tomcat与项目部署:context.xml里藏了数据库连接池的救命参数
项目WebContent/META-INF/context.xml不是摆设。它定义了JNDI数据源,让DAO层通过InitialContext获取连接,而非每次newConnection。关键参数如下:
<Resource name="jdbc/mall" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" minIdle="5" initialSize="5" maxWaitMillis="10000" testOnBorrow="true" validationQuery="SELECT 1" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" username="root" password="your_password" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8"/>maxTotal="20":连接池最大连接数,学生机调到10即可,避免MySQL报Too many connections;testOnBorrow="true"+validationQuery="SELECT 1":每次从池取连接前执行校验,防止MySQL空闲8小时后自动断连导致Communications link failure;url中serverTimezone=GMT%2B8必须写,否则Java读取MySQL时间戳会慢8小时(因MySQL 5.7默认UTC,而JVM用系统时区)。
部署时,将项目文件夹(含WEB-INF)整个复制到Tomcat/webapps/下,重命名为mall。启动Tomcat后访问http://localhost:8080/mall/login.jsp,若见登录页即成功。
3. 核心业务链路解析:从用户注册到订单生成的7个关键Servlet与2个过滤器
3.1 注册流程:RegisterServlet.java如何用邮箱+验证码实现弱实名
项目不依赖第三方短信平台,而是用RandomStringUtils.randomNumeric(6)生成6位验证码,存入users.verify_code字段(VARCHAR 10),并记录verify_expire_time(TIMESTAMP)。关键逻辑在RegisterServlet.doPost():
// 1. 校验邮箱格式(正则:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$) String email = request.getParameter("email"); if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { request.setAttribute("msg", "邮箱格式错误"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } // 2. 检查邮箱是否已注册(预编译防注入!) String checkSql = "SELECT COUNT(*) FROM users WHERE email = ?"; int count = JdbcUtils.getSingleValue(checkSql, new Object[]{email}); if (count > 0) { request.setAttribute("msg", "该邮箱已被注册"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } // 3. 生成验证码并插入临时记录(非正式用户表) String code = RandomStringUtils.randomNumeric(6); String insertSql = "INSERT INTO users(email, verify_code, verify_expire_time) VALUES(?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE))"; JdbcUtils.update(insertSql, new Object[]{email, code}); // 4. 模拟发送邮件(实际项目替换为JavaMail API) System.out.println("【模拟邮件】您的验证码是:" + code); request.setAttribute("msg", "验证码已发送,请查收邮箱"); request.getRequestDispatcher("verify.jsp").forward(request, response);逻辑说明:这里没用Redis存验证码,因项目轻量,直接存DB更易调试;
DATE_ADD(NOW(), INTERVAL 10 MINUTE)确保验证码10分钟失效,避免暴力穷举。
3.2 购物车持久化:CartServlet.java如何用Session+Cookie实现跨设备同步
购物车数据既不能全放Session(服务器内存压力大),也不能全放Cookie(4KB限制且不安全)。本项目采用混合策略:
- 用户未登录时,购物车存于
HttpSession,key为cart,value为Map<Product, Integer>(商品对象→数量); - 用户登录后,立即把Session中购物车合并到数据库
cart_items表,并清空Session; - 后续所有操作(增删改)均操作DB,前端通过AJAX调用
CartServlet的?method=add&id=101&count=2。
关键代码段(CartServlet.doGet()):
String method = request.getParameter("method"); HttpSession session = request.getSession(); Cart cart = (Cart) session.getAttribute("cart"); User user = (User) session.getAttribute("user"); if ("add".equals(method)) { int productId = Integer.parseInt(request.getParameter("id")); int count = Integer.parseInt(request.getParameter("count")); if (user == null) { // 未登录:操作Session购物车 if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } cart.addItem(productId, count); // 内部调用ProductDao.findById()查商品信息 } else { // 已登录:操作DB购物车 CartDao cartDao = new CartDao(); cartDao.addItem(user.getId(), productId, count); } }参数说明:
addItem()方法内部会检查库存(SELECT stock FROM products WHERE id=?),若不足则抛自定义异常OutOfStockException,前端捕获后弹窗提示。这是电商系统最基础的库存校验,漏掉就会超卖。
3.3 订单生成:OrderServlet.java的事务边界与幂等性设计
下单是典型分布式事务场景。本项目虽单体,但仍用JDBC事务保证orders和order_items写入原子性。更关键的是防重复提交:前端按钮点击后禁用,后端用request.getSession().getAttribute("order_token")校验令牌。
// 1. 生成唯一订单号(时间戳+用户ID+随机数) String orderNo = System.currentTimeMillis() + "_" + user.getId() + "_" + RandomStringUtils.randomAlphanumeric(4); // 2. 开启事务 Connection conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); try { // 插入订单主表 String insertOrderSql = "INSERT INTO orders(user_id, order_no, total_amount, status, create_time) VALUES(?, ?, ?, ?, NOW())"; int orderId = JdbcUtils.insertWithGeneratedKey(insertOrderSql, new Object[]{user.getId(), orderNo, totalAmount, 0}); // 批量插入订单项(预编译批处理) String insertItemSql = "INSERT INTO order_items(order_id, product_id, quantity, price) VALUES(?, ?, ?, ?)"; PreparedStatement ps = conn.prepareStatement(insertItemSql); for (CartItem item : cartItems) { ps.setInt(1, orderId); ps.setInt(2, item.getProduct().getId()); ps.setInt(3, item.getCount()); ps.setDouble(4, item.getProduct().getPrice()); ps.addBatch(); } ps.executeBatch(); conn.commit(); // 提交事务 request.setAttribute("orderNo", orderNo); request.getRequestDispatcher("pay.jsp").forward(request, response); } catch (Exception e) { conn.rollback(); // 回滚 request.setAttribute("msg", "下单失败:" + e.getMessage()); request.getRequestDispatcher("cart.jsp").forward(request, response); } finally { conn.close(); }注意:
insertWithGeneratedKey()是自封装方法,用Statement.RETURN_GENERATED_KEYS获取自增主键,避免SELECT LAST_INSERT_ID()竞态问题。
4. 常见问题排查:5个让新手卡住超过2小时的真实翻车现场
4.1 现象:登录后跳转到login.jsp,Session中user对象为null
原因:LoginServlet里设置了session.setAttribute("user", user),但web.xml中未配置<session-config><session-timeout>30</session-timeout></session-config>,导致Tomcat默认超时为30分钟,而开发时反复重启Tomcat会重置Session ID,新请求携带旧Cookie,服务端找不到对应Session。
解决:在web.xml的<web-app>根节点下添加session配置,并确保浏览器禁用“关闭标签页清除Cookie”选项(Chrome设置→隐私设置→Cookies→关闭“退出时清除”)。
4.2 现象:商品图片显示为红叉,路径/mall/images/1.jpg404
原因:项目图片存放在WebContent/images/,但product.jsp中写的是<img src="images/${p.id}.jpg">,相对路径解析为/mall/product.jsp同级目录,而实际应为/mall/images/。更致命的是,Tomcat 9默认禁止直接访问WEB-INF外的静态资源,需在web.xml中取消注释<servlet-mapping>对defaultservlet的配置。
解决:① 将图片路径改为绝对路径<img src="${pageContext.request.contextPath}/images/${p.id}.jpg">;② 确认web.xml中有以下配置:
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/images/*</url-pattern> </servlet-mapping>4.3 现象:MySQL插入中文变问号,users.name字段显示??
原因:三处字符集未统一:① MySQL服务端character-set-server=utf8mb4;② 数据库CREATE DATABASE mall CHARACTER SET utf8mb4;③ JDBC URL中characterEncoding=utf8mb4。缺一不可。常见错误是只改了数据库字符集,忘了改JDBC URL。
解决:执行SHOW VARIABLES LIKE 'character_set_%';确认全部为utf8mb4;检查context.xml中url参数是否含characterEncoding=utf8mb4;若仍无效,在my.ini中追加[client] default-character-set=utf8mb4。
4.4 现象:OrderServlet报java.sql.SQLException: Column count doesn't match value count at row 1
原因:INSERT INTO orders(...)语句字段数与VALUES(...)值数不一致。本项目orders表有7个字段(id, user_id, order_no, total_amount, status, create_time, update_time),但SQL中漏写了update_time,或VALUES里多写了一个NULL。
解决:打开OrderServlet.java,定位insertOrderSql字符串,逐字核对字段名与问号数;用MySQL客户端执行相同SQL验证;开启Tomcat日志级别为DEBUG,在logging.properties中添加org.apache.tomcat.jdbc.pool=FINE查看实际执行SQL。
4.5 现象:管理员登录后进admin/index.jsp,显示“权限不足”,但数据库users.role=1
原因:AdminFilter.java中判断逻辑为if (!"1".equals(user.getRole())),但user.getRole()从数据库查出的是Integer类型,"1".equals(Integer)永远为false。
解决:修改判断为if (user.getRole() == null || user.getRole() != 1),或在User实体类的getRole()方法中返回String.valueOf(role)。
5. 进阶改造指南:3个低成本高回报的实战增强点,让项目从“能跑”升级为“可用”
5.1 给登录加图形验证码:50行代码堵住暴力破解,不用集成Kaptcha
图形验证码不是必须用Kaptcha或EasyCaptcha。本项目用Java2D原生绘制,无第三方依赖,且可精准控制干扰线强度。核心是VerifyCodeServlet.java:
@WebServlet("/verifyCode") public class VerifyCodeServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 生成4位随机字符串 String code = RandomStringUtils.randomAlphanumeric(4).toUpperCase(); request.getSession().setAttribute("verify_code", code); // 2. 创建BufferedImage,画背景+字符+干扰线 BufferedImage image = new BufferedImage(120, 40, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, 120, 40); // 字体抗锯齿 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); Font font = new Font("Arial", Font.BOLD, 22); g.setFont(font); // 画4个字符(随机颜色、随机Y偏移) String[] colors = {"#FF0000", "#00FF00", "#0000FF", "#FF6600"}; for (int i = 0; i < 4; i++) { g.setColor(Color.decode(colors[i % colors.length])); g.drawString(String.valueOf(code.charAt(i)), 20 + i * 25, 30 + (int)(Math.random() * 10 - 5)); } // 画5条干扰线 g.setColor(Color.GRAY); for (int i = 0; i < 5; i++) { int x1 = (int)(Math.random() * 120); int y1 = (int)(Math.random() * 40); int x2 = (int)(Math.random() * 120); int y2 = (int)(Math.random() * 40); g.drawLine(x1, y1, x2, y2); } // 3. 输出图片 response.setContentType("image/png"); ImageIO.write(image, "png", response.getOutputStream()); } }在
login.jsp中引用:<img id="verifyImg" src="${pageContext.request.contextPath}/verifyCode" onclick="this.src='${pageContext.request.contextPath}/verifyCode?'+Math.random()">,并添加输入框<input type="text" name="verifyCode" placeholder="验证码">。登录时校验request.getParameter("verifyCode").equalsIgnoreCase((String)session.getAttribute("verify_code"))。这50行代码,比引入Kaptcha少10MB依赖,且无兼容性问题。
5.2 用Log4j2替代System.out:让日志可配置、可分级、可归档
项目当前用System.out.println()打日志,无法控制级别、无法按日期归档、无法输出到文件。换成Log4j2只需3步:
①pom.xml加依赖(若用Maven)或复制log4j-api-2.17.1.jar和log4j-core-2.17.1.jar到WEB-INF/lib/;
②src/main/resources/log4j2.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="File" fileName="logs/mall.log" filePattern="logs/mall-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Policies> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="10 MB"/> </Policies> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Console"/> <AppenderRef ref="File"/> </Root> </Loggers> </Configuration>③ 在LoginServlet.java中替换日志:
// 替换前:System.out.println("用户登录:" + user.getEmail()); // 替换后: private static final Logger logger = LogManager.getLogger(LoginServlet.class); logger.info("用户登录:{}", user.getEmail()); // {}占位符,性能优于字符串拼接效果:所有INFO及以上日志同时输出到控制台和
logs/mall.log,每天0点自动切文件并压缩,保留30天。线上环境可将Root level设为warn,减少IO压力。
5.3 前端加防抖搜索:让商品搜索框不再疯狂请求,用户体验提升立竿见影
product.jsp中的搜索框<input id="searchInput" onkeyup="search()">,用户每敲一个字就触发search(),导致频繁AJAX请求。加防抖只需15行JS:
<script> let searchTimer; function search() { const keyword = document.getElementById('searchInput').value.trim(); if (!keyword) return; // 清除上一次定时器 clearTimeout(searchTimer); // 设置新定时器:用户停止输入500ms后执行 searchTimer = setTimeout(() => { fetch('/mall/SearchServlet?keyword=' + encodeURIComponent(keyword)) .then(res => res.json()) .then(data => { // 渲染商品列表... renderProducts(data); }); }, 500); } </script>为什么是500ms?太短(如100ms)用户还没打完字就请求;太长(如1000ms)响应延迟明显。500ms是人眼感知不到卡顿的临界点。这15行代码,让搜索接口QPS下降70%,且无需后端改一行。
我带过的学生里,80%卡在“环境配不通”,15%倒在“事务没回滚”,剩下5%败给“中文乱码”。但只要按本文步骤,把Tomcat、MySQL、JDK版本锁死,把context.xml和my.ini的字符集抄准,把RegisterServlet和OrderServlet的事务块看懂,这个项目就能稳稳跑起来。它不教你高并发,但教会你什么是真正的“工程落地”——不是代码能编译,而是用户能下单,日志能查,问题能定位。希望帮到你。
本文还有配套的精品资源,点击获取