简介:这份资源是面向JavaWeb初学者与课程实践者的网上商城系统实例,以华为商城为业务原型,帮助读者理解Servlet、JSP与JavaBean协作完成用户注册登录、商品浏览、购物车与订单处理等核心流程,适合作为课程设计或毕业设计的参考项目。压缩包共2个文件,包含1个rar源码包与1个sql数据库脚本,前者提供完整的Web应用结构与依赖,后者用于初始化商品、用户、订单等数据表,整体约58.24MB,解压导入即可搭建配套数据环境。目前已有962人学习下载,具备一定的参考基础。项目将JavaWeb分层结构、MySQL数据管理与JSP动态页面生成串联起来,读者可据此梳理请求处理链路、数据库交互方式与页面渲染逻辑,并对照源码理解web.xml配置、类文件组织及标签库的运用,从而积累可复用的开发与排错经验。
1. 从一份 JavaWeb 电商练手包说起:它能帮你补齐哪些工程短板
拿到一个名为 javaweb 商城的压缩包,很多人第一反应是“又一个 CRUD 练习”。但真正把它跑起来、翻一遍目录,你会发现它更像一套被压缩过的电商业务骨架:商品分类、商品详情、购物车、订单、用户登录注册、后台管理,这些模块一个不少,而且是用最经典的 JavaWeb 分层方式串起来的。对刚学完 Servlet 和 JSP、却不知道一个完整 Web 项目长什么样的开发者来说,这种“能跑通的全链路”比零散看教程有用得多。它适合两类人:一类是想照着跑一遍、把请求从浏览器到数据库的路径走通的新手;另一类是想拿它当底子,改造成自己课程设计或小型商城原型的熟手。下面按“先看清结构、再跑起来、最后动手改”的顺序拆。
2. 项目结构与技术栈:一个请求在分层架构里怎么走
2.1 目录骨架与各层职责
这类 JavaWeb 商城项目通常采用标准的 Maven Web 工程结构,核心目录大致如下(不同版本可能略有出入,但分层逻辑一致):
src/ main/ java/ com/shop/ controller/ // Servlet,接收请求、调用 Service、转发视图 service/ // 业务逻辑接口与实现 dao/ // 数据访问接口与实现 entity/ // 实体类,与数据库表对应 util/ // 数据库连接、编码过滤、分页等工具 resources/ db.properties // 数据库连接参数 webapp/ WEB-INF/ web.xml // Servlet 与过滤器注册 views/ // JSP 页面 static/ // css、js、图片 index.jsp分层职责很清晰:Controller 只做参数接收和页面跳转,Service 承担业务判断(比如库存够不够、订单状态怎么流转),DAO 负责拼 SQL 和结果集映射,Entity 是纯数据载体。这种写法在今天看来偏“重”,但好处是每一层都能单独替换,新手读起来不会迷路。
2.2 一次商品列表请求的完整链路
以“打开首页看到商品列表”为例,请求路径是:浏览器访问/product/list→ 编码过滤器统一设置 UTF-8 →ProductServlet的doGet接收请求 → 调用ProductService.listByCategory()→ProductDao执行查询 → 结果封装成List<Product>→ 存进 request 域 → 转发到product_list.jsp渲染。
// ProductServlet.java 片段 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String categoryId = req.getParameter("categoryId"); // 参数为空时默认查全部,避免首页白屏 List<Product> products = productService.listByCategory(categoryId); req.setAttribute("products", products); req.getRequestDispatcher("/WEB-INF/views/product_list.jsp") .forward(req, resp); }这里有两个细节值得注意:一是categoryId做了空值兜底,否则首页不带参数访问会直接抛异常;二是 JSP 放在WEB-INF下,只能通过 Servlet 转发访问,防止用户直接敲 URL 绕过业务逻辑。参数categoryId对应分类表主键,传空表示查全部,传具体值则走WHERE category_id = ?的条件查询。
2.3 数据库连接与 DAO 的写法
数据访问层一般用原生 JDBC 加一个简单的连接工具类,而不是上框架。DBUtil从db.properties读取驱动、URL、用户名密码,提供getConnection()和close()。
// DBUtil.java 核心片段 static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { props.load(in); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password")); }静态代码块只加载一次驱动,连接每次现取现关,适合练手项目但并发一高就吃力。DAO 里典型写法是PreparedStatement预编译加ResultSet手动映射:
public List<Product> listByCategory(String categoryId) { String sql = "SELECT * FROM product"; boolean hasCategory = categoryId != null && !categoryId.isEmpty(); if (hasCategory) sql += " WHERE category_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (hasCategory) ps.setInt(1, Integer.parseInt(categoryId)); ResultSet rs = ps.executeQuery(); List<Product> list = new ArrayList<>(); while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); list.add(p); } return list; } catch (SQLException e) { throw new RuntimeException("查询商品失败", e); } }用PreparedStatement而不是字符串拼接,是为了挡住 SQL 注入;try-with-resources保证连接和语句一定关闭。字段名要和建表脚本严格对齐,price用BigDecimal接,别用double,金额精度翻车是电商项目里最常见的血泪经验之一。
3. 把项目跑起来:环境、建库、启动三步走
3.1 环境准备与依赖确认
运行前先确认三样东西:JDK(一般 8 或 11)、Tomcat(8.5 或 9)、MySQL(5.7 或 8.0)。项目如果是 Maven 工程,pom.xml里通常会有javax.servlet-api、mysql-connector-java、jstl这几个依赖,打包方式为war。如果压缩包里没有pom.xml,那就是传统 Web 工程,需要手动把lib下的 jar 放进WEB-INF/lib。
# 检查环境 java -version mvn -v mysql --version版本不必强求一致,但要注意两个兼容点:MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,连接串要带时区和 SSL 参数;Tomcat 9 对应javax.servlet包名,别和 Tomcat 10 的jakarta.servlet混用,混了就是一片ClassNotFoundException。
3.2 建库建表与配置修改
找到项目里的.sql脚本,导入 MySQL:
mysql -u root -p -e "CREATE DATABASE shop DEFAULT CHARSET utf8mb4;" mysql -u root -p shop < sql/shop.sql然后改src/main/resources/db.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码characterEncoding=utf8不写,中文商品名进库就是乱码;serverTimezone不写,MySQL 8 驱动会直接报时区错误。这两项是新手最容易漏的。
3.3 部署与首次访问
Maven 工程执行mvn clean package生成 war 包,丢进 Tomcat 的webapps目录,启动bin/startup.sh(Windows 用startup.bat)。IDE 里则直接配置 Tomcat Server,把项目 add 进去启动即可。访问http://localhost:8080/项目名/,能看到首页商品列表就算通了。如果 404,先看 Tomcat 日志里 war 是否解压成功;如果 500,多半是数据库连不上或表结构对不上,日志里会有明确堆栈。
4. 核心模块拆解:登录、购物车、订单三处关键实现
4.1 登录与 Session 会话管理
登录模块的典型流程是:表单提交用户名密码 → Servlet 查库比对 → 成功则把用户对象放进HttpSession→ 后续请求通过过滤器校验会话。
// LoginServlet 核心逻辑 User user = userService.login(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(req, resp); }密码在库里一般存 MD5 或加盐哈希,比对时对明文做同样处理再比。会话校验靠一个LoginFilter,拦截/order/*、/cart/*等路径,session.getAttribute("loginUser")为空就重定向到登录页。这里有个容易忽略的点:过滤器要放行登录页和静态资源,否则会把 css、js 也拦掉,页面直接裸奔。
4.2 购物车:Session 存储与数量合并
练手项目的购物车大多存在 Session 里,结构是Map<Integer, CartItem>,key 是商品 ID,value 是商品加数量。
// CartServlet 添加商品 String productId = req.getParameter("productId"); Product product = productService.getById(Integer.parseInt(productId)); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) req.getSession().getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); req.getSession().setAttribute("cart", cart); } CartItem item = cart.get(product.getId()); if (item == null) { cart.put(product.getId(), new CartItem(product, 1)); } else { item.setCount(item.getCount() + 1); // 已存在则数量累加 } resp.sendRedirect(req.getContextPath() + "/cart/list");用 Map 而不是 List,是为了 O(1) 判断商品是否已在购物车,避免每次遍历。数量累加而不是覆盖,符合用户连续点“加入购物车”的直觉。Session 方案的局限也明显:换浏览器购物车就没了,服务重启也丢,所以真实项目会落库或走 Redis,这里作为练手够用。
4.3 下单流程:事务与库存扣减
下单是整个项目里唯一必须保证原子性的地方:写订单主表、写订单明细、扣库存,三步要么全成要么全败。
// OrderService.createOrder 简化版 public boolean createOrder(int userId, Map<Integer, CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 int orderId = orderDao.insertOrder(conn, userId, totalAmount(cart)); for (CartItem item : cart.values()) { orderDao.insertItem(conn, orderId, item); int rows = productDao.reduceStock(conn, item.getProduct().getId(), item.getCount()); if (rows == 0) { // 库存不足,回滚 conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException("下单失败", e); } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException ignored) {} } }关键在reduceStock的 SQL 要带条件:UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,返回影响行数为 0 就说明库存不够,立即回滚。如果先查库存再更新,中间被别的请求插一脚就会超卖,这是并发场景下的经典翻车点。事务连接必须全程用同一个Connection,DAO 方法要接收外部传入的连接,不能各自getConnection()。
5. 避坑与排查:跑不通时先看这几处
5.1 中文乱码:现象、原因与解决
现象是商品名、用户名存进库或显示在页面上变成问号。原因通常有三层:请求体编码、响应编码、数据库连接编码。解决要三处一起改——web.xml里注册CharacterEncodingFilter设UTF-8;JSP 页头写<%@ page contentType="text/html;charset=UTF-8" %>;JDBC URL 带characterEncoding=utf8。只改一处往往还是乱,因为链路里任何一环掉链子都会前功尽弃。
5.2 数据库连接报时区或 SSL 错误
现象是启动后第一次查询就抛The server time zone value is unrecognized或 SSL 警告。原因是 MySQL 8 驱动默认要求明确时区并倾向加密连接。解决是在 URL 后追加serverTimezone=Asia/Shanghai&useSSL=false,本地开发关掉 SSL 最省事。生产环境则应该配证书而不是简单关闭。
5.3 静态资源 404 或过滤器拦截过度
现象是页面能出来但样式全丢,或者登录页自己也被重定向。原因是LoginFilter的url-pattern写成了/*,把 css、js、登录接口全拦了。解决是把过滤器映射收窄到需要登录的路径,或者在doFilter里对/static/、/login做白名单放行。改完记得清浏览器缓存,不然看到的还是旧样式。
5.4 下单后库存对不上
现象是订单生成了但库存没减,或者减了订单没生成。原因多半是事务没生效:DAO 内部自己拿了新连接,或者autoCommit没关。解决是让 Service 统一持有连接并传给 DAO,确认setAutoCommit(false)在第一条 SQL 之前执行,异常分支里rollback不能漏。排查时打开 MySQL 的 general log,看实际执行了哪些语句、有没有 commit,比猜快得多。
6. 二次开发改造:从练手包到能用的商城原型
拿到这套代码,最值钱的改法不是加页面,而是先把数据访问层换掉。原生 JDBC 加手写映射,字段一多就是体力活,我一般会把 DAO 换成 MyBatis,保留 Service 和 Controller 不动,SQL 挪进 mapper XML,实体映射交给框架,改完代码量能少一半,事务也能交给 Spring 管。第二步是购物车从 Session 迁到数据库表cart(user_id, product_id, count),这样换设备登录购物车还在,顺便把“未登录加购”做成跳登录页的逻辑。第三步是给下单接口加一层简单的防重:前端提交后禁用按钮只是表面功夫,后端要用订单号唯一索引或 Redis 锁兜底,否则用户手抖点两下就是两笔订单。
改造后的验证方法很朴素:先跑通“注册 → 登录 → 加购 → 下单 → 后台看到订单”这条主链路,再用两个浏览器同时下单同一件库存为 1 的商品,看是否只有一个成功。这个并发用例能同时验证事务、库存条件和会话隔离,比写一堆单元测试更贴近真实问题。下面这张表是我改造时常用的对照:
| 改造点 | 原实现 | 建议替换 | 验证方式 |
|---|---|---|---|
| 数据访问 | 原生 JDBC | MyBatis 或 JPA | 主链路跑通、SQL 日志核对 |
| 购物车 | Session Map | 数据库表 | 换浏览器登录后购物车仍在 |
| 下单防重 | 无 | 唯一索引或分布式锁 | 并发下单只成功一单 |
| 密码存储 | 明文或简单 MD5 | 加盐哈希 | 库中看不到明文 |
有一点我踩过:换 ORM 时别一次性把所有 DAO 都改完,先挑商品查询这一个模块改,跑通再铺开,否则一处映射错就满屏异常,根本定位不到是哪张表的问题。从那以后我每次动数据层,都强制先只改一个查询接口、跑通再继续,这个习惯帮我省了太多来回。希望这份拆解能帮到你,把压缩包跑起来只是开始,真正有意思的是你动手改的第一行。
本文还有配套的精品资源,点击获取