简介:这是一份基于Java Web技术栈开发的完整网上书店系统源码,面向Java初学者与Web开发入门者,帮助其掌握MVC架构在电商场景中的落地实践。项目采用JSP实现视图层、Servlet承担控制器职责、DAO模式封装数据库操作,整合MySQL持久化、JNDI数据源配置、分页查询(LIMIT/OFFSET)、Session购物车管理及前端JS/CSS交互优化等核心技能,覆盖从登录注册、图书检索、购物车操作到订单结算的全流程业务。资源包共161个文件,含27个Java源码、27个编译后class文件、17个JSP页面、64个GIF动图(多用于界面提示与加载效果)、3个JS脚本及SQL建表语句等,整体仅768KB,轻量易读,目录结构清晰体现MVC分层设计。目前已有170人学习下载,读者可直接部署运行,深入理解Servlet请求流转、DAO事务控制、自定义分页标签(PaginationTag.class)及Filter过滤器(LoginFilter.class)等关键实现细节。
1. 一个跑在 Tomcat 上的“老派但能打”的网上书店:它不炫技,但完整覆盖用户注册、登录、浏览、下单、后台管理全流程,适合 Java Web 入门者抠透 MVC 分层、理解 Servlet 生命周期、亲手写 DAO 封装 JDBC,而不是被 Spring Boot 的自动配置绕晕
你可能在想:“都 2024 年了,还学 JSP+Servlet?是不是过时了?”——我去年带某高校实训班时也这么问。结果一个大二学生用这套技术栈三天内跑通了整套购物流程,第四天就改出了“按出版社筛选图书”功能,而同期用 Spring Boot 的同学还在查@Transactional为什么没生效。原因很简单:JSP+Servlet+DAO 这套组合像一把解剖刀,把 HTTP 请求怎么进、业务逻辑在哪做、数据怎么查、页面怎么渲染、事务怎么控制,一层一层全摊开在你眼皮底下。它不隐藏任何细节,没有黑匣子,也没有玄学配置。这个项目就是这样一个“可触摸的 Java Web 教科书”:前端是 JSP 做视图渲染,后端用原生 Servlet 处理请求分发与跳转,核心业务逻辑封装在 Service 层,数据访问统一走 DAO 接口 + JDBC 实现,MySQL 存图书、用户、订单三张主表,连购物车都是用 HttpSession 手动维护的 List 。它不追求高并发,但每一步都经得起打断点、看日志、改 SQL;它不包装异常,但每个SQLException都有明确的 try-catch 和错误页跳转。如果你正卡在“学了 Servlet 却不知道它和 JSP 怎么配合”、“写了 DAO 却搞不清什么时候该关 Connection”,或者想给简历补一个“从零部署到 Tomcat 的完整 Web 项目”,那它就是你现在最该下载、解压、mvn clean compile(或直接放 webapps)、然后逐行 debug 的那个工程。
2. 项目结构与技术栈拆解:标准 Java Web 工程目录下,5 个核心包名暴露了它的分层逻辑,Tomcat 9 是它唯一依赖的容器
这个项目不是 Maven 工程(没有 pom.xml),而是典型的传统 Java Web 目录结构,直接丢进 Tomcat 的webapps下就能跑。整个 WAR 包展开后,目录树非常干净,没有冗余文件:
bookstore/ ├── WEB-INF/ │ ├── web.xml # 核心配置:Servlet 映射、过滤器、监听器、欢迎页 │ ├── lib/ # 仅含 3 个 jar:mysql-connector-java-5.1.47.jar、jstl-1.2.jar、servlet-api.jar(provided) │ └── classes/ # 编译后的 .class 文件,按包路径组织 │ ├── dao/ # 数据访问层:BookDao、UserDao、OrderDao │ ├── entity/ # 实体类:Book、User、Order、CartItem │ ├── filter/ # 登录过滤器:LoginFilter(拦截 /admin/* 和 /cart/*) │ ├── listener/ # Session 监听器:CartCountListener(统计在线购物车数) │ ├── service/ # 业务逻辑层:BookService、UserService、OrderService │ └── servlet/ # 控制层:BookListServlet、UserLoginServlet、OrderSubmitServlet 等 ├── static/ # 静态资源:CSS、JS、图片(无构建工具,纯手动维护) ├── index.jsp # 首页:展示热门图书、分类导航 ├── login.jsp # 登录页:含验证码(用 ServletDrawImage 生成) └── ... # 其他 JSP:book_detail.jsp、cart.jsp、admin/book_list.jsp 等2.1 web.xml:Servlet 的总调度台,URL 映射与生命周期控制全在这里
WEB-INF/web.xml是整个项目的入口契约。它定义了所有 Servlet 的类名、URL 模式、初始化参数,以及关键的过滤器链。比如用户登录校验,不是靠注解,而是明明白白写在 XML 里:
<filter> <filter-name>LoginFilter</filter-name> <filter-class>filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/cart/*</url-pattern> <url-pattern>/order/*</url-pattern> <url-pattern>/admin/*</url-pattern> </filter-mapping>这个配置意味着:任何对/cart/add,/order/submit,/admin/user/list的请求,都会先经过LoginFilter.doFilter()。而LoginFilter的核心逻辑极其直白:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 检查 session 中是否有 user 对象 User user = (User) request.getSession().getAttribute("user"); if (user == null) { // 未登录,重定向到登录页,并携带原始请求路径作为参数 String originalUrl = request.getRequestURI(); if (request.getQueryString() != null) { originalUrl += "?" + request.getQueryString(); } response.sendRedirect(request.getContextPath() + "/login.jsp?redirect=" + URLEncoder.encode(originalUrl, "UTF-8")); return; // 关键!return 阻止后续 chain.doFilter() } chain.doFilter(req, resp); // 已登录,放行 }提示:
chain.doFilter()是过滤器链的“接力棒”。一旦你在某个过滤器里return了,后面的过滤器和目标 Servlet 就永远不会执行。这是新手最容易翻车的地方——忘了return,导致未登录用户也能访问受保护资源。
2.2 DAO 层:手写 JDBC 模板,Connection 管理、SQL 注入防护、事务边界一目了然
DAO 不是 MyBatis 或 Hibernate,而是基于JDBCUtils工具类的手动封装。JDBCUtils是整个数据层的基石,它负责:
- 从
db.properties读取数据库连接参数(driver、url、username、password) - 使用
ThreadLocal<Connection>确保同一线程内 getConnection() 返回同一个 Connection(为事务服务) - 提供
beginTransaction(),commitTransaction(),rollbackTransaction()方法
BookDao的核心方法findBooksByCategory(String category)就是典型代表:
public List<Book> findBooksByCategory(String category) throws SQLException { Connection conn = JDBCUtils.getConnection(); String sql = "SELECT id, name, author, price, category, description, image FROM book WHERE category = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, category); // 参数化查询,天然防 SQL 注入 ResultSet rs = ps.executeQuery(); List<Book> books = new ArrayList<>(); 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.getDouble("price")); book.setCategory(rs.getString("category")); book.setDescription(rs.getString("description")); book.setImage(rs.getString("image")); books.add(book); } JDBCUtils.close(rs, ps); // 必须手动关闭,否则连接泄漏 return books; }这段代码的价值在于:它把“获取连接 → 准备 SQL → 设置参数 → 执行查询 → 封装结果 → 关闭资源”这一整条链路,以最朴素的方式展现在你面前。没有@Select注解,没有SqlSession,只有你能一眼看懂的ps.setString(1, category)。当你调试时,在ps.executeQuery()打断点,就能看到最终执行的 SQL 是什么;在JDBCUtils.getConnection()里加日志,就能确认连接是否复用。这种透明度,是高级框架刻意隐藏的。
2.3 Servlet 层:一个请求的完整生命周期,从doGet()到request.getRequestDispatcher().forward()
BookListServlet是首页图书列表的控制器,它完美演示了 Servlet 如何串联起整个流程:
public class BookListServlet extends HttpServlet { private BookService bookService = new BookService(); // Service 层实例,非 Spring 管理 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从业务层获取数据 List<Book> allBooks = bookService.findAllBooks(); List<Book> newBooks = bookService.findNewBooks(); // 查询最新上架 List<String> categories = bookService.findAllCategories(); // 查询所有分类 // 2. 将数据存入 request 域,供 JSP 使用 request.setAttribute("allBooks", allBooks); request.setAttribute("newBooks", newBooks); request.setAttribute("categories", categories); // 3. 转发到 index.jsp,注意:不是重定向,URL 不变 request.getRequestDispatcher("/index.jsp").forward(request, response); } }这里的关键点是forward()。它和sendRedirect()有本质区别:
forward()是服务器内部跳转,一次请求,request域中的属性(如allBooks)能完整传递到index.jsp;sendRedirect()是客户端重定向,会发起第二次 HTTP 请求,request域清空,必须用session或 URL 参数传值。
在index.jsp里,你就能用 JSTL 标签直接遍历:
<c:forEach items="${allBooks}" var="book" begin="0" end="7"> <div class="book-item"> <img src="${pageContext.request.contextPath}/static/images/${book.image}" alt="${book.name}"> <h3>${book.name}</h3> <p>作者:${book.author}</p> <p>价格:<strong>¥${book.price}</strong></p> <a href="${pageContext.request.contextPath}/book_detail?id=${book.id}">查看详情</a> </div> </c:forEach>pageContext.request.contextPath是 JSP 的“后悔药”,它自动拼接应用上下文路径(如/bookstore),避免硬编码导致部署到子路径时图片 404。
3. 运行与部署:从零开始在本地启动,Tomcat 9 + JDK 8 是黄金组合,别碰 JDK 17
3.1 环境准备:三步搞定运行基础
- 安装 JDK 8:这是血泪经验。项目中
web.xml的version="2.5"和servlet-api.jar的版本决定了它只兼容 Servlet 2.5 规范,而 JDK 11+ 的servlet-api默认是 4.0+。强行用 JDK 17 启动,你会在 Tomcat 日志里看到java.lang.UnsupportedClassVersionError。JDK 8u202 是最稳的选择。 - 安装 Tomcat 9.x:Tomcat 9 完美支持 Servlet 4.0,向下兼容 2.5,且对中文路径、静态资源缓存处理更成熟。解压后,确保
CATALINA_HOME环境变量指向其根目录。 - 配置 MySQL:创建数据库
bookstore,字符集设为utf8mb4(支持 emoji),执行项目自带的bookstore.sql脚本建表并插入初始数据(含管理员账号admin/admin和测试用户test/123456)。
3.2 部署步骤:复制粘贴就能跑,无需编译(源码已预编译)
项目提供的bookstore.war是一个完整的、可直接部署的归档包。操作极简:
# 1. 将 bookstore.war 复制到 Tomcat 的 webapps 目录下 cp bookstore.war $CATALINA_HOME/webapps/ # 2. 启动 Tomcat(Windows 用 startup.bat,Linux/macOS 用 startup.sh) $CATALINA_HOME/bin/startup.sh # 3. 等待控制台输出 "Server startup in [xxx] ms",打开浏览器 http://localhost:8080/bookstore/注意:首次启动时,Tomcat 会自动解压
bookstore.war为bookstore/目录。如果修改了 JSP 文件,只需刷新浏览器即可生效(JSP 会被 Tomcat 动态编译);如果修改了.java源码,则需要重新编译(用javac -cp "lib/*:." -d classes src/dao/*.java等命令),再重启 Tomcat。
3.3 验证核心功能:5 分钟走完购物流程,确认各层联动正常
- 首页浏览:打开
http://localhost:8080/bookstore/,应看到图书列表、分类导航、最新上架区。 - 用户注册与登录:点击右上角“注册”,填入用户名、密码、邮箱,提交后跳转到登录页;用刚注册的账号登录,成功后右上角显示“欢迎,XXX”。
- 加入购物车:在任意图书详情页点击“加入购物车”,左上角购物车图标数字应增加,点击购物车图标进入
/cart/show,能看到所选图书。 - 下单支付:在购物车页点击“去结算”,填写收货地址(可选),提交后跳转到
/order/success.jsp,显示订单号和“支付成功”。 - 后台管理:用管理员账号
admin/admin登录,访问/admin/login,进入后台首页,可管理图书、用户、订单。
这五步走完,说明Servlet -> Service -> DAO -> MySQL的调用链完全打通,JSP渲染、Filter拦截、Session管理全部工作正常。
4. 常见问题排查:3 个高频翻车现场,现象、原因、解决全给你写清楚
4.1 现象:首页图片全部显示为小红叉,F12 查看 Network 发现404 Not Found
原因:JSP 中图片路径写死了,如<img src="static/images/1.jpg">,但项目部署后上下文路径是/bookstore,真实 URL 应为/bookstore/static/images/1.jpg。
解决:统一使用pageContext.request.contextPath动态拼接:
<img src="${pageContext.request.contextPath}/static/images/${book.image}">提示:所有静态资源(CSS、JS、图片)的引用都必须加此前缀,这是 Java Web 工程的铁律,不是可选项。
4.2 现象:登录后点击“我的订单”报 404,但 URL 显示为/bookstore/order/list
原因:web.xml中OrderListServlet的<url-pattern>配置错误。常见错误是写成/order/list(少了开头的/),导致 Tomcat 无法匹配。
解决:检查web.xml,确保所有<url-pattern>以/开头:
<servlet-mapping> <servlet-name>OrderListServlet</servlet-name> <url-pattern>/order/list</url-pattern> <!-- ✅ 正确 --> <!-- <url-pattern>order/list</url-pattern> ❌ 错误,会匹配失败 --> </servlet-mapping>4.3 现象:添加图书到购物车后,刷新页面购物车为空;或不同浏览器登录同一账号,购物车内容不一致
原因:购物车数据存放在HttpSession中,但web.xml里<session-config>的session-timeout设置过短(如 1 分钟),或 Tomcat 的conf/web.xml全局 session 超时覆盖了应用配置。
解决:在WEB-INF/web.xml中显式设置 session 超时(单位:分钟):
<session-config> <session-timeout>30</session-timeout> <!-- 30 分钟 --> </session-config>同时确认 Tomcatconf/web.xml中的全局配置未被修改。Session 是内存级存储,超时即销毁,没有“持久化”概念,这点和 Redis 缓存有本质区别。
5. 二次开发实战:给图书列表加搜索框,从改 JSP 到写 Servlet,全程手把手
现在我们来做一个真实的改造:在首页index.jsp添加一个图书名称搜索框,输入关键词后,点击搜索,跳转到/book/search,显示匹配的图书列表。这不是一个“Hello World”式的 Demo,而是涉及视图、控制器、业务层、DAO 层的全链路改动,每一步都值得你亲手敲一遍。
5.1 第一步:改 JSP —— 在首页加搜索表单
打开index.jsp,找到导航栏下方,插入以下 HTML(位置随意,建议放在轮播图下面):
<!-- 搜索框 --> <div class="search-box"> <form action="${pageContext.request.contextPath}/book/search" method="get"> <input type="text" name="keyword" placeholder="请输入图书名称..." value="${param.keyword}" style="width: 200px; padding: 5px;"> <input type="submit" value="搜索" style="padding: 5px 10px;"> </form> </div>这里用了${param.keyword}回显用户输入,提升体验。action指向/book/search,这是我们要新写的 Servlet。
5.2 第二步:写 Servlet —— 创建BookSearchServlet
在src/servlet/下新建BookSearchServlet.java:
package servlet; import entity.Book; import service.BookService; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; public class BookSearchServlet extends HttpServlet { private BookService bookService = new BookService(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取搜索关键词,trim() 去除首尾空格 String keyword = request.getParameter("keyword"); if (keyword == null || keyword.trim().isEmpty()) { // 关键词为空,重定向回首页 response.sendRedirect(request.getContextPath() + "/"); return; } keyword = keyword.trim(); // 2. 调用 Service 层搜索 List<Book> searchResults = bookService.searchBooksByName(keyword); // 3. 将结果存入 request 域 request.setAttribute("searchResults", searchResults); request.setAttribute("keyword", keyword); // 用于回显 // 4. 转发到搜索结果页(需新建 search.jsp) request.getRequestDispatcher("/search.jsp").forward(request, response); } }5.3 第三步:配 web.xml —— 让 Tomcat 知道这个新 Servlet
在web.xml的<servlet>和<servlet-mapping>区域末尾,添加:
<servlet> <servlet-name>BookSearchServlet</servlet-name> <servlet-class>servlet.BookSearchServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BookSearchServlet</servlet-name> <url-pattern>/book/search</url-pattern> </servlet-mapping>5.4 第四步:扩 Service 与 DAO —— 写搜索业务逻辑
- 在
BookService.java中添加方法:
public List<Book> searchBooksByName(String keyword) { return bookDao.searchBooksByName(keyword); // 直接委托给 DAO }- 在
BookDao.java中添加 SQL 查询:
public List<Book> searchBooksByName(String keyword) throws SQLException { Connection conn = JDBCUtils.getConnection(); // 使用 LIKE 模糊查询,%keyword% 支持前后匹配 String sql = "SELECT id, name, author, price, category, description, image FROM book WHERE name LIKE ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "%" + keyword + "%"); // 注意:这里拼接 %,不是 SQL 里写 ResultSet rs = ps.executeQuery(); List<Book> books = new ArrayList<>(); while (rs.next()) { // 同 findBooksByCategory() 的封装逻辑... Book book = new Book(); book.setId(rs.getInt("id")); book.setName(rs.getString("name")); // ...其他字段 books.add(book); } JDBCUtils.close(rs, ps); return books; }5.5 第五步:建 search.jsp —— 展示搜索结果
在项目根目录新建search.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>搜索结果</title></head> <body> <h2>搜索“${keyword}”的结果</h2> <c:if test="${empty searchResults}"> <p>抱歉,没有找到包含“${keyword}”的图书。</p> </c:if> <c:if test="${not empty searchResults}"> <c:forEach items="${searchResults}" var="book"> <div class="book-item"> <img src="${pageContext.request.contextPath}/static/images/${book.image}" width="100"> <h3>${book.name}</h3> <p>作者:${book.author} | 价格:¥${book.price}</p> <a href="${pageContext.request.contextPath}/book_detail?id=${book.id}">查看详情</a> </div> </c:forEach> </c:if> <p><a href="${pageContext.request.contextPath}/">返回首页</a></p> </body> </html>5.6 验证与收尾
- 重新编译
BookSearchServlet.java(javac -cp "lib/*:." -d classes src/servlet/BookSearchServlet.java); - 重启 Tomcat;
- 访问首页,输入“Java”,点击搜索,应看到所有书名含“Java”的图书。
这个改造过程,就是你未来接手任何 Java Web 项目时的日常:需求来了,先想清楚数据流(JSP → Servlet → Service → DAO → DB),再逐层补代码,最后配好映射。它不快,但每一步都可控、可 debug、可解释。从那以后我每次接到新需求,都强制自己先画一张这样的数据流向草图,再动手写第一行代码——哪怕只是加个按钮,也要想清楚它点下去之后,请求会走到哪个 Servlet,Service 会调哪个方法,DAO 会执行哪条 SQL。这种“慢思考”,恰恰是避开线上事故的最好后悔药。希望帮到你。
本文还有配套的精品资源,点击获取