
简介这是一套面向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跟踪。已有162人学习下载读者可从中掌握Servlet请求处理、DAO持久化、过滤器与自定义标签等关键知识点并参考其目录组织与代码分层方式快速搭建自己的Java Web电商应用。1. JSPServlet 网上书店为什么它仍是理解 Java Web 的最佳练手项目如果你正在搜「jspservlet网上书店」大概率不是想听 MVC 发展史而是想找一个能跑起来、能写进简历、能讲清楚请求怎么从浏览器走到数据库再走回来的完整项目。JSP Servlet 这套组合看起来老但它把 HTTP 请求、会话管理、JDBC 连接、页面渲染这几件事全摊在明面上没有 Spring 帮你藏黑匣子。你写一个登录功能就得自己处理doPost、自己拿HttpSession、自己拼 SQL。这种「麻烦」恰恰是它最大的价值每一个环节你都能看到。网上书店这个场景选得也准。它天然包含用户、商品、订单三类核心实体覆盖了列表查询、详情展示、加入购物车、下单结算这条完整链路。你把这套东西用 JSP Servlet 写一遍再去看 Spring Boot会发现那些注解和自动配置不过是在帮你省掉重复代码底层逻辑你早就摸过了。这篇文章面向的是想动手的人在校学生做课设、转行的人补 Java Web 基础、或者工作几年但一直用框架、想回头把底层补上的开发者。接下来我会按「环境怎么搭 → 数据库怎么设计 → 核心功能怎么写 → 坑在哪 → 怎么验证」的顺序把每一步的参数和代码都落到能直接抄的程度。2. 环境搭建与项目骨架从零到第一个能跑的 Servlet2.1 JDK、Tomcat、IDE 的版本选择与目录结构版本选择这件事新手最容易在这里翻车。我一般推荐 JDK 8 或 JDK 11Tomcat 用 8.5 或 9.0。原因很直接JDK 8 之后的模块化系统会让一些老教程里的反射代码报错而 Tomcat 10 把javax.servlet包名改成了jakarta.servlet你照着网上大部分教程抄的 import 语句会全部标红。这不是玄学是包名真的换了。所以如果你搜到的教程用的是javax.servlet.*就老老实实配 Tomcat 9。目录结构按标准 Web 项目来不要自创。src/main/java放 Servlet 和 DAOsrc/main/webapp放 JSP 和静态资源WEB-INF下放web.xml和lib。如果你用 Mavenpom.xml里引入javax.servlet-api时记得把 scope 设成provided因为 Tomcat 自己带了这份 jar你打包时再带一份进去会冲突。!-- pom.xml 关键依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope !-- Tomcat 已提供打包时不重复引入 -- /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependencyprovided这个 scope 是重点。很多人本地跑没问题一部署到服务器就报ClassCastException或者类加载冲突就是因为把 servlet-api 打进了 war 包。JSTL 则是你在 JSP 里写c:forEach的前提不加这个依赖标签库会直接不解析。2.2 用 Maven 建 Web 项目并配置 Tomcat 运行建项目时选maven-archetype-webapp模板它会帮你生成webapp目录和web.xml。生成后第一件事是检查web.xml的版本声明4.0 的 schema 和 2.3 的写法差别很大用错了welcome-file-list可能不生效。# 用 Maven 命令行建项目也可以直接在 IDE 里选模板 mvn archetype:generate -DgroupIdcom.bookstore \ -DartifactIdbookstore -DarchetypeArtifactIdmaven-archetype-webapp \ -DinteractiveModefalse建完之后在 IDE 里配置 TomcatRun Configuration 里新增 Tomcat ServerDeployment 选项卡把bookstore:war exploded加进去Application context 设成/bookstore。用 exploded 模式而不是 war 包是因为改 JSP 不用重启改 Java 代码热部署也快。这一步做完启动 Tomcat浏览器访问http://localhost:8080/bookstore/能看到 index.jsp 的内容环境就算通了。2.3 第一个 Servlet请求怎么进来、响应怎么出去写一个HelloServlet继承HttpServlet重写doGet。这里的关键是理解WebServlet注解和web.xml配置二选一不要两个都写否则可能映射到同一个 URL 导致启动报错。WebServlet(/hello) // URL 映射访问 /hello 时进入这个 Servlet public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setAttribute(msg, 书店首页加载成功); // 存数据到 request 域 req.getRequestDispatcher(/index.jsp).forward(req, resp); // 转发到 JSP } }forward和redirect的区别是新手第一个必须搞懂的点。forward 是服务器内部跳转浏览器地址栏不变request 域里的数据能带过去redirect 是告诉浏览器「你去访问另一个地址」地址栏会变request 数据丢失。书店项目里查询完图书列表用 forward 到展示页登录成功后用 redirect 防止刷新重复提交这两个场景要分清楚。3. 数据库设计与实体建模图书、用户、订单三张核心表3.1 表结构设计与字段类型选择网上书店的数据模型不复杂但字段类型选错会埋雷。价格用DECIMAL(10,2)不要用FLOAT浮点数算钱会出现0.10.20.30000000000000004这种问题下单金额对不上就是血泪教训。库存用INT并加NOT NULL DEFAULT 0避免空指针。图书封面的图片路径存VARCHAR(255)存相对路径不存绝对路径换服务器时不用改数据。CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, cover_img VARCHAR(255), -- 存相对路径如 /img/cover/1.jpg category_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;utf8mb4而不是utf8因为 MySQL 的utf8其实是阉割版存不了 emoji 和部分生僻字。图书标题里如果有特殊符号用utf8会直接报错或者存成问号。用户表的密码字段至少VARCHAR(64)因为你要存哈希值而不是明文明文存密码是安全底线问题。3.2 JDBC 工具类封装与连接池配置每个 DAO 方法都写一遍DriverManager.getConnection是新手常见写法但这样每次请求都新建连接并发一上来数据库就扛不住。正确做法是封装一个DBUtil用连接池。Druid 或 HikariCP 都行我一般用 Druid配置直观。public class DBUtil { private static DataSource dataSource; static { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); ds.setUsername(root); ds.setPassword(your_password); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大连接数 ds.setMaxWait(3000); // 获取连接超时 3 秒 dataSource ds; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }serverTimezoneAsia/Shanghai这个参数不加MySQL 8 驱动会报时区错误这是最高频的翻车点之一。useSSLfalse在本地开发时关掉避免证书警告。连接池的maxActive设成 20 是保守值课设场景够用生产环境要根据数据库的max_connections反推。3.3 DAO 层增删改查以图书查询为例DAO 层只做数据访问不掺业务逻辑。查询全部图书和按条件查询可以合并成一个方法用动态拼 SQL 的方式处理。public ListBook findAll(String keyword) { ListBook list new ArrayList(); StringBuilder sql new StringBuilder(SELECT * FROM book WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND title LIKE ?); params.add(% keyword.trim() %); } sql.append( ORDER BY id DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); // 参数从 1 开始不是 0 } ResultSet rs ps.executeQuery(); while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setTitle(rs.getString(title)); b.setPrice(rs.getBigDecimal(price)); b.setStock(rs.getInt(stock)); list.add(b); } } catch (SQLException e) { e.printStackTrace(); } return list; }用PreparedStatement而不是Statement拼接字符串是为了防 SQL 注入。ps.setObject的索引从 1 开始这是 JDBC 的规矩写成 0 会报Parameter index out of range。WHERE 11是个小技巧方便后面无脑append( AND ...)不用判断是不是第一个条件。4. 核心功能实现登录、购物车、下单的完整链路4.1 用户登录与 Session 会话管理登录流程是表单 POST 到 LoginServlet → 查数据库比对密码 → 成功则把用户对象存进HttpSession→ 重定向到首页。密码比对要用哈希不能明文比。注册时用MessageDigest做 MD5 加盐登录时同样处理再比对。protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(md5(password user.getSalt()))) { req.getSession().setAttribute(currentUser, user); // 登录态存 Session resp.sendRedirect(req.getContextPath() /book/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } }Session 默认 30 分钟过期可以在web.xml里改。登录成功后用 redirect 而不是 forward是为了避免用户刷新页面时重复提交表单。判断是否登录可以在 JSP 里用${sessionScope.currentUser ! null}也可以在 Filter 里统一拦截。4.2 购物车Session 存储还是数据库存储购物车有两种做法存 Session 里或者存数据库表。课设场景我建议存 Session因为不用建表、不用处理并发逻辑简单。用一个MapInteger, CartItem存在 Session 里key 是图书 idvalue 是数量和图书信息。// 加入购物车 MapInteger, CartItem cart (MapInteger, CartItem) req.getSession().getAttribute(cart); if (cart null) { cart new HashMap(); req.getSession().setAttribute(cart, cart); } int bookId Integer.parseInt(req.getParameter(bookId)); CartItem item cart.get(bookId); if (item null) { Book book bookDao.findById(bookId); item new CartItem(book, 1); cart.put(bookId, item); } else { item.setQuantity(item.getQuantity() 1); // 已存在则数量加一 } resp.sendRedirect(req.getContextPath() /cart.jsp);存 Session 的代价是换设备购物车就没了而且服务器重启 Session 丢失。如果要做「登录后购物车合并」就得在登录成功时把 Session 购物车写进数据库表。这个取舍看你的需求课设演示用 Session 足够。4.3 下单流程与事务处理下单是最容易出问题的地方。你需要同时做三件事插入订单主表、插入订单明细、扣减库存。这三步必须在一个事务里否则可能出现订单生成了但库存没扣或者库存扣了订单没生成。public boolean createOrder(int userId, MapInteger, CartItem cart) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 int orderId orderDao.insertOrder(conn, userId, totalAmount); for (CartItem item : cart.values()) { orderDao.insertOrderItem(conn, orderId, item); int rows bookDao.reduceStock(conn, item.getBook().getId(), item.getQuantity()); if (rows 0) { conn.rollback(); // 库存不足回滚 return false; } } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) {} return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) {} } }reduceStock的 SQL 要写成UPDATE book SET stock stock - ? WHERE id ? AND stock ?把库存判断放进 SQL 里靠数据库的行锁保证并发安全。如果先查再扣两个请求同时进来会超卖。rows 0说明库存不够直接回滚。事务里的 Connection 必须手动管理不能从连接池拿了就自动关闭。5. 避坑与排查JSPServlet 项目里最容易翻车的 5 个地方5.1 中文乱码POST 和 GET 处理方式不同现象是表单提交的中文在数据库里变成问号或者页面显示乱码。原因是 Tomcat 默认用 ISO-8859-1 解码。POST 请求在 Servlet 里第一行加req.setCharacterEncoding(UTF-8)就能解决。GET 请求的参数在 URL 里Tomcat 8 之后默认 URIEncoding 是 UTF-8但如果你用的老版本需要在server.xml的 Connector 里加URIEncodingUTF-8。响应端还要加resp.setContentType(text/html;charsetUTF-8)。三个地方都设了乱码基本就没了。5.2 JSP 页面报 404路径到底该怎么写现象是点击链接跳转后 404。原因是 JSP 里的相对路径是相对于当前 URL 的不是相对于文件位置。比如你在/book/list页面里写a hrefdetail.jsp?id1浏览器会请求/book/detail.jsp但文件实际在/detail.jsp。解决办法是用${pageContext.request.contextPath}拼绝对路径或者用c:url标签。在 JSP 顶部加base href${pageContext.request.contextPath}/也能统一处理但要注意锚点链接会受影响。5.3 数据库连接泄漏连接池耗尽后请求全部卡死现象是项目跑一段时间后所有请求都超时日志里报wait millis 3000, active 20。原因是 DAO 里拿了 Connection 没关或者 ResultSet、PreparedStatement 没关。用 try-with-resources 语法能自动关闭但要注意 Connection 从连接池拿的时候close 是归还不是真关闭。检查方法是在 Druid 配置里打开removeAbandonedtrue和removeAbandonedTimeout180让连接池自动回收泄漏的连接但这只是补救根本还是要写对关闭逻辑。5.4 Session 失效与重复登录用户状态丢失现象是用户操作到一半突然跳回登录页或者同一账号在两个浏览器登录后互相踢下线。Session 默认 30 分钟过期可以在web.xml里用session-configsession-timeout60/session-timeout/session-config改成 60 分钟。如果要做单点登录限制需要在登录时把 Session ID 存到数据库或缓存新登录时让旧 Session 失效。课设场景一般不用做这么复杂但要知道 Session 是基于 Cookie 的 JSESSIONID 来识别的浏览器禁用 Cookie 就会失效。5.5 图片上传路径存哪儿、怎么读现象是上传的封面图在本地能显示部署到服务器就 404。原因是把图片存到了项目目录里重新部署 war 包时目录被覆盖。正确做法是存到项目外的固定目录比如/data/bookstore/upload/然后在 Tomcat 里配置虚拟路径映射或者写一个ImageServlet从磁盘读流输出。数据库里只存文件名不存完整路径读的时候动态拼。这样迁移服务器时只需要拷贝图片目录不用改数据。6. 进阶验证用 Postman 和日志把每个接口测透项目能跑起来只是第一步你得知道它为什么能跑、哪里可能跑挂。我习惯用 Postman 把每个 Servlet 的接口单独测一遍不依赖前端页面。比如登录接口直接 POSTusername和password看返回的 Cookie 里有没有 JSESSIONID再看重定向地址对不对。购物车接口测并发加购看数量有没有算错。下单接口测库存为 0 时能不能正确回滚。日志方面不要只用e.printStackTrace()。在 DAO 层用Logger记录 SQL 执行时间和参数在 Servlet 层记录请求 URL 和 Session ID。这样出问题时能快速定位是数据库慢还是业务逻辑错。我一般会在DBUtil里加一个拦截器统计每次查询耗时超过 500ms 就打警告。long start System.currentTimeMillis(); // ... 执行查询 ... long cost System.currentTimeMillis() - start; if (cost 500) { logger.warn(慢查询: {} 耗时 {}ms, sql, cost); }验证事务是否生效可以故意在扣库存后抛一个异常看订单表里有没有脏数据。验证连接池配置可以用 JMeter 开 50 个线程同时请求列表页观察响应时间和错误率。这些测试做完你对这套 JSP Servlet 的边界就有感觉了它能扛住课设演示的并发但真要做生产级书店连接池、缓存、SQL 优化一个都不能少。最后说个我自己的习惯每写完一个功能先把 Tomcat 停掉再启动一次看有没有报错。热部署有时候会骗你类没重新加载但你以为生效了重启一次能省掉很多「明明改了代码怎么没效果」的后悔药。这套东西不复杂但细节多耐心把每个坑踩一遍比看十篇教程都管用。希望帮到你。本文还有配套的精品资源点击获取