资讯详情

资讯详情

JSP+Servlet网上书店实战:环境搭建、购物车与事务处理全解析

简介这是一套基于JSPServletDAO模式构建的网上书店项目源码采用经典MVC分层设计适用于Java Web课程设计、毕业设计或入门进阶练习。项目覆盖用户登录验证、图书检索、购物车管理、分页展示等核心电商功能并通过JNDI配置数据库连接池结合MySQL脚本完成数据持久化。压缩包共包含161个文件以27个Java源码、27个Class编译文件、17个JSP页面为主辅以数十张GIF演示图、CSS/JS前端文件及SQL建库脚本整体大小仅768KB便于快速下载与部署。已有162人学习此资源适合希望在真实业务场景中理解Servlet控制器、DAO数据访问与前端交互的读者。通过阅读源码可掌握JSPServlet经典开发流程、页面分页实现思路及购物车会话管理技巧具有不错的参考价值。1. JSPServlet 网上书店这套老技术为什么还值得动手写「JSPServlet 网上书店」这个标题乍看像是十几年前课程设计的选题但真实情况是很多公司里跑着的内部系统、二手书交易平台的原型至今还是这套技术栈。它不像 Spring Boot 那样开箱即用但你亲手写完一个书店的注册、登录、分页、购物车、下单之后Servlet 生命周期、Session 机制、JDBC 事务这些底层概念会变成肌肉记忆后面再去碰任何 Java Web 框架都会觉得是在抄近道。这篇文章我按自己做书店系统的顺序来拆环境怎么搭、表怎么建、核心代码怎么写、哪些地方必踩坑以及做完之后怎么验证。想快速交作业的能直接抄想搞懂原理的也能找到解释。2. 搭骨架环境选型、四张表和一个干净的分层结构做 JSPServlet 项目最常见的翻车点不是代码写不出来而是环境版本没配对一启动 Tomcat 就报各种 ClassNotFound。这一章先把选型理由讲清楚再给数据库设计和项目分层结构。2.1 JDK、Tomcat、MySQL 怎么搭版本选错第一步就翻车从我自己的习惯说起。JDK 用 8 或 11Tomcat 用 8.5 或 9.0MySQL 用 5.7 或 8.0。这套组合是网上书店这类 CRUD 项目里被验证过最多次的搭配原因有三个第一JDK 8 和 Tomcat 9 对 javax.servlet 包的支持最稳定第二老教材和绝大多数网络教程都基于这套版本遇到问题搜得到答案第三Tomcat 10 已经换成了 jakarta.servlet 命名空间直接把老代码扔进去运行会报NoClassDefFoundError这个坑后面避坑章节专门讲。下载 Tomcat 时选 Core 下面的 zip 包解压就能用不要用安装版。配置环境变量时只需要JAVA_HOME指向 JDK 安装目录CATALINA_HOME指向 Tomcat 解压目录Path 里加上%CATALINA_HOME%\bin。启动后浏览器访问 8080 端口看到猫的页面说明环境就通了。Maven 不是必需品。如果只想快速跑通直接在 IDE 里新建 Dynamic Web Project把数据库驱动 jar 放进WEB-INF/lib就能开始写。但如果你习惯 Maven 管理依赖也可以在pom.xml里引入 servlet-api、jsp-api 和 mysql-connector-java注意这三个包的作用域要标成provided或compile否则部署到 Tomcat 时会和容器自带的类冲突。2.2 四张表设计book、user、order 与 order_item网上书店的核心业务就四张表多一张都算过度设计。用户要下单所以需要user用户要浏览所以需要book下单后要有订单主表和订单明细表订单主表存总金额和状态订单明细表存每一本书的购买数量和当时的单价。为什么要把订单和明细拆开因为一张订单可能包含好几本书如果全部塞到一张表里改一本的价格或数量会连带影响其他记录拆开后每张表职责单一。建表 SQL 如下直接复制到 MySQL 里执行即可CREATE DATABASE bookstore DEFAULT CHARSET utf8mb4; USE bookstore; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, category VARCHAR(30), cover_path VARCHAR(200), description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-未支付 1-已支付 2-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL DEFAULT 1, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT fk_item_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个需要注意的参数设计。price用DECIMAL(10,2)而不用double因为浮点数在金额计算上会出精度问题这个后面避坑章节细说。password字段长度给 64是因为我习惯存 MD5 加盐后的 32 位十六进制字符串但如果你后续想升级成 SHA-256 或 bcrypt64 的长度更从容。所有表的引擎都用 InnoDB因为下单扣库存需要事务支持MyISAM 不支持事务和行级锁做不了这个保证。orders表名我特意用了复数。order是 MySQL 的保留关键字直接用CREATE TABLE order会报语法错误新手经常在这句话上卡十分钟。如果你非要叫 order就得写成CREATE TABLE \order但我建议直接用 orders 绕开。2.3 项目目录为什么 Servlet 不能堆在一个包里网上书店项目规模不大但也不能把所有 Servlet 堆到同一个包里。我习惯按职责分五个包entity放实体类dao放数据库操作service放业务逻辑servlet放控制层util放工具类比如数据库连接、MD5 加密封装。JSP 页面按功能放两个目录/pages/user和/pages/admin分开避免前台和后台混在一起。src/ com.example.bookstore/ entity/ User.java Book.java Order.java OrderItem.java dao/ UserDao.java BookDao.java OrderDao.java service/ BookService.java OrderService.java servlet/ LoginServlet.java BookListServlet.java CartServlet.java OrderServlet.java util/ DBUtil.java MD5Util.java web/ pages/ user/ login.jsp register.jsp book_list.jsp cart.jsp order_confirm.jsp admin/ book_manage.jsp order_manage.jsp WEB-INF/ web.xml lib/ mysql-connector-java-x.x.x.jar为什么要分层你把 SQL 直接写在 Servlet 里确实能跑但一个页面要查图书列表、查分类、查库存代码会越堆越乱最后改一个需求要在一个 Servlet 里翻几百行。分层之后每层只干一件事DBUtil 管连接DAO 管 SQLService 管事务和业务判断Servlet 只做参数接收和页面跳转。网上书店这个体量不需要过度设计五层已经足够。这里有个实际问题service 层要不要在每个方法里都手动开事务我的习惯是在 OrderService 里处理下单逻辑时手动控制事务查询操作不强制事务因为查询不涉及数据一致性。这个第 4 章会给出具体代码。3. 登录和图书列表让第一个 Servlet 跑起来用户能不能进来图书能不能看到是一个书店系统最基础的两段流程。这一章的代码是后面购物车和订单的基础认真写一遍比看十遍都有用。3.1 注册与登录接口先绕过 JSP 直接看 Servlet 逻辑很多新手一上来就写 JSP 标签然后发现数据怎么都传不过去。我建议先写 Servlet用浏览器直接访问地址验证逻辑再写 JSP 页面去对接。注册的逻辑本质上是表单提交后把用户名密码写入 user 表登录的本质是拿表单数据和数据库比对成功就把用户信息放进 Session。注册 Servlet 的核心代码WebServlet(/register) public class RegisterServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { resp.sendRedirect(req.getContextPath() /pages/user/register.jsp?msgempty); return; } // 实际项目中这里应该用 BCrypt课程设计用 MD5 加盐也行 String hashedPwd MD5Util.md5WithSalt(password); boolean success userDao.insertUser(username, hashedPwd); if (success) { resp.sendRedirect(req.getContextPath() /pages/user/login.jsp); } else { resp.sendRedirect(req.getContextPath() /pages/user/register.jsp?msgduplicate); } } }这段代码里有几个关键参数需要说明。WebServlet(/register)是 Servlet 3.0 的注解写法省掉了在 web.xml 里配一堆servlet-mapping的麻烦前提是 web.xml 的版本声明要在 3.0 以上。req.setCharacterEncoding(UTF-8)必须在读取参数之前调用否则 POST 请求的中文会被容器按 ISO-8859-1 解码后面存进数据库全成问号。resp.sendRedirect后面跟的地址要加req.getContextPath()前缀否则部署到带应用名的路径下会 404。登录成功后有一个细节req.getSession().setAttribute(user, user)这行代码。Session 默认存活时间是 Tomcat 的web.xml里配置的 30 分钟从最后一次访问开始计算用户关掉浏览器再打开Session 就失效了得重新登录。这是网上书店里最常用的状态保持方式。3.2 图书分页pageNo、pageSize 与 LIMIT 的配合书店的书一多全查出来一次渲染太慢分页是必然需求。分页的核心就两个参数pageNo表示当前第几页pageSize表示每页多少条。从数据库查数据的 SQL 是LIMIT offset, pageSize其中offset (pageNo - 1) * pageSize。BookListServlet 的关键代码如下WebServlet(/book/list) public class BookListServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNo 1; int pageSize 12; String pageNoStr req.getParameter(pageNo); String category req.getParameter(category); if (pageNoStr ! null !pageNoStr.isEmpty()) { pageNo Integer.parseInt(pageNoStr); } String whereSql ; ListObject params new ArrayList(); if (category ! null !category.isEmpty()) { whereSql WHERE category ?; params.add(category); } int totalCount bookDao.countBooks(whereSql, params); int totalPages (totalCount pageSize - 1) / pageSize; pageNo Math.max(1, Math.min(pageNo, totalPages)); ListBook bookList bookDao.findBooks( whereSql, params, (pageNo - 1) * pageSize, pageSize ); req.setAttribute(bookList, bookList); req.setAttribute(pageNo, pageNo); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/pages/user/book_list.jsp).forward(req, resp); } }我一般把 pageSize 默认设成 12因为图书列表页常见的栅格布局是每行 4 本、3 行一屏翻页节奏刚好。注意totalPages的计算公式(totalCount pageSize - 1) / pageSize这是向上取整的经典写法除出来的结果不会少于一页。查询参数用ListObject params搭配WHERE category ?占位符目的只有一个防 SQL 注入。千万别用字符串拼接把 category 直接拼进 SQL用户传个 OR 11 --就能把你的书单全部拖出来。前端翻页按钮的地址长这样/book/list?category文学pageNo2。注意 category 是中文JSP 里拼这个 URL 时要用URLEncoder.encode先编码否则传到 Servlet 里取到的是乱码。这个坑我在正式环境见过不止一次。3.3 在 JSP 里渲染列表JSTL 和 EL 的入门写法Servlet 把数据 setAttribute 进 request 之后剩下的展示工作交给 JSP。JSP 页面里我会用 EL 表达式${book.bookName}和 JSTL 标签c:forEach来遍历不要在 JSP 里写 Java 脚本片段那会把页面结构搞得没法看甚至报错都难定位。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classbook-grid c:forEach varbook items${bookList} div classbook-card h3${book.bookName}/h3 p作者${book.author}/p p价格${book.price}/p a hrefcart/add?bookId${book.id}加入购物车/a /div /c:forEach /div使用 JSTL 有个隐藏依赖要先处理jstl.jar和standard.jar要放进WEB-INF/lib目录否则页面一访问就报错。如果你在 pom 里引了依赖但部署后找不到标签库注意 Tomcat 的 classpath 里有没有真正把这两个 jar 带上去。还有一个细节${book.price}的显示会直接输出数据库里的字节数比如49.00显示没有问题。但如果你的实体类里 price 是double这里可能输出49.0或者出现精度误差导致的48.999999要留意。这也是我坚持在数据库和实体类里用BigDecimal的原因。4. Session 购物车与下单事务书店里最值钱的两段逻辑购物车和下单是整个系统里逻辑最密集的部分也是面试或答辩时最容易被追问的两个模块。购物车方案选不好用户刷新一下页面东西就没了下单不控制事务库存和订单数据就会对不上。4.1 购物车对象放 Session一个 HashMap 撑起整个购物车购物车有几种设计方案存数据库、存 Cookie、存 Session。网上书店这个场景我选 Session理由是存数据库要单独建购物车表和明细表还要额外处理合并逻辑对这么轻量的业务来说太重存 Cookie 有 4KB 大小限制而且每次请求都要传输更麻烦Session 本身就是为「同一个用户的多次请求保持状态」设计的购物车这种临时数据放里面正合适。购物车类的实现public class Cart { // key: 图书ID, value: 购买数量 private MapInteger, Integer items new LinkedHashMap(); public void addBook(int bookId, int quantity) { items.put(bookId, items.getOrDefault(bookId, 0) quantity); } public void updateQuantity(int bookId, int quantity) { if (quantity 0) { items.remove(bookId); } else { items.put(bookId, quantity); } } public void removeBook(int bookId) { items.remove(bookId); } public SetInteger getBookIds() { return items.keySet(); } public int getQuantity(int bookId) { return items.getOrDefault(bookId, 0); } public boolean isEmpty() { return items.isEmpty(); } }用LinkedHashMap而不是HashMap是为了保持用户加购的顺序结账页显示的时候书目的顺序和用户操作一致体验更自然。购物车操作时不用立即查数据库判断库存用户加购的时候你查一次库存放到 Session 里就好点击提交订单时再查一次最终校验避免中间库存已经变了用户不知道。访问购物车的 Servlet 里每次操作前先用 Session 里拿购物车拿不到就 new 一个放进去HttpSession session req.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); }Session 里存购物车意味着用户一关浏览器购物车内容就没了。要不要做持久化看你的业务定位。如果是课程设计或内部系统不持久化完全合理省一张表如果是商业项目用户关浏览器再打开购物车还在是基本预期那就要在用户每次加购时同步写数据库登录状态下以数据库为准未登录时先用临时购物车登录后再合并。网上书店这个标题我建议先用 Session 方案跑通再按需考虑持久化。4.2 加购、改数量、删商品三个 Servlet 方法怎么写购物车操作的 URL 我习惯设计成/cart/add、/cart/update、/cart/remove三个地址共用一个 CartServlet通过action参数区分动作。这种写法比给每个动作建一个 Servlet 更省代码也方便统一处理 Session 获取。WebServlet(/cart/*) public class CartServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); String action path null ? : path.substring(1); HttpSession session req.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } String redirectUrl req.getContextPath() /pages/user/cart.jsp; if (add.equals(action)) { int bookId Integer.parseInt(req.getParameter(bookId)); int stock bookDao.getStock(bookId); if (stock 0) { resp.sendRedirect(redirectUrl ?msgoutOfStock); return; } cart.addBook(bookId, 1); } else if (update.equals(action)) { int bookId Integer.parseInt(req.getParameter(bookId)); int quantity Integer.parseInt(req.getParameter(quantity)); cart.updateQuantity(bookId, quantity); } else if (remove.equals(action)) { int bookId Integer.parseInt(req.getParameter(bookId)); cart.removeBook(bookId); } resp.sendRedirect(redirectUrl); } }WebServlet(/cart/*)这种通配写法会把/cart/add、/cart/update、/cart/remove都映射到同一个 Servlet然后用getPathInfo()拿到斜杠后面的字符串。网上书店的购物车操作用 GET 请求就够了但如果做的是正式商城加购这种改变数据的操作应该用 POST避免浏览器预加载或爬虫误触发。加购时要查一下库存这个查询是「软校验」只用来在用户操作时给出友好提示。真正的硬校验放在提交订单时因为从加购到下单的过程中别的用户可能已经把那本书买走了。数量参数如果不是数字Integer.parseInt会抛异常这里可以先 catch 住并重定向到错误页或者用正则预校验避免用户手改 URL 导致 500。4.3 下单扣库存一个事务里完成的原子操作下单是整个系统里对数据一致性要求最高的一步。一次下单至少要写三张表orders 插入一条订单主记录order_item 插入若干条明细book 表把对应图书的库存扣减。这三步必须在一个事务里完成否则用户提交订单时扣库存成功但插入订单失败钱收了货没发出去或者订单写进去了库存没扣超卖之后来一个投诉一个。用 JDBC 控制事务的关键代码public int createOrder(int userId, Cart cart) { String insertOrder INSERT INTO orders (user_id, total_price, status) VALUES (?, ?, 1); String insertItem INSERT INTO order_item (order_id, book_id, price, quantity) VALUES (?, ?, ?, ?); String deductStock UPDATE book SET stock stock - ? WHERE id ? AND stock ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启手工事务 BigDecimal total BigDecimal.ZERO; for (int bookId : cart.getBookIds()) { Book book bookDao.findBookById(conn, bookId); int quantity cart.getQuantity(bookId); if (book.getStock() quantity) { throw new RuntimeException(图书《 book.getBookName() 》库存不足); } BigDecimal subtotal book.getPrice().multiply(BigDecimal.valueOf(quantity)); total total.add(subtotal); } PreparedStatement psOrder conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setInt(1, userId); psOrder.setBigDecimal(2, total); psOrder.executeUpdate(); ResultSet rs psOrder.getGeneratedKeys(); int orderId 0; if (rs.next()) { orderId rs.getInt(1); } for (int bookId : cart.getBookIds()) { Book book bookDao.findBookById(conn, bookId); int quantity cart.getQuantity(bookId); PreparedStatement psItem conn.prepareStatement(insertItem); psItem.setInt(1, orderId); psItem.setInt(2, bookId); psItem.setBigDecimal(3, book.getPrice()); psItem.setInt(4, quantity); psItem.executeUpdate(); PreparedStatement psStock conn.prepareStatement(deductStock); psStock.setInt(1, quantity); psStock.setInt(2, bookId); psStock.setInt(3, quantity); int affected psStock.executeUpdate(); if (affected 0) { throw new RuntimeException(图书《 book.getBookName() 》库存不足下单失败); } } conn.commit(); return orderId; } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw new RuntimeException(创建订单失败 e.getMessage(), e); } finally { if (conn ! null) { conn.setAutoCommit(true); DBUtil.close(conn); } } }这段代码里最值得讲的是扣库存的 SQLUPDATE book SET stock stock - ? WHERE id ? AND stock ?。这个写法不是普通的先查再减而是把「库存是否够」作为更新条件交给数据库判断。executeUpdate返回 0 表示条件不满足说明库存已经被别的请求抢没了直接抛异常回滚整个事务。这样就避免了先SELECT stock查到有货、再UPDATE扣减时库存已变化的竞态问题。配合 InnoDB 的行锁两个用户同时下单同一本书时数据库会让他们排队更新不会出现两个订单共用最后一件库存的情况。事务里有几个参数值得注意。conn.setAutoCommit(false)之后的每一个 SQL 都不会真正落盘直到conn.commit()中间任何一步抛异常conn.rollback()把前面写的数据全部撤回。finally里先setAutoCommit(true)再关闭连接是因为连接池复用时如果返回池里的连接还处于手动提交状态下一个用户拿到手会出现「提交了别人没提交的事务」这种玄学问题。getGeneratedKeys可以拿到数据库自增生成的订单号这个订单号后续要展示给用户作为凭证。5. 网上书店开发避坑乱码、超卖与部署翻车现场下面的坑全部来自实际开发里见过或踩过的情况。每一条都按「现象、原因、解决」的顺序写遇到类似问题可以直接对照排查。5.1 POST 提交中文乱码Filter 能一次解决就不要每个 Servlet 重复设置现象注册页面输入中文用户名提交后数据库里存的是???或者 JSP 页面显示乱码。原因HTTP 请求的正文默认按 ISO-8859-1 解码JSP 页面提交的 POST 表单是 UTF-8 编码的两边对不上中文就变成问号。很多人知道在 Servlet 里写req.setCharacterEncoding(UTF-8)但每个 Servlet 的 doPost 里都写一遍又累又容易漏。解决写一个过滤器统一处理一个类搞定所有 ServletWebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html; charsetUTF-8); chain.doFilter(req, resp); } }注意WebFilter(/*)的过滤范围是全部请求千万别只写成/servlet/*之类的否则从页面直接访问 JSP 文件时绕过了过滤器中文照样乱。还有一个坑是resp.setContentType要在getWriter()之前设置否则响应头已经发出去了再设置就不生效了。response 的中文乱码问题往往比 request 更隐蔽因为 JSP 页面本身有pageEncoding属性两者要同时保持一致。5.2 Tomcat 10 跑老代码javax.servlet 和 jakarta.servlet 的坑现象代码在本地 Tomcat 9 上跑得好好的部署到服务器上启动就报ClassNotFoundException: javax.servlet.ServletException或者页面访问直接 500。原因Tomcat 10 开始把 Servlet 的包名从javax.servlet迁移到了jakarta.servlet这是 Oracle 把 Java EE 交给 Eclipse 基金会后做的品牌和命名空间调整。你的代码里还在import javax.servlet.http.HttpServletTomcat 10 里根本没有这个类当然找不到。解决网上书店这种旧项目直接用 Tomcat 9。如果你非要部署到 Tomcat 10 上要么把所有javax.servlet替换成jakarta.servlet包括依赖里的 servlet-api、jsp-api以及 JSTL 也要换成对应版本。但jakarta改名不是全局替换能解决的TagLib 的 URI 也会变你还要检查 web.xml 头部的 schema 版本。我的建议是不是非用不可就别给自己找麻烦Tomcat 9 再战几年完全没问题。5.3 价格计算用 double金额精度翻车现场现象订单总金额偶尔出现49.9999999999或者48.989999999999995这种脏数据写入数据库四舍五入后金额对不上账。原因计算机用二进制表示浮点数double无法精确表示 0.1 这样的十进制小数。单次计算误差小但累加多了误差就会暴露。书店系统里价格、数量、总价多次相乘相加误差就被放大了。解决从数据库字段到 Java 实体类的 price 类型一律用BigDecimal。BigDecimal.valueOf(2.5)比new BigDecimal(2.5)更精确前者直接按字符串转换后者把 double 的二进制值原样拿出来会出现2.500000000000000444...。计算时用multiply、add方法不要用double运算后强转。对用户展示时想保留两位小数JSP 里用fmt:formatNumber value${book.price} pattern0.00/。这套组合拳打下来金额问题就断了根。5.4 sendRedirect 后 Session“丢了”先检查浏览器 Cookie现象用户登录成功后跳转到首页页面没有报错但显示的还是未登录状态在同一个浏览器里其他功能也一直拿不到 Session 里的用户信息。原因Session 依赖 Cookie 保存一个JSESSIONID。sendRedirect本身不会丢 Session问题是目标地址跨了域名或端口浏览器按域名隔离 Cookie新的域名下没有 JSESSIONID服务器就认为这是一个新会话。还有一种可能是你登录成功后在代码里调用了session.invalidate()然后重定向Session 确实被自己清掉了。解决先用浏览器开发者工具看 Application 面板里的 Cookie检查 JSESSIONID 是否存在、有没有设置Path或Domain属性。如果重定向到/book/list没问题但跳转到/pages/user/index.jsp就丢那大概率是 Tomcat 配置了 sessionCookiePath把它改回/。代码层面登录成功后的重定向地址统一用req.getContextPath()拼出来别写死路径。6. 加一个登录检查过滤器验证代码并走向框架思维书店项目做完整套可以顺手加一个登录检查过滤器。这步不需要很复杂十几行代码但能让你把「过滤器」这个 Servlet 规范里的强大组件用熟还能验证 Session 是否正常工作。WebFilter(/pages/user/*) public class LoginCheckFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); boolean loggedIn session ! null session.getAttribute(user) ! null; String path request.getRequestURI().substring(request.getContextPath().length()); if (!loggedIn !path.endsWith(login.jsp) !path.endsWith(register.jsp)) { response.sendRedirect(request.getContextPath() /pages/user/login.jsp); return; } chain.doFilter(req, resp); } }这个过滤器的作用是把未登录用户挡在pages/user目录之外。request.getSession(false)里那个false很关键如果传true会自动创建一个新 Session那未登录用户每次访问都会新建 Session判断永远是「没有登录」而且会白白生成大量无效 Session。改用过滤器的思路后登录逻辑里就不需要每个 Servlet 都去检查了。验证方法很简单登录后访问购物车页正常退出浏览器再直接访问购物车 URL会被重定向到登录页。这套 JSPServlet 书店做下来最值钱的不是代码本身而是你终于理解了 HTTP 请求从浏览器到 Servlet、再到 JSP 渲染回浏览器的完整回路。之后再上手 Spring MVC你会发现DispatcherServlet本质就是一个超级过滤器加转发器Controller里的方法跟你写的 BookListServlet 的 doGet 承担的是同样的职责。带着这层理解去学框架看源码都会有底气。我从这个项目里收获最大的一个教训就是别急着追逐新技术框架把 Servlet 和 Session 这些底层机制亲手写一遍后面遇到什么问题都能一眼看穿。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →