ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JavaWeb课程设计实战:图书购物网站的JSP+MVC架构与MySQL实现

2026/9/23 7:31:09 拓冰建站 浏览量
JavaWeb课程设计实战:图书购物网站的JSP+MVC架构与MySQL实现 简介基于JSPMVC模式和MySQL的网上图书购物系统源码包适用于JavaWeb课程设计、期末大作业及毕业设计选题面向需要快速完成项目、争取高分的学生也适合刚入门JavaWeb、想通过完整实例理解前后端交互的学习者。压缩包共75个文件约47.77MB包含JSP页面、Java后台类、class编译文件、SQL数据库脚本、CSS样式、项目工程配置、展示视频mp4以及README文档从项目源码到部署说明均已打包无需额外寻找配套资料。目前已有347人学习是同类作业中认可度较高的参考。代码注释充分按MVC模式拆分视图、控制器与模型有助于理清图书展示、购物车、订单提交、后台管理等完整业务流程将SQL脚本导入MySQL并调整连接参数后即可本地运行。项目另附有演示视频便于掌握页面操作与答辩讲解思路整体功能完善、界面美观管理端与用户端模块齐全非常适合作为图书购物类课程设计或期末大作业的高分模板。1. 课程设计选这个题目图书购物网站的 JSPMVC 到底在做什么每年期末JavaWeb 课程设计最常见的选题就是图书购物网站靠 JSP 做页面、Servlet 做控制、MySQL 存数据一套标准的 MVC 组合。这个题目看似普通但它把 JavaWeb 的核心考点全部覆盖了请求响应、Session 会话、数据库增删改查、事务、分页、表单校验甚至前端页面渲染。你交上去的是一份源代码数据库脚本老师打开项目、导入 SQL、启动 Tomcat能跑通、能下单、有购物车、有订单记录这个分数基本就稳了。这篇文章不打算给你讲空泛的框架原理而是按我做这类课程设计的实际路径来拆项目结构怎么组织、数据库表怎么设计、核心代码怎么写、部署时最容易在哪里翻车。你跟着走完拿到的不只是能运行的代码而是一套能在答辩时讲清楚的设计思路。2. 用 MVC 把图书购物系统拆开从 Servlet 到 JSP 的职责划分与代码包组织2.1 为什么课程设计必须用 MVC三层职责怎么落很多同学交的大作业是 JSP 页面里直接写 JDBC、写out.println()输出表格代码全糊在一个文件里。这种写法叫 Model1适合只有三五页的小作业但图书购物网站至少包含用户、商品、购物车、订单四块功能用 Model1 写到最后你自己都改不动。老师要求 MVC本质是强迫你把取数据处理逻辑展示页面三件事拆开。JSP 的 Model2 思想就是 MVC 在 JavaWeb 里的落地JSP 只负责展示Servlet 负责接收请求和跳转JavaBean 和 DAO 负责数据操作。我在这个项目里分的三层是这样的View 层是webapp下的 JSP 页面不出现任何 Java 代码需要显示数据时只用 EL 表达式和 JSTL 标签Controller 层是 Servlet每个功能模块一个 Servlet比如LoginServlet、BookServlet、CartServlet、OrderServlet它们只做参数接收、调用 Service、决定跳转到哪个 JSPModel 层是entity实体类、daoJDBC 操作、service业务逻辑三部分组成。Service 这一层是容易被忽略的但恰恰是答辩时的加分项比如下单时要先查库存再扣库存这种业务规则放在 Servlet 里叫杂烩放在 Service 里叫设计。另外要注意的是JSP 页面本身不要直接访问数据库所有数据都通过 request 或 session 作用域传进去。比如图书列表页BookServlet查询后把结果request.setAttribute(bookList, list)然后转发到book_list.jspJSP 里用c:forEach循环输出。这种做法答辩时老师问为什么用 MVC你可以直接指着代码说JSP 里没有任何 JDBC 代码替换数据库实现不需要改页面。2.2 从数据库到页面的一次完整请求一次图书查询的路由过程理解一次完整请求怎么走是这个项目能不能讲清楚的关键。以用户查看图书列表为例我在本地跑通后的请求路径是这样的浏览器地址栏输入http://localhost:8080/bookshop/book?actionlistTomcat 根据web.xml或注解找到BookServletServlet 的doGet方法里用request.getParameter(action)判断是 list 还是 detail 或其他操作然后调用BookService的getAllBooks()方法Service 内部再调用BookDao的findAll()方法执行SELECT * FROM book结果集封装成ListBook返回最终 Servlet 把 list 放进 request 作用域转发到/book_list.jsp。JSP 拿到集合后遍历渲染表格。这套链路里Servlet 的doGet和doPost分工要看场景查列表、看详情这类幂等操作用 doGet注册、登录、下单这类会改数据的操作用 doPost。我在课程设计里是统一在一个 Servlet 里写doGet转发给doPost然后靠 action 参数分发这样代码集中但你要在答辩时说清楚为什么用 action 分发不然老师会问为什么不用多个 Servlet。常见做法是一个 Servlet 对应一个模块模块内部的方法用 action 区分这是 JavaWeb 课程设计最常见也最好讲的结构。下面是BookServlet里查询列表和详情的最小骨架WebServlet(/book) public class BookServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { ListBook bookList bookService.getAllBooks(); request.setAttribute(bookList, bookList); request.getRequestDispatcher(/book_list.jsp).forward(request, response); } else if (detail.equals(action)) { int id Integer.parseInt(request.getParameter(id)); Book book bookService.getBookById(id); request.setAttribute(book, book); request.getRequestDispatcher(/book_detail.jsp).forward(request, response); } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }这段代码里有几个参数要说明WebServlet(/book)是 Servlet 3.0 之后的注解式映射不需要在 web.xml 里注册IDEA 新建项目默认支持但如果你用的 Tomcat 版本较老或教学模板要求 web.xml 配置就改成在 web.xml 里加servlet和servlet-mapping。action参数是 URL 里的标识位它决定 Servlet 执行哪个分支这种设计就是 JSP Model2 思想里 Controller 的最简形式。forward是转发而不是重定向因为要带着 request 里的bookList去渲染 JSP如果用了sendRedirectrequest 里的数据会丢这也是新手最容易写错的地方。2.3 包结构与关键类课程设计源码的目录组织方式我建议你直接按下面的包结构组织源码这是黑马和动力节点等 JavaWeb 教学案例里最常用的分层方式老师一眼就能看懂src/main/java ├── com.bookshop.entity // 实体类User, Book, Category, CartItem, Order ├── com.bookshop.dao // 数据访问UserDao, BookDao, CartDao, OrderDao ├── com.bookshop.service // 业务逻辑UserService, BookService, CartService, OrderService ├── com.bookshop.servlet // 控制器LoginServlet, RegisterServlet, BookServlet... ├── com.bookshop.util // 工具类DBUtil, Md5Util src/main/webapp ├── WEB-INF │ └── web.xml ├── css, js, images // 静态资源 ├── login.jsp, register.jsp ├── book_list.jsp, book_detail.jsp ├── cart.jsp └── order_list.jsp, order_detail.jsp实体类对应数据库表的字段属性名和表字段名保持驼峰一致比如数据库字段user_name对应实体属性userName否则手写 ResultSet 封装时容易漏。DAO 里每个方法只做一件事findByUsername、insert、updateStock命名要直白。Service 层是 DAO 的组合比如注册时要先查用户名是否存在再插入这个判断写在 Service 里。工具类 DBUtil 是全局唯一的负责获取数据库连接。你如果拿到一份别人的源代码第一件事不是看页面而是看包结构。包结构混乱的项目八成是复制粘贴拼出来的包结构清晰的项目哪怕代码有小 bug 也好改。这个判断标准在你选课设参考项目时非常实用。3. 图书购物网站的 MySQL 设计六张表把用户、图书、购物车、订单串起来3.1 表结构与字段设计先想清楚关系再写代码图书购物网站最少需要六张表用户表、图书分类表、图书表、购物车表、订单表、订单明细表。购物车和订单是两个最容易设计错的点。购物车表有两种设计一种是把购物车数据存在 Session 里数据库不建表另一种是建购物车表每个用户一条记录对应一本书的购买数量。课程设计为了展示数据库设计能力建议建表但要明白购物车是临时数据下单后要清空。订单表里不能直接存多本书的完整信息而是拆成订单主表和订单明细表这是数据库范式的基本要求也是答辩必问的点。字段类型的选择上有几个经验价格用DECIMAL(10,2)而不用 FLOAT因为浮点数算总价会出精度问题比如 0.1 加 0.2 得到 0.30000000000000004库存用INT就够订单状态用TINYINT存数字0 表示未支付、1 表示已支付、2 表示已发货比存字符串节省空间且方便扩展时间字段用DATETIME在 Java 里对应java.util.DateJDBC 里用setTimestamp写入。下面是用例之间的关系说明一个用户对应多个订单一个订单对应一个用户一个订单对应多个订单明细一个明细对应一本书一个用户对应多个购物车记录一条购物车记录对应一本书一本书属于一个分类一个分类有多本书。搞清楚这些关系写外键时才不会乱。我的建议是外键在数据库层不物理创建而是在 Java 代码里维护关联因为课程设计用的 MySQL 版本五花八门物理外键在导入导出时经常报错逻辑外键足够应付。3.2 建库建表 SQL直接可执行的完整脚本直接给你一套可以原样执行的建表脚本字符集统一用 utf8mb4排序规则不用指定走数据库默认。注意order是 MySQL 关键字所以订单表取名ordersCREATE DATABASE IF NOT EXISTS bookshop DEFAULT CHARACTER SET utf8mb4; USE bookshop; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, nickname VARCHAR(50) DEFAULT COMMENT 昵称, phone VARCHAR(20) DEFAULT , address VARCHAR(200) DEFAULT COMMENT 收货地址, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 关联category.id, name VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT , price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0 COMMENT 销量下单成功后累加, image VARCHAR(500) DEFAULT COMMENT 封面图URL, description TEXT ) ENGINEInnoDB; CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, count INT NOT NULL DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号时间戳随机数, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已发货 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, book_name VARCHAR(200) NOT NULL COMMENT 下单时快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时快照, count INT NOT NULL ) ENGINEInnoDB;脚本里有几个设计细节要说明cart_item表加了UNIQUE KEY uk_user_book (user_id, book_id)作用是同一用户重复加购同一本书时用INSERT ... ON DUPLICATE KEY UPDATE count count 1实现数量累加而不需要先查一次再决定 insert 还是 update少一次数据库交互。order_item里的book_name和price是冗余字段下单时把当时的书名和价格复制一份因为图书价格以后可能修改但订单里应该保留用户下单那一刻的快照这就是为什么订单明细不直接关联 book 表。order_no唯一生成规则可以是用System.currentTimeMillis()加三位随机数并发不高的情况下够用。3.3 数据库连接与中文乱码连接串参数才是关键数据库表建好了Java 这边要用 JDBC 连上去。课程设计最简单的做法是写一个 DBUtil 工具类用DriverManager.getConnection获取连接。这里要特别注意 MySQL 8.0 和 5.x 的驱动差异MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.0 必须用com.mysql.cj.jdbc.Driver同时连接串要加serverTimezoneAsia/Shanghai否则会报时区错误。网上很多旧教程还在用 5.x 的写法你在 IDEA 2026 里新建项目配 MySQL 8.0 时第一步就容易在这里卡住。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bookshop ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查mysql-connector-java版本); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }几个必调参数拆开讲useUnicodetruecharacterEncodingutf8是保证从 Java 写入 MySQL 的中文不乱码同时要求表本身是 utf8mb4两者缺一不可useSSLfalse是关闭 SSL 警告本地开发不需要加密连接不关会有大段红字警告但能连上serverTimezoneAsia/Shanghai是 MySQL 8.0 的必填项因为驱动默认取的时区和本地不一致。PASSWORD 里的123456是本地 MySQL 的 root 密码这个每个人都不同你按自己安装 MySQL 时设置的密码改。MySQL 安装教程网上很多装 8.0 版本时记住了root 密码忘记是最常见的事真忘了就去看 MySQL 8.0 的--skip-grant-tables重置流程。4. 核心功能实现注册登录、图书列表、购物车与下单的代码落地4.1 用户注册与登录MD5 加密与 Session 会话用户模块的完整流程是注册页面提交用户名和密码到RegisterServletServlet 先调用UserService检查用户名是否已存在不存在则插入数据并重定向到登录页登录页面提交用户名和密码到LoginServlet查询数据库比对密码成功则把用户对象放进 Session失败则返回错误提示。密码不能明文存数据库用 MD5 加密后再入库这是底线数据库泄露了密码也不会直接暴露。MD5 加密工具类可以直接用public class Md5Util { public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(MD5加密失败, e); } } }注意String.format(%02x, b)的作用是把每个字节转成两位十六进制不满两位补零。直接用b.toString()会得到负数和单字符拼接出来的加密串是错的。MD5 在 2025 年的安全强度已经不够但课程设计里老师接受如果你想让代码更有含金量可以在注册时加盐比如md5(username password)这样两个用户同密码得到的密文不同答辩时可以主动讲这一点。注册时查重的 SQL 和代码public boolean register(User user) { // 先查用户名是否占用 String checkSql SELECT COUNT(*) FROM user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, user.getUsername()); ResultSet rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; } } catch (SQLException e) { e.printStackTrace(); } // 插入新用户 String insertSql INSERT INTO user (username, password, nickname) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, user.getUsername()); ps.setString(2, Md5Util.md5(user.getPassword())); ps.setString(3, user.getNickname()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }这里用了 try-with-resourcesPreparedStatement和Connection会自动关闭不需要手写 finally 关连接。有些同学用 Statement 拼接 SQL这是高危写法遇到用户名含单引号直接报错甚至被注入PreparedStatement 的?占位符能避免这个问题。注册成功后的密码加密时机要在 service 层做而不是在 Servlet 里做——Servlet 只知道接收页面参数业务规则归 service这个边界讲了老师会认可。登录成功后的 Session 操作是request.getSession().setAttribute(loginUser, user)然后重定向到图书列表页。之后每个页面顶部显示欢迎 xxxJSP 里用${not empty loginUser}判断是否已登录。购物车和下单都依赖这个 Session 里的 user 对象拿 user_id所以登录是其他所有功能的前置条件拦截器可以在 Filter 里写但课程设计不做强制要求你只要在每个需要登录的 Servlet 里检查 Session 为空就重定向到 login.jsp 就行。4.2 图书列表与分页SQL 的 LIMIT 与页码计算图书列表页是网站的门面功能上要支持按分类筛选、关键词搜索、分页显示。分页是课程设计必考的知识点它的核心是LIMIT offset, size这条 SQL。假设每页显示 8 本书当前是第 3 页offset 就是(3-1) * 8 16即跳过前 16 条取 8 条。在 MySQL 里分页就这么简单真正的坑在总数查询和页码边界。分页查询的 DAO 层代码public ListBook findPage(int page, int pageSize, String keyword) { ListBook list new ArrayList(); int offset (page - 1) * pageSize; String sql SELECT * FROM book WHERE name LIKE ? ORDER BY id LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, offset); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { 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(); } return list; }参数说明LIMIT ?, ?的两个占位符第一个是 offset第二个是 pageSize。LIKE ?配合% keyword %实现模糊搜索注意%是拼在参数里的不能写进 SQL 模板。ResultSet 里取price用getBigDecimal而不是getDouble这样金额不会丢精度。keyword 为空时传空字符串%%匹配全部。还有个细节查询总数要用SELECT COUNT(*)单独查一次然后算总页数。总页数 (total pageSize - 1) / pageSize用整数除法向上取整而不是total / pageSize否则最后一页数据会丢。当前页小于 1 时重置为 1大于总页数时置为总页数这两个边界判断写在 Servlet 里防止用户手动改 URL 参数时页面白屏。4.3 购物车与订单提交事务控制与库存扣减购物车这块我见过两种主流实现都值得说。第一种是把购物车放在 Session 里用MapInteger, Integer存 bookId 到数量的映射特点是不用操作数据库页面刷新不丢失但浏览器关了购物车就没了而且换设备不同步。第二种是操作cart_item表每个用户登录后从数据库加载购物车可以实现持久化。课程设计用第二种更有内容可写而且和项目标题里源代码数据库的定位匹配我就按数据库方案展开。购物车列表页的展示需要关联查三张表cart_item做中间关联book提供详细信息。SQL 这样写public ListCartItem findByUserId(int userId) { String sql SELECT c.id, c.count, b.name, b.price, b.image, b.stock FROM cart_item c JOIN book b ON c.book_id b.id WHERE c.user_id ?; // 执行查询并封装略 }JOIN 是必须的否则购物车里只有 book_id 没有书名和价格页面什么都显示不了。加购操作我用INSERT ... ON DUPLICATE KEY UPDATE合并了新增和数量累加两种场景这是 MySQL 特有的语法课程设计里用出来是个亮点public boolean addCart(int userId, int bookId, int count) { String sql INSERT INTO cart_item (user_id, book_id, count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE count count ?; // 参数依次设置 userId, bookId, count, count 后执行 }注意uk_user_book (user_id, book_id)这个唯一键是这条 SQL 能生效的前提不加唯一键它永远走 INSERT 分支数量永远是 1这是最容易踩的坑。下单是全程最核心的代码它必须保证三件事同时成功插入订单主表、插入订单明细、扣减图书库存。三件事任何一件失败都要回滚否则会出现订单生成了但库存没扣或者库存扣了但订单没了的数据不一致。所以在 Service 层手动开启事务public boolean createOrder(int userId, ListCartItem cartItems) throws SQLException { Connection conn DBUtil.getConnection(); boolean success false; try { conn.setAutoCommit(false); // 1. 计算总价生成订单号 BigDecimal total BigDecimal.ZERO; for (CartItem item : cartItems) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getCount()))); } String orderNo System.currentTimeMillis() (int)(Math.random() * 900 100); // 2. 插入订单主表 String insertOrder INSERT INTO orders (order_no, user_id, total_price, status) VALUES (?, ?, ?, 0); PreparedStatement ps1 conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, orderNo); ps1.setInt(2, userId); ps1.setBigDecimal(3, total); ps1.executeUpdate(); int orderId 0; ResultSet keys ps1.getGeneratedKeys(); if (keys.next()) { orderId keys.getInt(1); } // 3. 插入明细扣库存 String insertItem INSERT INTO order_item (order_id, book_id, book_name, price, count) VALUES (?, ?, ?, ?, ?); String updateStock UPDATE book SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement psItem conn.prepareStatement(insertItem); PreparedStatement psStock conn.prepareStatement(updateStock); for (CartItem item : cartItems) { psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setString(3, item.getBookName()); psItem.setBigDecimal(4, item.getPrice()); psItem.setInt(5, item.getCount()); psItem.addBatch(); psStock.setInt(1, item.getCount()); psStock.setInt(2, item.getBookId()); psStock.setInt(3, item.getCount()); int rows psStock.executeUpdate(); if (rows 0) { throw new SQLException(库存不足商品 item.getBookName()); } } psItem.executeBatch(); // 4. 清空购物车 String clearCart DELETE FROM cart_item WHERE user_id ?; PreparedStatement psClear conn.prepareStatement(clearCart); psClear.setInt(1, userId); psClear.executeUpdate(); conn.commit(); success true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } return success; }这段代码是全文含金量最高的部分答辩时把它讲透这门课基本稳了。核心逻辑拆解如下conn.setAutoCommit(false)是事务开关告诉 MySQL 先别自动提交后面所有操作要么一起成功要么一起失败Statement.RETURN_GENERATED_KEYS是拿自增主键的方式不写这个参数getGeneratedKeys()返回空stock stock - ? WHERE id ? AND stock ?这条 UPDATE 用条件判断实现了原子性的库存扣减如果库存不足影响行数为 0代码主动抛异常触发回滚比先 SELECT 再判断要安全得多并发下单时不会超卖addBatch和executeBatch是批量插入订单里有 5 本书只需要一次网络往返性能好而且代码整洁。最后 finally 里恢复setAutoCommit(true)是必须的因为连接要还给连接池如果保持 false 状态下一个使用者开启的事务会异常。5. 部署运行避坑从 Tomcat 报错到 MySQL 连不上的 5 个血泪经验这套项目我在本地跑通的路上踩了不少坑下面这几条是课程设计提交前最容易翻车的地方按现象到原因到解决写清楚。一条条对着排查能省下大量玄学报错的时间。第一坑Tomcat 启动后访问页面报 500控制台显示ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是项目里没有引入 MySQL 驱动 jar或者引入的 jar 版本与 MySQL 版本不匹配。常见有两种一是下载了mysql-connector-java5.x 的 jar 但数据库是 MySQL 8.0驱动类路径是com.mysql.jdbc.Driver不兼容二是 jar 放在了lib目录但 IDEA 的 Artifacts 没有把它打包进WEB-INF/lib。解决方法是下载mysql-connector-java8.0.x 的 jar然后打开 IDEA 的File - Project Structure - Artifacts确认所选 jar 出现在WEB-INF/lib列表里没有的话右键添加最后 Build 一次再看。第二坑MySQL 8.0 连接报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这一般是 MySQL 8.0 安装时没配置时区驱动读取服务器时区读到乱码。解决方法是连接串加serverTimezoneAsia/Shanghai或者在 MySQL 命令行执行SET GLOBAL time_zone 08:00。注意Asia/Shanghai要完整写只写GMT8在部分驱动版本上也会被拒。这个报错在安装完 MySQL 8.0 后第一次跑 JavaWeb 项目时基本都会遇到别慌。第三坑JSP 页面中文全部变成问号数据库里也是问号。这个要从四个地方排查页面本身的编码JSP 文件顶部要有% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %请求编码Servlet 里request.setCharacterEncoding(UTF-8)要在第一次读取参数之前调用数据库连接串的characterEncodingutf8表字符集建表时默认 utf8mb4。还有一个隐藏点IDEA 里 JSP 文件右下角的编码要显示 UTF-8如果显示 GBK文件里中文存进去就是乱码的改完编码后重新 Build。这四个地方全对了中文乱码才能根治。第四坑下单时提示库存不足但明明数据库里 stock 是够的。这个坑出在UPDATE book SET stock stock - ? WHERE id ? AND stock ?这条 SQL 上。如果book表的stock是INT UNSIGNED类型执行stock stock - 1时在 MySQL 内部遇到 UNSIGNED 类型溢出会报错或者不更新表现就是没扣成功。解决方法是把 stock 改成普通INT类型或者在 UPDATE 前先 SELECT 一次但不要先查再改并发时会出错。同样的道理适用于sales字段。建表脚本里我用的是INT NOT NULL DEFAULT 0就是避开 UNSIGNED 的坑。第五坑什么代码都没改第二次打开项目启动 Tomcat 报端口被占用Port 8080 was already in use。常见是上一次运行没有正常停止Tomcat 进程还残留在后台或者装了其他 Web 服务占用了 8080。解决方法是命令提示符执行netstat -ano | findstr 8080查到的 PID 用taskkill /F /PID 进程号强制杀掉。如果端口确定被其他软件占用且不方便关闭可以改 Tomcat 的端口在conf/server.xml里把Connector port8080改成 8081重启即可。注意改这一个地方就够了不用改port8005和8009那是关闭端口和 AJP 端口课程设计不需要动。6. 答辩前把代码再打磨一遍从能跑到能讲清楚课程设计通过了就万事大吉不是的你还得在老师面前演示。我给自己定的标准是除了代码本身能跑通还要准备 5 分钟讲清楚为什么这么设计。先说一个容易做又能加分的优化再把验收路径过一遍。最推荐的优化是把 DBUtil 换成 Druid 连接池。课程设计大部分同学用的都是DriverManager每次新建连接高并发场景下连接反复创建销毁性能差而且代码显得基础。Druid 只需要改 DBUtil 的初始化逻辑底层 API 不变DAO 层代码一行不用动public class DBUtil { private static DruidDataSource dataSource new DruidDataSource(); static { dataSource.setUrl(jdbc:mysql://localhost:3306/bookshop ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明setInitialSize(5)是启动时预建立 5 个连接setMaxActive(20)是最大连接数 20。引入 Druid 的 jar 后这段代码就能直接替换原来的 DriverManager 版本。答辩时老师问你你的项目能撑住多少并发你可以说数据库连接不会成为瓶颈因为连接池复用连接而不是每次请求都新建。这一句就能拉开和普通作业的差距。验收路径是最重要的每次改完代码不要只看页面 诶出来了 就收工。我推荐的回归顺序是注册一个新用户 → 退出 → 用新用户登录 → 搜索一本书 → 加入购物车 → 再加一次同一本书验证数量累加 → 去购物车改数量 → 下单 → 看库存是否减少、销量是否增加 → 再故意把某本书库存改成 0 下单验证会不会回滚。这条链路覆盖了注册、登录、会话、模糊查询、购物车、事务、异常处理七个模块任何一个环节出错当场就能定位。我自己的习惯是答辩前一晚把这条链路完整跑两遍第二天演示时基本不会在老师面前翻车。有一点经验想分享课程设计最怕的不是代码有 bug而是讲不清楚自己的代码。老师问你这个功能怎么实现的你支支吾吾说这个我调了网上别人的代码那基本就危险了。哪怕功能简单只要你能讲清楚请求进来先到 ServletServlet 调用 ServiceService 操作 DAO回去渲染 JSP把一个登录讲透比堆一堆自己都说不明白的花哨功能强得多。希望这篇文章能帮你少踩几个坑把课设做扎实。本文还有配套的精品资源点击获取