JavaWeb+Servlet购物车系统实战:从数据建模到Tomcat部署全解析

发布时间:2026/10/8 7:29:31
JavaWeb+Servlet购物车系统实战:从数据建模到Tomcat部署全解析
简介面向JavaWeb初学者的购物车系统实现资源基于JavaWeb、Servlet、Tomcat、HTML和CSS构建适合正在学习Servlet请求跳转、会话记录与基础Web交互的学生和开发者。项目通过商品展示页面上的“添加到购物车”链接触发Servlet在Servlet中记录所选商品和选购数量再跳转至另一Servlet以表格展示购物车内容完整演示了商品选择、参数传递和结果反馈的典型流程。压缩包共25个文件含3个Java源码、3个编译后的class文件、2个XML配置、1个HTML页面和8张商品展示图片另有工程配置文件整体仅491KB结构紧凑适合直接导入Eclipse等IDE进行源码阅读与运行测试。已有638人学习浏览可作为课程设计、Servlet入门练习或Tomcat部署实训的参考。资源内置完整WebContent目录与工程配置方便快速搭建环境对照代码理清从页面请求、Servlet处理到购物车表格展示的每一步实现细节。1. 先搞清楚javaWebservlet购物车系统到底在解决什么后端初学者第一次觉得“自己像在写系统”多半是从这个javaWebservlet购物车项目开始的。它不炫技不涉及Spring、MyBatis这些框架单靠Servlet接收请求、处理业务、返回响应再加一个HttpSession对象存购物车数据就能跑通“浏览商品 → 加入购物车 → 修改数量 → 结算”这条最典型的电商链路。做这个项目的核心收获不是学会了某个API而是第一次理解HTTP请求是怎么被一个Java类接住、处理、再回应给浏览器的。适合两类人一类是刚学完Java SE、想看看Java在Web里怎么用的人另一类是准备面试JavaWeb岗位、需要快速补齐Servlet规范和Session原理的人。下面按一条能直接落地的路径来讲。2. 先把数据模型和项目骨架立住建表、分层、配置Tomcat落地点2.1 购物车系统最少需要几张表商品表与购物车表的取舍一个“简单购物车系统”仍然绕不开数据模型。常见做法是建两张表一张product商品表一张cart_item购物车条目表。有人说购物车数据放Session里就行不需要表这话对了一半——Session存的是“当前用户这次会话中选了什么”但结算时要把这些条目落库所以cart_item表还是要建。CREATE DATABASE IF NOT EXISTS cart_db DEFAULT CHARSET utf8mb4; USE cart_db; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image_url VARCHAR(255) DEFAULT COMMENT 图片路径 ) ENGINEInnoDB; CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID简单版可写死为1, product_id INT NOT NULL COMMENT 商品ID, quantity INT NOT NULL DEFAULT 1 COMMENT 数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB;上面user_id在简单版可以直接写死为1因为还没有登录模块。DECIMAL(10,2)是价格类型的标准选择不要用FLOAT浮点做金额计算会翻车。utf8mb4是为了兼容商品名里的emoji和生僻字。2.2 标准JavaWeb项目目录为什么要坚持MVC分层结构上不建议把Servlet当万能类把所有代码塞进去。按三层来分entity放商品和购物车条目的实体类dao放JDBC操作数据库的代码servlet只做三件事——拿参数、调DAO、跳转或转发页面。这样做的直接好处是Servlet里不会出现Class.forName(com.mysql.jdbc.Driver)这种连接代码出了问题知道去哪找。常见骨架目录如下IDEA中创建JavaWeb项目后手动补齐即可。src/main/java ├─ com.cart.entity # Product.java, CartItem.java ├─ com.cart.dao # ProductDao.java, CartItemDao.java ├─ com.cart.servlet # ProductListServlet.java, CartServlet.java ├─ com.cart.util # DBUtil.java, CartUtil.java src/main/webapp ├─ WEB-INF/web.xml ├─ index.jsp ├─ product_list.jsp ├─ cart.jspWEB-INF下的web.xml是Servlet映射的“黑匣子”。Servlet 3.0 虽然支持注解WebServlet(/cart)但初学阶段强烈建议写一遍XML映射因为面试爱问而且版本兼容性最稳。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameSimple Cart System/display-name servlet servlet-nameProductListServlet/servlet-name servlet-classcom.cart.servlet.ProductListServlet/servlet-class /servlet servlet-mapping servlet-nameProductListServlet/servlet-name url-pattern/product-list/url-pattern /servlet-mapping servlet servlet-nameCartServlet/servlet-name servlet-classcom.cart.servlet.CartServlet/servlet-class /servlet servlet-mapping servlet-nameCartServlet/servlet-name url-pattern/cart/url-pattern /servlet-mapping welcome-file-list welcome-fileproduct_list.jsp/welcome-file /welcome-file-list /web-app2.3 Druid连接池接入DataSource比DriverManager强在哪初学者最容易犯的错是每个DAO方法里都写DriverManager.getConnection(url, user, password)。华为、阿里的面试官如果看到这种写法基本判定没有生产意识。正确做法是引入一个连接池。国内项目最常用的是德鲁伊Druid配置直观、自带监控。# src/main/resources/druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/cart_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait60000 minIdle3 validationQuerySELECT 1连接池的作用一句话讲透DriverManager每次请求都要建立物理连接高并发下数据库瞬间被打满连接池维护一定数量的连接反复复用把建立连接的时间从几十毫秒压到微秒级。maxActive20表示池里最多同时有20个连接超过就要排队等maxWait毫秒这个参数在压测时要重点看。3. 把Core写扎实购物车增删改查接口设计与Servlet实现3.1 一套统一的返回协议action参数与请求分发购物车操作不外乎四种加商品、删条目、改数量、清空。如果每个操作建一个Servlet类会爆炸。我一般用同一个CartServlet通过action参数区分操作。这是前后端联调时最重要的约定之一——先定义好协议再写代码。WebServlet(/cart) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (add.equals(action)) { addCart(req, resp); } else if (remove.equals(action)) { removeCart(req, resp); } else if (update.equals(action)) { updateCart(req, resp); } else if (clear.equals(action)) { clearCart(req, resp); } else { // action 为空时展示购物车页 req.getRequestDispatcher(/cart.jsp).forward(req, resp); } } }doGet直接转调doPost是一种省事的做法避免表单用POST、超链接用GET导致两个方法都要维护。但要注意REST风格严格来说GET和POST语义不同这里只是简单购物车系统为了减少重复代码的常见做法。req.getParameter(action)是Servlet读参数的唯一入口不存在第二个原生API。3.2 从Session读购物车还是从数据库读购物车购物车数据存哪里是这道题的核心取舍。简单版推荐用HttpSession存一个ListCartItem实现起来最直观进阶版则需要每次请求从cart_item表里查。两者区别在于Session方案用户刷新页面不会丢数据但一旦关浏览器、或者会话超时默认30分钟购物车就空了这也是它有“局限性”所在数据库方案只要表里有记录任何时候重新登录购物车都在。我给出的教学代码用Session存储并同时落库到cart_item表兼顾两边的学习价值。// CartServlet 内新增的 addCart 方法 private void addCart(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); HttpSession session req.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); session.setAttribute(cart, cart); } boolean exists false; for (CartItem item : cart) { if (item.getProductId() productId) { item.setQuantity(item.getQuantity() quantity); exists true; break; } } if (!exists) { CartItem newItem new CartItem(); newItem.setProductId(productId); newItem.setQuantity(quantity); Product p new ProductDao().findById(productId); newItem.setProductName(p.getName()); newItem.setPrice(p.getPrice()); cart.add(newItem); } // 同步入库 new CartItemDao().saveOrUpdate(1, productId, quantity); resp.sendRedirect(req.getContextPath() /cart?actionview); }这段代码里有两个关键点第一个是exists的判断购物车逻辑里同一个商品不能出现两行只能合并数量第二个是resp.sendRedirect(req.getContextPath() /cart?actionview)重定向的路径必须带getContextPath()也就是项目部署名否则部署到带路径的Tomcat时会404。CartItem实体类里放了productName和price这两个冗余字段目的是在渲染购物车页面时直接读取不用每条记录再查一次商品表。对简单系统来说这叫“空间换时间”值得学但心里清楚这在严格数据库设计中属于反范式。3.3 删除、修改数量、清空操作的边界条件删除和清空逻辑简单但“修改数量”有一个高频翻车点用户把数量改成负数。前端可以限制input min1但后端如果不校验负数就会传进来。Servlet后端校验是最后一道防线必须写。private void updateCart(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); if (quantity 1) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, 数量不合法); return; } HttpSession session req.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { resp.sendRedirect(req.getContextPath() /product-list); return; } for (CartItem item : cart) { if (item.getProductId() productId) { item.setQuantity(quantity); break; } } new CartItemDao().updateQuantity(1, productId, quantity); resp.sendRedirect(req.getContextPath() /cart?actionview); }resp.sendError(400)不同于sendRedirect它不会跳转而是直接返回一个HTTP 400错误页。浏览器收到后会展示Tomcat默认的错误页面这在实际系统中体验不好但对学习而言它能让你直观感受到“响应状态码”到底是什么。生产项目一般会改成返回JSON错误信息前端再用alert或弹窗提示那是后话。4. JSP页面三层渲染与Session购物车展示把后端数据变成能点的界面4.1 product_list.jsp商品列表怎么和Servlet联动JSP的本质是一个在服务器端执行的模板它最终会被翻译成Servlet。商品列表页的逻辑极其简单ProductListServlet查数据库把结果setAttribute放进请求域然后转发给JSPJSP用JSTL标签c:forEach渲染成表格或卡片。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title商品列表/title /head body h2商品货架/h2 table border1 cellpadding8 cellspacing0 tr thID/th th商品名/th th价格/th th库存/th th操作/th /tr c:forEach items${productList} varp tr td${p.id}/td td${p.name}/td td${p.price}/td td${p.stock}/td td form action${pageContext.request.contextPath}/cart methodpost styledisplay:inline; input typehidden nameaction valueadd/ input typehidden nameproductId value${p.id}/ input typenumber namequantity value1 min1 max${p.stock} stylewidth:50px;/ button typesubmit加购/button /form /td /tr /c:forEach /table pa href${pageContext.request.contextPath}/cart?actionview查看购物车/a/p /body /html这段JSP有一个值得说道的细节加购用的是form而不是a超链接。因为POST请求能承载表单数据前端隐藏域、数量输入框而GET只能拼URLURL一旦出现中文或复杂数据就得编码解码。${pageContext.request.contextPath}是JSP EL表达式获取项目部署路径的标准写法它能保证你的表单提交地址不会因为项目部署名的不同而失效。4.2 cart.jsp购物车列表的EL表达式渲染套路购物车页的渲染套路和商品列表不同因为购物车数据不是从请求域中来而是从Session中取。JSP里可以直接用${sessionScope.cart}拿到HttpSession中存的属性。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title购物车/title /head body h2我的购物车/h2 c:if test${empty sessionScope.cart} p购物车是空的去 a href${pageContext.request.contextPath}/product-list挑点东西/a 吧。/p /c:if c:if test${not empty sessionScope.cart} table border1 cellpadding8 cellspacing0 tr th商品名/th th单价/th th数量/th th小计/th th操作/th /tr c:forEach items${sessionScope.cart} varitem tr td${item.productName}/td td${item.price}/td td form action${pageContext.request.contextPath}/cart methodpost styledisplay:inline; input typehidden nameaction valueupdate/ input typehidden nameproductId value${item.productId}/ input typenumber namequantity value${item.quantity} min1 stylewidth:50px;/ button typesubmit改数量/button /form /td td${item.price * item.quantity}/td td a href${pageContext.request.contextPath}/cart?actionremoveproductId${item.productId}删除/a /td /tr /c:forEach /table p a href${pageContext.request.contextPath}/cart?actionclear清空购物车/a /p /c:if /body /html这里删掉一个常见坑改数量的表单如果包在表格单元格里浏览器渲染时form不能嵌套在tr外再包一个form吗可以但多个form出现在表格里要注意标签闭合顺序否则页面布局会跳。更稳妥的做法是用td内放一个form表格行内不嵌套form。${item.price * item.quantity}这个EL表达式直接在页面做金额运算适合教学演示。真实项目里建议在Java端算好小计放入一个VO值对象再传给页面避免JSP里出现业务计算。4.3 会话里丢失了购物车怎么办Cookie与Session的关系Session依赖Cookie实现。HttpSession本身不主动发Cookie而是依赖服务器给浏览器种一个JSESSIONID后续每次请求带上这个ID服务器用它对上号。浏览器 - POST /cart?actionaddproductId1quantity2 | v Request带上 Cookie: JSESSIONIDFE38C2... | v Tomcat根据JSESSIONID找到对应的HttpSession对象 | v Servlet操作Session中的cart集合第一次访问时Tomcat会创建一个新的Session并把ID通过响应头的Set-Cookie种下去。如果你看到浏览器开发者工具里没有JSESSIONID购物车就会“神秘消失”真相多半是以下之一浏览器禁用了Cookie请求走了跨域Servlet调用了一会儿getSession(false)一会儿getSession(true)导致不是同一个Session。教学代码统一用req.getSession()它保证如果没有Session就新建一个不会出现取空的问题。但如果你想判断用户是否已有Session必须用req.getSession(false)否则每次都会“赠送”一个新会话给你。5. IDEA运行JavaWeb项目配置避坑Tomcat、jar包与中文乱码排查5.1 Tomcat配置常见翻车点Artifact、URL路径和端口占用在IDEA里跑起这个javaWebservlet购物车项目很多新手卡在“明明代码看起来对就是访问不了”。90%的情况出在部署配置。现象启动Tomcat时不报错浏览器访问http://localhost:8080却是Tomcat默认首页看不到商品列表。原因IDEA里Tomcat的Deployment没添加该项目的Artifact或者Artifact类型选错了。解决Run/Debug Configurations → Tomcat Server → Deployment → 点“”选择cart:war exploded。“war exploded”是解压目录部署修改JSP后刷新即生效如果选“war”打包部署每次改代码都要重打war包调试效率极低。另一个常见坑是访问路径404Tomcat Server - Deployment - Application context 这个值如果这个值设成/cart那么访问商品列表就得走http://localhost:8080/cart/product-list而不是裸根路径。很多教程里写WebServlet(/product-list)没提到context path导致新手怎么都找不到页面。把Application context改成/可以避免这个困扰但多人共用一台Tomcat时不要用根路径。端口占用是最后一道坎因为IDEA经常残留没有完全停掉的Tomcat进程。启动控制台报Port 8080 was already in use解决方式不是反复重启而是用命令找占用进程再杀掉netstat -ano | findstr 8080 taskkill /PID pid /FLinux环境下对应的是lsof -i:8080与kill -9 pid。5.2 导入MySQL驱动与JSTL依赖lib目录的放置规则这个项目要连接MySQL绕不开mysql-connector-javajar包。很多新手把它放在IDE的Global Libraries里结果项目一部署到Tomcat就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。因为Tomcat运行时的类加载器只认项目WEB-INF/lib目录下的jar包Global Libraries不会被打进Artifact。正确做法src/main/webapp/WEB-INF/lib ├─ mysql-connector-java-8.0.33.jar ├─ druid-1.2.20.jar └─ jstl-1.2.jar放好后在IDEA中右键jar包 → Add as Library确认webapp/WEB-INF/lib被Tomcat的部署列表包含。同样地JSTL标签库的jar缺失时JSP页面会报“无法在标签库中定位标签”的错看一眼是不是没把JSTL放进lib。!-- pom.xml 不是必须教学项目可不用Maven如果用了Maven这样引入 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency5.3 三个必查的编码设置页面、请求、数据库中文乱码是javaWeb项目里最容易排查也最容易“玄学复发”的问题。乱码的分工很明确页面乱码看JSPcontentType请求参数乱码看Servlet里的setCharacterEncoding数据库里的乱码看JDBC URL和表字符集。三者缺一不可。% page contentTypetext/html;charsetUTF-8 languagejava %// 如果是POST请求必须在读取任何参数之前调用 req.setCharacterEncoding(UTF-8); // 如果是GET请求Tomcat 8及以后版本默认UTF-8解码一般不乱 // 但Tomcat 7及以前需要改server.xml里的URIEncoding# JDBC URL强制指定字符集 urljdbc:mysql://localhost:3306/cart_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai一个血泪经验POST乱码的根源是Tomcat默认用ISO-8859-1解码请求体。setCharacterEncoding必须在读取参数之前调用写在addCart方法中间、已经读过productId之后再去setCharacterEncoding就晚了。正确位置是doPost方法第一行或者用一个Filter统一设置。建议直接写一个EncodingFilter一次到位这也是你再往框架学习时接触的第一种“拦截器模式”。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }chain.doFilter(req, resp)这行是Filter的核心放行请求到下一个Filter或最终的Servlet。如果你忘了写页面就会卡死无响应排查时先检查有没有把doFilter掉。5.4 商品图片不显示和表单重复提交的坑商品图片404项目里图片放在webapp/images目录时页面引用应写img src${pageContext.request.contextPath}/images/xxx.png漏掉context path是404的头号原因。表单重复提交刷新场景加入购物车提交表单后如果最后一句是forward到JSP用户按F5会再次提交。解决方式很简单提交成功后用sendRedirect跳转到一个查询接口不让浏览器的最后一次请求是POST。上面代码里加购之后用的就是sendRedirect这个“提交后重定向”模式Post/Redirect/Get是Web开发的基本素养值得记死。6. 从能跑到跑好清单接口设计、Session边界验证与后续进阶6.1 建议先把购物车“数据”与“物品”分离如果只把CartItem塞进Session后面加“已勾选商品”“优惠券”“批量删除”时会发现改不动。推荐的改造是把购物车拆成两个模型Cart购物车整体和CartItem条目。Cart持有条目列表、提供add、remove、update、getTotalPrice方法Servlet只实现一个 “拆参数、调方法、给响应” 的组织角色。public class Cart { private ListCartItem items; public void add(CartItem item) { for (CartItem cartItem : items) { if (cartItem.getProductId() item.getProductId()) { cartItem.setQuantity(cartItem.getQuantity() item.getQuantity()); return; } } items.add(item); } public void remove(int productId) { items.removeIf(item - item.getProductId() productId); } public java.math.BigDecimal getTotalPrice() { java.math.BigDecimal total java.math.BigDecimal.ZERO; for (CartItem item : items) { total total.add(item.getPrice().multiply( java.math.BigDecimal.valueOf(item.getQuantity()))); } return total; } }用BigDecimal算总价唯一理由是精度。DECIMAL从数据库出来用BigDecimal接收不要为了省事转成double金额的“0.1 0.2”问题会让你在结算时对上不账。6.2 验证这套系统是否正常的三个核心检查项写完代码别急着交先按下面顺序验证能少很多面试追问时的尴尬。# 1. 验证商品列表加载 curl http://localhost:8080/cart/product-list | grep 商品货架 # 2. 验证加购后购物车接口返回重定向 curl -v -d actionaddproductId1quantity2 http://localhost:8080/cart # 观察响应码是否为 302Location 是否指向 /cart?actionview # 3. 验证Session保持 # 用 -c 保存Cookie-b 带上Cookie两次请求保持同一会话 curl -c cookies.txt -d actionaddproductId1quantity2 http://localhost:8080/cart curl -b cookies.txt http://localhost:8080/cart?actionview | grep 购物车如果第二次不带Cookie访问购物车就是空的这是Session机制的正常表现也能用来向别人说明“为什么关闭浏览器后购物车消失”。想在关浏览器后购物车还在就得放弃Session、改用数据库存储这也是这个项目可以往“记住我”方向演进的理由。6.3 下一步怎么走从Servlet到框架的跨界自觉做完这个购物车系统建议立刻做三件事第一把它改造成Maven结构因为你现在手动扔jar的方式在团队协作时完全行不通第二用Filter把登录校验、编码处理、日志记录串一遍你会发现Servlet没有想象中那样要被淘汰第三试着用JSON格式返回购物车数据写一个原生JS的列表页这一步是走向前后端分离的前奏。我自己的经验是当初若没有把这个购物车项目的Session机制彻底搞明白后面学Spring Session、Redis缓存会话时一定会卡壳。搞懂Servlet的生命周期、Session的存取边界以及请求流转过程再去看Spring MVC的核心DispatcherServlet会发现那些框架只是在这个流程上做了更规整的封装。希望这篇实战笔记帮到你把你的购物车跑通再把遇到的问题记下来这都是以后面试能讲的真实素材。提示项目代码里如果出现内存中保留Session的情况记得在Tomcat的context.xml里配置Manager的maxActiveSessions否则无限制创建Session会让内存溢出这也是线上购物车系统常见的隐患之一。本文还有配套的精品资源点击获取