资讯详情

资讯详情

JavaWeb购物车源码解析:Servlet+JSP+Session完整实现

简介这套JavaWeb简易购物车源代码适合Java Web初学者、课设学生及初级Web开发者用于理解在线购物车系统中Servlet、JSP、Session管理、JDBC数据库交互、MVC分层及JSTL/EL页面表达式等核心知识。压缩包共54个文件包含14个Java源码与6个JSP页面另有XML配置、properties配置、JAR依赖及说明文档按src、WebRoot、开发文档等目录组织完整覆盖从后台逻辑、页面展示到项目部署的常见工程结构压缩后仅1.58MB便于快速下载与本地运行。资源附带笔记资料和项目安装说明数据库设计、关键代码注释、SQL操作、Session状态管理及Tomcat部署步骤均有梳理同时涵盖异常处理与防SQL注入等安全性细节可辅助逐段吃透购物车添加、删除、更新数量和结算等典型功能。已有4124人学习下载配合清晰的目录划分适合课设参考、入门实战或作为Web开发面试前的复盘材料。 做JavaWeb的练手项目购物车几乎是绕不开的一站。我带过不少学JavaWeb的初学者发现一个普遍规律跟着教程敲的时候一切顺利一旦让自己从零写一个购物车大部分人都会卡住。卡住的原因通常不是不会写代码而是不知道购物车的数据在服务端到底用什么结构存、怎么跟着用户的请求流转。这份javaWeb简易购物车源代码就是用最传统的Servlet JSP JavaBean HttpSession组合把一整条购物车链路完整跑通的参考实现。它麻雀虽小五脏俱全。商品列表、加入购物车、修改数量、删除单项、清空购物车、总价计算这些核心功能都有而且没有引入任何框架和复杂前端依赖拿到就能跑跑起来就能看懂。适合正在学JavaWeb、需要一份项目源码参考的初学者也适合准备做课程设计但思路还没理清的同学。下面我从设计思路开始把这份源码的完整逻辑拆开讲。1. 从业务需求倒推设计购物车的容器为什么长这样1.1 先把用户操作翻译成功能清单动手写代码之前我喜欢先把用户在页面上能做的事列出来。购物车的核心功能不会超过五个从商品列表把某个商品加入购物车打开购物车页面看到已选商品和数量修改某个商品的数量删除单个商品或清空购物车看到当前购物车总价。这五件事对应到后端就是五个不同的请求处理动作。写代码时所有方法名、参数、跳转逻辑都围绕这五件事展开就不会出现写到一半不知道下一步该干什么的情况。1.2 技术选型为什么是Servlet JSP这套老组合现在Spring Boot已经是主流但学购物车这个阶段我反而强烈建议先用Servlet JSP把整条链路打通。原因是Spring Boot的自动配置和内置服务器把太多细节藏了起来初学者很难直观感受到一个请求从浏览器发出之后Tomcat怎么找到对应的ServletServlet处理完怎么把数据交给JSP渲染的全过程。Servlet JSP虽然写起来繁琐但每一个环节都是显式的你把这条路走通一次之后再学Spring MVC的时候会发现很多东西只是在做同一条路上的进出口分工。1.3 核心数据结构Map的key和value为什么不能随意选购物车在服务端的本质就是一个记录用户选了什么、选了多少的容器。我会用HashMapString, CartItem来设计key是商品idvalue是一个购物项对象购物项里包含一个Product对象和一个数量count。为什么不直接用MapString, Integer来存id和数量因为购物车页面要展示商品名称、单价、小计如果只存id每次渲染页面都得反复查数据库效率低而且逻辑特别散。为什么不直接MapString, Product因为同一个商品可能是多个数量Product里没有数量字段硬塞进去会破坏商品对象的职责。所以拆出一个CartItem类把商品数量小计计算封装在一起这就是JavaBean思想最朴素的体现。购物车为什么要放在HttpSession里而不是Cookie里Cookie存放在客户端有4KB的容量限制而且用户可以直接修改Cookie内容如果把价格数据放在里面非常容易被篡改。Session是服务端为每个用户单独开辟的存储区域通过JSESSIONID这个Cookie识别归属你改不到服务端Session里的内容。对登录前也能加购的简易购物车来说Session就是最合适的位置。2. 工程骨架与运行环境项目建不起来后面全白搭很多从网上扒源码的人卡住的第一步不是代码而是项目本身建不起来。2.1 版本组合先把这套组合锁死这份购物车源码基于JavaWeb传统链路编写版本建议直接用下面这套不要在环境上浪费太多时间JDK 8或11Tomcat 8.5或9.0Maven 3.6IDEA 2023。特别注意Tomcat版本Tomcat 10开始把javax.servlet包迁移到了jakarta.servlet网上大量老教程和老代码用的是javax直接扔进Tomcat 10会报ClassNotFoundException这是初学者最容易踩的暗坑。2.2 IDEA 2023建JavaWeb项目的步骤和坑IDEA 2023和旧版最大的不同是新建项目时不再直接提供JavaWeb模板需要自己手动补Web支持。完整步骤先新建一个普通的Maven项目在pom.xml里声明打包方式为war并加入servlet-api依赖scope设为provided然后右键模块选择Add Framework Support勾选Web ApplicationIDEA会自动生成webapp目录和web.xml最后配置Tomcat运行时。更省事的方式是在pom.xml里引入tomcat7-maven-plugin插件用一条mvn tomcat7:run命令启动社区版IDEA没有应用服务器按钮走这条路能绕开限制。这里还有一个小细节如果用WebServlet注解注册Servletweb.xml里必须保留默认的metadata-completefalse配置否则容器不会扫描注解访问任意Servlet都会404。这个坑曾让不少同学排查了一整个晚上。2.3 目录结构规划按包划分entity放Product、CartItem、Cart三个核心类dao放ProductDaoweb放CartServlet、ProductListServletwebapp目录下放JSP页面。这样一个简单的分层已经把实体、数据访问、请求控制、视图四条职责分开了。这份源码的包结构不算庞大但这套分层的习惯会直接影响你后面学框架时的理解速度。3. 核心代码逐段拆解四类Java对象各司其职这一节把源码里最核心的部分拿出来逐一过一遍。3.1 商品实体Product与ProductDao商品数据从哪来Product类就是一张商品表的映射id、name、price、description加上构造方法、getter、setter和toString没有任何业务逻辑。为了聚焦购物车本身ProductDao先用一个静态List模拟数据库里面放四本书public class ProductDao { private static final ListProduct PRODUCT_LIST new ArrayList(); static { PRODUCT_LIST.add(new Product(1, Java编程思想, 89.0, Java入门经典)); PRODUCT_LIST.add(new Product(2, Spring实战, 79.0, Spring框架实践)); PRODUCT_LIST.add(new Product(3, MySQL必知必会, 39.0, 数据库入门)); PRODUCT_LIST.add(new Product(4, 算法导论, 128.0, 程序员必读)); } public ListProduct findAll() { return PRODUCT_LIST; } public Product findById(Integer id) { for (Product product : PRODUCT_LIST) { if (product.getId().equals(id)) { return product; } } return null; } }用模拟数据的好处是跑购物车逻辑时不会被数据库环境卡住。真实项目中findById里替换成JDBC或MyBatis查MySQL即可外部接口和调用逻辑完全不用动。3.2 购物项CartItem与购物车Cart购物车的骨架CartItem是两个核心字段的组合小计金额由一个方法算出来public class CartItem { private Product product; private Integer count; public CartItem() {} public Double getSubtotal() { return product.getPrice() * count; } // getter / setter 略 }Cart是购物车的大脑内部维护一个Map和总价public class Cart { private MapString, CartItem map new HashMap(); private Double totalPrice 0.0; public void addProduct(Product product, Integer count) { String key String.valueOf(product.getId()); CartItem item map.get(key); if (item null) { item new CartItem(); item.setProduct(product); item.setCount(count); map.put(key, item); } else { item.setCount(item.getCount() count); } recalculate(); } public void updateCount(String productId, Integer count) { CartItem item map.get(productId); if (item ! null) { item.setCount(count); recalculate(); } } public void deleteProduct(String productId) { map.remove(productId); recalculate(); } public void clear() { map.clear(); totalPrice 0.0; } private void recalculate() { totalPrice 0.0; for (CartItem item : map.values()) { totalPrice item.getSubtotal(); } } // map、totalPrice 的 getter 略 }这里有一个容易被忽略的细节map的key统一使用字符串形式的商品id。这样add、delete、updateCount的入参就都统一成String避免调用时Integer和String混用导致map取不到值。recalculate每次增删改后重新算总价虽然简单但把总价唯一来源收拢到一个方法里后续要加优惠、加运费都只改这一处。3.3 CartServlet唯一的请求调度入口购物车的所有请求统一走/cart这一个Servlet用action参数区分动作WebServlet(/cart) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { execute(request, response); } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); execute(request, response); } private void execute(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } String action request.getParameter(action); if (add.equals(action)) { Integer productId Integer.valueOf(request.getParameter(productId)); Integer count Integer.valueOf(request.getParameter(count)); Product product new ProductDao().findById(productId); cart.addProduct(product, count); } else if (updateCount.equals(action)) { String productId request.getParameter(productId); Integer count Integer.valueOf(request.getParameter(count)); cart.updateCount(productId, count); } else if (delete.equals(action)) { cart.deleteProduct(request.getParameter(productId)); } else if (clear.equals(action)) { cart.clear(); } response.sendRedirect(request.getContextPath() /cart.jsp); } }把doGet和doPost都指向同一个execute方法是为了避免链接能点表单一提交就405这种新手常见问题。从Session取购物车时还要注意第一次访问时Session里根本没有cart必须先new一个放进去。这个判空逻辑是购物车能正常跑起来的前提。3.4 JSP渲染把购物车变成看得见的页面list.jsp负责商品列表加购入口是一个普通的GET链接c:forEach items${products} varp tr td${p.name}/td td${p.price} 元/td tda hrefcart?actionaddproductId${p.id}count1加入购物车/a/td /tr /c:forEachcart.jsp里遍历购物车Map用entry取出key和valuec:forEach items${cart.map} varentry tr td${entry.value.product.name}/td td${entry.value.product.price} 元/td td form actioncart methodpost input typehidden nameaction valueupdateCount input typehidden nameproductId value${entry.key} input typetext namecount value${entry.value.count} size3 button typesubmit修改/button /form /td td${entry.value.subtotal} 元/td tda hrefcart?actiondeleteproductId${entry.key}删除/a/td /tr /c:forEach空购物车的判断也要单独写否则页面会显示一个只有表头的空表格体验很差。注意修改数量用了POST表单其他几个动作是GET链接这在请求语义上有区分POST用于修改数据避免浏览器地址栏或收藏夹误触发操作。4. 五大业务动作背后的请求流转与页面跳转4.1 动作清单把五个动作整理成一张表方便对照理解业务动作请求地址关键参数请求方式操作后去向加入购物车/cartactionadd、productId、countGET重定向cart.jsp查看购物车/cart.jsp无GET直接渲染页面修改数量/cartactionupdateCount、productId、countPOST重定向cart.jsp删除单项/cartactiondelete、productIdGET重定向cart.jsp清空购物车/cartactionclear无重定向cart.jsp4.2 为什么每次操作完都要重定向而不是转发这是购物车项目里最值得理解的一个细节。如果用request.getRequestDispatcher(cart.jsp).forward()来跳转URL会停留在/cart用户按F5刷新时浏览器会把刚才的请求原样再发一次加购就变成加两次数量直接翻倍。用response.sendRedirect()发出重定向浏览器会重新发起一个GET请求访问cart.jspURL变成购物车页面地址刷新只刷新购物车页面不会重复提交之前的动作。这就是经典的PRG模式Post-Redirect-Get购物车这种高频操作场景里必须用。4.3 商品列表页的数据从哪来list.jsp里的products由ProductListServlet提供这个Servlet很简单查全量商品放进request域转发到list.jsp。这里用转发而不是重定向没有问题因为页面本身没有提交后刷新的场景而且用request域传数据比重定向后用Session传更干净。新手经常分不清什么时候用forward什么时候用sendRedirect记住一句话就够了接下来要展示的数据是一次性的就用forward操作产生了副作用而且不希望被重复提交就用sendRedirect。5. 这份源码最常见的五类翻车现场5.1 注解不生效访问Servlet直接404现象是项目能启动、JSP能打开但所有/cart开头的请求都404。常见原因有两个一是web.xml里metadata-complete属性被配置成了true导致容器不再扫描WebServlet注解二是Maven的war插件版本和web.xml版本不匹配。解决方法是把metadata-complete显式设为false或者干脆删掉web-app标签里的这个属性让Servlet按注解路由。5.2 中文乱码三处设置不到位商品名称在页面上显示成问号几乎所有初学者都会遇到。乱码的源头有三个地方JSP页面本身要声明contentTypetext/html;charsetUTF-8POST请求的Servlet里要写request.setCharacterEncoding(UTF-8)Tomcat的server.xml里Connector标签建议加上URIEncodingUTF-8处理GET参数。三处一起配基本能根除中文问题。很多教程只教setCharacterEncoding遇到GET链接里的中文参数一样乱原因就是漏了最后那一处。5.3 加购之后按F5刷新数量翻倍这个坑本质上是4.2节说的问题。如果用了forward跳转购物车页面URL停留在带action参数的地址刷新即重新提交。最直观的排查方式就是看浏览器地址栏地址栏是/cart而不是/cart.jsp时就要警惕重复提交了。改写成sendRedirect之后地址栏变成/cart.jsp问题自然消失。5.4 修改数量点提交报405 Method Not Allowed表单method写的是post但Servlet只重写了doGetTomcat找不到doPost方法就返回405。解决方案就是让doGet和doPost都指向同一个业务方法这也是我在3.3节把doGet和doPost统一收敛到execute方法的原因一劳永逸后面再加新动作都不用再操心请求方式。5.5 商品明明在购物车里删除时却删不掉出现这个bug十有八九是Map的key类型不一致。add时用product.getId()这个Integer直接做keydelete时却传入String类型的productIdInteger和String的hashCode不同map.get返回nullremove自然也没效果。整份源码里我统一用String.valueOf(product.getId())作为key就是从源头消灭这种类型混乱。6. 从Demo到能上线还差这四步6.1 登录态与购物车归属Session购物车的最大问题是关浏览器就没了而且没登录也能加购。真实系统里通常的做法是用户未登录时先用临时购物车存在Session登录后把Session里的购物车合并进数据库里的用户购物车下次登录再加载回来。这看起来复杂核心其实就是多一张user_id和购物车项的关联表。6.2 把购物车从Session搬进MySQL持久化购物车表设计可以很简单CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, count INT NOT NULL DEFAULT 1 );每次增删改都同步写这张表登录时查询该用户的所有购物车项拼装回Cart对象。注意价格不要存到这张表里价格以商品表为准购物车只存用户、商品、数量三类信息否则商品调价后购物车里的旧价格会失真结算时容易出大问题。6.3 用Filter统一解决编码与登录拦截Demo阶段的编码问题要靠每个Servlet自己写setCharacterEncoding项目一多就很容易漏。更优雅的做法是写一个EncodingFilter在doFilter里统一设置请求和响应编码登录校验也可以用Filter实现判断Session里有没有user对象没有就重定向到login.jsp。这两个Filter一写完Servlet里的重复代码能砍掉一大截。6.4 前端与性能框架和缓存的引入把JSP里的内容用Bootstrap或Vue重写一遍交互体验会从能用变成好用加购、删改都可以改成AJAX局部刷新不需要整页跳来跳去。数据访问层可以引入MyBatis高频读的商品信息放进Redis缓存。这些属于进阶玩法但每一条都是从这份简易代码往生产级项目演进的必经方向。最后分享一点个人体会。这份简易购物车代码我带学生反复用了很多年每次迭代都能看到有人在同一批坑里摔倒类型不一致、乱码、重复提交、405。后来我要求他们都写代码前先回答自己三个问题购物车数据放在哪里一次请求进来由谁处理处理完页面应该刷新成什么三个问题答清楚写出来的代码基本不会跑偏。如果你正在学JavaWeb建议别只抄代码把CartServlet那张action分发表背下来再自己改一版用数据库存商品的版本跑通之后你对JavaWeb的整个请求链路会有完全不一样的感觉。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →