ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Servlet手写购物车:从请求链路到会话管理的JavaWeb实战

Servlet手写购物车:从请求链路到会话管理的JavaWeb实战 简介这是一份面向JavaWeb初学者的购物车系统学习资源适合正在学习Servlet、需要完成课程设计或希望理解JavaWeb基础交互流程的读者。项目基于Servlet Tomcat HTML CSS实现包含商品展示页面与“添加到购物车”超链接点击后跳转Servlet记录选购商品信息及数量再由另一个Servlet以表格形式输出购物车内容完整演示了Servlet接收请求、处理参数、业务存储与响应输出的典型流程。压缩包共25个文件涵盖Java源码、class字节码、HTML页面、jpg图片、Eclipse工程配置及XML描述文件等其中源码可直接导入IDE运行class文件便于对照检查工程配置可帮助理解Web项目标准目录结构整体压缩包约491KB轻量易用。目前已有638人浏览学习适合作为课程作业参考或Servlet入门实战练习也可在此基础上扩展商品管理、结算等功能性价比高。1. 用 servlet 手写购物车这个 JavaWeb 小项目到底值不值得拆如果你正在被javaWeb课设折磨或者刚学完 servlet 想找个能完整跑通的项目练手那shop010.zip这个购物车系统应该正对你的胃口。它没有 Spring、没有 MyBatis甚至没有 JSP就是纯粹的servlet Tomcat HTML CSS一个商品展示页shop.html点击“添加到购物车”超链接跳进 Servlet 把商品信息记到 session 里再跳到另一个 Servlet 用表格把购物车内容打出来。很多人一上来就啃黑马笔记里的电商项目结果被 SSM 全家桶压得喘不过气反而忽略了 servlet 生命周期、请求跳转、会话保持这些最核心的机制。这个资源就是把“购物车”这个业务拆到最小让你一眼看穿请求是怎么进容器、怎么被 Servlet 处理、又怎么把数据带走的。适合两类人一是刚写完 servlet 入门 demo、想做一个“有业务逻辑”的完整结构的人二是课设验收需要现场演示“购物车能加能查”的同学。2. 购物车的请求链路Servlet 生命周期和两条跳转路径怎么选2.1 Servlet 生命周期容器什么时候调用 init、service、destroy写购物车之前得先把 servlet 的运行机制理一遍。Tomcat 启动后根据web.xml里的servlet-mapping做 URL 到类的映射。第一次有请求打到/addCart时Tomcat 才会实例化对应的 AddCartServlet然后调init()执行初始化init在整个生命周期里只执行一次。之后每次请求都走service()由容器根据 HTTP 方法分发给doGet或doPost。服务停止或应用卸载时才调destroy()。我在给学生讲这个项目的时候会特别强调一句Servlet 默认是单例多线程的一个 AddCartServlet 实例要同时服务多个用户请求所以你千万不要在 Servlet 里定义“购物车”这种可变成员变量来存数据。正确的做法是在doGet内部从request.getSession()拿会话对象把购物车列表塞进 session 里这样用户拿到的是一份自己的车。public class AddCartServlet extends HttpServlet { // init 可以在这里读取 web.xml 的 init-param购物车数据不要放这 Override public void init() throws ServletException { super.init(); System.out.println(AddCartServlet 初始化完成生命周期中只执行一次); } Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 核心业务把商品写进 session } }这段代码里最关键的是init()的注释含义它适合加载全局配置但不适合存业务数据。因为同一个 Servlet 实例会被所有访问者共享你把购物车 List 定义成 servlet 字段A 用户加商品B 用户也看得到这就是典型的线程安全问题。购物车数据要放在HttpSession里session 才是每个客户端独享的存储区域。2.2 添加到购物车用 doGet 还是 doPost商品展示页里每个商品的“添加到购物车”如果写成a href超链接那默认就是 GET 请求Servlet 里要重写doGet。如果你想用doPost那页面得改成form methodpost每个商品按钮用submit触发。我一般建议课设项目用超链接因为代码量少、浏览器地址栏里能看到请求参数演示的时候更好说清楚“商品 ID 是怎么传进去的”。这里有个新手特别容易踩的坑你以为写了doPost就能接住超链接的请求但超链接永远发 GETTomcat 最后会返回 405 错误。反过来如果表单设了methodpost你只在 servlet 里写了doGet同样 405。稳妥的做法是先判断再转发Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 自己控制方法分发兼容 GET 和 POST 两种提交方式 if (POST.equalsIgnoreCase(req.getMethod())) { doPost(req, resp); } else { doGet(req, resp); } }这段代码的逻辑在于service()是 Servlet 处理请求的入口默认实现就是按请求方法调doGet/doPost这里手动重写是为了让同一个 deGet 和 doPost 都走同一段“加购”逻辑。实际开发中很少有人这么写但课设里这么处理答辩时你能多讲出一个知识点Servlet 的方法分发机制。2.3 用 request 还是 session 传递商品参数超链接里带的goodsId、goodsName、price是请求参数它们的存活周期只在这一次 HTTP 请求里。假如你在 AddCartServlet 里用request.setAttribute(goodsName, name)然后request.getRequestDispatcher(/showCart).forward()转发那 showCart 这个 Servlet 是能拿到的因为转发是同一个请求内部跳转。但如果你在加购完成后用response.sendRedirect(/showCart)重定向浏览器会再发一次全新的请求request.setAttribute里的东西就全丢了。购物车这种业务商品数据必须放进 session因为用户加完商品之后可能还要去逛逛商品页再回来查看购物车这中间隔了好几轮请求只有 session 能跨请求存活。session 的底层是JSESSIONIDTomcat 第一次调用request.getSession()时会生成这个 ID通过 Set-Cookie 响应头种到浏览器里之后每次请求浏览器自动带上这个 Cookie容器根据 ID 找到对应的 HttpSession 对象。我把这个链路的代码骨架写出来你照这个结构填充业务逻辑就行public class AddCartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 处理中文乱码POST 和 GET 处理方式不同 request.setCharacterEncoding(UTF-8); // 2. 从超链接的?后面取参数 String goodsId request.getParameter(goodsId); String goodsName request.getParameter(goodsName); String priceStr request.getParameter(price); // 3. 拿到当前用户的 session购物车是 ListCartItem HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayListCartItem(); session.setAttribute(cart, cart); } // 4. 加到购物车同款商品数量累加下面第 4 章会细讲 addToCart(cart, goodsId, goodsName, priceStr); // 5. 重定向到展示购物车的 Servlet防止表单/超链接重复提交 response.sendRedirect(request.getContextPath() /showCart); } }这里的参数说明要留意三点。第一request.getParameter拿到的一律是字符串价格字段后面要转成 double 或 BigDecimal。第二request.getContextPath()返回的是/shop这样的项目根路径重定向必须拼上它不然部署在多级路径下会找不到 Servlet。第三sendRedirect是告诉浏览器“你去访问 /showCart”浏览器地址栏会变这时刷新页面就只是刷新购物车列表不会重新触发加购动作这是 PRG 模式Post/Redirect/Get的核心思想。3. 搭一个能跑的 JavaWeb 购物车目录结构、shop.html 和 web.xml3.1 把 shop010.zip 导入到 Eclipse 或 IDEA解压shop010.zip之后你会看到.project、.classpath、.settings这些 Eclipse 工程文件这说明它原本是 Eclipse 的动态 Web 项目。如果你用的是 Eclipse直接File - Import - Existing Projects into Workspace选择解压目录JDK 版本选 1.8然后右键项目Properties - Targeted Runtimes勾上你的 Tomcat 版本Run on Server就能启动。如果你用的是 IDEA导入方式略有不同File - New - Project from Existing Sources选解压目录后直接 Finish再右键项目Add Framework Support - Web把 Web 根目录指向WebContent。注意 IDEA 里默认的 Web 根目录是web如果你不手动改成WebContent静态资源shop.html会 404。这一步就是最常见的第一坑可视化的配置差异但能卡住不少人。3.2 商品展示页 shop.html超链接怎么拼接参数shop.html是这个项目里的静态商品页包里自带img目录放商品图片。页面本身不复杂关键在“添加到购物车”这个超链接的参数拼接格式!DOCTYPE html html head meta charsetUTF-8 title简易购物车 - 商品列表/title /head body h2商品列表/h2 div classgoods img srcimg/keyboard.jpg width120 height120 p机械键盘/p p价格399.00 元/p !-- 点击超链接GET 请求参数直接拼在 URL 后面 -- a href/shop/addCart?goodsId1001goodsName机械键盘price399.00添加到购物车/a /div div classgoods img srcimg/mouse.jpg width120 height120 p无线鼠标/p p价格129.00 元/p a href/shop/addCart?goodsId1002goodsName无线鼠标price129.00添加到购物车/a /div /body /html这个 HTML 里的核心是href的写法/shop/addCart是最外层绝对路径/shop是你的项目名/addCart是 web.xml 里配的 URL-Pattern后面用?连接参数多个参数之间用分隔。如果地址写错点超链接要么 404要么参数传不全。我习惯把价格在 URL 里传成字符串进入 Servlet 后再用Double.parseDouble转换因为 URL 参数本质都是字符串。3.3 web.xmlServlet 注册和 URL-Pattern 的讲究这个项目没有用注解WebServlet而是把 Servlet 配置集中在WEB-INF/web.xml里这对老一点的项目来说反而更稳。你需要维护两个映射一个是/addCart对应加购的 Servlet一个是/showCart对应展示购物车表格的 Servlet。?xml version1.0 encodingUTF-8? 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_3_1.xsd version3.1 !-- 商品详情管理购物车的 Servlet 真实位置 -- servlet servlet-nameAddCartServlet/servlet-name servlet-classcom.cart.servlet.AddCartServlet/servlet-class /servlet !-- 把 /addCart 这个 URL 映射到上面注册的 Servlet -- servlet-mapping servlet-nameAddCartServlet/servlet-name url-pattern/addCart/url-pattern /servlet-mapping !-- 展示购物车列表 -- servlet servlet-nameShowCartServlet/servlet-name servlet-classcom.cart.servlet.ShowCartServlet/servlet-class /servlet servlet-mapping servlet-nameShowCartServlet/servlet-name url-pattern/showCart/url-pattern /servlet-mapping !-- 会话超时默认 30 分钟课设演示时可以调长一点 -- session-config session-timeout30/session-timeout /session-config /web-app这里要特别注意url-pattern的写法。/addCart和/*或者/是三种完全不同的匹配逻辑/addCart是精确匹配只有访问/addCart才会进这个 Servlet/*是路径通配会拦截所有请求包括静态的 HTML、CSS、JS这是个极其常见的翻车点/是缺省 Servlet 的匹配意思是不满足任何精确匹配的请求才轮到你。所以这个购物车项目里html页面能正常展示就是因为 URL-Pattern 是精确的/addCart没有把静态资源拦截掉。4. 核心代码实现商品 JavaBean、购物车列表和金额统计4.1 CartItem 实体类购物车里的一行记录购物车里每条记录不能只存商品 ID它还需要商品名、单价、购买数量。所以先写一个CartItem实体类正常 JavaBean 的结构属性私有提供 getter/setterpackage com.cart.domain; public class CartItem { private String goodsId; // 商品唯一标识 private String goodsName; // 商品名称 private double price; // 单价 private int count; // 购买数量 public CartItem() {} public CartItem(String goodsId, String goodsName, double price, int count) { this.goodsId goodsId; this.goodsName goodsName; this.price price; this.count count; } // getter 和 setter 省略Eclipse 快捷键 AltShiftS 自动生成 public double getTotalPrice() { return price * count; } }这个类为什么要单独写因为 session 里存ListCartItem之后你才能在 AddCartServlet 里遍历判断“这个商品是不是已经加过了”。如果你只存一个商品 ID 字符串后面展示表格时你得反过来再去商品页查名称、价格多此一举。getTotalPrice()是计算当前行小计的方法表格里每一行都能直接调用。4.2 AddCartServlet同款商品数量累加而不是重复插行加购逻辑最忌讳的做法是每次点击“添加到购物车”就往 List 里 add 一个新对象结果同一个商品在购物车表格里出现十几行。正确写法是先判断列表里有没有相同goodsId有就把 count 加 1没有才 new 一个新项加进去。package com.cart.servlet; import com.cart.domain.CartItem; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class AddCartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 请求参数统一设 UTF-8防止中文商品名乱码 request.setCharacterEncoding(UTF-8); String goodsId request.getParameter(goodsId); String goodsName request.getParameter(goodsName); String priceStr request.getParameter(price); // 如果商品 ID 为空说明链接拼错了直接打回商品页 if (goodsId null || goodsId.trim().isEmpty()) { response.sendRedirect(request.getContextPath() /shop.html); return; } double price 0; try { price Double.parseDouble(priceStr); } catch (NumberFormatException e) { price 0; // 价格参数传错时兜底不让程序崩溃 } HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayListCartItem(); session.setAttribute(cart, cart); } // 核心先找同款找到了就累加数量 boolean exists false; for (CartItem item : cart) { if (goodsId.equals(item.getGoodsId())) { item.setCount(item.getCount() 1); exists true; break; } } // 购物车里没有这个商品才 new 新对象 if (!exists) { CartItem newItem new CartItem(goodsId, goodsName, price, 1); cart.add(newItem); } // 重定向到展示页刷新不会重复加购 response.sendRedirect(request.getContextPath() /showCart); } }这段代码里有三个细节值得展开。第一个是request.setCharacterEncoding(UTF-8)的位置它必须放在getParameter之前才有效这是对 POST 请求的 body 解码设置。第二个是parseDouble的异常处理因为 URL 里写price399.00没问题但如果你手贱写成price399元程序会直接抛NumberFormatException整个请求就 500 了。第三是break的用途找到同款商品后马上跳出循环不然同一个商品会被累加两次。4.3 ShowCartServlet用 HTML 表格把购物车打出来第二个 Servlet 要做的事是从 session 里取出购物车遍历ListCartItem在响应里拼接 HTML 表格。这个项目没有引入 JSP所以直接在 Servlet 里输出 HTML 是非常务实的做法。package com.cart.servlet; import com.cart.domain.CartItem; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; import java.util.List; public class ShowCartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(text/html;charsetUTF-8); PrintWriter out response.getWriter(); HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); out.println(htmlheadmeta charsetUTF-8); out.println(title购物车/title/headbody); out.println(h2我的购物车/h2); // 购物车是 null 或空列表显示“还没有商品” if (cart null || cart.isEmpty()) { out.println(p购物车是空的a hrefshop.html去逛逛/a/p); } else { out.println(table border1 cellpadding6); out.println(trth商品ID/thth商品名/thth单价/thth数量/thth小计/th/tr); double total 0; for (CartItem item : cart) { double subtotal item.getTotalPrice(); total subtotal; out.println(tr); out.println(td item.getGoodsId() /td); out.println(td item.getGoodsName() /td); out.println(td item.getPrice() /td); out.println(td item.getCount() /td); out.println(td subtotal /td); out.println(/tr); } out.println(trtd colspan4合计/tdtd total /td/tr); out.println(/table); out.println(pa hrefshop.html继续购物/a/p); } out.println(/body/html); } }要注意response.setContentType(text/html;charsetUTF-8)这一行不能省。如果你只写text/html默认的字符编码可能是 ISO-8859-1中文商品名就全乱码了。另外这里判断空购物车用的是cart null || cart.isEmpty()双重判断因为首次访问时 session 里根本没有“cart”这个属性getAttribute返回 null直接cart.isEmpty()会空指针异常。5. 避坑购物车项目的常见翻车点与排查顺序5.1 点击“添加到购物车”直接 404现象商品页能打开但点超链接后地址栏变成/shop/addCart页面上是 Tomcat 的 404 错误页。原因三种可能性最大。第一web.xml里没有注册 AddCartServlet或者url-pattern写成了/addCart/多了个斜杠第二Servlet 类编译后的.class文件没有输出到WEB-INF/classesEclipse 有时不会自动编译第三Servlet 类里的WebServlet(/addCart)注解和 web.xml 里的servlet-mapping重复定义了但映射路径大小写不一致。解决先检查WEB-INF/classes/com/cart/servlet/下有没有AddCartServlet.class没有就Project - Clean重新编译。再看 web.xml 的url-pattern是不是/addCart这种精确路径。最后确认浏览器访问路径里写的是不是/项目名/addCart而不是/addCart。5.2 商品名中文乱码现象购物车表格里商品名显示成氺槰ç这样的乱码。原因URL 参数里的中文在 Tomcat 7 及以下版本默认按 ISO-8859-1 解码而你页面是 UTF-8 编码。即使你把request.setCharacterEncoding(UTF-8)写在 doGet 里也没用因为 GET 请求参数的解码发生在容器处理 URL 的时候早于你的 Servlet 代码执行。解决两种方案。方案一在 Tomcat 的server.xml里给Connector加上URIEncodingUTF-8这是从源头解决方案二在 Servlet 里手动转码用new String(goodsName.getBytes(ISO-8859-1), UTF-8)这段代码只在中文字符被错误解码时生效是课设里常见的兜底写法。我个人倾向方案一因为方案二写起来晦涩答辩时也不好解释。5.3 刷新页面同一个商品被加了两三件现象用户查看购物车后发现数量对不上好像多买了好几件。原因加购完成后的跳转方式有问题。如果你用的是request.getRequestDispatcher(/showCart).forward()转发浏览器地址栏停留在/addCart用户一按 F5整个加购请求会重新执行一遍商品数量自然翻倍。解决把加购的跳转改成response.sendRedirect(request.getContextPath() /showCart)。重定向是两次独立的 HTTP 请求第一次请求处理完就结束了浏览器地址栏变成/showCart刷新时只刷新列表页不会重新执行加购逻辑。这就是 PRG 模式POST after Redirect GET。5.4 部署后直接访问 /shop.html 是错的现象在 IDEA 里直接右键 Run 启动成功浏览器访问http://localhost:8080/shop.html页面 404但访问http://localhost:8080/shop/shop.html就能打开。原因你部署的是一个名为/shop的 Web 应用Tomcat 默认的根路径应用是ROOT。你在浏览器里访问http://localhost:8080/时走的是 ROOT不是你的项目。启动时 IDEA 的配置页面里Deployment标签下 Application context 如果没有设置/shop就会出现这个现象。解决在 IDEA 的Run Configuration - Deployment - Application context里设置/shop然后统一用带/shop前缀的地址访问。同时商品页里的超链接也要跟着改成/shop/addCart或者在 HTML 里写base href%request.getContextPath()%/来统一前缀这样你迁移项目名时不用到处改链接。5.5 购物车偶尔“凭空消失”现象用户逛了一圈回来购物车里一个商品都没有了但刚才确实加过。原因session 超时了。Tomcat 默认的session-timeout是 30 分钟用户如果 30 分钟之内没有发任何请求容器就把这个 HttpSession 对象销毁下次request.getSession()会重新创建一个空 session。另一个原因是浏览器清掉了 CookieJSESSIONID 丢了服务端找不到旧 session。解决在web.xml里调大session-timeout比如改成 60 或 120方便课设演示。正规产品不应该靠调大这个数来解决但作为演示项目这是一种常见的临时措施。真正要做得稳得把购物车同步到数据库但那样复杂度就上去了不是这个小项目的定位。6. 一个小技巧计算购物车合计金额时用 BigDecimal 代替 double如果你把购物车里的商品单价设计成小数比如 39.90、99.90那你会发现用double计算小计和合计时会有诡异的结果39.90 * 3算出来不是119.70而是119.70000000000002。这是因为 double 用二进制小数表示十进制小数的精度问题。这是购物车系统最容易被忽略的一个数学坑课设答辩时如果你能把double换成BigDecimal顺便讲出浮点误差的原理负责验收的老师通常会眼前一亮。改造思路很直接在CartItem里不再用double price而是改成BigDecimal price数量count保留 int计算小计时用price.multiply(BigDecimal.valueOf(count))。AddCartServlet 里转换价格时也别再Double.parseDouble改用new BigDecimal(priceStr)它自带格式校验传非法字符串会抛异常你需要处理一下。import java.math.BigDecimal; public class CartItem { private String goodsId; private String goodsName; private BigDecimal price; private int count; public BigDecimal getTotalPrice() { // 单价 * 数量BigDecimal 没有 * 运算符必须调 multiply return price.multiply(BigDecimal.valueOf(count)); } }另一个我在实际项目中养成的习惯是在 AddCartServlet 里加一个“价格合法性校验”不要盲目信任 URL 里传过来的参数。比如用户可以手动修改超链接price0.01用一分钱把商品加进购物车。更稳妥的做法是在 Servlet 里维护一个允许的商品 ID 与价格的对照表或者干脆从数据库或静态 Map 里查真实价格忽略前端传进来的 price。这个小项目里没有数据库但你可以写一个MapString, BigDecimal作为商品篱笆表放在init()里初始化加购时优先按 goodsId 从 Map 取值取不到才用前端参数。还有一个关于“结算”的操作顺序先计算总价、生成订单数据、然后把 session 里的购物车清空。顺序反了就会出问题。我带过的学生里有人想当然地在刚进结算页时就session.removeAttribute(cart)结果返回上一页再点“我的购物车”车已经空了页面报空指针。正确的顺序应该是结算 Servlet 先把ListCartItem取出来、算好总金额、生成订单号等确认支付成功后再清空购物车。从那以后我每次给购物车项目组装“结算”流程时都强制自己先走一遍“读取购物车数据 → 保存订单 → 再清空”的顺序避免把中间状态搞丢。这个小项目虽然简单但把请求转发、会话管理和浮点计算这几个基本功练扎实了之后再碰 SSM 或 Spring Boot 的购物车你会发现问题模式基本是相同的。希望帮到你。本文还有配套的精品资源点击获取
返回列表