javaWeb+servlet购物车项目:从零跑通核心链路

发布时间:2026/10/9 0:42:49
javaWeb+servlet购物车项目:从零跑通核心链路 简介一套基于JavaWeb与Servlet的简易购物车系统源码面向JavaWeb初学者与课程设计人群演示如何通过Servlet接收商品添加请求、记录选购信息与数量并跳转至另一个Servlet以表格形式展示购物车内容。工程基于Eclipse构建技术栈为JavaWeb Servlet Tomcat HTML CSS通过商品展示页面和超链接参数传递触发后台添加逻辑覆盖了从页面展示到会话内数据记录的核心流程。压缩包共25个文件主要包括3个Java源文件、对应3个class编译文件、1个HTML页面、2个XML配置及8张JPG图片等完整保留Eclipse项目结构便于直接导入运行整包仅491KB轻量易用。资源目前已有638人学习下载适合用来理解购物车模块的后台交互流程可作为课设或Servlet入门练手参考。1. javaWebservlet购物车这个项目能让你在一天内跑通servlet核心链路做javaWeb课程设计或者刚接触servlet的人通常会卡在“明明代码能编译部署到Tomcat就404/500”。这里拆的shop010是一个极其简洁但完整的servlet购物车demoHTML展示商品点击“添加到购物车”跳转到Servlet用Session记录选购信息和数量再跳转到另一个Servlet用表格输出购物车明细。它没有Spring、没有Maven正好把HttpServletRequest、HttpSession、Servlet跳转这几个最核心的机制暴露出来。适合刚学完Java SE、想弄懂servlet请求响应模型的人也适合做课程设计参考。下面从目录结构开始一步一步讲清楚怎么跑、怎么改、哪些地方容易翻车。2. 项目结构与运行环境读懂shop010再动手2.1 目录结构src、build、WebContent各管什么事先花五分钟把shop010的目录结构看清楚。这个项目是经典的Eclipse Dynamic Web Project布局项目正文里列出的文件已经足够还原它的骨架。srcJava源码目录。Servlet类、过滤器、工具类都放这里包名一般类似com.shop.servlet。IDE保存时自动编译生成到build/classes。buildEclipse的编译输出目录默认叫build/classesTomcat运行时实际加载的是这里的.class文件。所以改完Java代码后必须触发重新编译否则Tomcat用的还是旧class。WebContentWeb应用根目录。这个目录下的shop.html、img、WEB-INF会被原样发布到Tomcat的webapps/shop010下。浏览器能直接访问根目录下的静态文件但WEB-INF不能通过URL直接访问。WEB-INF里面最关键的是web.xmlServlet 3.0之前和lib目录。如果是Servlet 3.0注解方式web.xml可以不存在。Spring项目会把依赖jar放在WEB-INF/lib但这个教学项目没有依赖所以不需要。.classpath、.projectEclipse的项目描述文件导入时靠它们识别项目类型。IDEA导入Eclipse项目时也会读这两个文件。为什么Tomcat一定要找WEB-INF/classes因为Servlet是Java类Tomcat启动时会根据映射关系用类加载器去WEB-INF/classes目录下找对应的.class文件。源码.java文件对Tomcat是透明的它只认字节码。所以如果只把WebContent复制到Tomcat的webapps下WEB-INF/classes为空服务能启动但所有Servlet路径404。这是这个项目最容易踩的部署坑后面避坑章节会再展开。2.2 环境与部署JDKTomcatEclipse的推荐组合这个项目用的是老式Servlet API没有Maven环境搭配有一个“安全区”JDK 8不要用JDK 17以上除非你愿意处理JAXB缺失等一堆编译问题Tomcat 8.5或9.0不要用Tomcat 10原因见避坑章节Eclipse IDE for Enterprise Java或者IDEA二选一以Eclipse为例导入部署步骤如下File Import Existing Projects into Workspace选择shop010.zip解压后的文件夹。如果项目没有显示为Web项目右键项目Properties Project Facets勾选Dynamic Web Module版本选3.0。在Java Build Path Libraries Add Library Server Runtime里选Tomcat 9。右键项目Run As Run on Server选择刚配置的Tomcat。启动后访问http://localhost:8080/shop010/shop.html。IDE可以换但部署逻辑不变关键是让IDE把build/classes里的Servlet class和WebContent一起发布到Tomcat的应用目录。Eclipse的“Run on Server”会自动完成合并这也是为什么在Eclipse里跑得好好的手动拷目录到webapps却404的原因。IDEA用户导入Eclipse项目后需要手动配置Tomcat。进入Run Edit Configurations Tomcat Server Local在Deployment标签页点选Artifact选择shop010:war explodedApplication context填/shop010。这里的war exploded模式表示按展开目录方式部署改Java文件后可以热更新适合调试。有两个部署参数很关键Tomcat的port和应用的context path。端口默认8080被占用时在server.xml的Connector标签里改port8090。Context path决定了URL前缀Eclipse自动部署时通常是/shop010IDEA要自己填填错就会出现“访问shop.html 404但Eclipse能跑”的怪事。2.3 没有web.xml也能跑Servlet 3.0的注解映射这个购物车项目如果在Servlet类上看到WebServlet就不需要web.xml来注册Servlet。这是Servlet 3.0Java EE 6引入的注解式映射配置更轻也适合课程设计。WebServlet(name AddCartServlet, urlPatterns { /addCart }) public class AddCartServlet extends HttpServlet { private static final long serialVersionUID 1L; }如果用传统的web.xml对应的映射是这样的servlet servlet-nameAddCartServlet/servlet-name servlet-classcom.shop.servlet.AddCartServlet/servlet-class /servlet servlet-mapping servlet-nameAddCartServlet/servlet-name url-pattern/addCart/url-pattern /servlet-mapping这里有几个细节必须记住url-pattern不能漏掉前面的斜杠/写成addCart会变形为默认路径匹配。servlet-class必须是编译后存在于WEB-INF/classes的全限定名包名类名一个都不能错。同一个Servlet不能既用WebServlet又在web.xml注册同一个URL否则Tomcat启动时控制台会报Ambiguous mapping应用直接启动失败。判断这个项目用的是哪种映射很简单打开WebContent/WEB-INF看有没有web.xml以及Servlet类源码上有没有WebServlet注解。两种方式都能跑注解方式代码就近维护web.xml集中管理。课堂答辩时如果老师让我解释URL怎么找到Servlet我建议你先把url-pattern和servlet-class这两行讲清楚讲“匹配过程”会比只念代码得分高。2.4 请求路径是怎么和Servlet对上的顺手补一个关键概念浏览器发起GET /shop010/addCart?goodsId1001后Tomcat先按/shop010找到应用再在应用里查/addCart的映射。映射匹配按“精确匹配 路径匹配 扩展名匹配 默认”的优先级执行。我们这个项目全是精确匹配所以/addCart必须与注解或web.xml里的urlPatterns完全一致多一个末尾斜杠都返回404。如果用注解URL参数里的?后的内容不属于映射匹配范围属于request.getParameter的领地。所以/addCart和/addCart?foobar匹配的是同一个Servlet参数不同只是导致Servlet内取到的值不同。理解这一点后调试404时思路就很清晰先看URL的path部分是否匹配再去Servlet内部Debug参数。3. 商品展示与添加到购物车核心链路怎么串起来3.1 商品页HTML里的超链接就是请求入口shop.html是项目的门面。它不需要JSP不需要EL表达式纯HTMLCSS就能把商品展示做得足够干净。因为需求里只要求“点击添加到购物车超链接后跳转到Servlet”所以每个商品卡片上放一个超链接链接地址直接携带商品参数。一个典型的商品卡片长这样div classgoods-card img srcimg/goods_01.jpg alt商品图 h3黑色签字笔/h3 p classprice5.00/p a hrefaddCart?goodsIdG001goodsName黑色签字笔price5.00添加到购物车/a /div要注意这个超链接是GET请求商品信息会暴露在地址栏。对教学项目不是问题但如果参数里有中文浏览器地址栏会显示URL编码后的字符比如%E9%92%A2%E7%AC%94这是正常现象。初学者容易踩的路径坑如果shop.html在应用根目录hrefaddCart是相对路径浏览器基于当前页面URL解析成/shop010/addCart正确。但如果shop.html在pages子目录下hrefaddCart会解析成/shop010/pages/addCart导致404。这时候应该写绝对路径/shop010/addCart或者先用${pageContext.request.contextPath}JSP里拼出上下文。项目里shop.html在根目录所以相对路径最稳。你可能会问为什么不用formbutton也完全可以。但超链接的写法更直观地展示了“GET参数在URL上”这个事实能让新手一眼看懂Servlet拿参数的过程。如果要用POST方式传递商品数据就得改成表单后面会在3.3提一下。3.2 AddCartServlet把商品写入Session购物车Servlet收到请求后需要完成三件事读取参数、更新购物车、跳转到显示页面。下面这段代码是符合项目需求的最小实现WebServlet(/addCart) public class AddCartServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String goodsId request.getParameter(goodsId); String goodsName request.getParameter(goodsName); String priceStr request.getParameter(price); // 参数校验goodsId空则返回提示避免后面空指针 if (goodsId null || goodsId.trim().isEmpty()) { response.setContentType(text/html; charsetUTF-8); response.getWriter().print(h3缺少商品ID无法添加/h3); return; } // 从Session取购物车不存在则新建 HttpSession session request.getSession(); MapString, Integer cart (MapString, Integer) session.getAttribute(cart); if (cart null) { cart new LinkedHashMapString, Integer(); } // 数量累加同一个商品再次加入时数量1 Integer oldCount cart.get(goodsId); cart.put(goodsId, oldCount null ? 1 : oldCount 1); session.setAttribute(cart, cart); // 写操作完成后重定向到明细页 response.sendRedirect(showCart); } }逻辑说明request.getParameter()返回String如果后续要把商品ID转成数字用Integer.parseInt(goodsId)前必须先校验格式否则抛NumberFormatException。购物车选LinkedHashMapString, Integerkey是商品IDvalue是数量。LinkedHashMap记录插入顺序显示时按用户添加顺序排列比HashMap更符合购物车直觉。oldCount null ? 1 : oldCount 1避免对null做1的空指针。Integer是包装类型自动拆箱时null会触发NPE所以这里用三目判断。response.sendRedirect(showCart)是重定向浏览器会重新请求/showCart地址栏变化。如果改成request.getRequestDispatcher(showCart).forward(request, response)转发地址栏保持/addCart用户按F5刷新时商品会被重复添加所以写操作后跳转我一般用重定向。这里还隐含一个设计决策购物车放在Session里。Session是每个用户独立的所以不同用户购物车天然隔离。如果放在ServletContext或static变量里所有请求共享同一份数据那就不叫购物车而叫“商品计数器”了。看懂这个决策就能理解Servlet为什么不能把实例变量用作请求间数据。3.3 中文乱码与参数转码GET与POST各有一套商品名是中文时最容易出乱码。乱码分三层HTML页面编码、GET参数编码、POST请求体编码。HTML页面meta charsetUTF-8只是告诉浏览器按UTF-8解码但文件本身如果是GBK保存浏览器解码后照样是乱码。在Eclipse里检查Window Preferences Web HTML Files编码设为UTF-8IDEA里打开右下角文件编码检查一下。GET参数GET参数走URLTomcat 8以上默认按UTF-8解码Tomcat 7及以下默认ISO-8859-1。如果项目跑在旧Tomcat上request.setCharacterEncoding(UTF-8)对GET无效因为它只影响POST body。POST请求必须在读取任何参数之前调用request.setCharacterEncoding(UTF-8)并且放在Servlet的doPost第一行。这个设置只对本次请求body生效不是全局开关。如果遇到GET乱码且不想改server.xml可以用“后悔药”式转码String goodsName request.getParameter(goodsName); if (goodsName ! null) { goodsName new String(goodsName.getBytes(ISO-8859-1), UTF-8); }思路把Tomcat按ISO-8859-1解码得到的字符重新转回原始字节再用UTF-8解码。只有原始字节确实是UTF-8时才能还原。更推荐直接改Tomcat的server.xml一次解决Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /改完重启Tomcat所有GET参数按UTF-8处理。注意是重启不是热加载。这个配置对动态请求和静态请求都生效属于我每次搭环境必改的第一项。4. 购物车明细展示表格输出与数量联动的四种写法4.1 ShowCartServlet从Session读取并渲染表格添加完商品后重定向到/showCart这个Servlet负责把Session里的购物车变成HTML表格。最直接的写法是在Servlet中拼HTML字符串用response.getWriter()输出。WebServlet(/showCart) public class ShowCartServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(text/html; charsetUTF-8); HttpSession session request.getSession(); MapString, Integer cart (MapString, Integer) session.getAttribute(cart); PrintWriter out response.getWriter(); out.println(htmlheadtitle购物车/title/headbody); out.println(table border1); out.println(trth商品ID/thth商品名称/thth数量/th/tr); if (cart ! null !cart.isEmpty()) { for (Map.EntryString, Integer entry : cart.entrySet()) { String id entry.getKey(); int count entry.getValue(); out.println(trtd id /tdtd id /tdtd count /td/tr); } } else { out.println(trtd colspan3购物车是空的/td/tr); } out.println(/table); out.println(a hrefshop.html继续购物/a); out.println(/body/html); } }这段代码暴露了一个设计缺陷MapString, Integer只记录商品ID和数量商品名称、价格在AddCartServlet里被丢了所以表格“商品名称”列只能显示ID。如果原项目确实这样实现它满足“显示选购信息”的最低要求但如果你想做出更完整的课程设计建议按4.2升级。4.2 用CartItem类替代Map表格才能显示完整信息更完整的做法是自定义CartItem类把商品ID、名称、单价、数量封在一起。该类通常放com.shop.model包。public class CartItem { private String goodsId; private String goodsName; private double price; private int count; public CartItem(String goodsId, String goodsName, double price, int count) { this.goodsId goodsId; this.goodsName goodsName; this.price price; this.count count; } // getter/setter省略 }AddCartServlet里维护ListCartItemListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayListCartItem(); } boolean found false; for (CartItem item : cart) { if (item.getGoodsId().equals(goodsId)) { item.setCount(item.getCount() 1); found true; break; } } if (!found) { cart.add(new CartItem(goodsId, goodsName, Double.parseDouble(priceStr), 1)); } session.setAttribute(cart, cart);这里有两处和Map版本不同的处理逻辑重复商品通过遍历List判断goodsId是否已存在而不是直接get(goodsId)。List的查找是O(n)购物车商品数量很小性能不是问题。Double.parseDouble(priceStr)可能抛NumberFormatException所以在解析前应该先try-catch或正则校验课程设计里至少做到非空判断。表格里要显示价格和小计循环内累加tr td1001/td td钢笔/td td12.5/td td2/td td25.0/td /tr金额计算用double会有精度问题但只是显示购物车不涉及支付结算教学上可以接受。想专业就用BigDecimalnew BigDecimal(String.valueOf(price)).multiply(new BigDecimal(String.valueOf(count)))。4.3 表格里怎么加“删除”和“修改数量”如果需求只要求显示这节可以跳过。但老师经常追问“如何删除某商品”。在ShowCartServlet表格的每行加一个删除链接a hrefremoveCart?goodsId1001删除/a然后增加RemoveCartServletWebServlet(/removeCart) public class RemoveCartServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); MapString, Integer cart (MapString, Integer) session.getAttribute(cart); if (cart ! null) { cart.remove(request.getParameter(goodsId)); session.setAttribute(cart, cart); } response.sendRedirect(showCart); } }注意这里的Session引用语义cart是从Session取出的对象引用remove修改了这个对象本身Session里的同一对象也变了所以setAttribute不是必需的。但如果代码里把cart重新赋值为新Map就必须setAttribute回去。判断标准只有一句话对象是否被替换。如果要“修改数量”可以做一个简单的加减链接addCart?goodsId1001数量1和reduceCart?goodsId1001数量-1。reduce逻辑就是count-1减到0就移除。多一个Servlet也就十来行不再展开。4.4 购物车为空与Session过期时的兜底用户直接访问/showCart或者Session超时后session.getAttribute(cart)返回null。如果代码直接调用cart.size()就会空指针。4.1的代码先判空再迭代这是必须的。Session默认超时时间30分钟可在web.xml配置session-config session-timeout30/session-timeout /session-config单位是分钟。答辩时如果老师开着浏览器一阵子再点可能遇到Session过期。两种处置方式把超时调大或者在AddCartServlet首次添加时不管Session是否存在都新建购物车。我一般推荐第二种因为购物车本来就不需要长期保存用户重新添加是正常行为。另外如果用户清除了浏览器CookieJSESSIONID会丢失Session也找不到表现为购物车“丢失”。这不是代码bug是HTTP无状态协议的天然限制。要记住这个边界不要试图用Session做持久化。5. 避坑与常见问题部署这个servlet购物车时的五个典型坑5.1 坑1Session取出来强转ClassCastException现象第一次点“添加到购物车”正常第二次再点直接500Tomcat日志报java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map。原因Session里cart属性被之前某段代码可能是测试时存成了String或其他类型当前强转MapString, Integer失败。这种问题在多人协作或反复修改代码时特别容易出现。解决先清浏览器JSESSIONID或重启Tomcat清空Session。代码层面加上类型判断不要盲转Object cartObj session.getAttribute(cart); MapString, Integer cart; if (cartObj instanceof Map?, ?) { cart (MapString, Integer) cartObj; } else { cart new LinkedHashMapString, Integer(); }这个写法以后每次处理Session属性都成立先instanceof判断再强转。从那以后我再没被ClassCastException坑过。5.2 坑2加了商品刷新购物车后数量翻倍现象点击“添加到购物车”控制台日志只打印了一次请求但购物车数量变成2、3。原因最常见的是showCart页面上的某个资源比如图片路径不对导致浏览器对同一个Servlet发出了额外请求。但购物车数量增加只可能是AddCartServlet被再次调用。需要确认是不是HTML里有两个相同超链接或者页面里有个img的src写成了addCart?goodsId...浏览器加载图片时也触发了Servlet。解决在AddCartServlet第一行加System.out.println(addCart called);观察日志条数。如果发送多次检查HTML标签。如果只有一次但数量仍翻倍检查是不是sendRedirect之后浏览器自动重新请求了addCart——通常不会除非你用了Forward导致地址栏不变。5.3 坑3Tomcat 10选错版本javax变成jakarta现象项目导入后所有javax.servlet.*都报红运行直接抛NoClassDefFoundError: javax/servlet/ServletException。原因Tomcat 10开始采用Jakarta EE 9规范Servlet包名从javax.servlet迁移为jakarta.servlet。这个项目是老的javax写法在Tomcat 10上连编译都过不了。解决最简单换Tomcat 8.5或9。判断下载的Tomcat版本看lib目录下有没有servlet-api.jarTomcat 8/9有Tomcat 10只有jakarta.servlet-api.jar。如果你非要用Tomcat 10就得全局替换javax.servlet为jakarta.servlet但由于Tomcat 10还有EL/JSP等配套差异课程设计不推荐。5.4 坑4Eclipse里能跑手动拷贝WebContent却404现象在Eclipse里点Run on Server没问题但把WebContent整个目录复制到Tomcat的webapps/shop010启动后访问shop.html正常/addCart和/showCart全部404。原因手动复制时只复制了WebContentbuild/classes里的Servlet class没有被放进去。Tomcat在WEB-INF/classes里找不到对应的.class自然无法完成Servlet映射。解决不要手动拷贝用WAR部署。在Eclipse里右键项目Export WAR file把生成的shop010.war放到Tomcat的webapps目录Tomcat启动后自动解压。用命令行打包也可以jar -cf shop010.war -C WebContent .前提是WebContent/WEB-INF/classes下已经有编译好的class。如果没有先确认build/classes里的对应类存在再手动复制过去。5.5 坑5重定向和转发的选择导致刷新时重复添加现象添加商品后地址栏显示/addCart按F5刷新购物车数量又加了一次。原因AddCartServlet里用了request.getRequestDispatcher(showCart).forward(request, response)。Forward是服务器内部跳转浏览器地址栏不会变刷新时浏览器重新发送/addCart请求于是重复执行添加逻辑。解决写操作后跳转一律用response.sendRedirect(showCart)。重定向返回302浏览器地址栏变成/showCart刷新只刷新显示页不会重复添加。这是一种被称为“PRG模式”Post/Redirect/Get的通用做法只不过这里不是Post而是Get。记住“写操作重定向读操作转发”这个口诀能避开很多重复提交问题。6. 再进一步用Cookie把购物车数量恢复到上次会话这个项目的购物车放在Session里Session一过期数据就没了。想让购物车更“像样”可以改成用户关闭浏览器再打开购物车里的商品还在。做法是把购物车数据序列化进Cookie再次访问时由Servlet读Cookie恢复。先把购物车Map拼成goodsId:count,goodsId:count的字符串并反序列化回来private static final String CART_COOKIE cart_cookie; public static String cartToString(MapString, Integer cart) { StringBuilder sb new StringBuilder(); for (Map.EntryString, Integer e : cart.entrySet()) { if (sb.length() 0) sb.append(,); sb.append(e.getKey()).append(:).append(e.getValue()); } return sb.toString(); } public static MapString, Integer stringToCart(String value) { MapString, Integer cart new LinkedHashMapString, Integer(); if (value null || value.isEmpty()) return cart; for (String item : value.split(,)) { String[] pair item.split(:); cart.put(pair[0], Integer.parseInt(pair[1])); } return cart; }AddCartServlet写完Session后同步写CookieCookie cookie new Cookie(CART_COOKIE, cartToString(cart)); cookie.setMaxAge(60 * 60 * 24 * 7); response.addCookie(cookie);ShowCartServlet读Session如果为空就尝试从Cookie恢复HttpSession session request.getSession(); MapString, Integer cart (MapString, Integer) session.getAttribute(cart); if (cart null request.getCookies() ! null) { for (Cookie c : request.getCookies()) { if (CART_COOKIE.equals(c.getName())) { cart stringToCart(c.getValue()); session.setAttribute(cart, cart); break; } } }Cookie方案有自己的边界Cookie总大小约4KB只能存短ID和数量存不了商品名和价格用户禁用Cookie就直接失效。所以Session仍然是主路径Cookie只适合做“上次购物车找回”。注意修改购物车后必须同步更新Cookie否则删了商品Cookie里还是旧数据下次又会“复活”。我当时就吃过这个亏调试半天以为Cookie没写入最后发现是删了Session的Map却没重写Cookie。从那以后我每次双写Session和Cookie都会强制走一遍“修改→刷新→关浏览器→重开”四步验证。你下载这份shop010练手时建议先按第2章导入把链路跑通再试这个增强希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询