本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接扔进Tomcat就能跑的轻量级购物车Demo,纯Servlet开发,不依赖Spring等框架。包含商品展示页shop.html,点击‘添加到购物车’触发AddToCartServlet,把商品ID、名称、单价、数量等存入HttpSession;再由ShowCartServlet从session读取数据,用HTML表格清晰列出当前购物车内容。项目结构完整:标准WEB-INF目录、web.xml配置(含Servlet映射)、编译后的class文件放在正确路径、图片资源统一放在img子目录。支持Eclipse或IDEA快速导入,无需额外配置即可运行。覆盖JavaWeb核心实践点:HTTP GET/POST请求处理、request与response基础操作、session会话生命周期管理、Servlet间跳转(重定向/请求转发)、静态资源引用、简单前后端数据传递。适合刚学完Servlet基础、想动手验证会话机制和页面联动的新手,也能作为课堂演示或课设参考模板。

1. 项目概述:为什么这个购物车Demo值得你花30分钟亲手跑一遍

我带过十几届JavaWeb入门班,也给不少刚转行的朋友做过技术辅导。每次讲完HttpSession的生命周期、setAttributegetAttribute的用法,总有人问:“老师,session到底存在哪?我点了两次‘添加’,它真能记住吗?跳到另一个Servlet里还能取出来?”——这种问题,文档解释一百遍,不如你亲手在Tomcat里点开shop.html,连点三次“添加到购物车”,再刷新cart.html看表格里多出三行数据来得实在。这个项目就是为解决这个“眼见为实”的卡点而生的:它不炫技、不堆砌,就用最原始的javax.servlet.* API,把HTTP请求怎么来、session怎么存、页面怎么跳、数据怎么传这些看似抽象的概念,全摊开在你眼皮底下运行。

它不是玩具,而是真实Web开发逻辑的微缩模型。你看到的AddToCartServlet里那几行session.setAttribute("cartItems", list),对应的是电商系统里用户点击“加入购物车”按钮后服务端真正的第一道处理逻辑;ShowCartServlet中遍历session.getAttribute("cartItems")并拼接HTML表格的过程,就是模板引擎(比如JSP或Thymeleaf)底层真正干的事——只不过这里我们手动写了out.println("<tr>...")。整个项目没有一行Spring注解、没有Maven依赖管理、没有XML配置以外的任何元数据,所有路径、映射、资源引用都严格遵循Servlet规范定义的目录结构。这意味着:你把它整个文件夹拖进Tomcat的webapps目录,启动服务器,访问http://localhost:8080/your-app/shop.html,就能立刻看到效果。不需要mvn clean package,不需要application.properties,甚至不需要知道什么是IOC容器。它只做一件事:让你看清JavaWeb最底层的“毛细血管”是怎么搏动的。

关键词里的“Servlet购物车”不是噱头,它代表一种刻意回归本质的学习路径;“HttpSession实现”是核心机制,不是调用API那么简单,而是要理解session ID如何通过Cookie传递、服务器如何用内存Map维护会话状态、超时时间怎么生效;“Tomcat部署”强调零配置落地能力——你不需要改任何Tomcat默认配置,只要它能跑,这个项目就能跑;“JavaWeb实战”则意味着每一个.html文件的路径、每一个<a href>里的URL、每一个web.xml里的<servlet-mapping>,都经得起生产环境级的推敲。如果你刚学完HttpServletRequestHttpServletResponse的常用方法,但还没写过一个完整流程的前后端联动;如果你在IDE里调试过doGet()却没在浏览器地址栏里亲手输入过/addToCart?id=101&name=MacBook&price=12999&qty=1;如果你对“重定向302”和“请求转发”还停留在概念层面——那么这个项目就是为你量身定做的第一块实战垫脚石。它不教你如何高并发,但教会你第一个请求进来时,服务器到底做了什么。

2. 整体架构与设计思路:为什么不用框架?为什么坚持手工配置?

2.1 拒绝框架封装,直击Servlet容器本质

很多人一上来就想用Spring Boot写购物车,觉得“自动配置Tomcat、内嵌服务器、起步依赖”省事。但这就像是学开车先坐上自动驾驶汽车——你确实能从A点到B点,但永远不知道油门怎么踩、离合怎么松、档位怎么挂。这个项目坚持使用原生Servlet,根本原因在于:只有剥离所有中间层,你才能看清HTTP协议与Java代码之间那层薄薄的契约是如何被履行的。比如,当你在shop.html里写<a href="addToCart?id=101">添加</a>,浏览器发出GET请求,Tomcat收到后,必须根据web.xml里的<url-pattern>/addToCart</url-pattern>找到对应的AddToCartServlet类,然后反射创建实例、调用doGet()方法。这个过程里,request.getParameter("id")拿到的字符串,就是HTTP请求行里?id=101这部分原始数据的直接映射;response.sendRedirect("showCart")触发的302响应头,就是服务器告诉浏览器“你去访问另一个URL”的明确指令。Spring MVC把这些都封装成@RequestParamreturn "redirect:/cart",很优雅,但初学者容易误以为“参数是自动变出来的”“重定向是魔法”。而在这里,每一行代码都在告诉你:数据从哪来,到哪去,谁在中间搬运。

再比如HttpSession。框架里你可能只写session.setAttribute("user", user),但背后是Tomcat在内存里维护了一个ConcurrentHashMap<String, Session>,key是JSESSIONID Cookie值,value是一个包含属性Map、创建时间、最后访问时间、最大空闲秒数的StandardSession对象。当AddToCartServlet调用session.setAttribute("cartItems", items)时,实际是把这个ArrayList存进了这个StandardSession的内部属性Map里;而ShowCartServlet调用session.getAttribute("cartItems"),就是从同一个StandardSession实例里取出那个ArrayList。没有框架,你就必须亲手处理null检查(比如第一次访问购物车时cartItems为空)、类型转换(getAttribute()返回Object,必须强制转为List<CartItem>)、线程安全(多个请求同时操作同一个session,需考虑同步)。这些“麻烦”,恰恰是理解Web应用状态管理不可绕过的门槛。

2.2 目录结构即规范:为什么WEB-INF必须在根下?为什么class要放/WEB-INF/classes?

项目目录树里出现的WebContentWEB-INFimgsrc,不是随意命名,而是Servlet规范(JSR 340)白纸黑字规定的标准布局。WebContent是Eclipse等IDE默认的Web根目录(对应Tomcat的webapps/your-app/),所有能被浏览器直接访问的静态资源——HTML、CSS、JS、图片——都必须放在这里。而WEB-INF是特殊目录,它的核心规则只有一条:任何位于WEB-INF及其子目录下的资源,都不能被客户端通过URL直接访问。这是安全底线。试想,如果web.xml能被用户随便下载,他就能看到你的Servlet类名、初始化参数;如果classes目录暴露,攻击者就能下载你的.class文件反编译看业务逻辑。所以,web.xml必须放在WEB-INF/下,编译后的.class文件必须放在WEB-INF/classes/下(或打包成.jarWEB-INF/lib/),这是Tomcat类加载器(WebappClassLoader)查找类的固定路径。你把AddToCartServlet.class放进WEB-INF/classes/com/example/cart/,Tomcat启动时就会扫描这个路径,根据web.xml里的<servlet-class>声明,用这个类加载器加载它。这个过程,和你写javac HelloWorld.java && java HelloWorld本质上是一样的,只是容器帮你完成了类路径管理和生命周期托管。

img目录放在WebContent/img/,是因为它属于静态资源,需要被HTML直接引用。比如shop.html里有<img src="img/macbook.jpg" alt="MacBook">,浏览器解析这个src时,会向服务器发起一个GET请求:GET /your-app/img/macbook.jpg。Tomcat收到后,发现img/不在WEB-INF里,就直接从文件系统读取WebContent/img/macbook.jpg返回二进制流。这个路径匹配逻辑,完全由Tomcat的DefaultServlet(负责处理静态资源的默认Servlet)完成。如果你把图片错放到WEB-INF/img/,浏览器请求会得到404,因为DefaultServlet被禁止访问WEB-INF下的任何内容。这种“路径即权限”的设计,是Web容器最基础的安全栅栏,而这个项目的所有目录安排,都是在手把手教你画这道栅栏。

2.3 web.xml配置:为什么现在还要手写XML?它到底管什么?

尽管Servlet 3.0+支持注解配置(@WebServlet),但这个项目坚持使用web.xml,原因很实在:它是整个Web应用的“宪法”,清晰定义了容器与开发者之间的契约边界。打开WEB-INF/web.xml,你会看到三个核心部分:

首先是<servlet>声明:

<servlet>
    <servlet-name>AddToCartServlet</servlet-name>
    <servlet-class>com.example.cart.AddToCartServlet</servlet-class>
</servlet>

这里告诉Tomcat:“当有请求匹配某个URL模式时,请用这个全限定类名去创建Servlet实例”。注意,<servlet-class>必须是编译后的.class文件的完整包路径,不能写src/com/example/cart/AddToCartServlet.java。Tomcat的类加载器会去WEB-INF/classes/下找com/example/cart/AddToCartServlet.class,找不到就抛ClassNotFoundException

其次是<servlet-mapping>映射:

<servlet-mapping>
    <servlet-name>AddToCartServlet</servlet-name>
    <url-pattern>/addToCart</url-pattern>
</servlet-mapping>

这是关键的路由规则。/addToCart是一个相对路径,相对于你的应用上下文路径(Context Path)。假设你把项目文件夹命名为shopping-cart,放进Tomcat的webapps/,那么应用路径就是/shopping-cart,完整的可访问URL就是http://localhost:8080/shopping-cart/addToCart。这个映射是大小写敏感的,/addtocart/addToCart是两个不同的模式。Tomcat在收到请求时,会遍历所有<url-pattern>,用最长路径匹配原则(Longest Path Match)选择最具体的Servlet。比如你同时配置了/addToCart/addToCart/*,那么/addToCart?id=101会匹配前者,而/addToCart/item/101会匹配后者。

最后是<welcome-file-list>

<welcome-file-list>
    <welcome-file>shop.html</welcome-file>
</welcome-file-list>

