尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

2025年用JSP+Servlet写蛋糕店商城:JavaWeb全流程实战解析

2025年用JSP+Servlet写蛋糕店商城:JavaWeb全流程实战解析 简介一套基于JSP和Servlet的JavaWeb蛋糕店售卖网站源码搭配MySQL数据库适合JavaWeb学习者、课程设计或毕业设计参考也能作为电商项目二次开发的起点。项目开发环境为IDEA 2017、Tomcat 8.5和MySQL 5.7采用C3P0连接池与DBUtil工具进行数据操作。网站分为前台和后台两部分前台支持推荐商品、新品热销、分类浏览、商品详情、购物车管理、用户注册登录、个人信息维护、订单支付查询、关键字搜索后台则覆盖订单状态管理、用户管理、商品类目维护、商品增删改查等完整流程。压缩包共290个文件包括58个JSP页面呈现页面结构、55个Java类处理业务逻辑与数据访问、20个XML配置维护运行参数、14个JS文件与12个CSS样式表完善交互与样式并携带SQL数据库脚本及图片素材整体体积仅19.11MB结构清晰、便于导入和调试。目前已有126人学习使用能帮助快速掌握传统JavaWeb开发模式。1. 为什么 2025 年还要用 JSP Servlet 写蛋糕店商城接过一个用 IDEA 2017.3.5、Tomcat 8.5.35、MySQL 5.7 搭建的 JSP Servlet 商城项目第一反应通常是「这年头谁还写 JSP」。但真把十几张表、前台购物车和后台订单管理梳理完会发现这套老技术恰恰是最容易讲清楚 JavaWeb 请求生命周期、连接池管理和 Session 状态的载体。这个蛋糕店售卖网站就是这样一个完整案例前台有商品条幅推荐、热销新品、类型列表、商品详情、购物车数量修改、用户注册登录、收货信息维护后台有按状态查订单、改发货状态、管理用户、商品类目和商品信息。它不像 Spring Boot 那样自动封装所有 Servlet 映射、数据库连接、JSP 取值都能一眼看透适合刚学完 JavaWeb 想练全流程的人也适合需要给老项目做维护或二次开发的人。下面就从工程结构和数据层开始拆。2. 工程结构与数据层设计从 C3P0 到 DBUtil 的封装思路2.1 一个典型 JSP Servlet 项目的目录划分拿到项目源码先别急着跑把目录结构过一遍。这类项目一般分成 src 主目录和 web 应用目录两大部分。src 下会看到bean/entity、dao、service、servlet/controller、util、filter这几个包web 目录下则有admin、css、js、images、WEB-INF等。这个蛋糕店项目的前台页面直接放在根目录或WEB-INF之外的page下后台管理页放在admin下静态资源里出现了 bootstrap.css、style.css、layer.css说明前端用了 Bootstrap 弹层和自定义样式。不同模块的职责可以用一张表说清楚目录/包主要文件职责beanUser.java / Product.java / CartItem.java数据库表结构对应的 JavaBean存字段和 getter/setterdaoUserDao / ProductDao / OrderDao / CategoryDao直接执行 SQL返回 JavaBean 或 ListservletUserServlet / ProductServlet / CartServlet / OrderServlet接收请求调用 dao转发或重定向到 JSPfilterAdminFilter.java拦截后台管理地址校验管理员登录状态web/WEB-INF/libc3p0 jar、mysql 驱动数据库连接池与驱动依赖web/adminlist.jsp / order_list.jsp / category_list.jsp后台管理页面web/cssbootstrap.css / style.css / layer.css页面样式与弹层组件注意老项目经常把service层省略Servlet 直接调 DAO。这个蛋糕店项目虽然叫 JavaWeb但登录、注册、订单状态变更这类逻辑不复杂所以直接写 DAO 也说得过去。如果你要二次开发建议补一层 service否则后面加事务会非常痛苦。2.2 C3P0 配置与连接池初始化项目配置里写的是 C3P0这是早期 JavaWeb 项目最常见的连接池。它的配置一般放在src/c3p0-config.xml核心目的是在项目启动时预创建一批数据库连接避免每次请求都走一次完整的三次握手。下面是一份典型配置?xml version1.0 encodingUTF-8? c3p0-config named-config namecake property namedriverClasscom.mysql.jdbc.Driver/property property namejdbcUrljdbc:mysql://localhost:3306/cake?useUnicodetrueamp;characterEncodingutf-8/property property nameuserroot/property property namepassword123456/property property nameinitialPoolSize5/property property namemaxPoolSize20/property property nameminPoolSize5/property property namecheckoutTimeout3000/property /named-config /c3p0-config这里最容易踩的坑有两个。一个是com.mysql.jdbc.Driver只适用于 MySQL 5.x如果你本地用了 MySQL 8要换成com.mysql.cj.jdbc.Driver。另一个是characterEncodingutf-8必须跟着useUnicodetrue一起写否则中文商品名入库会变成问号。c3p0 通过DataSource接口暴露连接对象只要项目依赖了 c3p0 包就能在工具类里通过new ComboPooledDataSource(cake)拿到数据源。2.3 DBUtil 封装从 Connection 到 QueryRunner这个项目里提到的 DBUtil实际上是对 JDBC 操作的二次封装。它一般提供三块能力从 C3P0 数据源取连接、释放资源、执行通用增删改查。虽然 Apache DbUtils 也有现成的QueryRunner但手写一个最小的 DBUtil 反而更符合老项目风格也好理解底层原理。看一下常见的写法public class DBUtil { private static DataSource ds new ComboPooledDataSource(cake); public static Connection getConnection() throws SQLException { return ds.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }注意conn.close()在这里不是真正断开连接而是把连接归还给 c3p0 连接池。所以每次用完后必须调用close否则连接池耗尽系统会卡在checkoutTimeout后报超时异常。另外写 DAO 时要使用 PreparedStatement 而不是 Statement因为蛋糕名称、用户收货地址这种字符串一旦包含单引号或中文标点直接拼 SQL 轻则报错重则被注入。2.4 DAO 层以商品为例商品表是这个项目的核心前台展示、搜索、后台管理都围绕它。一个 ProductDao 至少包含以下方法根据推荐位查询、按类型查询、按关键字搜索、分页查询、插入、更新、删除。这里给出两个典型查询方法public ListProduct findByRecommend(int recType) throws SQLException { String sql SELECT * FROM product WHERE rec_type ? AND status 1 ORDER BY sale_count DESC; return query(sql, recType); } public ListProduct search(String keyword) throws SQLException { String sql SELECT * FROM product WHERE name LIKE ? AND status 1; return query(sql, % keyword %); }recType通常用 0、1、2 分别表示条幅推荐、热销推荐、新品推荐status 1控制商品是否上架。这里用了LIKE ?而不是直接拼接%keyword%本质是为了让 PreparedStatement 帮你处理特殊字符。查询方法内部就是获取连接、PreparedStatement 绑定参数、执行查询、把 ResultSet 映射成 Product 对象的重复动作DBUtil 的意义就是把这段重复代码收拢到一个地方。3. 前台业务流商品展示、购物车数量修改与关键字搜索3.1 首页推荐位的数据源与 JSP 取值蛋糕店网站的首页一般分三块顶部条幅推荐中间热销商品底部新品推荐。Controller 里常见做法是在一个IndexServlet中调用三次 findByRecommend把结果放进 request再forward到index.jsp。JSP 里用 JSTL 或最原始的% %取数据。以热销推荐为例ul classhot-list % ListProduct hotList (ListProduct) request.getAttribute(hotList); for (Product p : hotList) { % li a href% request.getContextPath() %/product/detail?id% p.getId() % img src% p.getImagePath() % alt% p.getName() %/ /a p classprice¥% p.getPrice() %/p /li % } % /ul这里有一个容易被忽略的细节商品图片、价格、详情都是在ProductBean 中直接给到页面的但如果页面要展示商品分类名称而不是分类 ID就需要在查询 Product 时把category_id转换成category_name。常见做法是 SQL 联表查询把category表也 join 进来而不是在 JSP 里再去查一次数据库。3.2 购物车设计Map 还是独立表购物车是这个项目里最需要想清楚的模块。常见做法有两种一是用数据库购物车表适合用户要求跨设备保存的场景二是用 Session 中的MapInteger, CartItem适合这种结构简单的 JavaWeb 项目。本项目的需求是「修改购物车内商品信息例如数量」如果每次增减数量都要把整个购物车对象重新存一遍Session 存 Map 会更合理。定义一个 CartItem 结构public class CartItem { private Product product; private int count; private double subtotal; public double getSubtotal() { return product.getPrice() * count; } }然后在CartServlet里维护购物车HttpSession session request.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMapInteger, CartItem(); session.setAttribute(cart, cart); }为什么要用MapInteger, CartItem因为加入购物车时只需要判断商品 ID 是否已存在存在就直接把 count 加 1不存在就 new 一个 CartItem 放进去。如果用的是 List每次添加都要遍历整张列表数量多了以后性能会变差。但要注意Session 里的Map默认是HashMap无法保证顺序如果页面想按添加时间倒序显示需要在显示时对cart.values()排序。3.3 数量增减与价格重算的 Servlet 接口购物车数量的修改通常是一个 Ajax 请求前端点击加号或减号把商品 ID 和新的 count 发给后端。一个比较简洁的 Servlet 接口长这样protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int productId Integer.parseInt(request.getParameter(productId)); int count Integer.parseInt(request.getParameter(count)); MapInteger, CartItem cart (MapInteger, CartItem) request.getSession() .getAttribute(cart); if (cart ! null) { CartItem item cart.get(productId); if (item ! null) { item.setCount(count); if (count 0) { cart.remove(productId); } } } response.getWriter().write(ok); }这个接口没有直接返回价格而是返回固定字符串前端再重新刷新购物车列表。这种做法好处是简单坏处是页面会闪动。进阶一点可以在响应里返回新的小计和总价比如{subtotal: 99.0, total: 199.0}前端用 JS 直接更新。价格计算时不要用double做累加多件商品数量大时会有精度问题建议用BigDecimal或者至少保证所有价格单位统一用分显示时再转换成元。3.4 关键字搜索的 LIKE 注入与参数绑定搜索功能在前台是个小点但特别容易写脏。商品名的搜索 SQL 十有八九是WHERE name LIKE %蛋糕%但很多新手喜欢用字符串拼接String sql SELECT * FROM product WHERE name LIKE % keyword %;这个写法问题很大。比如用户输入一个%相当于把全表商品都匹配出来输入; DROP TABLE product; --轻则报错重则数据被删。注意这个项目用的 MySQL 5.7 PreparedStatement 完全可以避免这个问题。正确写法是先写占位符再绑定参数。搜索页面还要注意编码GET请求中的中文在 Tomcat 8.5 默认是 UTF-8但在某些老版本下需要单独配置URIEncoding。前台搜索框通常带一个下拉选择「按名称或按分类」实现时可以在ProductDao.search里加一个搜索类型参数如果是名称搜索就WHERE name LIKE ?如果是分类搜索就WHERE category_id ?而不是把所有条件用 OR 拼到一条 SQL 里。这样索引利用率更高中文模糊搜索时也不至于全表扫描太久。3.5 JSP 个人信息展示页面的常见坑热搜词里经常出现「jsp个人信息展示页面」这个功能在蛋糕店项目里就是「我的账户」页面。用户登录后在 Session 里存的是 User 对象用户修改了收货地址如果只更新了数据库没有更新 Session 中的 User页面上展示的仍然是旧地址。正确做法是在用户信息更新成功后重新把当前用户查出并写回 SessionUser updatedUser userDao.findById(user.getId()); request.getSession().setAttribute(loginUser, updatedUser);另一个坑是 JSP 页面直接用${user.receiveAddress}取值但如果字段名与 JavaBean 属性不对应EL 表达式会静默输出空字符串不报错。排查时可以先用% ((User)session.getAttribute(loginUser)).getReceiveAddress() %验证一遍确认数据已经在 Session 里再回头改 EL。4. 后台权限与管理的落地订单状态机、类目级联与用户增删改查4.1 管理员入口与 Servlet 过滤器后台功能是通过「管理员用户登录后显示后台管理按钮」进入的。这里需要做的不仅是按钮显示更重要的是 URL 层面的拦截。因为即使页面上不显示后台入口用户直接访问/admin/order_list.jsp照样能看到数据。常见做法是写一个AdminFilter拦截所有/admin/*路径WebFilter(/admin/*) public class AdminFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; User user (User) request.getSession().getAttribute(loginUser); if (user null || !admin.equals(user.getRole())) { request.getRequestDispatcher(/login.jsp).forward(request, response); return; } chain.doFilter(request, response); } }注意角色判断不能只靠前端传入的参数。有些老项目会把role存在 Session 里这没问题但千万不要在 JSP 里用隐藏域input typehidden namerole valueadmin否则用户改一下就变成管理员了。所有后台操作必须经过这个过滤器包括 Ajax 请求。chain.doFilter之后才允许访问 JSP 或 Servlet。4.2 订单按状态查询与状态机流转后台订单操作包括按状态查询、修改状态发货、完成、删除。订单状态不是随便改的需要定义清晰的状态流转。常见状态码如下状态码含义可执行操作触发源0未付款取消 / 付款用户1已付款未发货发货管理员2已发货完成管理员3已完成删除管理员4已删除无系统订单查询接口可以接收一个state参数为空时查全部不为空时精确查某一种状态。修改状态的 SQL 一般写成一个状态机UPDATE orders SET status ? WHERE id ? AND status ?第三个status是当前状态用来防止并发下重复发货。比如管理员A和管理员B同时打开同一个订单A先点了发货B再点发货时因为当前状态已经不是「未发货」更新影响行数为 0B 就能感知到操作无效。Servlet 里判断int rows orderDao.updateStatus(targetState, orderId, currentState)如果 rows 为 0就提示「订单状态已被修改请刷新」。这种乐观锁思路比直接加synchronized更轻量。4.3 商品类目删除的级联保护后台有一个「删除商品类目」的功能。如果类目下已经存在商品直接删除会报外键约束异常或者在页面上看到商品变成了「无分类」。常见做法是删除前先统计该类目下商品数量public int countByCategory(int categoryId) { String sql SELECT COUNT(*) FROM product WHERE category_id ?; ... }如果数量大于 0则在 Servlet 里返回错误提示「该类目下还有商品请先删除或转移商品」。这一步虽然简单但很多项目会漏掉。注意这里的「删除」也可以改成逻辑删除给 category 表加一个deleted字段商品查询时同时过滤掉已删除的类目这样历史订单里商品名称还正常。但老项目为了简洁经常直接物理删除代价是历史关联数据会丢失。4.4 用户新增与密码加密的取舍后台用户管理包括新增用户、修改密码、修改信息和删除用户。新增用户时最常见的错误是明文保存密码INSERT INTO user(username, password, phone, address) VALUES (?, ?, ?, ?)这个蛋糕店项目没有在摘要中明确说加密方式作为接手的人我建议至少做到加盐 MD5。Java 原生 MD5 不够安全但比明文好很多String salt UUID.randomUUID().toString().substring(0, 6); String encrypted md5(md5(password) salt);数据库存salt和encrypted两列。登录时用相同规则重新算一遍再与数据库比对。修改密码时也要重新生成盐不要复用旧盐。后台新增用户时管理员输入初始密码系统加密后入库。删除用户的处理也需要谨慎如果用户已经下过订单物理删除会让订单关联不到用户前台查询订单时就容易出现空指针。比较稳妥的做法是给 user 表加status字段删除时置为禁用而不是真删。如果项目结构已经固定、没法加字段那至少要确保删除前检查该用户是否有未完成订单。5. 部署到 Tomcat 8.5 MySQL 5.7高频异常与验证清单5.1 启动与数据库连接验证Tomcat 8.5.35 对应的是 Servlet 3.1 规范所以项目里用WebServlet注解是没有问题的。如果改成 Tomcat 9 或 10javax.servlet 会变成 jakarta.servlet老代码要全部替换包名这里不建议升级。启动前先确认 MySQL 5.7 的 root 密码和 c3p0-config.xml 里一致然后单独用下面的 JDBC 测试类验证连通性public static void main(String[] args) { Connection conn DBUtil.getConnection(); System.out.println(conn ! null ? 连接成功 : 连接失败); DBUtil.close(conn, null, null); }如果报Access denied for user多半是密码或账号问题报Communications link failure先检查 MySQL 服务是否启动再检查端口是否被改过。数据库创建字符集建议用utf8mb4不然 emoji 表情符号无法入库。5.2 三个高频异常404、500、ClassNotFoundException部署完总会遇到几个典型问题。404 大多数是项目上下文路径不对访问地址应该包含请求根路径/项目名比如http://localhost:8080/cake_war/而不是http://localhost:8080/cake_war_exploded/。IDEA 2017.3 下 Artifact 输出目录名经常和项目名不一致需要在 Run Configuration 的 Deployment 里检查 Application context。500 错误要分两层看。页面报 500 且 Tomcat 日志里是NullPointerException最可能是把request.getParameter(id)直接Integer.parseInt但前端传来的 id 为空。建议先判空再转换。另一种情况是 JSP 中使用${product.price}而getPrice()返回的是double显示没问题但如果返回nullEL 表达式输出空字符串也不会报错这时候就不好查。ClassNotFoundException 最常见的原因是驱动包和 c3p0 包没有打进 WEB-INF/lib。IDEA 中 Artifact 需要把依赖添加到WEB-INF/lib下而不是只在本地 classpath 里。如果项目是直接拷到 Tomcat webapps 下部署必须确认 lib 目录里有 mysql-connector-java 和 c3p0 两个 jar否则启动时不会报错第一次访问数据库才会闪断。5.3 验证技巧按模块写一个快速回归脚本老项目改完搜索、购物车或订单状态后最好按一套固定请求走一遍避免漏改。可以自己写一个简单的 Servlet 或直接用 curl 模拟关键链路curl http://localhost:8080/cake/user/login?usernameadminpassword123456 curl http://localhost:8080/cake/product/search?keyword芝士 curl http://localhost:8080/cake/cart/change?productId1count2建议每个请求返回的内容都是可控的纯文本或 JSON而不是一个完整的 HTML 页面。HTML 对交互验证有意义但自动化脚本里解析成本高。把登录、查商品、加购物车、改数量、下订单、后台发货这六个动作做成一个测试脚本之后每次改动后执行一遍就能在 3 分钟内确认核心业务没有回归。这个习惯对后续维护老项目特别有用尤其是像蛋糕店这类前后台功能都齐全的中型项目手动点页面太容易漏掉细节。本文还有配套的精品资源点击获取
返回列表