JavaWeb购物车从零实现:Servlet+JSP+JDBC三层架构实战

📅 发布时间:2026/9/9 4:51:53
JavaWeb购物车从零实现:Servlet+JSP+JDBC三层架构实战
简介采用ServletJSP技术栈的JavaWeb购物车源代码面向刚接触JavaWeb开发的初学者与需要参考完整业务流的开发者覆盖商品浏览、加入购物车、数量修改、删除与结算的完整购物流程并配套详细笔记资料。压缩包共54个文件以Java类、JSP页面、XML及properties配置文件为主另含数据库脚本、项目安装说明和开发文档整体仅1.58MB下载后可快速导入Eclipse或IntelliJ IDEA等IDE直接运行学习。目前已有4124人学习浏览内容涉及Servlet请求处理、Session购物车状态维持、JDBC数据库交互、MVC分层设计以及JSTL/EL表达式展示等核心知识点适合系统梳理JavaWeb常用技术。借助笔记中的关键代码解释与数据库设计说明可以深入理解购物车数据如何存储、订单如何流转同时了解异常处理和SQL注入防护等工程实践无论用于课程作业还是个人提升都很实用。 购物车几乎是每个Java Web学习者绕不开的第一个“完整业务”练手项目。很多教程上来就甩给你一堆代码压缩包下载完跑不起来或者跑起来了却看不懂每一层在干什么。这篇文章不打算给你贴一个几百行的“万能源码”而是带你从零敲一个真正能跑、能看懂、能自己改的简易购物车把Servlet、JSP、JDBC、三层架构这些零散知识点串成一条线。适合刚学完Java基础和数据库、准备做第一个Web项目的同学也适合需要快速回顾javaWeb核心流程的开发者。我实际操作中见过太多初学者在购物车上栽跟头要么是对象的生命周期搞不清要么是表单提交的乱码问题折腾一下午还有就是浏览器前进后退导致的重复提交。我会把这些问题连同解决思路一并整理出来你在自己动手时可以直接对照排查。1. 购物车项目整体设计与思路拆解1.1 核心需求解析简易购物车听起来简单但麻雀虽小五脏俱全。它涉及Web开发中最典型的一整条请求链路浏览器发起请求服务器端的Servlet接收参数调用业务逻辑层处理数据通过DAO层访问MySQL数据库最后把结果封装成对象由JSP页面渲染成HTML返回给用户。这个项目里购物车的核心功能一般拆成四块往购物车里添加商品、查看购物车列表、修改商品数量、删除商品也常包括清空购物车。有些进阶版本会加上订单结算、库存扣减但对于“简易”定位来说前面四个功能已经足够覆盖Servlet和JSP的核心用法了。有个关键设计决策需要在一开始就明确购物车数据存在哪里。方案无非两种一种是存在Session里一种是持久化到MySQL数据库表。初学者往往纠结这个其实选型逻辑很直接——简易项目练手重点是理解会话跟踪和对象封装用Session存储最直观。把购物车对象放进Session用户在整个会话期间都能访问同一个购物车实例刷新页面数据也不丢。而数据库方案更适合后续对接订单系统、需要跨设备保留数据的真实场景。我建议你先用Session把整套流程跑通然后再尝试把购物车表建到MySQL里用JDBC读写这样能够同时理解两种存储方案各自的优缺点对后续做正式项目帮助很大。1.2 三层架构的必要性很多新手写代码喜欢把所有逻辑全塞进Servlet里一个doGet方法写上百行数据库查询、业务判断、页面跳转全在里面。这样做在几十行的小Demo里没问题但一旦功能增多代码会迅速腐化到难以维护。我个人一开始也这么干过后果是每次改需求都要在那个巨型方法里翻找半天。所以这个项目虽然叫“简易”我强烈建议你从一开始就按三层架构来组织分包Servlet层也叫Web层/控制层负责接收参数和跳转页面Service层负责业务逻辑比如计算总价、判断商品是否已在购物车中DAO层只负责跟数据库打交道。每一层各司其职调用关系也清晰。在实际写代码的时候如果你发现某个Servlet方法超过50行通常就是职责划分出了问题。业务逻辑应该下沉到Service层数据访问应该下沉到DAO层Servlet里只留参数解析、调用服务和结果分发。这样后续你要把购物车从Session方案改成数据库方案只需要替换Service层的实现Servlet和页面几乎不用动。2. 环境准备与项目搭建2.1 开发工具选型做javaWeb开发IDE这块现在基本是IntelliJ IDEA的天下。我用的是2023版本界面布局和项目创建向导跟之前有些差异如果你刚接触有几个地方需要注意。JDK推荐用JDK 8或者JDK 11这两个版本稳定跟Tomcat 8.5或9的兼容性最好。JDK版本太高比如JDK 17有些老项目的依赖会报模块访问错误排查起来比较头疼。Tomcat我用的8.5因为大多数教程和参考代码是基于这个版本写的你的代码跑在8.5上遇到的问题搜索解决方案时最容易命中。数据库方面MySQL 5.7或8.0都可以。需要注意MySQL 8.0的JDBC驱动类名和连接URL与5.7有所不同驱动是com.mysql.cj.jdbc.DriverURL需要加上serverTimezoneAsia/Shanghai这样的时区参数否则连库的时候会报时区错误。2.2 IDEA 2023创建JavaWeb项目的关键步骤用IDEA 2023创建JavaWeb项目和早期版本有些区别。新版IDEA里新建项目时选择“Web”或者通过Maven骨架创建都可以。如果只是为了学Servlet和JSP不必非要走Maven直接创建一个普通的Java项目然后手动添加Web目录结构也是可行方案而且能让你更清晰地理解项目结构。手动创建的话项目名右键选择“Add Framework Support”添加框架支持勾选“Web Application”Web应用IDEA会自动生成web/WEB-INF目录和web.xml配置文件。接着需要配置Tomcat进入Run菜单选择Edit Configurations点加号找到Tomcat Server选择Local然后在Application Server里指定你本地Tomcat的安装目录Deployment选项卡里点加号把当前项目部署上去。这里有一个新手最容易卡住的点部署后访问URL怎么确定。默认情况下IDEA部署Web项目时的Application context应用上下文是项目名比如项目叫cart-demo那么访问地址就是http://localhost:8080/cart-demo/。上下文路径在Deployment选项卡里可以改改动后要记清楚否则你访问首页一直404会以为是代码问题其实是路径不匹配。2.3 数据库表结构设计用Session方式存购物车数据库表可以设计得很简单只需要一张商品表。但如果想顺带练习JDBC操作可以再加一张用户表模拟登录购物车表则可以作为进阶扩展。我实际项目中的商品表设计如下CREATE TABLE t_book ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, price decimal(10,2) NOT NULL, author varchar(50) DEFAULT NULL, publish varchar(100) DEFAULT NULL, stock int(11) DEFAULT 100, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;这里有个小细节价格字段用decimal(10,2)而不是double或float。Float和double在计算机里是浮点数计算总价时会出现0.1 0.2 ! 0.3这种精度问题。decimal是定点数适合金额计算这是做电商类项目的基本常识。同样的Java实体类里的价格字段建议用BigDecimal来对应做运算时也不会丢失精度。建表后插入几条测试数据书名、价格随意尽量用中文这样可以顺带验证编码配置是否正确。3. 核心代码实现与实操要点3.1 实体类设计实体类对应数据库表结构购物车项目里需要两个基础实体Book和CartItem再加一个购物车实体Cart。Book类字段对应表结构没什么特别的。CartItem是购物车条目它和Book的区别在于多了quantity数量和subtotal小计两个字段。Cart是购物车本体里面持有一个MapInteger, CartItemkey是商品id优点是查询和修改单个商品时间复杂度是O(1)比用List遍历查找快而且天然去重——同一个商品在购物车中只会有一条记录只是数量会累加。Cart类里我还封装了几个业务方法addItem添加条目、deleteItem删除条目、clear清空购物车、getTotalPrice计算总价。把购物车本身的增删改算逻辑放在实体类里Service层就不需要写重复的循环计算代码了这也是面向对象的一种小体现。3.2 Servlet层编写流程Servlet层每个功能对应一个Servlet。我测试过的最少配置是四个ServletAddCartServlet、DeleteCartServlet、UpdateCartServlet、ClearCartServlet外加一个配合JSP的ListCartServlet或者直接在JSP里从Session中读取购物车。以最关键的AddCartServlet为例核心逻辑如下从请求中拿到商品id调用Service层查询该商品的详细信息然后从Session中获取购物车对象。如果Session里还没有购物车就new一个Cart放进去。接着调用cart.addItem方法把商品加进去最后重定向回购物车列表页面。有一个细节容易忽略重定向这个操作。很多初学者在Servlet处理完请求后喜欢用request.getRequestDispatcher(cart.jsp).forward(request, response)来转发这在功能上没问题但会导致一个问题——用户刷新页面时浏览器会重新请求上一次的URL如果用户是从添加购物车页面刷新商品会被再次添加造成重复数据。用sendRedirect重定向就能避免这个问题因为重定向后地址栏变成了购物车列表页面的URL刷新时只会刷新列表页不会再次触发添加操作。3.3 JSP页面渲染购物车JSP页面可以直接用JSTL标签库加EL表达式来遍历购物车内容比传统的% %脚本片段简洁得多。EL表达式中${cart.items}取到的是Cart类的getItems()方法的返回值JSTL的forEach循环遍历这个Map每次循环的item就是CartItem对象。用EL表达式时有个坑如果某个对象为nullEL表达式会直接显示空字符串而不是报错。这看起来是好事但也会掩盖问题——你以为是页面没数据其实可能是Servlet里没把购物车set进request或session。排查时可以用% System.out.println(cart); %在JSP里打印日志或者直接开浏览器开发者工具看网络响应观察HTML源码里有没有对应的数据。JSP页面如果要显示总价直接在Cart类里写一个getTotalPrice()方法EL表达式调用${cart.totalPrice}即可。这里要注意金额格式化可以在页面用fmt:formatNumber标签保留两位小数避免出现99.9这种不美观的显示。3.4 JDBC连接与DAO层实现DAO层直接跟MySQL打交道我用原生JDBC实现没有引入MyBatis或DBUtils目的是让你看清底层流程加载驱动、获取连接、创建Statement、执行SQL、遍历ResultSet、关闭资源。这部分有一个非常反直觉的点资源关闭顺序必须和创建顺序相反。先创建Connection再创建Statement最后是ResultSet关闭时则反过来先关ResultSet再关Statement最后关Connection。否则在某些数据库驱动下会出现资源泄漏连接池被占满后应用假死。为了避免每个方法里都写一遍重复的关闭代码我写了一个小的JdbcUtils工具类把获取连接和关闭资源封装成静态方法。工具类里连接参数写死在常量里简易项目可以接受但正式项目绝对要用配置文件加连接池。3.5 购物车实际操作的完整代码参考为了让整条逻辑更直观我把最核心的Cart类代码整理出来去掉一些无关键盘代码。你可以直接参考这个封装思路根据自己的业务字段修改。public class Cart { // 用一个Map来装购物车条目key是商品id private MapInteger, CartItem items new LinkedHashMap(); // 添加商品到购物车 public void addItem(CartItem item) { // 如果购物车中已经有这个商品了数量累加 CartItem existingItem items.get(item.getBook().getId()); if (existingItem ! null) { existingItem.setQuantity(existingItem.getQuantity() item.getQuantity()); } else { items.put(item.getBook().getId(), item); } } // 删除购物车中某个商品 public void deleteItem(Integer bookId) { items.remove(bookId); } // 清空购物车 public void clear() { items.clear(); } // 获取购物车中所有条目 public MapInteger, CartItem getItems() { return items; } // 计算购物车总价 public BigDecimal getTotalPrice() { BigDecimal total BigDecimal.ZERO; for (CartItem item : items.values()) { total total.add(item.getSubtotal()); } return total; } }这段代码里我用LinkedHashMap而不是HashMap意图是保持商品添加顺序和页面展示顺序一致。如果你用HashMap遍历时顺序是不确定的可能出现用户加了A再B页面却显示B在A前面的情况。这种小细节正是体现水平的地方。AddCartServlet的完整关键代码如下WebServlet(/addCart) public class AddCartServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置请求编码解决中文参数乱码问题 req.setCharacterEncoding(UTF-8); // 1. 获取商品id int bookId Integer.parseInt(req.getParameter(bookId)); // 2. 查询商品信息 Book book bookService.findBookById(bookId); if (book null) { resp.sendRedirect(req.getContextPath() /bookList); return; } // 3. 从Session中获取购物车如果没有则创建 HttpSession session req.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } // 4. 组装购物车条目并添加 CartItem item new CartItem(); item.setBook(book); item.setQuantity(1); // 小计 单价 * 数量 item.setSubtotal(book.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); cart.addItem(item); // 5. 重定向到购物车页面 resp.sendRedirect(req.getContextPath() /cart.jsp); } }注意Servlet我用的是WebServlet注解配置URL映射省去了web.xml里的配置。IDEA创建项目时默认生成的web.xml版本如果比较老有些注解可能不被识别你可以在web.xml里检查一下metadata-complete属性是否为false或者干脆删掉这个属性。这是新手很常遇到的一个隐形问题。3.6 连接MySQL的DAO层实现逻辑DAO层我以查询单个商品为例展示JDBC标准流程public class BookDao { // 根据id查询商品 public Book findById(int id) { String sql SELECT id, name, price, author, publish, stock FROM t_book WHERE id ?; Connection conn null; PreparedStatement ps null; ResultSet rs null; Book book null; try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); ps.setInt(1, id); rs ps.executeQuery(); if (rs.next()) { book new Book(); book.setId(rs.getInt(id)); book.setName(rs.getString(name)); book.setPrice(rs.getBigDecimal(price)); book.setAuthor(rs.getString(author)); book.setPublish(rs.getString(publish)); book.setStock(rs.getInt(stock)); } } catch (SQLException e) { e.printStackTrace(); } finally { JdbcUtils.close(rs, ps, conn); } return book; } }SQL语句我用的PreparedStatement而不是Statement这是个安全习惯问题。Statement做字符串拼接SQL容易受到SQL注入攻击。PreparedStatement使用占位符?预编译参数会被当作纯数据处理不会拼接进SQL结构里这是一道基本防线。购物车哪怕是个学习项目也要从一开始就养成这个习惯。4. 常见问题与排查技巧实录4.1 症状页面中文显示乱码这个事儿我刚开始做JavaWeb时被折磨得够呛后来总结了一下乱码原因无非就是编码不一致的链条问题。要解决乱码需要从三个环节分别排查浏览器请求的编码、服务器响应输出的编码、数据库存储的编码。Tomcat 8及以上版本HTTP请求的POST参数默认按UTF-8处理但GET请求的参数编码默认不是UTF-8需要在Servlet里调用req.setCharacterEncoding(UTF-8)同时Tomcat的server.xml里给Connector加上URIEncodingUTF-8属性。响应乱码则在Servlet里设置resp.setContentType(text/html;charsetUTF-8)。数据库方面建表时统一用utf8或utf8mb4字符集连接URL加characterEncodingutf8参数。这四层检查一遍乱码基本能解决。注意UTF-8和UTF8在MySQL里是同一个意思但JDBC URL里一般写作characterEncodingutf8。4.2 症状点击添加购物车后页面404这个问题的根因大多数时候是URL路径不对。看URL得先看协议、主机、端口之后的应用上下文路径。我在2.2节里提过IDEA部署时的Application context默认为项目名。如果JSP页面里的表单action写的是/addCart那是域名根路径不是应用上下文路径服务器会到http://localhost:8080/addCart去找Servlet而你的Servlet实际注册在http://localhost:8080/cart-demo/addCart自然就404了。正确做法有两种要么在JSP页面里用${pageContext.request.contextPath}/addCart来拼绝对路径要么在Servlet的WebServlet注解里不写前导斜杠其实注解里必须写从页面来看就是统一用${pageContext.request.contextPath}开头。我推荐前面那种这样项目改了上下文路径页面里的链接依然正确。4.3 症状添加商品后列表里数量不增加这个问题的隐蔽性很高多半是JSP页面使用EL表达式时访问CartItem类的某个属性出问题。比如你的CartItem类里有getItemQuantity()方法但页面写的是${item.quantity}EL会去找getQuantity()方法找不到就返回空。所以属性名和方法名必须严格对应——EL表达式${item.quantity}要求CartItem类里有getQuantity()方法。另外Map遍历时forEach的写法也要注意。如果你的items是MapInteger, CartItem在JSP里用c:forEach items${cart.items} varentry遍历那么每次循环的entry是Map.Entry对象要用${entry.value.quantity}来访问条目数据而不是${entry.quantity}。这种小坑调试起来费时间报错信息还不明确。4.4 症状重启Tomcat后购物车数据消失这是Session方案的天然特性不是bug。Session数据默认保存在服务器内存中进程一停就没了。浏览器端表现为关闭浏览器再打开购物车也是空的因为SessionID存在Cookie里浏览器关了Cookie通常也会被清理会话就断了。如果想让购物车数据持久化就得迁移到数据库方案。操作思路是新建购物车表id、user_id、book_id、quantity用户在Session中保留购物车idDAO层从购物车表实时查询条目。这个改造过程能让你理解为什么真实电商网站的购物车数据能跨设备同步——因为数据在服务器数据库里存着不在浏览器里。4.5 编码踩坑后的三个习惯基于我的实际经验给三个建议养成习惯后能少走很多弯路。第一所有新创建的JSP页面第一行都写明pageEncodingUTF-8同时contentType里也带上charsetUTF-8。第二使用配套的MySQL驱动版本MySQL 8以上要记得在URL里加serverTimezoneAsia/Shanghai否则报错时区无法识别。第三SQL里涉及中文字符常量时确保连接URL带characterEncodingutf8否则即使表和页面都是UTF-8写进去的中文也会变成乱码。5. 从简易到进阶购物车项目还能怎么扩展5.1 增加用户登录与购物车关联Session方案做完后一个很自然的扩展是引入用户表实现登录后才能使用购物车。这样Cart对象就和用户绑定登录成功后从数据库加载该用户的购物车数据到Session每次修改购物车时同时更新数据库。这一步做下来你相当于同时复习了会话管理、Cookie机制、数据库关联查询等知识点。5.2 引入连接池改造DAO层原生JDBC每次操作都建立物理连接连接创建开销其实很大。正式项目普遍使用Druid、HikariCP这类连接池。改造思路不复杂把JdbcUtils里的getConnection改为从连接池获取用Druid的话只需要配置一个druid.properties文件然后通过DruidDataSourceFactory创建数据源。改好后你会发现代码结构没怎么变但性能明显提升。5.3 前端交互升级为Ajax页面上点击“加入购物车”后整页刷新体验比较原始。用Ajax改造的路径是点击按钮时用JavaScript发异步请求到后端ServletServlet只返回一个JSON串比如总数量、总价格前端拿到数据后用DOM操作局部更新页面。这一步会牵扯到Json转换工具常见的有Jackson、Fastjson等。做一次这个改造你就能理解Web开发中前后端分离的基本思路了。我在实际做这个升级时遇到过跨域的问题。因为前端和后端如果部署在不同端口浏览器会拦截非同源的Ajax请求需要在服务端配置CORS跨域头。用相同端口部署则不存在这个问题学习阶段建议先不搞跨域保持同源部署。5.4 代码层面的优化思路等你的购物车项目完整跑通后回头看代码有几个点值得优化把Servlet里的参数校验补全判空处理做完整避免非数字参数导致500错误把业务逻辑中重复出现的常量抽成工具类或枚举给关键操作加日志输出方便线上问题排查。这些优化没有多高深的技术含量但直接影响代码的可维护性。我在项目实战中发现真正让代码拉开差距的往往就是这些细节异常处理是吞掉还是向上抛、日志是打印到控制台还是统一收集、参数校验是写在Servlet还是Service层。这些习惯一旦在你写第一个完整项目时养成后续写什么项目都会受益。这个购物车项目我前前后后带过不少新人重写过最明显的感受是卡住的地方往往不是某个语法不会而是对请求-响应的生命周期、Session的作用范围这些宏观概念理解不透。所以如果你在调试过程中觉得代码看懂了但就是跑不对不妨退一步从浏览器按F12观察网络请求和Cookie变化顺着请求走一遍代码很多问题会豁然开朗。按照我上面给的步骤从环境搭建、数据库建表到实体类、Servlet、DAO层再到JSP页面渲染一步步敲下来中间不要复制粘贴你会发现得到的收获远比下载一份现成源码来得扎实。我强烈建议你自己动手把整个过程走完一遍遇到问题再回来对照这篇文章排查效果一定好过直接跑通别人的代码。本文还有配套的精品资源点击获取