简介这是一套面向Java Web初学者与课程设计者的完整网上书店项目源码基于JSPServletDAO技术栈采用经典MVC分层架构适合用于毕业设计、课程实训或自学练手。压缩包共161个文件约768KB包含27个java源文件与27个class编译文件、17个jsp页面、64个gif及6个jpg图片资源另有js、xml、sql脚本与tld标签库等配置覆盖视图、控制、模型与数据库各层。项目实现了用户登录验证、图书搜索、分页浏览、购物车增删改与结算等核心功能数据库连接借助JNDI管理连接池分页通过SQL的LIMIT与OFFSET配合Servlet参数动态生成购物车状态则依托Session跟踪。已有169人学习下载读者可借此理解Servlet请求处理、DAO持久化、过滤器与自定义分页标签的协作方式并参考其目录组织与前端JavaScript、CSS交互实现快速搭建自己的Java Web电商应用。1. 从一份 JSPServlet 网上书店源码说起它到底能跑出什么如果你手头正拿着一份 jspservletdao 网上书店的源码包打开一看全是.jsp、.java和web.xml却不知道从哪下手那这篇拆解就是写给你的。这个项目本质上是一个典型的 Java Web 入门级电商系统用最原生的 JSP Servlet DAO 三层结构把「图书展示、分类浏览、加入购物车、下单结算、后台管理」这条完整链路跑通。它没有用 Spring、MyBatis 这些框架所有请求转发、数据库连接、事务控制都靠手写代码完成正因为如此它特别适合想搞懂 Java Web 底层请求流转的人——你能清楚看到一次点击从浏览器到数据库再回到页面的全过程。适合谁刚学完 Servlet 想找个完整项目练手的同学或者需要一套轻量级电商骨架做二次开发的人。它不花哨但结构清晰改起来心里有底。2. 项目结构与环境搭建从目录到 Tomcat 跑通第一屏2.1 目录结构长什么样拿到源码先别急着改代码把目录结构看明白后面找文件能省一半时间。这类原生 Java Web 项目通常遵循标准 Web 工程布局我按常见做法给你梳理一遍网上书店/ ├── src/ │ └── com/bookstore/ │ ├── dao/ # 数据访问层直接操作数据库 │ │ ├── BookDao.java │ │ ├── UserDao.java │ │ └── OrderDao.java │ ├── entity/ # 实体类和数据库表一一对应 │ │ ├── Book.java │ │ ├── User.java │ │ └── Order.java │ ├── servlet/ # 控制层接收请求、调 DAO、转发页面 │ │ ├── BookServlet.java │ │ ├── CartServlet.java │ │ └── OrderServlet.java │ └── util/ # 工具类数据库连接、分页等 │ └── DBUtil.java ├── WebContent/ 或 web/ │ ├── css/ js/ images/ # 静态资源 │ ├── jsp/ # 页面文件 │ │ ├── index.jsp │ │ ├── book_list.jsp │ │ ├── cart.jsp │ │ └── order_success.jsp │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器配置 │ │ └── lib/ # 第三方 jar 包 │ └── admin/ # 后台管理页面 └── database/ └── bookstore.sql # 建表脚本和初始数据这个结构里src下按dao、entity、servlet、util分包是原生 Java Web 最经典的分层方式。WebContent有些 IDE 里叫web放所有对外可访问的资源WEB-INF下的东西浏览器直接访问不到必须通过 Servlet 转发这也是为什么web.xml和lib要放里面。数据库脚本单独放database目录导入时直接执行就行。2.2 环境准备与数据库导入跑起来之前环境得先对齐。这个项目对版本不挑但组合不对照样报错。我一般用这套组件推荐版本说明JDK1.8 或 111.8 最稳高版本注意编译级别Tomcat8.5 或 9.0别用 10Servlet 命名空间变了MySQL5.7 或 8.08.0 要改驱动类和连接串IDEEclipse / IDEA能配 Tomcat 就行数据库导入是第一步。打开database/bookstore.sql里面通常包含建库、建表、插入初始图书数据三部分。在 MySQL 客户端里直接执行-- 创建数据库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4; USE bookstore; -- 图书表价格用 decimal 保证精度 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), category_id INT, stock INT DEFAULT 0, image VARCHAR(200) ); -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE, password VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) );执行完用SELECT * FROM book;确认有数据。如果中文显示成问号检查连接串有没有加useUnicodetruecharacterEncodingutf8这是血泪经验编码问题能卡你半天。2.3 数据库连接工具类 DBUtil原生项目没有连接池框架数据库连接全靠一个工具类。打开DBUtil.java核心就是加载驱动、拿连接、关资源public class DBUtil { // 数据库连接参数改成你自己的库名和密码 private static final String URL jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneUTC; private static final String USER root; private static final String PASSWORD 123456; // 静态块加载驱动只执行一次 static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源顺序不能反先 ResultSet 再 Statement 最后 Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里有几个参数必须改URL里的库名、USER和PASSWORD。MySQL 8.0 的驱动类是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver写错直接抛ClassNotFoundException。连接串里的serverTimezoneUTC在 8.0 下不加会报时区错误。close方法的关闭顺序不能乱先关结果集再关语句再关连接反了可能资源泄漏。2.4 部署到 Tomcat 并访问首页代码和数据库都就绪后在 IDE 里把项目部署到 Tomcat。Eclipse 里右键项目 → Properties → Project Facets确认 Dynamic Web Module 版本和 Tomcat 匹配IDEA 里在 Run Configuration 加 Tomcat LocalDeployment 选war exploded。启动后浏览器访问http://localhost:8080/网上书店/index.jsp能看到图书列表就说明通了。如果 404先看web.xml里welcome-file配的是不是index.jsp如果 500看控制台堆栈多半是数据库连不上或驱动没放进WEB-INF/lib。这一步跑通后面改代码才有反馈。3. 核心链路拆解一个请求从 JSP 到 DAO 再回页面的完整流转3.1 图书列表Servlet 如何调度 DAO 并转发 JSP首页图书列表是整个项目的入口也是理解请求流转最好的样本。用户在浏览器输入地址或点击分类请求先到BookServletServlet 调BookDao查数据库把结果塞进request再转发给book_list.jsp渲染。先看 ServletWebServlet(/book) public class BookServlet extends HttpServlet { private BookDao bookDao new BookDao(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取操作类型列表页默认 list String action request.getParameter(action); if (action null) action list; if (list.equals(action)) { // 可选按分类筛选 String categoryId request.getParameter(categoryId); ListBook books; if (categoryId ! null !categoryId.isEmpty()) { books bookDao.findByCategory(Integer.parseInt(categoryId)); } else { books bookDao.findAll(); } // 把数据放进 request 域转发后 JSP 能取到 request.setAttribute(bookList, books); request.getRequestDispatcher(/jsp/book_list.jsp).forward(request, response); } } }WebServlet(/book)是注解式映射等价于在web.xml里配url-pattern。action参数是这类项目的惯用手法一个 Servlet 处理多种操作靠参数分流。request.setAttribute存的数据只在一次请求内有效配合forward转发JSP 里用 EL 表达式${bookList}就能取到。注意这里用的是forward不是redirect转发是服务器内部跳转地址栏不变request 域数据还在重定向是两次请求数据会丢。3.2 DAO 层JDBC 查询与实体映射DAO 层干的是最脏最累的活——拼 SQL、执行、把 ResultSet 映射成实体对象。看BookDao.findAll()public class BookDao { public ListBook findAll() { ListBook list new ArrayList(); String sql SELECT * FROM book ORDER BY id DESC; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); // 遍历结果集逐行映射成 Book 对象 while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setName(rs.getString(name)); book.setAuthor(rs.getString(author)); book.setPrice(rs.getBigDecimal(price)); book.setStock(rs.getInt(stock)); book.setImage(rs.getString(image)); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return list; } }这里用PreparedStatement而不是Statement一是防 SQL 注入二是预编译效率高。rs.getBigDecimal(price)对应实体里的BigDecimal类型金额别用double精度会丢。finally块保证资源一定关闭这是 JDBC 的铁律。如果查询慢先看book表的category_id有没有建索引数据量大了全表扫描很要命。3.3 购物车Session 存储与数量增减购物车是电商的核心交互这个项目用Session存购物车简单直接。CartServlet负责加购、改数量、删除WebServlet(/cart) public class CartServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); HttpSession session request.getSession(); // 购物车用 Map 存key 是图书 idvalue 是数量 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } if (add.equals(action)) { int bookId Integer.parseInt(request.getParameter(bookId)); // 已存在就数量加一否则新增 cart.put(bookId, cart.getOrDefault(bookId, 0) 1); } else if (remove.equals(action)) { int bookId Integer.parseInt(request.getParameter(bookId)); cart.remove(bookId); } response.sendRedirect(request.getContextPath() /jsp/cart.jsp); } }session.getAttribute(cart)拿到的购物车在整个会话期间有效关浏览器才失效。用MapInteger,Integer存 id 和数量比存整个 Book 对象省内存展示时再根据 id 查详情。getOrDefault是 Java 8 的方法避免空指针。加购后sendRedirect重定向到购物车页防止用户刷新重复提交。这里有个坑如果用户没登录就加购Session 还在但下单时要校验登录状态否则订单不知道算谁的。3.4 下单流程事务控制与库存扣减下单是最容易出问题的地方涉及订单表插入和库存扣减必须在一个事务里。看OrderServlet的下单逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User user (User) session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /jsp/login.jsp); return; } MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 OrderDao orderDao new OrderDao(); int orderId orderDao.insertOrder(conn, user.getId()); for (Map.EntryInteger, Integer entry : cart.entrySet()) { orderDao.insertOrderItem(conn, orderId, entry.getKey(), entry.getValue()); // 扣库存库存不足返回 false 触发回滚 if (!orderDao.reduceStock(conn, entry.getKey(), entry.getValue())) { throw new SQLException(库存不足); } } conn.commit(); // 全部成功才提交 session.removeAttribute(cart); // 清空购物车 response.sendRedirect(request.getContextPath() /jsp/order_success.jsp); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); response.sendRedirect(request.getContextPath() /jsp/error.jsp); } finally { try { if (conn ! null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }关键点conn.setAutoCommit(false)开启事务后所有 DAO 操作共用同一个conn这样才能保证原子性。reduceStock里执行UPDATE book SET stock stock - ? WHERE id ? AND stock ?用stock ?条件防止超卖返回受影响行数为 0 就说明库存不够抛异常回滚。下单成功后清空 Session 里的购物车。如果这里不控制事务订单插了库存没扣或者扣了库存订单没插数据就乱了。这是电商项目最不能省的一步。4. 避坑与常见问题排查跑不起来先看这几条4.1 中文乱码从请求到响应全链路排查现象页面显示问号或乱码表单提交的中文存进数据库变乱码。原因通常有三处JSP 页面编码、请求编码、数据库编码。解决JSP 顶部加% page contentTypetext/html;charsetUTF-8 %Servlet 里在doGet/doPost第一行加request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)数据库连接串加characterEncodingutf8建库用utf8mb4。三处都对齐才彻底解决。4.2 404 错误路径与映射对不上现象访问某个页面或提交表单报 404。原因WebServlet的路径和表单action不一致或者 JSP 放在WEB-INF下被直接访问。解决表单action用${pageContext.request.contextPath}/book这种带上下文路径的写法别写死/bookWEB-INF下的 JSP 只能通过 Servlet 转发访问不能直接输地址。检查web.xml的url-pattern和注解有没有冲突。4.3 数据库连接失败驱动、时区、权限三连现象启动后第一次查库就抛SQLException。原因驱动类写错、连接串缺时区参数、MySQL 用户没远程权限。解决MySQL 8.0 用com.mysql.cj.jdbc.Driver连接串加serverTimezoneUTC确认WEB-INF/lib下有mysql-connector-java的 jar本地开发用root或单独建用户授权GRANT ALL ON bookstore.* TO userlocalhost。4.4 购物车数据丢失Session 超时与作用域现象加购后刷新页面购物车空了。原因Session 超时时间太短或者代码里误用了request域存购物车。解决在web.xml里配session-configsession-timeout30/session-timeout/session-config延长到 30 分钟购物车必须存session不是requestrequest一次请求就没了。如果用了集群部署Session 还得考虑共享原生项目一般单机跑不用管。4.5 下单后库存没变事务没生效现象订单生成了但库存还是原数。原因DAO 方法各自拿了新连接没共用事务里的conn。解决所有参与事务的 DAO 方法都要接收外部传入的Connection参数别在方法内部自己DBUtil.getConnection()。检查insertOrder、insertOrderItem、reduceStock是不是都用了同一个conncommit之前任何一步失败都要rollback。5. 二次开发改造从原生 JDBC 到连接池与分页优化5.1 引入 Druid 连接池替换 DBUtil原生DriverManager每次请求都新建连接并发一高就崩。改造第一步是换连接池。我一般用 Druid加个配置文件druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneUTC usernameroot password123456 initialSize5 maxActive20 maxWait3000然后改DBUtilpublic class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); // 从类路径加载配置 props.load(DBUtil.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 从池里拿不是新建 } }改完后getConnection从池里借连接close时归还而不是真关闭。maxActive20控制最大并发连接maxWait3000是拿不到连接等 3 秒就超时。这一步改完并发能力提升明显也不用每次请求都握手数据库。5.2 手写分页替换全量查询图书多了以后findAll一次查几千条页面卡数据库也累。分页改造分两步DAO 加limit查询和总数查询Servlet 算页码。// DAO 分页查询 public ListBook findByPage(int page, int pageSize) { ListBook list new ArrayList(); String sql SELECT * FROM book ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); // 起始偏移 ps.setInt(2, pageSize); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { // 映射逻辑同上略 } } catch (SQLException e) { e.printStackTrace(); } return list; } // 查总数用于算总页数 public int countAll() { String sql SELECT COUNT(*) FROM book; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } return 0; }Servlet 里接收page参数默认 1算出totalPage (count pageSize - 1) / pageSize把page、totalPage、bookList一起塞进 requestJSP 里渲染上一页下一页。LIMIT的第一个参数是偏移量公式(page-1)*pageSize别写错否则第一页会跳过数据。这里用了 try-with-resources连接和语句自动关闭比手写 finally 干净。5.3 改造验证与我的习惯改完别急着上线按这个顺序验证先单独跑findByPage(1, 5)看返回条数对不对再访问列表页翻到最后一页确认没有空页或重复然后并发加购下单看连接池监控里活跃连接数是否正常回落。我一般会在BookDao里留一个main方法做单元验证改完直接跑比启动 Tomcat 快得多。从那以后我每次接手这类原生 Java Web 项目都强制先跑通「列表 → 加购 → 下单」这条主链路再动任何一行代码。因为这条链路串起了 Servlet 调度、DAO 查询、Session 管理和事务控制它通了项目就活了它不通改再多页面也是白搭。希望这份拆解能帮你少走点弯路把这份 jspservletdao 网上书店源码真正跑起来、改起来。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?