
简介一套面向计算机专业学生的JavaWeb在线图书销售系统源码与数据库适合期末大作业、课程设计与实战练习。系统基于经典Java Web技术栈实现前端采用HTML、CSS、JavaScript与Ajax增强交互后端使用Servlet和JSP处理请求及动态页面并遵循MVC分层思想通过JDBC连接MySQL数据库。项目覆盖用户注册登录、图书分类浏览与检索、购物车管理、订单提交、个人中心以及管理员对图书信息、库存和订单的后台维护等完整业务链路能够帮助学习者打通从表结构设计到前后端联调的全过程。压缩包共包含125个文件大小仅5.44MB其中43个Java源码、19个JSP页面、11个依赖Jar包是主体另有SQL初始化脚本、XML配置文件、CSS样式、JavaScript脚本及项目说明文档等资源结构清晰可直接导入IDE运行调试。已有53人学习下载。整体还附带需求分析、系统设计、数据库设计和测试用例等说明材料同时内置Druid连接池、Kaptcha验证码和MySQL驱动等常用组件方便在此基础上扩展订单统计、支付模拟等功能适合作为期末答辩、项目复盘或毕业设计的前期蓝本。1. 为什么期末项目选它JavaWeb图书销售系统的真实分量拿到一份评审98分的JavaWeb在线图书销售系统源码第一反应不应该是解压、导库、点运行而是先想清楚它为什么值这个分数。表面看这就是登录注册、图书列表、购物车、订单的经典CRUD组合但实际把Servlet生命周期、MVC分层、JDBC事务、Druid连接池、kaptcha验证码这几个JavaWeb必考点全串了起来。对要做期末大作业或课程设计的学生它既是能现场演示的完整案例也是答辩时能解释为什么这么设计的素材库。下面按我拆项目的习惯把技术选型、运行步骤、购物车与订单细节、以及答辩高频问题依次过一遍每一处都给出可以直接复现的命令和代码。2. 技术栈拆解MVC分层、Druid连接池与JDBC交互细节2.1 包结构与MVC三层在源码里怎么对应解压源码后不要急着点IDE的运行按钮先看包结构。这类javaweb项目完整案例一般按com.bookstore.controller、service、dao、entity、util分包controller放Servletservice写业务规则dao用JDBC操作数据库entity是对应数据表的JavaBeanutil放JdbcUtils、MD5Util这类工具。这个分包方式就是MVC模式的落地JSP负责展示ViewServlet接收请求并做转发ControllerService加Dao处理数据Model前端用style.css、logo.gif这些静态资源撑起页面注册时检查用户名是否占用、加入购物车不刷新页面这些交互则交给JavaScript和Ajax处理。我拿到源码后的第一件事是顺着controller包把所有WebServlet注解扫一遍快速画出请求映射表。这张表既是理解系统的入口也是答辩时画系统流程图的基础。Servlet映射路径职责RegisterServlet / LoginServlet/user/register、/user/login注册、登录、验证码校验BookServlet/book/list、/book/detail图书分页列表、详情CartServlet/cart/add、/cart/show、/cart/remove购物车增删查OrderServlet/order/create、/order/list下单、订单列表2.2 Druid连接池为什么不用DriverManager直连早期课程作业最常见的写法是Class.forName(com.mysql.jdbc.Driver)加DriverManager.getConnection(url, user, password)。这种写法每次请求都新建一条TCP连接图书列表页一次分页查询就握手一次并发一上来MySQL默认最大连接数很快被打满报Too many connections。Druid连接池把连接预先建好放在池里用完归还这是生产环境的常规做法也是这份源码在根目录放druid-1.1.10.jar的原因。src下druid.properties里几个关键参数的含义如下这也是javaweb配置里最常被拷问的部分driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalse usernameroot password123456 initialSize5 maxActive20 maxWait3000initialSize是启动时预建的连接数maxActive是池中最大连接数maxWait是取不到连接时的最长等待毫秒数。url里的useUnicode和characterEncoding必须带上否则中文书名在页面会乱码useSSLfalse是为了避免MySQL 5.7以上版本在每次连接时打印SSL警告。另外Druid自带一个StatViewServlet监控页把initialSize、maxActive配好后演示时打开/druid能看到当前活跃连接数、SQL执行次数和慢查询统计跑一遍购物流程再截图放进文档期末大作业的系统测试章节会充实不少。2.3 JdbcUtils封装与数据库增删改查有了连接池Dao层通常配合一个JdbcUtils工具类把连接获取和关闭收敛到一处public class JdbcUtils { private static DruidDataSource dataSource; static { Properties props new Properties(); try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)) { props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement st, ResultSet rs) { // 依次关闭 rs、st、conn注意判空 } }静态代码块保证连接池只在类加载时初始化一次getConnection从池里取连接close在Druid里实际是把连接归还给池而不是物理断开。Dao层所有图书增删改查都基于这个工具类写PreparedStatement而不是拼接SQL字符串具体差异在第五章验证SQL注入时会演示。这里要提醒一点close方法里三个参数都要判空并且按rs、st、conn的顺序关闭漏掉任何一个都会造成连接泄漏症状是页面运行一段时间后越来越慢、最后整个服务卡死。3. 从零跑通Maven构建、MySQL初始化与登录注册链路3.1 用Maven Wrapper构建锁版本避免踩坑项目根目录出现mvnw.cmd说明工程带了Maven Wrapper。它存在的意义是锁定Maven版本避免我本地3.9能过、你3.6报错的适配问题。Windows下在项目根目录直接执行cd book-store mvnw.cmd clean package如果你用的是Mac或Linux而整个工程只有mvnw.cmd没有mvnw的shell脚本那直接用本机Maven执行mvn clean package即可效果一样。第一次运行Wrapper会去下载它指定的Maven版本网络受限时走本机Maven更省事。构建成功后在target目录得到war包扔进Tomcat的webapps下启动或用IDEA配置Tomcat运行。如果自己从零搭建javaweb工程必须确认druid、mysql-connector-java、kaptcha三个jar已加入依赖或WEB-INF/lib缺任何一个都会在运行时抛ClassNotFoundException。3.2 数据库初始化字符集是第一个坑源码里sql目录下一般有一份bookstore.sql包含建库、建表、插入图书示例数据。手动导入时字符集没指定对导入后中文会全部变成问号mysql -uroot -p --default-character-setutf8mb4 bookstore.sql核心表结构大致如下用户表、图书表、订单表、订单项表是四个必备实体CREATE TABLE tb_user ( uid INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL, email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tb_book ( bid INT PRIMARY KEY AUTO_INCREMENT, bname VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, image VARCHAR(255) );password用CHAR(32)是因为项目用MD5摘要固定32位十六进制price用DECIMAL而不是DOUBLE避免浮点误差把订单金额算错stock和sales在第四章扣库存时会用到。建完表后要往里插入十几条带image路径的图书记录列表页才有展示效果。数据库课程设计阶段很多人只建表不测数据演示时页面空空如也这是最常见的低分原因。3.3 登录注册链路从表单到Session的请求流登录功能看似简单却是考核Servlet生命周期和Session的典型题目。注册时密码不能明文入库常见做法是MD5摘要后存储登录时把用户输入同样做摘要再比对。登录Servlet的核心逻辑如下WebServlet(/user/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 先校验 kaptcha 验证码防止脚本批量重放 String inputCode req.getParameter(code); String sessionCode (String) req.getSession().getAttribute(kaptcha_code); if (sessionCode null || !sessionCode.equalsIgnoreCase(inputCode)) { resp.sendRedirect(req.getContextPath() /login.jsp?errorcode); return; } // 2. 密码做 MD5 摘要后再比对数据库 String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); User user userService.login(username, password); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp?erroruser); return; } // 3. 登录态写入 Session后续页面用它判断是否已登录 req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /book/list); } }顺序是关键先验证码后账号密码验证码不通过直接return避免无效请求打到数据库。Session里存loginUser后购物车模块也依赖这个Session下一章会讲到。注意resp.sendRedirect用的是带contextPath的路径直接写死/login.jsp在部署到非ROOT路径时会404这是实际部署时最容易忽略的坑。3.4 运行期高频报错对照表报错信息原因处理方式ClassNotFoundException: com.mysql.jdbc.Drivermysql驱动jar未引入确认mysql-connector-java-5.1.37-bin.jar在WEB-INF/libAccess denied for user rootlocalhostdruid.properties密码错误改成当前MySQL实际密码Communications link failureMySQL未启动或端口不是3306启动MySQL并检查url端口404 for /book/listWebServlet路径与页面请求路径不一致统一映射路径中文乱码建库字符集不是utf8mb4重建库并指定default character set4. 购物车与订单模块会话级存储与事务扣库存4.1 购物车为什么放在Session而不是建表很多课程设计把购物车做成数据库表但真实商城在用户未登录时也允许加购所以这份源码把购物车设计成Session里的一个Map图书ID, 购物项。好处是未登录也能加购购物车数据随会话存在不污染数据库缺点显而易见Session存在内存里服务重启购物车就丢期末项目场景下这是可接受的取舍。答辩被问到为什么不用表存购物车时这个回答比老师这么教的有说服力得多。购物项JavaBean大致长这样public class CartItem { private Book book; // 图书信息含单价和库存 private Integer count; // 购买数量 public double getSubtotal() { return book.getPrice() * count; // 小计 } }4.2 生成订单与扣库存的事务边界下单是最容易出bug的环节。先扣库存再生成订单订单失败会导致库存丢失先创建订单再扣库存库存不足时脏订单已经落库。正确做法是把校验库存、扣减库存、生成订单、写入订单项包进同一个数据库事务public boolean createOrder(Order order, ListCartItem items) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 1. 关闭自动提交 // 2. 带条件的 UPDATEstock ? 保证不会扣成负数 String sql UPDATE tb_book SET stock stock - ?, sales sales ? WHERE bid ? AND stock ?; PreparedStatement ps conn.prepareStatement(sql); for (CartItem item : items) { ps.setInt(1, item.getCount()); ps.setInt(2, item.getCount()); ps.setInt(3, item.getBook().getBid()); ps.setInt(4, item.getCount()); int rows ps.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足 item.getBook().getBname()); } } // 3. 插入订单主表和订单项表 // 4. 全部成功才提交 conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { } } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }这里最容易翻车的有两点一是catch里忘记rollback前面对stock的UPDATE会留在事务里后续commit时把库存一起改了二是finally里没把连接还回池子不还就是连接泄漏。另外WHERE stock ?这种带条件的UPDATE在InnoDB下是行级锁两个请求同时买同一本书时后一个会阻塞等前一个提交再判断条件是否满足天然避免了超卖。这个点答辩时能讲清楚比我这个项目功能齐全值钱得多。4.3 订单状态字段与管理端流转订单表通常用一个state字段驱动整个流程管理端按状态过滤用户中心按状态显示不同操作按钮state含义用户端管理端0待付款去支付、取消无操作1已付款/待发货无操作发货2已发货确认收货查物流3已完成/已取消查看详情归档个人中心查询订单用WHERE uid ? ORDER BY order_time DESC加LIMIT分页是数据库课程设计里增删改查中查的进阶形态。管理端对图书做增删改查后一定要用redirect回到列表页而不是forward否则浏览器刷新时会重复提交上一次表单。5. 答辩加分项kaptcha验证码与SQL注入防护的实测打法5.1 kaptcha验证码的接入kaptcha-2.3.2.jar的原理是服务端生成图片、答案存Session前端取图、提交比对。一个最简实现是单独开一个Servlet输出图片WebServlet(/captcha) public class CaptchaServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); // kaptcha 自动把答案写入 session 的 kaptcha_code 属性 CaptchaUtil.writeCaptcha(session, resp.getOutputStream()); } }JSP里用img src${pageContext.request.contextPath}/captcha onclickthis.src...?tMath.random()实现点击刷新。实测里要注意kaptcha每次输出图片都会覆盖session里的kaptcha_code所以登录校验通过后要立即remove这个属性否则同一个验证码可以在会话内被反复重放。5.2 SQL注入验证PreparedStatement与Statement的差别答辩时老师最喜欢现场在登录框输入 OR 11试试。用Statement拼接时SELECT * FROM tb_user WHERE username OR 11恒成立直接绕过登录PreparedStatement把参数当字面量预编译同样输入只会匹配username等于那一整串的用户查不到直接登录失败。我的建议是全局搜索Statement和字符串拼接SQL的写法全部换成?占位形式再配一个统一UTF-8编码的Filter。改完之后用curl http://localhost:8080/bookstore/user/login带注入参数实测一次返回不是成功跳转而是错误页防护就算验证通过。本文还有配套的精品资源点击获取