首页 / 资讯中心 / 文章详情

Jsp+Bean+Servlet实战:从零构建网上购书系统

Jsp+Bean+Servlet实战:从零构建网上购书系统 ★ FEATURED ARTICLE
简介这是一套面向Java Web初学者与课程设计学习者的网上购书系统完整源码采用JSPJavaBeanServlet的经典B/S架构实现适合用于课程综合训练、毕业设计参考或Java Web入门练手。系统实现了用户注册、用户登录、图书信息查询与浏览等核心功能覆盖了从页面展示到业务处理再到数据访问的典型分层流程能帮助读者理解Servlet控制流程、JavaBean封装数据以及JSP页面渲染之间的协作方式。压缩包共55个文件约1.21MB包含15个java源文件与15个class编译文件、6个jsp页面、2个sql脚本、2个xml配置、2个properties资源文件以及jar依赖、项目配置与说明文档等结构完整、便于导入运行。目前已有173人学习下载。读者可据此快速搭建可运行的购书系统原型对照源码梳理注册登录与查询浏览的实现思路并借助SQL脚本与配置文件完成数据库初始化是Java Web入门阶段较为实用的参考案例。1. 从零手搓一个网上购书系统JspBeanServlet 到底能撑起多大的项目很多人第一次接触 Java Web都是从「网上购书系统」这个题目开始的。它几乎是高校课程设计和初级面试的标配有用户注册登录、有图书列表、有购物车、有订单业务闭环完整又不至于复杂到劝退。但真正动手时你会发现网上搜到的代码要么是半成品要么把 Jsp 当模板引擎用、把 Java 代码全塞进% %里跑起来能看改起来要命。这套基于 JspBeanServlet 的实现思路核心价值就在于把「页面展示」「业务数据」「请求调度」三层拆开Jsp 只负责渲染Bean 承载数据与业务Servlet 接管所有请求入口。它适合刚学完 Servlet 生命周期、想找一个完整项目练手的人也适合需要交付课程设计、又不想被框架黑匣子绕晕的开发者。下面我按实际搭建顺序把选型理由、目录结构、每个模块的代码和参数都摊开讲。2. 三层架构怎么落地Jsp 只做展示Bean 管数据Servlet 管调度2.1 为什么不用 Jsp 直接连数据库新手最容易翻车的地方就是在 Jsp 页面顶部写% page importjava.sql.* %然后一路Connection、Statement、ResultSet写到底。这样做在单页面测试时能跑通但一旦页面超过五个数据库连接代码就会散落在各处改一个表名要全局搜索替换连接忘记关闭还会导致连接池耗尽。更关键的是Jsp 本质是一个 Servlet它在第一次被访问时编译成 class 文件如果里面混入大量 Java 逻辑编译报错时你看到的堆栈会指向_jspService方法排查成本极高。常见做法是把数据库访问收敛到 DAO 层由 Bean 承载查询结果。Servlet 收到请求后调用 DAO把 Bean 放进request作用域再转发给 Jsp。Jsp 里只用 EL 表达式${book.title}取值不出现任何import和数据库代码。这样做的直接好处是Jsp 可以交给前端同学改样式Servlet 和 DAO 可以单独写单元测试三层各司其职。2.2 项目目录结构与依赖准备一个能跑起来的 Web 工程目录结构必须符合 Servlet 规范否则 Tomcat 找不到类。我一般会按下面这样组织bookstore/ ├── src/ │ └── com/bookstore/ │ ├── bean/ # 实体类Book、User、Order、CartItem │ ├── dao/ # 数据访问BookDao、UserDao、OrderDao │ ├── servlet/ # 控制器LoginServlet、BookListServlet 等 │ └── util/ # 工具类DBUtil 数据库连接 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射配置 │ │ └── lib/ # 第三方 jarmysql-connector-java │ ├── css/ │ ├── js/ │ ├── index.jsp # 首页 │ ├── login.jsp │ ├── bookList.jsp │ └── cart.jsp依赖只需要一个 MySQL 驱动 jar放进WEB-INF/lib下Tomcat 启动时会自动加载。如果你用 Maven在pom.xml里加mysql-connector-java和javax.servlet-api两个坐标即可注意servlet-api的 scope 要设为provided因为 Tomcat 自带了这个包重复打包会导致类冲突。2.3 数据库连接工具类的三个必调参数数据库连接是整套系统的命脉写一个DBUtil类集中管理连接比在每个 DAO 里重复写DriverManager.getConnection要可靠得多。下面是我常用的写法public class DBUtil { // 数据库驱动类名MySQL 8.x 用 com.mysql.cj.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; // 连接地址必须带上时区和字符集参数否则中文乱码 private static final String URL jdbc:mysql://localhost:3306/bookstore ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException(数据库驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序ResultSet - Statement - Connection反了会报错 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这里有三个参数必须调对。第一serverTimezone不写会报The server time zone value is unrecognizedMySQL 8 之后尤其明显。第二characterEncodingutf8要配合数据库建表时的CHARSETutf8mb4否则图书标题里的特殊符号会变成问号。第三Class.forName在静态代码块里执行一次即可不要每次获取连接都反射加载那样会拖慢响应。关闭顺序也要注意先关结果集再关连接反过来会抛异常。2.4 Bean 的实例化方式与作用域选择Bean 在这个系统里承担两种角色一种是数据载体比如Book类只有属性和 getter/setter另一种是业务对象比如Cart类里带addItem、removeItem方法。数据载体类的实例化通常由 DAO 在查询时new出来逐行填充业务对象则要看作用域。购物车 Bean 就是一个典型例子。如果你在 Servlet 里new Cart()然后放进request那每次请求购物车都是空的用户加购后刷新页面就丢。正确做法是放进sessionrequest.getSession().setAttribute(cart, cart)。这样同一个浏览器会话共享一个购物车实例。但要注意session里的 Bean 如果被多个线程同时修改会有并发问题。简单系统里可以在 Servlet 的doPost方法上加synchronized或者用ConcurrentHashMap存购物项。我一般会提醒Bean 放request还是session取决于数据要不要跨请求存活购物车、登录用户信息放session图书列表、订单详情放request。3. 核心模块编码从登录到下单的完整链路3.1 用户登录 Servlet 的请求校验与转发登录是第一个要跑通的闭环。前端login.jsp提交表单到LoginServletServlet 取参数、查数据库、判断结果、决定跳转。代码骨架如下WebServlet(/login) public class LoginServlet 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()) { req.setAttribute(msg, 用户名或密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { req.getSession().setAttribute(user, user); resp.sendRedirect(req.getContextPath() /bookList); } else { req.setAttribute(msg, 账号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这段代码里有两个容易忽略的点。req.setCharacterEncoding(UTF-8)必须放在getParameter之前放在之后无效。转发用forward浏览器地址栏不变跳转用sendRedirect地址栏会变。登录成功后用重定向是为了防止用户刷新页面时重复提交表单。失败时用转发是为了把错误信息带回原页面显示。3.2 图书列表分页查询的 SQL 与参数计算图书列表如果一次查全表数据量大了页面会卡。分页是必须做的。核心 SQL 用LIMIT实现SELECT id, title, author, price, stock, cover FROM book WHERE title LIKE ? ORDER BY id DESC LIMIT ?, ?;三个占位符分别对应模糊查询关键词、起始行号、每页条数。起始行号的计算公式是(当前页码 - 1) * 每页条数。比如每页 8 条第 3 页的起始行号是 16。Servlet 里这样处理int page 1; String pageParam req.getParameter(page); if (pageParam ! null pageParam.matches(\\d)) { page Integer.parseInt(pageParam); } int pageSize 8; int offset (page - 1) * pageSize; ListBook books bookDao.findByPage(keyword, offset, pageSize); int total bookDao.countByKeyword(keyword); int totalPages (total pageSize - 1) / pageSize; req.setAttribute(books, books); req.setAttribute(page, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/bookList.jsp).forward(req, resp);pageParam.matches(\\d)是防止用户手动改 URL 传入负数或字母导致 SQL 异常。totalPages用向上取整避免最后一页数据被截断。Jsp 里用${books}配合 JSTL 的c:forEach遍历不要用脚本片段。3.3 购物车的 Bean 设计与 session 同步购物车 Bean 我一般设计成MapInteger, CartItem结构key 是图书 IDvalue 是购物项。这样加购时判断是否已存在存在就数量加一不存在就新建。关键方法public class Cart { private MapInteger, CartItem items new LinkedHashMap(); public void add(Book book, int quantity) { CartItem item items.get(book.getId()); if (item null) { item new CartItem(); item.setBook(book); item.setQuantity(quantity); items.put(book.getId(), item); } else { item.setQuantity(item.getQuantity() quantity); } } public void remove(int bookId) { items.remove(bookId); } public double getTotalPrice() { double total 0; for (CartItem item : items.values()) { total item.getBook().getPrice() * item.getQuantity(); } return total; } }用LinkedHashMap而不是HashMap是为了让购物车里的图书按加入顺序展示用户体验更自然。getTotalPrice每次实时计算不缓存总价避免数量修改后总价不同步。Servlet 里加购时先从 session 取购物车取不到就新建再放回去这个「取-判-建-存」四步是 session 操作的标准套路。3.4 下单流程的事务控制与库存扣减下单是整个系统里唯一需要事务的地方插入订单、插入订单明细、扣减库存三步必须同时成功或同时失败。DAO 层不能各自获取连接否则事务无法控制。正确做法是把Connection从 Servlet 传到 DAOpublic class OrderService { public boolean createOrder(int userId, Cart cart) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 OrderDao orderDao new OrderDao(conn); int orderId orderDao.insertOrder(userId, cart.getTotalPrice()); for (CartItem item : cart.getItems().values()) { orderDao.insertOrderItem(orderId, item); // 扣库存时加 stock ? 条件防止超卖 int affected orderDao.reduceStock(item.getBook().getId(), item.getQuantity()); if (affected 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} return false; } finally { DBUtil.close(conn, null, null); } } }扣库存的 SQL 要写成UPDATE book SET stock stock - ? WHERE id ? AND stock ?第三个参数是购买数量。如果库存不足affected返回 0立即回滚。这个条件判断是防止并发下单超卖的关键比先查库存再扣减要可靠得多。4. 避坑与排查那些让系统跑不起来的细节4.1 中文乱码现象是页面显示问号原因是编码链没对齐现象登录时输入中文用户名数据库里存进去变成???或者 Jsp 页面显示乱码。原因通常有三处Jsp 页面头部没写% page contentTypetext/html;charsetUTF-8 %Servlet 里没调req.setCharacterEncoding(UTF-8)数据库连接 URL 缺characterEncodingutf8。解决方法是三处同时检查缺一不可。另外 Tomcat 8 之后 GET 请求默认用 UTF-8但 POST 请求仍要手动设置不要只改一处就以为好了。4.2 404 错误现象是访问 Servlet 报找不到原因是映射路径写错现象浏览器访问/login返回 404控制台没有报错。原因可能是web.xml里url-pattern写成了/LoginServlet而表单提交的是/login或者用了WebServlet注解但web.xml的metadata-completetrue导致注解被忽略。解决方法是先确认访问路径和映射路径完全一致再检查web.xml的版本声明Servlet 3.0 以上才支持注解。如果两个都配了以web.xml为准。4.3 数据库连接泄漏现象是跑一会儿就报 Too many connections现象系统刚启动正常操作几次后报Too many connections。原因是 DAO 里获取了Connection但异常路径下没关闭或者close方法只关了ResultSet忘了关Connection。解决方法是在finally块里统一关闭并且关闭顺序不能反。更稳妥的做法是用 try-with-resources但老项目里手动关闭更常见。每次写完 DAO 方法我都会扫一眼有没有finally。4.4 session 失效导致购物车清空现象是加购后跳转页面购物车空了现象用户加购后点击结算购物车显示为空。原因是加购的 Servlet 把Cart放进了request而不是session或者 Jsp 页面里用了sessionfalse。解决方法是确认加购、查看购物车、结算三个环节都从session取同一个Cart对象。另外 session 默认 30 分钟过期测试时如果间隔太久也会丢可以在web.xml里调session-timeout。4.5 Jsp 页面 EL 表达式不解析现象是页面直接显示${book.title}现象Jsp 页面把${book.title}原样输出没有替换成实际值。原因是web.xml的版本低于 2.4或者 Jsp 页面isELIgnoredtrue。解决方法是把web.xml声明改成 Servlet 3.0 以上的版本或者在页面头部加% page isELIgnoredfalse %。这个问题在新版 Tomcat 里少见但用老版本 Tomcat 6 时会遇到。5. 进阶技巧用过滤器统一编码和登录拦截系统跑通之后你会发现每个 Servlet 里都写一遍setCharacterEncoding很啰嗦而且有些页面没登录也能直接访问。这时候该上过滤器了。写一个EncodingFilter实现Filter接口在doFilter里统一设置请求和响应编码再写一个LoginFilter判断 session 里有没有 user没有就重定向到登录页。两个过滤器在web.xml里配置EncodingFilter放前面LoginFilter放后面顺序不能反。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); // 放行交给下一个过滤器或 Servlet } }LoginFilter里要放行登录页、登录 Servlet 和静态资源否则用户永远进不了登录页。判断逻辑是先看请求路径是否在白名单里不在就检查 sessionsession 没有 user 就重定向。白名单我一般用String[]存/login.jsp、/login、/css/、/js/这几个前缀用startsWith匹配。验证过滤器是否生效有个简单办法在doFilter里打印请求 URL 和 session ID观察控制台输出顺序。如果EncodingFilter的日志在LoginFilter之前说明顺序对了。另外注意过滤器只对匹配的 URL 生效WebFilter(/*)会拦截所有请求包括图片如果图片也走过滤器会拖慢加载可以在白名单里加上.css、.js、.png后缀判断。最后说一个我自己的习惯每写完一个模块先不急着写页面用curl或 Postman 直接打 Servlet 的 URL确认返回数据正确再写 Jsp 渲染。这样能把「后端逻辑错」和「前端展示错」分开排查省掉很多来回改的时间。这套 JspBeanServlet 的结构虽然老但把三层边界划清楚之后后面换 Spring 或换前端框架迁移成本比想象中低得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站