这定义了当用户访问应用根路径(如http://localhost:8080/shopping-cart/)时,默认展示哪个文件。Tomcat会依次查找列表中的文件,找到第一个存在的就返回。没有这个配置,访问根路径会得到404。这个看似简单的配置,背后是容器对“首页”概念的标准化约定。

手写web.xml,就是强迫你直面这些底层约定。它不像注解那样“写在哪就在哪生效”,而是集中在一个文件里,让你一眼看清整个应用的入口点、路由关系和默认行为。对于学习者,这是建立系统性认知的最佳方式。

3. 核心细节解析与实操要点:从HTML到Servlet的每一步都经得起拷问

3.1 shop.html:静态页面里的动态灵魂——URL参数设计的艺术

shop.html表面看只是一个商品列表页,但它的每一个<a>标签,都是驱动整个购物车流程的开关。我们来看关键代码片段:

<div class="product">
    <img src="img/macbook.jpg" alt="MacBook Pro">
    <h3>MacBook Pro 16英寸</h3>
    <p class="price">¥12,999.00</p>
    <a href="addToCart?id=101&name=MacBook%20Pro&price=12999&qty=1" 
       class="btn-add">添加到购物车</a>
</div>

这里藏着三个必须掌握的细节:

第一,URL编码(URL Encoding)的必要性name=MacBook%20Pro中的%20是空格的URL编码。如果直接写name=MacBook Pro,浏览器在发送请求时,空格会被当作URL分隔符截断,导致request.getParameter("name")只能拿到"MacBook"。中文更甚,name=笔记本电脑必须编码为name=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91。虽然现代浏览器会自动编码,但作为开发者,你必须理解这个过程,并在Servlet端用URLDecoder.decode(request.getParameter("name"), "UTF-8")解码(本项目为简化,商品名用英文,规避此问题,但原理必须懂)。

第二,GET vs POST的选择逻辑。这里用<a href>发起GET请求,是因为“添加到购物车”在这个Demo里是幂等操作(多次点击同一商品,结果都是购物车里有一条该商品记录,不会重复增加)。但现实中,真正的电商系统会用POST,因为GET请求的参数会暴露在浏览器地址栏、服务器日志、代理缓存中,有安全风险;且URL长度有限制(通常2KB),无法携带大量商品信息。本项目用GET是为了教学直观——你能在地址栏直接看到?id=101&name=...,方便调试。但你要清楚,生产环境必须切换为表单POST,并在Servlet中用doPost()处理。

第三,参数设计的健壮性考量。当前传递了idnamepriceqty四个参数。id是唯一标识,用于后续去重(同ID商品多次添加应合并数量);nameprice是展示信息,避免从数据库反复查询;qty是用户意图,但实际业务中,前端不应信任用户传来的qty,而应由后端校验库存、设置默认值(如qty=1)。本项目为简化,直接使用,但你在扩展时,必须在AddToCartServlet里加入if (qty < 1) qty = 1;这样的防御性代码。

提示:在shop.html中,所有商品链接的href值必须与web.xml<url-pattern>完全一致(包括大小写和斜杠)。比如web.xml配的是/addToCart,链接就必须是href="addToCart?...",不能是href="/addToCart?..."(开头的/表示绝对路径,会跳到应用根,而非当前应用上下文)。

3.2 AddToCartServlet:会话管理的临界点——如何安全地增删改查购物车

AddToCartServlet是整个流程的“心脏起搏器”,它的核心任务只有一个:接收请求参数,更新HttpSession中的购物车数据。我们拆解其doGet()方法的关键逻辑:

protected void doGet(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
    // 1. 获取请求参数
    String id = request.getParameter("id");
    String name = request.getParameter("name");
    double price = Double.parseDouble(request.getParameter("price"));
    int qty = Integer.parseInt(request.getParameter("qty"));

    // 2. 从session获取购物车列表,若不存在则创建新列表
    HttpSession session = request.getSession(); // 关键!获取当前会话
    List<CartItem> cartItems = (List<CartItem>) session.getAttribute("cartItems");
    if (cartItems == null) {
        cartItems = new ArrayList<>();
        session.setAttribute("cartItems", cartItems); // 首次创建,存入session
    }

    // 3. 查找是否已有同ID商品,有则数量叠加,无则新增
    boolean found = false;
    for (CartItem item : cartItems) {
        if (item.getId().equals(id)) {
            item.setQty(item.getQty() + qty);
            found = true;
            break;
        }
    }
    if (!found) {
        cartItems.add(new CartItem(id, name, price, qty));
    }

    // 4. 重定向到显示页,避免F5刷新重复提交
    response.sendRedirect("showCart");
}

这段代码有五个必须深究的点:

第一,request.getSession()的隐含逻辑。这个方法默认等价于request.getSession(true),即“如果当前请求没有关联session,则创建一个新的”。它会检查请求头中是否有Cookie: JSESSIONID=xxx,如果有,就去Tomcat的session仓库里找这个ID对应的StandardSession对象;如果没有,就生成一个新ID(如ABC123DEF456),创建新StandardSession,并通过Set-Cookie: JSESSIONID=ABC123DEF456响应头发回浏览器。这个过程是透明的,但你必须知道:session的生命周期始于第一次调用getSession(),终于超时或手动invalidate()

第二,session.setAttribute()的线程安全陷阱cartItems是一个ArrayList,被多个请求(同一用户的不同浏览器标签页)共享。如果两个请求几乎同时执行cartItems.add(...),可能导致数据错乱。本项目未加锁,因为是单机演示,但生产环境必须用Collections.synchronizedList(new ArrayList<>())CopyOnWriteArrayList。更佳实践是:将购物车操作封装成原子方法,或使用session.setAttribute()时,确保每次getset的是一个全新对象(如session.setAttribute("cartItems", new ArrayList<>(cartItems))),但这会丢失引用,需权衡。

第三,“去重合并”的算法选择。当前用简单for循环遍历,时间复杂度O(n)。如果购物车商品很多(>100),可以改用HashMap<String, CartItem>以ID为key,将查找优化到O(1),但需额外维护一个Map。本项目用List,是为了让新手一眼看懂逻辑,且小数据量下性能差异可忽略。

第四,response.sendRedirect("showCart")的深层含义。这是重定向(Redirect),它会让浏览器收到302状态码和Location: showCart响应头,然后浏览器自动发起一个新的GET请求到/showCart。好处是:URL栏变成/showCart,用户刷新时不会重复执行添加逻辑(避免“确认提交表单”弹窗)。坏处是:重定向是两次HTTP往返(第一次请求/addToCart,第二次请求/showCart),比请求转发慢。而request.getRequestDispatcher("/showCart").forward(request, response)请求转发(Forward),服务器内部跳转,URL不变,只有一次往返。本项目选重定向,是为了教学清晰——你能在浏览器地址栏看到路径变化,直观理解“跳转”概念。

第五,异常处理的缺失与补救Double.parseDouble()Integer.parseInt()可能抛NumberFormatException,当前代码没捕获,会导致500错误。实际应包裹try-catch,返回友好提示或跳转到错误页。例如:

try {
    double price = Double.parseDouble(request.getParameter("price"));
} catch (NumberFormatException e) {
    request.setAttribute("error", "价格格式错误");
    request.getRequestDispatcher("error.html").forward(request, response);
    return;
}

3.3 ShowCartServlet:从会话到视图的终极渲染——HTML表格生成的硬核技巧

ShowCartServlet的任务是“读取”和“呈现”。它不修改数据,只从session里取出cartItems,然后用纯Java代码拼接HTML字符串返回给浏览器。这是JSP诞生前最原始的视图渲染方式,也是理解模板引擎原理的基石。

protected void doGet(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8"); // 必须设置,否则中文乱码
    PrintWriter out = response.getWriter();

    HttpSession session = request.getSession(false); // false表示不创建新session
    List<CartItem> cartItems = (List<CartItem>) session.getAttribute("cartItems");

    // 生成HTML页面骨架
    out.println("<!DOCTYPE html>");
    out.println("<html><head><title>我的购物车</title>");
    out.println("<style>table{border-collapse:collapse;width:100%;} th,td{border:1px solid #ccc;padding:8px;text-align:left;}</style>");
    out.println("</head><body>");
    out.println("<h1>🛒 我的购物车</h1>");

    // 判断购物车是否为空
    if (cartItems == null || cartItems.isEmpty()) {
        out.println("<p>购物车还是空的,快去<a href='shop.html'>挑选商品</a>吧!</p>");
    } else {
        // 生成商品表格
        out.println("<table>");
        out.println("<thead><tr><th>商品ID</th><th>名称</th><th>单价</th><th>数量</th><th>小计</th></tr></thead>");
        out.println("<tbody>");

        double total = 0.0;
        for (CartItem item : cartItems) {
            double subtotal = item.getPrice() * item.getQty();
            total += subtotal;
            out.printf("<tr><td>%s</td><td>%s</td><td>¥%.2f</td><td>%d</td><td>¥%.2f</td></tr>",
                    item.getId(), item.getName(), item.getPrice(), item.getQty(), subtotal);
        }

        out.println("</tbody>");
        out.println("<tfoot><tr><td colspan='4' style='text-align:right'><strong>总计:</strong></td><td><strong>¥%.2f</strong></td></tr></tfoot>", total);
        out.println("</table>");
    }

    out.println("<br><a href='shop.html' class='btn-back'>继续购物</a>");
    out.println("</body></html>");
}

这段代码的教学价值极高,体现在五个方面:

第一,response.setContentType("text/html;charset=UTF-8")的强制性。这是告诉浏览器:“接下来的内容是HTML,用UTF-8编码解析”。如果漏掉,浏览器可能用ISO-8859-1解析,导致中文显示为????charset=UTF-8必须紧跟在text/html后面,中间不能有空格。

第二,request.getSession(false)的精准控制false参数意味着“只获取已存在的session,不要创建新的”。如果用户直接访问/showCart而没有先访问/addToCart(即没有session),getSession(false)会返回null,从而getAttribute("cartItems")也返回null,程序自然进入“购物车为空”的分支。这比getSession(true)更安全,避免无意义地创建session。

第三,HTML字符串拼接的可维护性困境out.println("<table>...")这种方式,在真实项目中早已被JSP、Thymeleaf等模板引擎取代,因为混合Java逻辑和HTML标签极易出错(比如忘了闭合<tr>,或者printf格式串写错)。但正是这种“笨办法”,让你看清:所有模板引擎最终都编译成类似的PrintWriter输出逻辑。你写的<th th:text="${item.name}"></th>,底层就是out.print(item.getName())

第四,金额计算的精度陷阱double price用于计算subtotaltotal,但在金融场景中,double会因二进制浮点数精度丢失导致0.1 + 0.2 != 0.3。正确做法是用BigDecimal,但本项目为简化,用double并用%.2f格式化输出,视觉上正确。你需要知道这个隐患,并在真实电商系统中替换为BigDecimal price = new BigDecimal(request.getParameter("price"))

第五,空购物车的用户体验设计if (cartItems == null || cartItems.isEmpty())的双重检查是必要的。session.getAttribute()在key不存在时返回null,而不是空集合;即使你之前setAttribute("cartItems", new ArrayList<>())getAttribute()也可能返回null(比如session超时被Tomcat清理)。所以必须同时检查nullisEmpty()。给出的“继续购物”链接,是引导用户闭环操作的关键,避免用户卡在空白页。

4. 实操过程与核心环节实现:从零开始搭建、编译、部署的完整流水线

4.1 环境准备:Tomcat 9+与JDK 8+的黄金组合

这个项目对环境要求极低,但必须严格匹配版本,否则会出现UnsupportedClassVersionError(类版本不兼容)或ClassNotFoundException(Servlet API找不到)。以下是经过实测的最小可行配置:

  • JDK版本:必须是JDK 8u202或更高版本(推荐JDK 8u361)。为什么不是JDK 17?因为Tomcat 9默认使用Servlet 4.0规范,而HttpSession的核心API在Servlet 3.1(JDK 7+)就已稳定。JDK 8是企业级JavaWeb项目的事实标准,兼容性最好。安装后,验证java -version输出应为java version "1.8.0_XXX"

  • Tomcat版本:必须是Tomcat 9.0.x(推荐9.0.83)。Tomcat 10+迁移到jakarta.servlet.*命名空间(如jakarta.servlet.http.HttpServlet),而本项目代码用的是javax.servlet.*,直接运行会报NoClassDefFoundError。Tomcat 9.0.x完美支持Servlet 4.0,且对web.xml的DTD声明兼容性最佳。下载地址:https://tomcat.apache.org/download.cgi (选择tar.gzzip,非exe安装版,便于观察目录结构)。

  • IDE配置(可选但强烈推荐):Eclipse IDE for Enterprise Java and Web Developers(2023-09版)或IntelliJ IDEA Ultimate(2023.2版)。免费社区版IDEA也能用,但需手动配置Tomcat插件。关键配置项:

  • Project SDK:指向你的JDK 8安装目录。
  • Project language level:8 - Lambdas, type annotations etc.
  • Facets:添加Web facet,Version选3.1(对应Servlet 3.1,兼容web.xml DTD)。
  • Deployment Assembly:确保src目录输出到WEB-INF/classesWebContent目录映射到/(根路径)。

注意:不要用Windows自带的记事本编辑web.xml或Java文件!它默认保存为ANSI编码,会导致中文乱码。务必使用VS Code、Notepad++或IDE内置编辑器,并将文件编码统一设为UTF-8(无BOM)。

4.2 项目结构搭建:手把手还原标准Web应用骨架

假设你从零开始,没有现成的资源包,需要自己创建。以下是精确到每个文件夹、每个文件的创建步骤(以Windows为例,Linux/Mac路径分隔符改为/):

步骤1:创建项目根目录

mkdir shopping-cart
cd shopping-cart

步骤2:构建标准目录结构

# 创建Web根目录(对应Tomcat的webapps/shopping-cart/)
mkdir WebContent

# 在WebContent下创建标准子目录
mkdir WebContent/WEB-INF
mkdir WebContent/WEB-INF/classes
mkdir WebContent/WEB-INF/lib  # 本项目不用,但规范要求存在
mkdir WebContent/img

# 创建源码目录(IDE用,编译后class放WEB-INF/classes)
mkdir src
mkdir src/com
mkdir src/com/example
mkdir src/com/example/cart

步骤3:编写核心Java文件
- 创建src/com/example/cart/CartItem.java

package com.example.cart;

public class CartItem {
    private String id;
    private String name;
    private double price;
    private int qty;

    public CartItem(String id, String name, double price, int qty) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.qty = qty;
    }

    // getter/setter方法(此处省略,IDE可自动生成)
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public int getQty() { return qty; }
    public void setQty(int qty) { this.qty = qty; }
}
  • 创建src/com/example/cart/AddToCartServlet.java(内容见前文)。
  • 创建src/com/example/cart/ShowCartServlet.java(内容见前文)。

步骤4:编写web.xml
- 创建WebContent/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>Shopping Cart Demo</display-name>

    <!-- Servlet声明 -->
    <servlet>
        <servlet-name>AddToCartServlet</servlet-name>
        <servlet-class>com.example.cart.AddToCartServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>ShowCartServlet</servlet-name>
        <servlet-class>com.example.cart.ShowCartServlet</servlet-class>
    </servlet>

    <!-- URL映射 -->
    <servlet-mapping>
        <servlet-name>AddToCartServlet</servlet-name>
        <url-pattern>/addToCart</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>ShowCartServlet</servlet-name>
        <url-pattern>/showCart</url-pattern>
    </servlet-mapping>

    <!-- 欢迎文件 -->
    <welcome-file-list>
        <welcome-file>shop.html</welcome-file>
    </welcome-file-list>
</web-app>

注意:xsi:schemaLocation中的web-app_4_0.xsd必须与version="4.0"匹配,这是Tomcat 9的要求。

步骤5:编写shop.html和静态资源
- 创建WebContent/shop.html(内容见前文)。
- 将商品图片(如macbook.jpg)放入WebContent/img/

至此,一个符合Servlet规范的完整项目骨架就搭建完毕。目录结构如下:

shopping-cart/
├── src/
│   └── com/example/cart/
│       ├── CartItem.java
│       ├── AddToCartServlet.java
│       └── ShowCartServlet.java
└── WebContent/
    ├── shop.html
    ├── img/
    │   └── macbook.jpg
    └── WEB-INF/
        ├── web.xml
        └── classes/  # 编译后class将放这里

