恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Servlet+JSP+MySQL手写三层商城实战
首页
资讯中心
/
Servlet+JSP+MySQL手写三层商城实战
Servlet+JSP+MySQL手写三层商城实战
发布时间:2026/10/16 11:28:02
简介本资源是一个基于JavaWeb技术实现的华为商城电子商务系统实战项目面向Java初学者及Web开发入门者帮助其系统掌握Servlet、JSP、MySQL数据库交互等核心Web开发技能。压缩包共2个文件含1个RAR源码包含完整Web应用结构WEB-INF配置、Java类、JSP页面、lib依赖库和1个SQL脚本文件huawei.sql用于一键初始化商品、用户、订单等数据库表结构与基础数据整体包体58.24MB轻量实用便于本地快速部署运行。已有961人学习下载说明其在实践教学与自学验证中具备良好口碑。读者可直接导入IDE运行项目体验注册登录、商品浏览、购物车管理、订单提交等全流程功能并通过源码深入理解MVC分层设计、JSP动态渲染、JSTL标签应用及MySQL CRUD操作的工程化落地方式。1. 一个跑在 Tomcat 上的「真·商城」不是 Spring Boot 脚手架而是 ServletJSPMySQL 手写三层的完整闭环你可能刚学完 JavaWeb 三大组件Servlet、Filter、Listener也照着教程敲过用户登录、商品列表但合上书就卡在「怎么才算一个能上线的商城」——这个javaweb华为商城.zip就是答案它不依赖任何现代框架没用 Spring、没碰 MyBatis纯靠HttpServlet接口、JDBC手写 DAO、JSP做视图、JSTL控制流程外加一个带登录态校验、购物车持久化、订单生成与状态流转的完整业务流。它不是教学 Demo而是一个可部署、可调试、可改价、可加商品、甚至能模拟用户下单失败重试的「最小可行商城」。适合两类人一是刚学完 JDBC 和 MVC 分层、想验证自己是否真懂「请求怎么从浏览器进到数据库再吐回页面」的新手二是需要快速搭一个 JavaWeb 底层教学案例的讲师或导师——所有代码都在 src 下没有隐藏黑盒连 MySQL 建表语句都塞在sql/目录里连web.xml的 URL 映射都一行行写清楚。这不是玩具项目它是用最原始但最透明的方式把 Web 开发的「血肉」一节节摊开给你看。2. 技术栈与工程结构Servlet 3.0 JSP 2.3 MySQL 5.7 的经典组合这个项目没玩花活技术选型非常克制全部落在 JavaWeb 教学与企业遗留系统维护的真实交界面上。它不追求新但求稳、求透、求可调试。下面我带你一层层剥开它的骨架重点不是“用了什么”而是“为什么这么用”、“哪里藏着关键设计”。2.1 核心依赖与版本锚点项目基于标准 JavaEE Web 模块结构WEB-INF/web.xml明确声明为 Servlet 3.0 规范web-app version3.0这意味着它兼容 Tomcat 7实测 Tomcat 8.5 / 9.0 完全无压力。JSP 使用的是 JSP 2.3对应 Servlet 3.0所以 EL 表达式${user.name}和 JSTL 标签c:if、c:forEach可直接用无需额外配置 TLD。数据库驱动用的是mysql-connector-java-5.1.47.jar这是 MySQL 5.7 的黄金匹配版本——太新如 8.x 驱动会因默认时区、SSL 策略报错太旧如 5.0.x则不支持useSSLfalse参数导致本地调试连不上。这个细节我在第一次部署时就栽过坑后面「避坑章节」会细说。提示项目未使用 Maven所有.jar全部放在WEB-INF/lib/下共 6 个mysql-connector-java-5.1.47.jar、jstl-1.2.jar、standard-1.1.2.jarJSTL 运行时依赖、servlet-api-3.0.jar仅编译用Tomcat 运行时不加载、commons-beanutils-1.9.4.jar用于BeanUtils.populate()自动封装表单数据、commons-logging-1.2.jar日志门面。没有一个多余。2.2 工程目录结构教科书级分层但每层都有真实业务逻辑javaweb华为商城/ ├── sql/ # 建库建表 SQL含初始化数据admin 用户、测试商品 ├── src/ │ ├── cn/huawei/web/ # Servlet 层所有控制器命名直白如 UserServlet、OrderServlet │ ├── cn/huawei/service/ # Service 层接口 实现类含事务控制手动 commit/rollback │ ├── cn/huawei/dao/ # DAO 层JDBC 操作封装Connection 由 BaseDao 统一管理 │ ├── cn/huawei/domain/ # Domain 层纯 POJO字段与数据库表一一对应含 getter/setter │ └── cn/huawei/util/ # 工具类DBUtil获取/关闭连接、CookieUtil读写购物车 Cookie ├── WebContent/ # Web 根目录Eclipse 时代叫法等同于 modern 的 src/main/webapp │ ├── WEB-INF/ │ │ ├── web.xml # 全量 URL 映射、Filter 配置、欢迎页 │ │ └── lib/ # 上述 6 个 jar │ ├── index.jsp # 首页含轮播图、新品推荐数据来自 request.setAttribute │ ├── user/ # 用户相关页面login.jsp、register.jsp、userinfo.jsp │ ├── product/ # 商品页product_list.jsp、product_detail.jsp │ ├── cart/ # 购物车cart.jsp显示 Cookie 中商品、cart_add.jsp加购逻辑 │ └── order/ # 订单页order_confirm.jsp确认收货信息、order_success.jsp注意WebContent是 Eclipse Dynamic Web Project 的默认 WebRoot不是 Maven 结构。如果你用 IDEA导入时需选「Import project from external model → Eclipse」否则静态资源路径会错。这个结构看似老旧但它的好处是——所有路径都是硬编码、可追踪、无魔法。比如cart_add.jsp里form actioncart_add.jsp methodpost后端CartAddServlet的doPost()就处理这个请求中间没有任何路由解析器、注解扫描器或自动映射。新手能一眼看清「点击按钮 → 提交到哪个文件 → 调哪个 Servlet → 走哪段 Java 代码」。2.3 关键配置文件解读web.xml是整个项目的总开关web.xml不是摆设它定义了这个商城的「运行契约」。我们挑三个最核心的配置来看!-- 1. 全局初始化参数数据库连接信息 -- context-param param-namedriver/param-name param-valuecom.mysql.jdbc.Driver/param-value /context-param context-param param-nameurl/param-name param-valuejdbc:mysql://localhost:3306/huawei_mall?useUnicodetrueamp;characterEncodingUTF-8amp;useSSLfalse/param-value /context-param context-param param-nameusername/param-name param-valueroot/param-value /context-param context-param param-namepassword/param-name param-value123456/param-value /context-param这里useSSLfalse是必须加的否则 MySQL 5.7 默认要求 SSL 连接本地开发环境没配证书就会报Communications link failure。amp;是 XML 转义实际拼接 URL 时会被解析为。!-- 2. Filter统一登录拦截 -- filter filter-nameLoginFilter/filter-name filter-classcn.huawei.web.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/user/*/url-pattern url-pattern/cart/*/url-pattern url-pattern/order/*/url-pattern /filter-mapping这个 Filter 是商城安全边界的起点。它检查session.getAttribute(user)是否存在且不为 null不存在则重定向到/user/login.jsp。注意它不拦截/index.jsp或/product/因为游客可以浏览商品——这是真实商城逻辑不是所有页面都要登录。!-- 3. Servlet 映射明确告诉容器「谁处理什么」 -- servlet servlet-nameUserServlet/servlet-name servlet-classcn.huawei.web.UserServlet/servlet-class /servlet servlet-mapping servlet-nameUserServlet/servlet-name url-pattern/user/url-pattern /servlet-mappingUserServlet是一个典型的多方法分发 Servlet通过request.getParameter(method)判断是login、logout还是register然后调用对应私有方法。这种写法在 Servlet 3.0 之前很常见虽不如WebServlet注解简洁但逻辑完全暴露新手 debug 时打个断点就能看到methodlogin是怎么走到userLogin()里的。3. 核心模块拆解从用户登录到订单生成的完整链路这个商城最值得细看的不是某个炫酷功能而是一个真实用户操作如何穿透整个分层架构。我们以「用户登录 → 加购商品 → 提交订单」为主线逐层拆解代码不跳步、不省略、不假设你已懂某一步。3.1 用户登录Servlet 接参 → Service 校验 → DAO 查库 → JSP 渲染登录入口是user/login.jsp表单提交到UserServletURL/user!-- user/login.jsp 片段 -- form action/user methodpost input typehidden namemethod valuelogin/ input typetext nameusername placeholder用户名/ input typepassword namepassword placeholder密码/ button typesubmit登录/button /form后端UserServlet.java的doPost()方法protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method request.getParameter(method); if (login.equals(method)) { userLogin(request, response); // 调用私有方法 } else if (logout.equals(method)) { userLogout(request, response); } }userLogin()方法是关键private void userLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数原始方式非 Spring 的 RequestParam String username request.getParameter(username); String password request.getParameter(password); // 2. 调用 Service 层传入原始参数 UserService userService new UserServiceImpl(); User user userService.login(username, password); // 3. Service 返回 null说明账号密码错 if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/user/login.jsp).forward(request, response); return; } // 4. 登录成功存入 session并重定向到首页 request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /index.jsp); }这里没有Autowired没有 IOC 容器new UserServiceImpl()是硬编码创建对象。好处是你能清晰看到依赖关系——UserServlet依赖UserServiceImplUserServiceImpl又依赖UserDaoImpl。UserServiceImpl.login()方法public User login(String username, String password) { UserDao userDao new UserDaoImpl(); // 注意密码是明文比对教学项目简化真实场景必须 BCrypt 加密 return userDao.findByUsernameAndPassword(username, password); }UserDaoImpl.findByUsernameAndPassword()最终执行 JDBC 查询public User findByUsernameAndPassword(String username, String password) { Connection conn null; PreparedStatement stmt null; ResultSet rs null; try { conn DBUtil.getConnection(); // 从 web.xml 读取参数创建 Connection String sql SELECT id, username, password, email, phone FROM users WHERE username ? AND password ?; stmt conn.prepareStatement(sql); stmt.setString(1, username); stmt.setString(2, password); rs stmt.executeQuery(); if (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); user.setPhone(rs.getString(phone)); return user; } } catch (SQLException e) { e.printStackTrace(); // 教学项目直接打印堆栈 } finally { DBUtil.close(rs, stmt, conn); // 必须关闭否则连接泄漏 } return null; }整个链路JSP 表单 → Servlet 获取参数 → Service 新建 DAO → DAO 执行 JDBC → 返回 User 对象 → Servlet 存入 Session → 重定向。每一行代码都可打断点、可修改、可替换。这就是它作为教学载体的价值——没有抽象掉“连接怎么来、结果怎么转对象、异常怎么处理”的过程。3.2 购物车Cookie 存储 Servlet 同步 → 解决无 Session 场景这个商城的购物车设计很务实它不强制用户登录才能加购。游客也能把商品加入购物车数据存在 Cookie 里登录后再把 Cookie 购物车合并到用户账户的数据库购物车中。这解决了教学中一个经典矛盾既要演示 Session又要演示 Cookie。加购逻辑在CartAddServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取商品 ID 和数量来自 product_detail.jsp 表单 String pid request.getParameter(pid); String countStr request.getParameter(count); int count 1; if (countStr ! null !countStr.trim().isEmpty()) { count Integer.parseInt(countStr); } // 2. 从 Cookie 读取现有购物车格式pid1-count1|pid2-count2 Cookie[] cookies request.getCookies(); String cartValue ; if (cookies ! null) { for (Cookie cookie : cookies) { if (cart.equals(cookie.getName())) { cartValue cookie.getValue(); break; } } } // 3. 解析并更新购物车字符串简单字符串拼接非 JSON StringBuilder newCart new StringBuilder(); if (cartValue ! null !cartValue.isEmpty()) { String[] items cartValue.split(\\|); boolean found false; for (String item : items) { if (item.startsWith(pid -)) { // 已存在更新数量 int oldCount Integer.parseInt(item.split(-)[1]); newCart.append(pid).append(-).append(oldCount count).append(|); found true; } else { newCart.append(item).append(|); } } if (!found) { newCart.append(pid).append(-).append(count).append(|); } } else { // 购物车为空直接添加 newCart.append(pid).append(-).append(count).append(|); } // 4. 写回 Cookie有效期 7 天 Cookie cartCookie new Cookie(cart, newCart.toString()); cartCookie.setMaxAge(7 * 24 * 3600); cartCookie.setPath(/); response.addCookie(cartCookie); // 5. 重定向回商品详情页显示成功提示 request.setAttribute(msg, 已加入购物车); request.getRequestDispatcher(/product/product_detail.jsp).forward(request, response); }注意这里用|和-分隔是典型教学项目做法——不引入 JSON 库用最基础字符串操作。真实项目会用Jackson或Gson序列化MapLong, Integer但新手看cartValue.split(\\|)比看jsonObject.getJSONArray(items)更容易理解数据结构。3.3 订单生成事务控制 状态机雏形订单模块是整个项目复杂度最高的部分它体现了「业务完整性」扣库存、生成订单号、关联用户、写订单项、更新订单状态。OrderServlet.java的createOrder()方法展示了手动事务控制private void createOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取当前登录用户 User user (User) request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /user/login.jsp); return; } // 2. 从 Cookie 读购物车游客模式或从数据库查登录用户 ListCartItem cartItems getCartItemsFromCookieOrDB(request); // 3. 开启事务所有操作要么全成功要么全回滚 Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 步骤 A扣减商品库存for 循环每个商品 update ProductDao productDao new ProductDaoImpl(); for (CartItem item : cartItems) { boolean updated productDao.updateStock(item.getPid(), -item.getCount()); if (!updated) { throw new RuntimeException(商品库存不足 item.getPid()); } } // 步骤 B生成订单主表orders Order order new Order(); order.setOid(UUID.randomUUID().toString().replace(-, )); // 32位订单号 order.setUid(user.getId()); order.setTotal(getTotalPrice(cartItems)); order.setStatus(1); // 1待付款2已付款3已发货... order.setOrdertime(new Date()); OrderDao orderDao new OrderDaoImpl(); orderDao.addOrder(order, conn); // 传入 conn保证同一事务 // 步骤 C生成订单项order_items OrderItemDao orderItemDao new OrderItemDaoImpl(); for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setIid(UUID.randomUUID().toString().replace(-, )); orderItem.setOid(order.getOid()); orderItem.setPid(item.getPid()); orderItem.setCount(item.getCount()); orderItem.setSubtotal(item.getSubtotal()); orderItemDao.addItem(orderItem, conn); } // 全部成功提交事务 conn.commit(); // 清空购物车 Cookie Cookie cartCookie new Cookie(cart, ); cartCookie.setMaxAge(0); cartCookie.setPath(/); response.addCookie(cartCookie); // 跳转成功页 request.setAttribute(order, order); request.getRequestDispatcher(/order/order_success.jsp).forward(request, response); } catch (Exception e) { // 出错回滚 if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(msg, 订单创建失败 e.getMessage()); request.getRequestDispatcher(/cart/cart.jsp).forward(request, response); } finally { DBUtil.close(null, null, conn); } }这个方法有三个关键点手动事务conn.setAutoCommit(false)conn.commit()/conn.rollback()比 Spring 的Transactional更底层让你看清事务边界在哪状态码硬编码order.setStatus(1)后续扩展「已付款」「已发货」只需改这里和对应页面判断订单号生成用UUID去-保证全局唯一不依赖数据库自增 ID避免并发问题。这就是一个「小而全」的订单状态机起点——它没做消息队列、没对接支付网关但把「创建订单」这个原子操作的完整性用最朴素的 JDBC 方式实现了。4. 避坑指南部署与调试中最常翻车的 4 个点这个项目看着简单但新手在本地跑通往往要折腾 1–2 小时。我当年第一次部署时光解决乱码和连接失败就花了半天。下面这 4 条全是血泪经验总结按出现频率排序每条都附带「现象 → 原因 → 解决」照着做基本能一次过。4.1 现象中文乱码登录页输入「张三」后台收到「å¼ ä¸」原因Tomcat 默认使用 ISO-8859-1 解码 POST 请求体而浏览器发送的是 UTF-8 编码的中文。request.getParameter()拿到的就是乱码字节数组。解决在UserServlet的doPost()开头强制设置请求编码request.setCharacterEncoding(UTF-8);注意这行必须在getParameter()之前调用否则无效。更彻底的做法是在web.xml中配置CharacterEncodingFilter但本项目没配所以每个 Servlet 的doPost()都得手动加这一行。别偷懒我试过漏加一个结果注册用户姓名全乱码查数据库时一脸懵。4.2 现象java.sql.SQLException: Access denied for user rootlocalhost或Communications link failure原因MySQL 5.7 默认开启skip-grant-tables或validate_password策略或者web.xml里的url参数少了useSSLfalse。解决确保 MySQL 用户有远程访问权限即使本地也要host%CREATE USER root% IDENTIFIED BY 123456; GRANT ALL PRIVILEGES ON *.* TO root%; FLUSH PRIVILEGES;web.xml中的url必须包含useSSLfalse且要写成amp;XML 转义param-valuejdbc:mysql://localhost:3306/huawei_mall?useUnicodetrueamp;characterEncodingUTF-8amp;useSSLfalse/param-value如果还连不上用命令行验证mysql -h 127.0.0.1 -P 3306 -u root -p确保能登录。4.3 现象HTTP Status 404 – /user访问/user报 404但index.jsp能打开原因web.xml中servlet-mapping的url-pattern和浏览器地址栏输入的 URL 不匹配。常见错误在web.xml里写了url-pattern/user/url-pattern但浏览器访问的是http://localhost:8080/user缺项目上下文路径或者项目名不是javaweb华为商城而是被 IDE 自动改成javaweb%E5%8D%8E%E4%B8%BA%E5%95%86%E5%9F%8EURL 编码导致上下文路径错。解决查看 Tomcat 控制台启动日志找这行Deploying web application directory [...]后面跟着的实际路径就是上下文路径Context Path浏览器访问时必须带上上下文路径例如http://localhost:8080/javaweb华为商城/user更稳妥做法在web.xml的welcome-file-list中指定index.jsp然后所有跳转用相对路径如a href${pageContext.request.contextPath}/user登录/a这样无论项目名是什么都能适配。4.4 现象购物车加购后cart.jsp显示为空或数量总是 1原因CartAddServlet中解析 Cookie 的正则或分割逻辑出错或者cart.jsp读取 Cookie 时没正确 split。解决在CartAddServlet的doPost()中加一行日志打印cartValueSystem.out.println(原始购物车 Cookie: cartValue);检查cart.jsp中读取逻辑它应该用CookieUtil.getCartItems(request)项目自带工具类而不是自己手写getCookies()关键cartValue.split(\\|)中的\\|是转义因为|是正则特殊字符。如果写成.split(|)会按空字符切得到一堆空字符串。这是 Java 字符串分割的经典玄学坑我踩过三次才记住。5. 二次开发实战如何快速接入微信支付、替换前端 UI、增加商品搜索现在你已经跑通了整个商城下一步往往是「我想改点东西」。下面我给你三条最实用的改造路径每条都给出具体改什么文件、改哪几行、预期效果以及我踩过的坑。不讲大道理只给可粘贴、可验证的代码。5.1 接入微信支付替换「订单成功页」为支付跳转原项目订单成功页order_success.jsp只是静态展示我们要让它变成「调起微信 JSAPI 支付」。微信支付需要后端统一下单接口返回prepay_id再由前端调WeixinJSBridge.invoke()。改造分两步第一步新增WxPayServlet.java放在src/cn/huawei/web/WebServlet(/wxpay) public class WxPayServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取订单号来自 order_success.jsp 的 hidden input String oid request.getParameter(oid); // 2. 查询订单金额真实项目应查 DB此处简化为固定值 double totalFee 99.9; // 单位分 → 9990 // 3. 调用微信统一下单接口此处用伪代码实际需引入 wxpay-sdk // WxPayService wxPayService new WxPayService(); // MapString, String result wxPayService.unifiedOrder(oid, totalFee); // String prepayId result.get(prepay_id); // 4. 为演示我们 mock 一个 prepay_id String prepayId wx System.currentTimeMillis() mock; // 5. 构造 JSAPI 签名参数关键时间戳、随机串、签名必须一致 String timeStamp String.valueOf(System.currentTimeMillis() / 1000); String nonceStr UUID.randomUUID().toString().replace(-, ); String packageStr prepay_id prepayId; String signType MD5; // 签名原文appIdwx...nonceStr...packageprepay_id...signTypeMD5timeStamp...keyyourKey String paySign generatePaySign(packageStr, timeStamp, nonceStr, signType); // 6. 返回 JSON 给前端 response.setContentType(application/json;charsetUTF-8); PrintWriter out response.getWriter(); out.print({\appId\:\wx123456789\,\timeStamp\:\ timeStamp \,\nonceStr\:\ nonceStr \,\package\:\ packageStr \,\signType\:\ signType \,\paySign\:\ paySign \}); out.flush(); } private String generatePaySign(String packageStr, String timeStamp, String nonceStr, String signType) { // 真实项目需用官方 SDK 的 WXPayUtil.sign()此处简化为返回固定值 return FAKE_PAY_SIGN_123456; } }第二步修改order_success.jsp加 JS 调起支付% page contentTypetext/html;charsetUTF-8 languagejava % html head title订单成功/title script srchttps://res.wx.qq.com/open/js/jweixin-1.6.0.js/script /head body h2订单提交成功/h2 p订单号% request.getAttribute(order).getOid() %/p p金额% request.getAttribute(order).getTotal() % 元/p !-- 隐藏订单号供 JS 使用 -- input typehidden idoid value% request.getAttribute(order).getOid() % button onclickcallWxPay()立即支付/button script function callWxPay() { const oid document.getElementById(oid).value; fetch(/javaweb华为商城/wxpay, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: oid oid }) .then(res res.json()) .then(data { // 调起微信支付 WeixinJSBridge.invoke(getBrandWCPayRequest, { appId: data.appId, timeStamp: data.timeStamp, nonceStr: data.nonceStr, package: data.package, signType: data.signType, paySign: data.paySign }, function(res) { if (res.err_msg get_brand_wcpay_request:ok) { alert(支付成功); window.location.href /index.jsp; } else { alert(支付失败 res.err_msg); } }); }); } /script /body /html注意微信 JSAPI 支付必须在微信内置浏览器中调用且域名已配置 JSAPI 白名单。本地调试要用「微信开发者工具」或真机扫码。别在 Chrome 里点按钮会报WeixinJSBridge is not defined。5.2 替换前端 UI用 Bootstrap 4 重写首页原index.jsp是纯 HTML 表格布局丑且难维护。我们用 Bootstrap 4 快速升级下载 Bootstrap 4 CSS/JS放入WebContent/css/bootstrap.min.css和WebContent/js/bootstrap.min.js修改index.jsp头部link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css script src${pageContext.request.contextPath}/js/bootstrap.min.js/script商品列表区域把原来的table替换为 Bootstrap Card Griddiv classcontainer mt-4 h2热门商品/h2 div classrow c:forEach items${products} varp div classcol-md-3 mb-4 div classcard h-100 img src${pageContext.request.contextPath}/images/${p.pimage} classcard-img-top alt${p.pname} styleheight: 150px; object-fit: cover; div classcard-body h5 classcard-title${p.pname}/h5 p classcard-text text-danger¥${p.shop_price}/p a href${pageContext.request.contextPath}/product/product_detail.jsp?pid${p.pid} classbtn btn-primary btn-sm查看详情/a /div /div /div /c:forEach /div /div重启 Tomcat首页立刻变样。Bootstrap 的栅格系统让响应式布局一步到位不用写一行 media query。5.3 增加商品搜索在首页加搜索框后端用 LIKE 模糊查询这是最常被问的需求。只需改两处第一处index.jsp加搜索表单div classcontainer mt-4 form classform-inline my-2 my-lg-0 action${pageContext.request.contextPath}/product/search methodget input classform-control mr-sm-2 typesearch nameq placeholder搜索商品... aria-labelSearch button classbtn btn-outline-success my-2 my-sm-0 typesubmit搜索/button /form /div第二处新增ProductSearchServlet.javaWebServlet(/product/search) public class ProductSearchServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String q request.getParameter(q); if (q null || q.trim().isEmpty()) { // 搜索空重定向回首页 response.sendRedirect(request.getContextPath() /index.jsp); return; } ProductDao productDao new ProductDaoImpl(); ListProduct products productDao.searchByName(q.trim()); // 新增方法 request.setAttribute(products, products); request.setAttribute(searchQuery, q); request.getRequestDispatcher(/product/product_list.jsp).forward(request, response); } }第三处在ProductDaoImpl.java中新增searchByName()方法public ListProduct searchByName(String keyword) { Connection conn null; PreparedStatement stmt null; ResultSet rs null; ListProduct list new ArrayList(); try { conn DBUtil.getConnection(); // 使用 %?% 实现模糊搜索 String sql SELECT * FROM products WHERE pname LIKE ?; stmt conn.prepareStatement(sql); stmt.setString(1, % keyword %); rs stmt.executeQuery(); while (rs.next()) { Product p new Product(); p.setPid(rs.getLong(pid)); p.setPname(rs.getString(pname)); p.setShop_price(rs.getDouble(shop_price)); p.setPimage(rs.getString(pimage)); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, stmt, conn); } return list; }提示LIKE搜索性能一般数据量大时需加索引。在products(pname)上执行ALTER TABLE products ADD INDEX idx_pname(pname);即可。从那以后我每次接手一个老 JavaWeb 项目第一件事就是打开它的web.xml看servlet-mapping和filter-mapping再扫一眼WEB-INF/lib/有哪些本文还有配套的精品资源点击获取