☰
JavaWeb原生开发实战:Servlet+JSP蛋糕商城从零搭建
2026/10/8 2:26:18 网站建设 项目流程

简介:这是一套基于JavaWeb技术栈开发的完整蛋糕商城系统,面向Java初学者与Web开发入门者,聚焦Servlet+JSP+MySQL传统三层架构实践,帮助学习者掌握用户管理、商品运营与订单处理等核心电商模块开发。资源包共225个文件,包含52个Java源码(如UserDaoImp、GoodsDaoImp、OrderDaoImp等DAO层实现)、52个编译后class文件、45张商品及界面图片(jpg/webp/png)、23个JSP页面、16个CSS样式文件、11个依赖jar包及1个SQL建表脚本,整体压缩包仅10.97MB,轻量易部署。已有6784人学习下载,适合作为课程设计、毕业实训或自学项目参考。读者可直接运行调试,完整复现登录注册、管理员后台、购物车(基于Cookie+JSON实现)、商品分类与搜索、多维度推荐(条幅/热销/新品)及订单全流程管理等功能,代码结构清晰,各层职责分明,具备良好的教学示范性与工程参考价值。

1. 为什么一个“蛋糕商城”能当JavaWeb教学项目的硬核入口?

你不是在学JavaWeb,你是在学怎么把「用户想下单买蛋糕」这件事,用Servlet、JSP、MySQL和Tomcat一五一十地钉死在浏览器里——不靠框架、不靠脚手架、不靠Spring Boot自动装配,就靠原生JavaEE那一套:HTTP请求怎么进来的、Session怎么存购物车、数据库怎么防超卖、页面跳转怎么不丢数据、表单提交怎么校验再回显。这不是玩具项目,是当年企业真实上线前的最小可行验证:用户注册→浏览蛋糕列表→加购→登录→提交订单→支付模拟→后台管理。黑马、尚硅谷、B站高赞JavaWeb合集里反复出现的“完整案例”,核心就在这儿:它用最朴素的技术栈,逼你直面Web开发最原始的契约——HTTP状态码、Cookie生命周期、JDBC连接池泄漏、JSP EL表达式作用域、web.xml部署描述符的优先级。如果你卡在“IDEA里跑不起来Tomcat”“MySQL连上了但查不到数据”“加购后刷新购物车就空了”,那说明你还没真正踩进JavaWeb的地基裂缝里。这篇笔记,就是帮你把裂缝里的碎石一块块抠出来,铺成路。


2. 从零搭起蛋糕商城:环境、结构与核心组件选型逻辑

2.1 开发环境配置:IDEA + Tomcat + MySQL 的最小闭环

JavaWeb项目不是写完代码就能跑,它依赖三个活体组件的精确咬合:IDEA负责编译和热部署,Tomcat是容器引擎,MySQL是数据心脏。常见翻车点不是代码写错,而是版本错配或路径污染。我一般会锁死这三者的组合:

  • IDEA:2022.3 或更高(低版本对Servlet 4.0+支持弱,尤其JSP编译报错)
  • Tomcat:9.0.83(Servlet 4.0规范,兼容JSP 2.3,避免Tomcat 10+因包名迁移导致javax.servlet.*全报红)
  • MySQL:8.0.33(注意驱动必须用mysql-connector-java-8.0.33.jar,5.x驱动连8.0会报Public Key Retrieval is not allowed)

提示:Tomcat的conf/context.xml里务必加一行<Resource name="jdbc/cake" auth="Container" type="javax.sql.DataSource"... />,这是后续JNDI数据源的基础,别等DAO层写了才发现没配。

在IDEA中新建项目时,不要选“Maven”模板直接开干。先选“Empty Project”,再手动添加Module → “Java Enterprise” → 勾选“Web Application”和“Servlet”(版本选4.0),最后再右键Module → “Add Framework Support” → 加Tomcat Server。这样能看清web/WEB-INF/web.xml的原始结构,而不是被Maven的pom.xml和插件遮住视线。

2.2 项目目录结构:为什么/web下必须有/WEB-INF和/static?

JavaWeb的目录不是随便放的,Tomcat按约定俗成的路径规则加载资源。一个标准蛋糕商城的web目录长这样:

web/ ├── index.jsp ← 首页,可被直接访问 ├── login.jsp ← 登录页,可被直接访问 ├── static/ ← 存放CSS/JS/图片,**不经过Servlet拦截** │ ├── css/ │ │ └── style.css │ ├── js/ │ │ └── cart.js │ └── img/ │ └── cake1.jpg ├── WEB-INF/ ← **所有JSP/Servlet都在这里受保护** │ ├── web.xml ← 部署描述符,Servlet映射、过滤器、监听器全在这 │ ├── jsp/ ← JSP文件放这里(如order.jsp),外部无法直连 │ └── classes/ ← 编译后的.class文件(IDEA自动输出到此处) └── lib/ ← 手动放jar包:mysql-connector-java-8.0.33.jar、jstl-1.2.jar

关键逻辑:/static下的资源走静态文件服务(Tomcat内置),而/WEB-INF及其子目录永远不响应HTTP请求——这是安全底线。用户打http://localhost:8080/WEB-INF/web.xml,Tomcat直接返回404,而不是把配置文件内容吐出来。所以购物车JSP必须放在/WEB-INF/jsp/cart.jsp,然后由Servletrequest.getRequestDispatcher("/WEB-INF/jsp/cart.jsp").forward(request, response)转发过去,绝不能让用户通过URL直接敲开。

2.3 核心组件选型:为什么不用Spring MVC,而坚持Servlet+JSP?

新手常问:“现在都用Spring Boot了,为啥还要啃Servlet?”答案很现实:Servlet是HTTP协议在Java侧的具象化翻译器。当你写doPost(HttpServletRequest req, HttpServletResponse resp)时,你就是在亲手拆解HTTP POST请求头、解析表单参数、设置响应状态码、写入Cookie。这种“手撕协议”的过程,是理解RESTful、JWT、跨域、CSRF防护的底层支点。

蛋糕商城里,每个核心动作都对应一个Servlet:

  • UserLoginServlet:处理登录表单,校验账号密码,存HttpSession.setAttribute("user", user)
  • AddToCartServlet:接收cakeId和quantity,操作session.getAttribute("cart")(一个Map<String, Integer>)
  • OrderSubmitServlet:开启事务,扣库存(UPDATE cakes SET stock = stock - ? WHERE id = ? AND stock >= ?),插入订单主表+明细表,失败则conn.rollback()

JSP不是过时技术,它是服务端模板引擎的原始形态。<c:forEach items="${cart}" var="item">背后是JSTL标签库对EL表达式的封装,而${item.cakeName}的解析过程,暴露了PageContext、HttpServletRequest、HttpSession三者作用域的优先级链。这些细节,在Spring Thymeleaf里被抽象掉了,但问题发生时——比如“购物车数量显示为null”——你得回到JSP的pageContext.findAttribute()逻辑里去debug。


3. 数据库设计与JDBC实战:从蛋糕表到订单事务的落地细节

3.1 MySQL建表:为什么cakes表要加stock字段和CHECK(stock >= 0)?

蛋糕商城最脆弱的环节不是前端样式,而是库存一致性。一张cakes表,必须同时满足业务语义和数据库约束:

CREATE TABLE cakes ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0), description TEXT, img_path VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id VARCHAR(32) PRIMARY KEY, -- UUID生成,避免暴露订单量 user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status ENUM('unpaid','paid','shipped','completed') DEFAULT 'unpaid', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id VARCHAR(32) NOT NULL, cake_id INT NOT NULL, quantity INT NOT NULL, price_per_unit DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (cake_id) REFERENCES cakes(id) );

重点看stock字段的CHECK(stock >= 0):这是数据库层面的兜底。即使Java代码里忘了判断库存是否足够,执行UPDATE cakes SET stock = stock - 1 WHERE id = 1时,若原值为0,MySQL会直接抛出CHECK constraint violated异常,事务回滚。这比在Service层写if (cake.getStock() < quantity) throw new Exception()更可靠——因为后者可能被并发请求绕过(两个线程同时读到stock=1,都判断通过,然后都扣减,最终stock=-1)。

3.2 JDBC连接池:为什么不用Class.forName().newInstance(),而用DBCP?

手写JDBC连接是教学必需,但生产级必须用连接池。HikariCP虽快,但对新手太黑盒;DBCP(Apache Commons DBCP 1.4)配置透明、报错友好,是教学首选。

在WEB-INF/classes/db.properties里写:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/cake_shop?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=123456 initialSize=5 maxActive=20 maxWait=60000 testOnBorrow=true validationQuery=SELECT 1

然后在ServletContextListener里初始化:

public class DBInitListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { try { Properties props = new Properties(); props.load(sce.getServletContext().getResourceAsStream("/WEB-INF/classes/db.properties")); BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName(props.getProperty("driverClassName")); ds.setUrl(props.getProperty("url")); ds.setUsername(props.getProperty("username")); ds.setPassword(props.getProperty("password")); ds.setInitialSize(Integer.parseInt(props.getProperty("initialSize"))); ds.setMaxActive(Integer.parseInt(props.getProperty("maxActive"))); ds.setMaxWait(Long.parseLong(props.getProperty("maxWait"))); ds.setTestOnBorrow(Boolean.parseBoolean(props.getProperty("testOnBorrow"))); ds.setValidationQuery(props.getProperty("validationQuery")); sce.getServletContext().setAttribute("dataSource", ds); } catch (Exception e) { throw new RuntimeException("DB init failed", e); } } }

逻辑说明:ServletContextListener在Tomcat启动时触发,把BasicDataSource实例存进ServletContext全局作用域。后续所有Servlet通过getServletContext().getAttribute("dataSource")获取连接池,避免重复创建。testOnBorrow=true确保每次借连接前执行SELECT 1检测连通性,防止数据库重启后连接池里全是失效连接。

3.3 事务控制:为什么OrderSubmitServlet里必须手动commit/rollback?

JavaWeb里没有@Transactional注解,事务必须手动控制。OrderSubmitServlet的伪代码骨架如下:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Connection conn = null; try { DataSource ds = (DataSource) getServletContext().getAttribute("dataSource"); conn = ds.getConnection(); conn.setAutoCommit(false); // 关键:关闭自动提交 // 1. 扣库存(带库存检查) String sql1 = "UPDATE cakes SET stock = stock - ? WHERE id = ? AND stock >= ?"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, quantity); ps1.setInt(2, cakeId); ps1.setInt(3, quantity); int rows = ps1.executeUpdate(); if (rows == 0) { throw new RuntimeException("库存不足"); } // 2. 插入订单主表 String sql2 = "INSERT INTO orders (id, user_id, total_amount, status) VALUES (?, ?, ?, ?)"; PreparedStatement ps2 = conn.prepareStatement(sql2); ps2.setString(1, orderId); ps2.setInt(2, userId); ps2.setBigDecimal(3, totalAmount); ps2.setString(4, "unpaid"); ps2.executeUpdate(); // 3. 插入订单明细 String sql3 = "INSERT INTO order_items (order_id, cake_id, quantity, price_per_unit) VALUES (?, ?, ?, ?)"; PreparedStatement ps3 = conn.prepareStatement(sql3); ps3.setString(1, orderId); ps3.setInt(2, cakeId); ps3.setInt(3, quantity); ps3.setBigDecimal(4, unitPrice); ps3.executeUpdate(); conn.commit(); // 全部成功才提交 req.setAttribute("msg", "下单成功"); req.getRequestDispatcher("/WEB-INF/jsp/success.jsp").forward(req, resp); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } req.setAttribute("msg", "下单失败:" + e.getMessage()); req.getRequestDispatcher("/WEB-INF/jsp/fail.jsp").forward(req, resp); } finally { if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

参数说明:

  • conn.setAutoCommit(false)是事务起点,之后所有SQL都在同一事务上下文;
  • UPDATE ... AND stock >= ?是乐观锁思想,避免先SELECT再UPDATE的竞态;
  • rollback()必须在catch块里显式调用,否则连接归还池后,未提交的变更仍挂在事务里,造成脏读;
  • conn.close()不是真的关闭,而是归还给DBCP连接池,所以必须放在finally里保证执行。

4. 前后端交互与状态管理:Session、Cookie与JSP EL的实战边界

4.1 购物车为什么必须存在Session里,而不是Cookie或URL参数?

用户把蛋糕加入购物车,这个行为必须跨多个HTTP请求保持状态。HTTP本身是无状态的,解决方案只有三种:URL重写、Cookie、Session。蛋糕商城选Session,原因很硬核:

  • URL重写:addtocart?cakeId=1&quantity=2&jsessionid=xxx,但URL长度有限(通常2KB),且敏感信息暴露在日志和浏览器历史里;
  • Cookie:Set-Cookie: cart={"1":2,"2":1},但Cookie有4KB上限,且每次请求都携带,浪费带宽;更致命的是,JSON字符串需encodeURIComponent()编码,JSP里取值极麻烦;
  • Session:数据存在服务器内存(或Redis),只传一个JSESSIONIDCookie(默认路径/,HttpOnly=true),既安全又高效。

实现上,AddToCartServlet这样写:

HttpSession session = req.getSession(true); // true表示不存在则创建 Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } String cakeId = req.getParameter("cakeId"); Integer quantity = Integer.parseInt(req.getParameter("quantity")); cart.put(cakeId, cart.getOrDefault(cakeId, 0) + quantity); // 注意:无需session.setAttribute("cart", cart),因为Map是引用类型,修改即生效

关键点:session.setAttribute("cart", cart)只需在首次创建时调用一次。后续对cartMap的put()操作,因Map对象本身存在Session中,所以直接生效。这是Java引用传递的特性,也是新手常踩的坑——以为每次都要setAttribute。

4.2 JSP EL表达式:为什么${cart[cake.id]}能取值,而${cart.get(cake.id)}会报错?

JSP EL(Expression Language)不是Java代码,它是独立的表达式语言,语法糖专为Web展示设计。${cart[cake.id]}能工作,是因为EL引擎会自动调用Map的get(Object key)方法;但${cart.get(cake.id)}会报错,因为EL不支持方法调用语法(除非用函数库,如fn:length())。

在/WEB-INF/jsp/cart.jsp里遍历购物车:

<c:forEach items="${cart}" var="entry"> <tr> <td>${entry.key}</td> <!-- cakeId --> <td>${cakes[entry.key].name}</td> <!-- 从requestScope的cakes Map里取蛋糕名 --> <td>${entry.value}</td> <!-- quantity --> <td>¥${cakes[entry.key].price * entry.value}</td> <td><a href="RemoveFromCartServlet?cakeId=${entry.key}">删除</a></td> </tr> </c:forEach>

这里${cakes[entry.key].name}依赖一个前提:在Servlet里已把Map<Integer, Cake>存入request.setAttribute("cakes", cakesMap)。EL的.和[]操作符会按pageContext → request → session → application顺序查找属性,所以cakes必须在request作用域,否则${cakes[entry.key]}返回null。

4.3 过滤器(Filter):如何用LoginFilter拦截未登录用户的订单页访问?

用户没登录就直击/order.jsp?必须拦。Filter是Servlet规范里的拦截器,比Spring AOP更底层。

写一个LoginFilter:

public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 放行静态资源和登录相关页面 String uri = request.getRequestURI(); if (uri.endsWith(".css") || uri.endsWith(".js") || uri.contains("/login") || uri.contains("/register")) { chain.doFilter(req, resp); return; } // 检查Session中是否有user HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { request.setAttribute("msg", "请先登录"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } chain.doFilter(req, resp); // 放行 } }

在web.xml里配置:

<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.cake.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/WEB-INF/jsp/*</url-pattern> <!-- 拦截所有JSP页面 --> </filter-mapping>

注意:<url-pattern>/WEB-INF/jsp/*</url-pattern>是关键。因为JSP放在/WEB-INF/jsp/下,外部无法直连,但Filter能拦截RequestDispatcher.forward()的内部跳转。这样,用户即使伪造URL访问/WEB-INF/jsp/order.jsp,也会被Filter捕获并重定向到登录页。


5. 避坑指南:IDEA运行JavaWeb项目最常见的5个血泪问题

5.1 现象:IDEA启动Tomcat后,浏览器打开http://localhost:8080显示404,但http://localhost:8080/index.jsp能访问

原因:web.xml里没配置欢迎文件列表(welcome-file-list),Tomcat找不到默认首页。
解决:在web.xml的<web-app>根节点内添加:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

注意:welcome-file值必须是web目录下的真实文件名,不能带路径(如/index.jsp会失败)。

5.2 现象:JSP页面里<c:forEach>标签报错org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved

原因:JSTL库缺失或TLD文件未被识别。Tomcat 9默认不带JSTL,需手动引入。
解决:下载jstl-1.2.jar和standard-1.1.2.jar(JSTL 1.2配套),放入web/WEB-INF/lib/;并在JSP顶部声明:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

血泪经验:别用Maven坐标引入JSTL,IDEA的Artifact打包常漏掉TLD文件;必须手动放jar包。

5.3 现象:MySQL连接成功,但执行SELECT * FROM cakes返回空结果集,而Navicat里明明有数据

原因:数据库字符集不一致。MySQL 8.0默认utf8mb4,而JDBC URL没指定characterEncoding=utf8。
解决:修改db.properties中的URL:

url=jdbc:mysql://localhost:3306/cake_shop?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8

提示:同时执行ALTER DATABASE cake_shop CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;确保库表字符集统一。

5.4 现象:登录成功后,HttpSession.setAttribute("user", user)存了,但在下一个页面session.getAttribute("user")返回null

原因:两次请求的Session ID不一致,本质是Cookie未正确发送。常见于:① 浏览器禁用Cookie;② IDEA的Tomcat配置里勾选了“On update action: Redeploy”导致Session重置;③ JSP页面没写<%@ page session="true" %>(虽然默认true,但显式声明更稳)。
解决:

  • 检查浏览器开发者工具→Application→Cookies,确认JSESSIONID存在且Path为/;
  • IDEA中:File → Project Structure → Artifacts → 选中war exploded → Output Layout → 右键WEB-INF/lib→ “Create Archive from Artifact” → 确保web.xml在正确位置;
  • 在所有JSP顶部加<%@ page session="true" %>。

5.5 现象:OrderSubmitServlet里conn.commit()后,数据库查不到新订单,但程序没报错

原因:MySQL的存储引擎不是InnoDB。MyISAM不支持事务,commit/rollback无效。
解决:执行SHOW CREATE TABLE orders;,若Engine=MyISAM,则转为InnoDB:

ALTER TABLE orders ENGINE=InnoDB; ALTER TABLE order_items ENGINE=InnoDB; ALTER TABLE cakes ENGINE=InnoDB;

玄学排查法:在conn.commit()后立即执行SELECT LAST_INSERT_ID(),如果返回0,说明事务根本没生效——立刻查存储引擎。


6. 进阶验证与调试技巧:让蛋糕商城从“能跑”到“可维护”

6.1 用Postman验证Servlet接口:绕过JSP,直击业务逻辑

JSP只是视图层,真正的业务在Servlet。用Postman发POST请求,能快速定位是前端传参问题,还是后端逻辑问题。例如测试登录:

  • Method: POST
  • URL:http://localhost:8080/UserLoginServlet
  • Body: x-www-form-urlencoded
    • username:admin
    • password:123456

观察响应:如果返回302 Found且Header里有Location: /index.jsp,说明登录成功并重定向;如果返回200 OK且Body是登录失败,说明校验逻辑触发。这比在浏览器里点登录按钮、看页面跳转再猜错在哪,效率高十倍。

6.2 日志分级:为什么要在web.xml里配<logger>,而不是用System.out.println?

System.out.println在Tomcat里输出到catalina.out,混杂着启动日志、GC日志,根本找不到你的调试信息。必须用SLF4J+Logback,但教学项目可简化:用java.util.logging,在web.xml里配置:

<context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/log4j.properties</param-value> </context-param>

更轻量的做法:在Servlet里用ServletContext.log():

public class UserLoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { getServletContext().log("UserLoginServlet: start login process"); // 输出到catalina.out,但带时间戳和类名 // ... 业务逻辑 getServletContext().log("UserLoginServlet: login success for user " + username); } }

优势:ServletContext.log()自动带时间戳、线程名、类名,且不会被Tomcat的ConsoleHandler吞掉,比System.out可靠得多。

6.3 数据库操作原子性验证:用SELECT ... FOR UPDATE模拟高并发抢购

教学项目常忽略并发场景。验证库存扣减是否真能防超卖,最简单方法是用MySQL命令行开两个窗口:

窗口1(模拟用户A):

BEGIN; SELECT stock FROM cakes WHERE id = 1 FOR UPDATE; -- 加行锁 -- 此时不COMMIT,让锁一直持有

窗口2(模拟用户B):

BEGIN; SELECT stock FROM cakes WHERE id = 1 FOR UPDATE; -- 会被阻塞,直到窗口1 COMMIT或ROLLBACK

然后在窗口1执行:

UPDATE cakes SET stock = stock - 1 WHERE id = 1 AND stock >= 1; COMMIT;

窗口2立刻获得锁,再次查询stock,发现已是0,UPDATE返回0行影响——证明并发控制生效。这个操作比写Java多线程测试简单十倍,且直击数据库本质。

6.4 项目可维护性 checklist:5个硬指标

一个“能跑”的蛋糕商城,离“可维护”还有距离。我验收自己写的项目,必查这5点:

检查项合格标准不合格表现
Servlet职责单一每个Servlet只处理一种HTTP动作(如UserLoginServlet只做登录,不处理注册)一个Servlet里if (action.equals("login")) {...} else if (action.equals("register")) {...}
SQL硬编码隔离所有SQL语句放在/WEB-INF/classes/sql.properties里,Servlet只通过key获取Servlet里直接写String sql = "SELECT * FROM users WHERE ...";
错误页面统一web.xml配置<error-page>,404/500都跳转到/error.jsp每个Servlet自己resp.sendError(404),页面五花八门
资源释放确定性Connection/PreparedStatement/ResultSet都在finally块里close(),且判空ps.close()前没判ps != null,NPE风险
JSP无Java脚本全用EL和JSTL,禁用<% %>和<%= %>index.jsp里有<% List<Cake> list = (List<Cake>) request.getAttribute("cakes"); %>

最后一句实话:我带过的实习生,凡是能把蛋糕商城的Session购物车、JDBC事务、Filter拦截这三关打通的,三个月内都能独立接小型Web需求。不是因为项目多难,而是它强迫你把HTTP、Java、SQL、HTML四条线拧成一股绳——绳子不断,路就通了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询