4.3 编译与部署:三步走,让项目在Tomcat上活起来

第一步:编译Java源码
打开命令行,进入项目根目录shopping-cart,执行:

# 设置classpath,包含Tomcat的servlet-api.jar(关键!)
# Windows:
set CLASSPATH=%CATALINA_HOME%\lib\servlet-api.jar;src

# Linux/Mac:
export CLASSPATH=$CATALINA_HOME/lib/servlet-api.jar:src

# 编译所有Java文件,输出到WEB-INF/classes
javac -d WebContent/WEB-INF/classes src/com/example/cart/*.java

-d参数指定输出目录,src/com/example/cart/*.java是源文件路径。编译成功后,WebContent/WEB-INF/classes/com/example/cart/下会出现CartItem.classAddToCartServlet.class等文件。如果报错package javax.servlet does not exist,说明CLASSPATH没设对,servlet-api.jar路径错误。

第二步:复制项目到Tomcat
- 将整个shopping-cart文件夹(不是里面的WebContent,而是shopping-cart这个文件夹本身)复制到Tomcat的webapps/目录下。
- 启动Tomcat:运行bin/startup.bat(Windows)或bin/startup.sh(Linux/Mac)。
- 观察logs/catalina.out,看到Deploying web application directory [...] shopping-cart即表示部署成功。

第三步:浏览器验证
- 打开浏览器,访问http://localhost:8080/shopping-cart/。由于web.xml配置了<welcome-file>shop.html</welcome-file>,Tomcat会自动加载WebContent/shop.html
- 点击任意“添加到购物车”链接,URL变为http://localhost:8080/shopping-cart/addToCart?id=101&...,稍等片刻(重定向),页面跳转到http://localhost:8080/shopping-cart/showCart,显示购物车表格。
- 刷新showCart页面,数据依然存在(session未超时)。
- 关闭浏览器,重新打开,访问/showCart,会显示“购物车为空”(因为新会话,没有cartItems)。

实操心得:我第一次部署失败,是因为把shopping-cart文件夹放错了位置——放到了webapps/ROOT/下面,导致路径变成/shopping-cart嵌套在/里。正确做法是直接放webapps/平级。另外,Tomcat启动后,webapps/shopping-cart/目录下会自动生成META-INF/WEB-INF/classes/(如果没手动编译),但里面的class是空的,必须手动编译并复制。别指望Tomcat自动编译Java源码,它只负责加载已编译的.class

4.4 IDE导入指南:Eclipse与IDEA的零配置接入

Eclipse导入步骤:
1. File -> Import -> Existing Projects into Workspace
2. Browse选择shopping-cart文件夹(即包含srcWebContent的父目录)。
3. 勾选项目,点击Finish
4. 右键项目 -> Properties -> Project Facets,勾选Dynamic Web Module,Version选3.1
5. Deployment Assembly中,确保src映射到/WEB-INF/classesWebContent映射到/
6. 右键项目 -> Run As -> Run on Server,选择已配置的Tomcat 9服务器。

IntelliJ IDEA导入步骤:
1. File -> Open,选择shopping-cart文件夹。
2. IDEA会自动识别为Maven项目(即使没有pom.xml),点击OK
3. File -> Project Structure -> Project,设置Project SDK为JDK 8,Project language level为8。
4. Modules -> shopping-cart -> Sources,将src标记为SourcesWebContent标记为Resources
5. Facets -> Web,点击+添加Web Facet,Web resource directoryWebContentWeb.xmlWebContent/WEB-INF/web.xml
6. Artifacts -> + -> Web Application: Exploded -> From modules...,选择shopping-cart,点击OK
7. Run -> Edit Configurations -> + -> Tomcat Server -> LocalDeployment -> + -> Artifact,选择刚创建的shopping-cart:war exploded
8. 点击Run按钮,IDEA会自动启动Tomcat并部署。

注意:IDEA中,WebContent目录在项目视图里可能显示为web,这是IDEA的默认命名,不影响功能,只要web.xml路径正确即可。如果遇到Cannot resolve symbol 'javax.servlet',右键项目 -> Open Module Settings -> Libraries -> + -> Java,添加$CATALINA_HOME/lib/servlet-api.jar

5. 常见问题与排查技巧实录:那些让你抓耳挠腮的500错误,其实都有迹可循

5.1 HTTP 404错误:页面找不着?先查这三个地方

404是最常见的错误,意味着Tomcat找不到你请求的资源。它可能发生在shop.html/addToCart/showCart任何一个环节。按优先级排查:

第一,检查URL路径是否与web.xml映射完全一致。这是90%的404根源。打开浏览器开发者工具(F12),切换到Network标签,点击“添加到购物车”,观察发出的请求URL。如果显示http://localhost:8080/shopping-cart/addToCart?id=101,但web.xml里配的是<url-pattern>/addtocart</url-pattern>(小写),就会404。解决方案:严格对照web.xml,确保<a href>里的路径、<url-pattern>里的路径、浏览器地址栏里的路径,三者完全一致(包括大小写、斜杠、有无前缀)。

第二,确认项目是否真的部署到Tomcat。访问http://localhost:8080/manager/html(Tomcat Manager App),输入用户名密码(需在conf/tomcat-users.xml中配置),查看shopping-cart是否在应用列表中,且状态为running。如果不在列表中,说明部署失败;如果状态是stopped,点击Start。部署失败的常见原因:web.xml语法错误(如标签没闭合)、WEB-INF/classes/下缺少.class文件、servlet-class路径写错(如com.example.CartItem少了个cart)。

第三,静态资源路径错误。如果shop.html能打开,但图片显示为红叉,检查<img src="img/macbook.jpg">。此时浏览器请求的是http://localhost:8080/shopping-cart/img/macbook.jpg。如果图片实际放在WebContent/images/macbook.jpg,就会404。解决方案:确保img目录在WebContent/下,且HTML中src路径与文件系统路径完全匹配。用开发者工具的Network标签,看图片请求的URL和响应状态码,一目了然。

问题现象 可能原因 快速验证方法 解决方案
访问/shopping-cart/显示404 项目未部署或部署失败 查看webapps/目录下是否有shopping-cart文件夹;检查logs/catalina.out是否有部署日志 重启Tomcat;确认web.xml无语法错误;手动复制项目到webapps/
点击“添加”后跳转到404 <url-pattern>href不匹配 浏览器地址栏看请求URL;对比web.xml 修改web.xml或HTML中的路径,确保一致
图片不显示(红叉) src路径错误或图片文件不存在 Network标签看图片请求URL和状态码;文件管理器确认图片位置 调整src路径,确保指向WebContent/img/下的真实文件

5.2 HTTP 500错误:服务器炸了?聚焦Java代码的致命伤

500错误意味着Servlet执行过程中抛出了未捕获的异常。Tomcat会在logs/catalina.out中打印详细堆栈,这是唯一的真相来源。

最常见的500原因:NullPointerException(NPE)。典型场景:
- AddToCartServlet中,request.getParameter("id")返回null(用户手动修改URL,删掉了?id=101),接着id.equals("101")就NPE。
- ShowCartServlet中,session.getAttribute("cartItems")返回null,接着cartItems.isEmpty()就NPE。

排查步骤:
1. 立即打开logs/catalina.out,搜索SEVEREException,找到最近的堆栈。
2. 定位到出错的Java文件和行号,如AddToCartServlet.java:25
3. 分析该行代码:String id = request.getParameter("id");之后是否直接用了id.equals(...)?如果是,必须加if (id == null || id.trim().isEmpty())校验。
4. 在IDE中,给该行打个断点,用Debug模式运行,观察变量值。

另一个高频500:NumberFormatException。当用户在URL里恶意传入?price=abcDouble.parseDouble("abc")就会抛此异常。解决方案:所有parseXxx()调用必须包裹try-catch,并提供友好的错误反馈,而不是让Tomcat返回500白页。

终极技巧:在Servlet开头加日志。在doGet()第一行插入:

System.out.println("AddToCartServlet received: " + request.getQueryString());

这样,每次请求,catalina.out里都会打印完整的参数字符串,方便你确认前端传了什么,后端收到了什么。这是比任何调试器都直接的“真相之眼”。

5.3 中文乱码:从URL到页面,字符集必须全程贯通

中文乱码表现为:URL参数显示为%E4%BD%A0%E5%A5%BD(这是UTF-8编码,正常),但request.getParameter("name")拿到的是浣犲ソ(乱码),或者页面HTML中显示??

根本原因:字符集在请求、响应、文件存储三个环节不一致

解决方案分三步:
1. 请求参数解码:Tomcat默认用ISO-8859-1解码URL参数。在AddToCartServlet中,对每个中文参数手动解码:
java String name = URLDecoder.decode(request.getParameter("name"), "UTF-8");
2. 响应内容编码:在ShowCartServlet开头,必须设置:
java response.setContentType("text/html;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); // 更保险
3. 文件本身编码:确保shop.htmlweb.xml、所有Java文件,都用UTF-8(无BOM)保存。在IDE中,File -> File Encoding设为UTF-8。

实操心得:我曾为乱码折腾两小时,最后发现是shop.html用记事本保存成了ANSI。用VS Code打开,右下角看到“GBK”,点击切换为“UTF-8 with BOM”,再保存,问题立解。记住:所有文本文件,编码必须统一为UTF-8,这是现代Web开发的铁律

5.4 购物车数据不持久:session为何“失忆”?

用户添加商品后,跳转到showCart能看到,但刷新页面或过几分钟再看,购物车又空了。这不是bug,而是HttpSession的正常行为,但你可以控制它。

session失效的三大原因:
- 超时(Timeout):Tomcat默认session超时30分钟(1800秒)。在conf/web.xml中可全局修改,或在WEB-INF/web.xml中为当前应用设置:
xml <session-config> <session-timeout>60</session-timeout> <!-- 单位:分钟 --> </session-config>
- 浏览器关闭:session ID存在浏览器Cookie中,关闭浏览器,Cookie消失,下次访问就是新session。
- 手动失效:调用session.invalidate()

验证session是否有效:在ShowCartServlet中加一行:

System.out.println("Session ID: " + session.getId() + ", isNew: " + session.isNew() + ", MaxInactiveInterval: " + session.getMaxInactiveInterval());

启动Tomcat,访问几次,观察catalina.out输出的session ID是否变化。如果每次都不一样,说明没拿到旧session,可能是Cookie被禁用或路径配置错误。

最后一个小技巧:如果你想在开发时“永不失效”,可以在AddToCartServlet中加:
java session.setMaxInactiveInterval(60 * 60); // 设为1小时
这样,只要用户1小时内有操作,session就不会过期。上线前记得删掉,避免内存泄漏。

6. 项目扩展与进阶思考:从Demo到真实系统的跨越之路

这个购物车Demo的价值,不仅在于它能跑,更在于它是一块“乐高底板”,你可以基于它,一块一块往上搭,无限接近真实系统。以下是我从企业项目中提炼出的、最值得你下一步动手的三个扩展方向,每个都附带了具体的技术点和实现提示。

6.1 引入JSON与AJAX:告别页面跳转,实现局部刷新

当前流程是“点击->跳转新页面->显示”,体验割裂。升级为AJAX,用户点击“添加”后,页面不刷新,购物车数量实时更新。这需要三步改造:

第一步:后端提供JSON接口。新建一个CartApiServletdoPost()接收JSON请求体(如{"id":"101","name":"MacBook","price":12999,"qty":1}),处理逻辑同AddToCartServlet,但响应不再是HTML,而是JSON:

response.setContentType("application/json;charset=UTF-8");
out.println("{\"success\":true,\"message\":\"添加成功\",\"cartSize\":" + cartItems.size() + "}");

第二步:前端用JavaScript调用。在shop.html中,移除<a href>,改为按钮:

<button onclick="addToCart('101', 'MacBook', 12999, 1)">添加到购物车</button>
<script>
function addToCart(id, name, price, qty) {
    fetch('/shopping-cart/api/cart', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({id, name, price, qty})
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            alert(data.message);
            // 更新页面右上角购物车图标数字
            document.getElementById('cart-count').textContent = data.cartSize;
        }
    });
}
</script>

第三步:配置CORS(跨域资源共享)。如果前端和后端域名不同(如前端用http://localhost:3000,后端用http://localhost:8080),需在CartApiServlet响应头中加:

response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");

这个扩展的价值在于:它引入了前后端分离的雏形,让你理解RESTful API的设计、JSON数据交换、浏览器同源策略与CORS的对抗。这些都是现代Web开发的基石。

6.2 数据库持久化:让购物车数据不再随session消亡

当前购物车数据存在内存里,Tomcat重启就没了。接入MySQL,让数据落盘,是走向生产的第一步。核心改动在AddToCartServletShowCartServlet

第一步:添加JDBC驱动。下载mysql-connector-java-8.0.33.jar,放入WebContent/WEB-INF/lib/。Tomcat启动时会自动将其加入class path。

第二步:编写数据库工具类。创建src/com/example/db/DBUtil.java,封装getConnection()方法,使用连接池(如HikariCP)更佳,但Demo可用简单DriverManager

第三步:改造Servlet逻辑AddToCartServlet中,不再操作session.getAttribute("cartItems"),而是执行SQL:

String sql = "INSERT INTO cart_items (user_id, product_id, product_name, price, quantity) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)";
// 使用PreparedStatement执行

ShowCartServlet中,执行SELECT * FROM cart_items WHERE user_id = ?,从数据库查数据,而非session。

这个扩展直击数据一致性痛点。它迫使你思考:用户登录态如何与数据库user_id关联?session超时后,数据库里的购物车数据要不要自动清理?这些问题,就是真实电商系统每天要面对的挑战。

6.3 添加用户认证:从“游客购物车”到“专属购物车”

当前购物车是“无主”的,所有用户共享同一个session。添加登录功能,让每个用户有自己的购物车。这需要:

  • 新建LoginServlet处理用户名密码,验证后将userId存入session:session.setAttribute("userId", userId)
  • AddToCartServlet中,从session获取userId,作为外键存入数据库。
  • ShowCartServlet中,查询时加上WHERE user_id = ?条件。

关键难点:密码安全。绝不能明文存储密码!必须用BCrypt加密:

// 存储时
String hashedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt());

// 验证时
if (BCrypt.checkpw(inputPassword, storedHash)) {
    // 登录成功
}

下载bcrypt-3.0.jar,或用Spring Security的BCryptPasswordEncoder(但本项目坚持无框架,所以用轻量级jBCrypt库)。

这个扩展将项目从“功能Demo”拉升到“可用系统”。它引入了安全领域的核心概念:认证(Authentication)、授权(Authorization)、密码哈希。当你亲手实现BCrypt,你就真正理解了为什么“123456”不能当密码。

我个人在实际操作中发现,最有效的学习路径是:先跑通原始Demo,确保每个环节都理解;然后选一个扩展点(推荐从AJAX开始),花半天时间动手实现;遇到问题,就回到catalina.out日志和浏览器Network标签里找答案。不要追求一步到位,把大目标拆解成一个个可验证的小胜利。这个购物车项目,就像一把瑞士军刀,它本身不锋利,但当你亲手磨砺过它的每一把小刀,你就有能力去锻造属于自己的、更强大的工具。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接扔进Tomcat就能跑的轻量级购物车Demo,纯Servlet开发,不依赖Spring等框架。包含商品展示页shop.html,点击‘添加到购物车’触发AddToCartServlet,把商品ID、名称、单价、数量等存入HttpSession;再由ShowCartServlet从session读取数据,用HTML表格清晰列出当前购物车内容。项目结构完整:标准WEB-INF目录、web.xml配置(含Servlet映射)、编译后的class文件放在正确路径、图片资源统一放在img子目录。支持Eclipse或IDEA快速导入,无需额外配置即可运行。覆盖JavaWeb核心实践点:HTTP GET/POST请求处理、request与response基础操作、session会话生命周期管理、Servlet间跳转(重定向/请求转发)、静态资源引用、简单前后端数据传递。适合刚学完Servlet基础、想动手验证会话机制和页面联动的新手,也能作为课堂演示或课设参考模板。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