简介这是一套面向Java Web初学者与课程设计者的完整网上书店项目源码基于JSPServletDAO经典MVC架构实现适合用于毕业设计参考、课程实训或自学Java Web开发。项目覆盖用户登录验证、图书搜索、分页浏览、购物车增删改与结算等核心电商功能并采用JNDI配置数据库连接池结合MySQL完成数据持久化前端辅以JavaScript与CSS优化交互体验。资源包共161个文件约768KB包含27个java源文件与27个class编译文件、17个jsp页面、64个gif及6个jpg图片素材另有js脚本、xml配置、sql建表脚本与tld标签库文件结构完整便于直接导入运行。目前已有169人学习下载。通过研读源码读者可掌握Servlet控制器与DAO分层设计、分页标签实现、Session购物车状态管理等关键知识点是理解Java Web技术栈与电商业务逻辑的实用范例。1. 从一份 JSPServlet 网上书店源码说起为什么它值得你花时间跑一遍如果你手头正拿着一份 JSPServlet 网上书店的源码包打开一看全是.jsp、.java、web.xml和一堆 JDBC 连接代码第一反应可能是“这玩意儿也太老了”。但先别急着关掉。我见过太多人学完 Spring Boot 之后回头翻这种“老项目”结果发现自己连一个最基础的请求转发都写不利索。JSPServletDAO 这套组合恰恰是理解 Java Web 底层请求流转最直白的入口——没有自动配置帮你兜底没有注解帮你隐藏web.xml每一个HttpServletRequest和HttpServletResponse都得自己处理。这份网上书店源码把用户注册登录、图书浏览、购物车、订单生成这条完整链路都串起来了适合两类人一是刚学完 Java 基础、想找个能跑通的项目练手的在校生二是工作几年但一直用框架、想回头补一补 Servlet 容器基础的老手。接下来我按“能干什么→怎么跑→怎么改”的顺序把这份源码拆开讲清楚。2. 项目架构与目录结构一个请求从浏览器到数据库的完整链路2.1 分层设计JSP 负责展示Servlet 负责调度DAO 负责数据这份网上书店源码采用的是典型的 Model1 向 Model2 过渡的结构严格来说更接近 Model2也就是 JSP 只做视图渲染Servlet 做控制器JavaBean 做模型DAO 做数据访问。整个请求链路是这样的浏览器发起/bookList请求web.xml里配置的BookListServlet拦截Servlet 调用BookDAO.findAll()拿到ListBook把数据塞进request.setAttribute然后forward到bookList.jspJSP 用 JSTL 或脚本片段遍历输出。这个链路里没有任何框架介入所有跳转都是显式的对新手来说反而更容易看清“一个请求到底经历了什么”。常见做法是在src下按包名分层com.bookstore.servlet放所有 Servletcom.bookstore.dao放数据库操作类com.bookstore.entity放实体类com.bookstore.util放数据库连接工具。这种包结构虽然简单但已经具备了后来三层架构的雏形。你拿到源码后先别急着改代码把包结构画一遍后面改起来会顺很多。2.2 目录结构拆解每个文件夹到底放什么一份标准的 JSPServlet 网上书店源码目录结构大致如下bookstore/ ├── src/ │ └── com/bookstore/ │ ├── entity/ # 实体类Book、User、Order、OrderItem │ ├── dao/ # 数据访问BookDAO、UserDAO、OrderDAO │ ├── servlet/ # 控制器LoginServlet、CartServlet 等 │ └── util/ # 工具类DBUtil、PageBean ├── WebContent/ 或 web/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器、欢迎页配置 │ │ └── lib/ # 依赖 jarmysql-connector、jstl │ ├── css/ # 样式文件 │ ├── js/ # 前端脚本 │ ├── images/ # 图书封面图 │ ├── index.jsp # 首页 │ ├── bookList.jsp # 图书列表页 │ ├── cart.jsp # 购物车页 │ └── orderList.jsp # 订单列表页 └── sql/ └── bookstore.sql # 建库建表脚本这里有几个点值得注意。WEB-INF目录下的文件浏览器无法直接访问所以web.xml和lib放这里是对的但如果你把 JSP 也放进WEB-INF那就必须通过 Servlet 转发才能访问安全性更高不过这份源码大概率是直接放在WebContent根目录下的方便直接跳转。sql文件夹里的建表脚本一定要先执行否则后面所有数据库操作都会报Table bookstore.book doesnt exist。2.3 web.xml 配置Servlet 映射与过滤器web.xml是这份源码的“交通枢纽”所有 Servlet 的访问路径都在这里定义。一个典型的配置片段如下!-- 图书列表 Servlet 映射 -- servlet servlet-nameBookListServlet/servlet-name servlet-classcom.bookstore.servlet.BookListServlet/servlet-class /servlet servlet-mapping servlet-nameBookListServlet/servlet-name url-pattern/bookList/url-pattern /servlet-mapping !-- 字符编码过滤器解决中文乱码 -- filter filter-nameEncodingFilter/filter-name filter-classcom.bookstore.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern决定了浏览器访问的路径比如/bookList对应BookListServlet。过滤器这里配的是/*表示拦截所有请求在doFilter方法里统一设置request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)。这一步不做后面表单提交中文书名时必然乱码这是血泪经验。注意如果你用的 Servlet 版本是 3.0其实可以用WebServlet(/bookList)注解替代web.xml配置但这份源码既然保留了web.xml建议你先按原样跑通再尝试改成注解方式对比两种配置的差异。3. 环境搭建与运行步骤从零把项目跑起来3.1 开发环境选型JDK、Tomcat、MySQL 版本怎么配这份源码对版本不挑剔但有几个组合是经过验证比较稳的JDK 8 Tomcat 8.5 MySQL 5.7。JDK 8 是因为很多老项目的web.xml声明的是 Servlet 3.1Tomcat 8.5 刚好支持MySQL 5.7 是因为建表脚本里可能用了ENGINEInnoDB DEFAULT CHARSETutf8这种写法MySQL 8.0 默认字符集变了导入时可能报错。如果你只有 MySQL 8.0导入前先把脚本里的utf8改成utf8mb4或者在连接 URL 里加?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。IDE 用 Eclipse 或 IntelliJ IDEA 都行。Eclipse 对老项目更友好因为它的 Dynamic Web Project 结构和源码目录天然匹配IDEA 需要手动配 Artifacts 和 Tomcat 运行配置稍微麻烦一点但用惯了也快。我一般会先在 Eclipse 里跑通再用 IDEA 重构这样能排除掉大部分环境问题。3.2 数据库初始化建库、建表、导入数据拿到源码后第一件事不是打开 IDE而是先把数据库搞定。找到sql/bookstore.sql用命令行或 Navicat 执行# 登录 MySQL mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换到该数据库 USE bookstore; # 导入建表脚本 source /path/to/bookstore.sql;执行完之后用SHOW TABLES;确认一下通常会有user、book、category、orders、order_item这几张表。book表里一般会有id、name、author、price、image、category_id、stock这些字段。如果脚本里没有插入初始数据你需要手动往book表里插几条记录否则图书列表页是空的你会以为代码写错了。提示导入脚本时如果报Unknown character set: utf8说明你的 MySQL 版本较新把脚本里的utf8全部替换成utf8mb4再执行。3.3 项目导入与 Tomcat 配置让首页能打开数据库准备好之后打开 IDE 导入项目。以 Eclipse 为例File → Import → Existing Projects into Workspace选中源码根目录。如果项目没有.project文件就新建一个Dynamic Web Project把src和WebContent的内容拷进去。接下来改数据库连接配置。找到DBUtil.java或db.properties把 URL、用户名、密码改成你本地的// DBUtil.java 中的连接配置 private static final String URL jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的密码; // 加载驱动 static { try { Class.forName(com.mysql.cj.jdbc.Driver); // MySQL 8.0 用 cj5.7 用 com.mysql.jdbc.Driver } catch (ClassNotFoundException e) { e.printStackTrace(); } }Class.forName这一行是 JDBC 的老规矩虽然 JDBC 4.0 之后可以省略但老项目里保留着更稳妥。serverTimezone参数是 MySQL 8.0 才需要的不加会报时区错误。改完配置后右键项目 →Run As → Run on Server选 Tomcat启动后浏览器访问http://localhost:8080/bookstore/能看到首页就说明环境通了。3.4 常见启动报错与排查顺序启动失败时不要慌按这个顺序排查先看 Tomcat 控制台有没有ClassNotFoundException有的话说明lib下的 jar 没被正确加载检查mysql-connector-java和jstl是否在WEB-INF/lib下再看有没有Access denied for user那是数据库密码错了最后看 404说明url-pattern和访问路径对不上检查web.xml里的映射。这三步能解决 80% 的启动问题。4. 核心模块代码拆解登录、购物车、订单三条链路4.1 用户登录从 LoginServlet 到 session 写入登录模块是理解 Servlet 请求处理的最佳入口。前端login.jsp提交表单到/login后端LoginServlet的doPost方法接收参数、调用 DAO 查询、判断结果、写入 session// LoginServlet.java 核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数 String username request.getParameter(username); String password request.getParameter(password); // 2. 调用 DAO 查询用户 UserDAO userDAO new UserDAO(); User user userDAO.findByUsernameAndPassword(username, password); // 3. 判断并跳转 if (user ! null) { // 登录成功写入 session request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /bookList); } else { // 登录失败带回错误信息 request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }这里有两个关键点sendRedirect和forward的区别。登录成功用sendRedirect因为要防止用户刷新页面重复提交表单登录失败用forward因为要把msg带到login.jsp显示。request.getSession()会自动创建或获取当前会话currentUser这个属性后面在购物车和订单模块里会反复用到用来判断用户是否登录。UserDAO.findByUsernameAndPassword里就是一条SELECT * FROM user WHERE username? AND password?用PreparedStatement设置参数。这里不要用字符串拼接否则会有 SQL 注入风险虽然是个练习项目但习惯要养好。4.2 购物车实现session 存储与数量增减购物车模块没有用数据库而是把MapInteger, CartItem存在 session 里。CartItem包含Book对象和quantity数量。添加购物车的 Servlet 逻辑如下// CartServlet.java 添加商品到购物车 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 检查登录状态 User user (User) request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 2. 获取图书 id 和操作类型 String bookIdStr request.getParameter(bookId); String action request.getParameter(action); // add / remove / clear // 3. 从 session 获取购物车没有就新建 MapInteger, CartItem cart (MapInteger, CartItem) request.getSession().getAttribute(cart); if (cart null) { cart new HashMap(); request.getSession().setAttribute(cart, cart); } // 4. 根据 action 执行不同操作 if (add.equals(action)) { int bookId Integer.parseInt(bookIdStr); BookDAO bookDAO new BookDAO(); Book book bookDAO.findById(bookId); if (cart.containsKey(bookId)) { cart.get(bookId).setQuantity(cart.get(bookId).getQuantity() 1); } else { cart.put(bookId, new CartItem(book, 1)); } } else if (remove.equals(action)) { cart.remove(Integer.parseInt(bookIdStr)); } else if (clear.equals(action)) { cart.clear(); } // 5. 回到购物车页面 response.sendRedirect(request.getContextPath() /cart.jsp); }action参数控制三种操作add加购、remove删除单项、clear清空。用Map而不是List的好处是查找快判断某本书是否已在购物车里只需要containsKey。CartItem里存Book对象而不是bookId是为了在cart.jsp里直接显示书名和价格不用再查一次数据库。注意session 里的购物车在用户关闭浏览器后会消失这是正常的。如果你想让购物车持久化需要把Map存到数据库的cart表里这是二次开发的一个方向。4.3 订单生成事务处理与订单项批量插入订单模块是整份源码里最接近真实业务的部分。用户点击“结算”后OrderServlet需要做三件事创建订单主记录、批量插入订单项、清空购物车。这三步必须在一个事务里完成否则会出现订单生成了但订单项没插入的脏数据。// OrderServlet.java 生成订单 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user (User) request.getSession().getAttribute(currentUser); MapInteger, CartItem cart (MapInteger, CartItem) request.getSession().getAttribute(cart); if (user null || cart null || cart.isEmpty()) { response.sendRedirect(request.getContextPath() /cart.jsp); return; } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主记录 Order order new Order(); order.setUserId(user.getId()); order.setTotalPrice(calculateTotal(cart)); order.setStatus(待付款); int orderId new OrderDAO().insertOrder(conn, order); // 2. 批量插入订单项 for (CartItem item : cart.values()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(orderId); orderItem.setBookId(item.getBook().getId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(item.getBook().getPrice()); new OrderDAO().insertOrderItem(conn, orderItem); } conn.commit(); // 提交事务 request.getSession().removeAttribute(cart); // 清空购物车 response.sendRedirect(request.getContextPath() /orderList); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute(msg, 订单生成失败请重试); request.getRequestDispatcher(/cart.jsp).forward(request, response); } finally { DBUtil.close(conn); } }conn.setAutoCommit(false)是事务的起点所有插入操作共用同一个Connection最后commit一次性提交。如果中间任何一步抛异常rollback会撤销之前的所有操作。OrderDAO.insertOrder里需要用Statement.RETURN_GENERATED_KEYS拿到自增的orderId再传给订单项。这个细节如果漏了订单项会关联不上订单。5. 避坑与常见问题那些让你卡半天的坑5.1 中文乱码POST 和 GET 处理方式不一样现象表单提交中文书名数据库里存的是???。原因POST 请求默认用 ISO-8859-1 解码GET 请求在 Tomcat 8 之前也用 ISO-8859-1。解决POST 在过滤器里设request.setCharacterEncoding(UTF-8)GET 要么改 Tomcat 的server.xml加URIEncodingUTF-8要么在代码里手动new String(request.getParameter(name).getBytes(ISO-8859-1), UTF-8)。我一般直接改server.xml一劳永逸。5.2 数据库连接池未关闭Tomcat 重启后连接数暴涨现象反复重启 Tomcat 后MySQL 里SHOW PROCESSLIST看到大量Sleep连接。原因DBUtil.getConnection()每次新建连接但代码里忘了在finally块里close。解决确保每个 DAO 方法都在finally里关闭Connection、PreparedStatement、ResultSet。如果嫌麻烦可以引入 Druid 或 C3P0 连接池但那是二次开发的事了。5.3 JSTL 标签不解析直接原样输出到页面现象bookList.jsp里写的c:forEach在浏览器里原样显示。原因jstl.jar和standard.jar没放进WEB-INF/lib或者web.xml的版本太低不支持 JSTL。解决确认两个 jar 都在并且web.xml声明的是 Servlet 2.4 以上。如果还不行在 JSP 顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。5.4 购物车数量不更新session 对象引用问题现象在购物车页面点“1”数量没变。原因CartItem的quantity改了但 session 里的Map没有重新setAttribute。解决Map是引用类型改里面的对象不需要重新setAttribute但如果你在 Servlet 里重新new了一个Map并覆盖就会丢数据。检查代码里有没有request.getSession().setAttribute(cart, new HashMap())这种误操作。5.5 订单列表查不到数据外键关联字段写错现象订单生成了但orderList.jsp显示空白。原因OrderDAO.findByUserId里 SQL 写的是WHERE user_id ?但插入时order.setUserId设的是user.getId()字段对不上。解决核对数据库表字段名和实体类属性名确保order表的user_id和Order类的userId对应。MyBatis 有自动映射原生 JDBC 必须手动rs.getInt(user_id)。6. 二次开发改造从能跑到好用我一般会做这几件事这份源码跑通之后你可能会觉得它“能用但不好用”。比如没有分页、没有搜索、密码明文存储、购物车不能持久化。这些恰恰是二次开发的切入点。我一般会按优先级从高到低改先加分页再加搜索然后改密码加密最后考虑购物车持久化。分页的实现思路是在BookDAO里加一个findByPage(int page, int size)方法SQL 用LIMIT ?, ?同时加一个PageBean类封装totalCount、currentPage、totalPage、list。Servlet 里接收page参数默认 1调用 DAO 后把PageBean塞进 requestJSP 里渲染上一页/下一页链接。这个改动涉及 DAO、Servlet、JSP 三层是很好的分层练习。搜索功能更简单在bookList.jsp加一个表单提交到/bookList?keywordxxxBookDAO.findAll改成findByKeywordSQL 用WHERE name LIKE ?参数设% keyword %。注意LIKE的通配符要拼在 Java 代码里不要拼在 SQL 字符串里否则还是注入风险。密码加密我建议用 MD5 加盐虽然 MD5 现在不安全但比明文强。在UserDAO.register里把password替换成MD5Util.md5(password salt)登录时同样处理再比对。盐可以固定也可以每个用户随机生成存到user表里。这个改动会让你理解“为什么数据库里不能存明文密码”。购物车持久化是最大的改动。需要新建cart表字段user_id、book_id、quantity然后把CartServlet里的 session 操作全部换成 DAO 操作。好处是换台电脑登录购物车还在坏处是每次加购都要写数据库性能会降。我一般会保留 session 做缓存异步同步到数据库但这已经超出这份源码的范围了。最后说一个我踩过的坑改完代码后一定要清 Tomcat 的work目录否则 JSP 编译缓存会导致改动不生效。从那以后我每次重启 Tomcat 前都强制删一遍work/Catalina这个习惯帮我省了无数“为什么改了没反应”的时间。希望帮到你。本文还有配套的精品资源点击获取