news 2026/10/22 18:24:34

基于JSP的网上书店系统设计与实现:从环境搭建到购物车事务全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JSP的网上书店系统设计与实现:从环境搭建到购物车事务全解析

简介:一份基于JSP的网上书店系统毕业设计论文文档,主要面向计算机相关专业学生,尤其适合正在准备课程设计或毕业设计的读者。该文档完整阐述了网上书店系统的设计与实现过程,内容涵盖JSP与JAVA技术基础、系统可行性分析与需求分析、功能模块划分、数据库模式设计,以及前台图书检索、购物车管理、后台图书管理与订单处理等核心功能,可直接作为论文写作框架和系统开发思路的借鉴。资源仅包含一个doc文件,压缩包大小约1.09MB,文档内含中英文摘要、关键词、目录及主要章节,结构完整,便于快速查阅。已有352人学习,对于需要梳理论文结构、理解JSP结合MySQL数据库的项目实现流程、参考网上书店典型业务场景的读者,是一份相当实用的参考资料。

1. 毕业设计做JSP网上书店:为什么这个老技术还能打

2025年还有人选JSP做网上书店系统?说实话,不少毕业生被Spring Boot压得喘不过气,最后回到基于JSP的网上书店系统的设计与实现上来。这个题目的价值不在技术新,而在链路完整:用户注册、图书展示、购物车、下单、后台管理,能用一条交易闭环把Servlet、JSP、JDBC、MySQL全部串起来,还不用碰分布式。它适合两类人:一类是毕设题目已定、需要稳妥落地的同学,另一类是刚学完Java Web、想通过一个真实项目消化知识点的初学者。这篇文章会从环境搭建、数据库设计,一直讲到购物车事务、部署验证,每步告诉你为什么这么设计、参数怎么调、坑在哪里。

2. 搭建开发环境与项目骨架:从JDK到Tomcat的完整链路

2.1 环境版本怎么选:JDK、Tomcat、MySQL的兼容矩阵

基于JSP的网上书店系统不是运行在浏览器里的,它需要一套Java Web容器。JSP本质上是被容器翻译成Servlet再执行,所以你的环境至少要能编译Servlet、能跑JSP解析器。兼容性最好的组合是 JDK 1.8 + Tomcat 8.5 + MySQL 5.7,我近几年的毕设辅导都建议学生用这套,不是图新,而是图稳。

组件推荐版本理由
JDK1.8学校机房普遍安装,Tomcat 8.5 完全兼容,语法特性够用
Tomcat8.5.x配套教程最多,默认端口8080,javax.servlet包路径不用改
MySQL5.7事务、外键、中文排序稳定,不像8.0那样改认证插件
IDEIntelliJ IDEA社区版免费,支持Maven Web项目,够用

这里提醒一句:别用 Tomcat 10。Tomcat 10 把javax.servlet迁到了jakarta.servlet,你按教程写的import javax.servlet.http.HttpServlet全部要改,编译期直接报红。如果你因为某些原因必须用 Tomcat 10,那下面的代码块和依赖也要换成 jakarta 命名空间,但网上书店这种小项目不值得为这个折腾。

2.2 创建JSP项目并用Maven管理依赖

创建项目时,我不用 Eclipse 的 Dynamic Web Project 那套,而是用 Maven 的 webapp 骨架。这样打出来的 war 包结构清晰,交作业也能体现工程化意识。IDEA 里新建项目选 Maven,archetype 选maven-archetype-webapp,生成目录后把src/main/java和src/main/resources补上,因为骨架默认没有这两个目录。

<dependencies> <!-- Servlet API:编译期需要,运行期由 Tomcat 提供,故用 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSTL:JSP 页面里用 c:forEach / c:if 替代 Java 代码 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动:5.1.49 与 MySQL 5.7 配合良好 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>

逻辑说明:Servlet API 的 scope 必须设为provided,否则打出的 war 会把javax.servlet-api的 jar 包也塞进 WEB-INF/lib,和 Tomcat 自带的重复,运行时可能报方法不存在。JSTL 1.2 是最后一个使用javax.servlet命名空间的稳定版本,和 Tomcat 8.5 绝配。数据库驱动版本不用追新,5.1.49 对 MySQL 5.7 的 utf8mb4、rewriteBatchedStatements都支持到位。

参数说明:如果你用的是 MySQL 8.0,驱动要换成 8.0.x,同时连接 URL 要写serverTimezone=Asia/Shanghai,否则会报时区错误。这算是版本兼容里的一个隐藏坑,下面 db.properties 里我会再提。

2.3 配置web.xml与数据库连接池

JSP 项目里,WEB-INF/web.xml是 web 应用的入口描述。虽然 Servlet 3.0 支持注解,但我会在 web.xml 里配置字符编码过滤器和欢迎页,让启动过程更透明。数据库这块,我不用每次请求都新建连接的老写法,而是用 DBCP 连接池。连接池的参数放在src/main/resources/db.properties里,由工具类加载。

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.bookstore.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

逻辑说明:过滤器类CharacterEncodingFilter需要你自己实现,核心逻辑只有一行:req.setCharacterEncoding("UTF-8")和resp.setCharacterEncoding("UTF-8"),然后在chain.doFilter前调用。url-pattern 写/*才能拦截到所有 JSP 和 Servlet,只写/不会覆盖 JSP 请求。欢迎页写 index.jsp 后,访问http://localhost:8080/bookstore/就会自动跳转到首页。

package com.bookstore.util; import org.apache.commons.dbcp2.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DbUtil { private static BasicDataSource dataSource; static { try { Properties props = new Properties(); props.load(DbUtil.class.getClassLoader() .getResourceAsStream("db.properties")); dataSource = new BasicDataSource(); dataSource.setDriverClassName(props.getProperty("jdbc.driver")); dataSource.setUrl(props.getProperty("jdbc.url")); dataSource.setUsername(props.getProperty("jdbc.username")); dataSource.setPassword(props.getProperty("jdbc.password")); // 初始化连接数 5,最大 20,等待 3 秒后超时 dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxWaitMillis(3000); // 空闲连接心跳检测,防止被 MySQL 踢掉 dataSource.setTestWhileIdle(true); dataSource.setValidationQuery("SELECT 1"); } catch (Exception e) { throw new ExceptionInInitializerError( "连接池初始化失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

逻辑说明:getConnection()到手的是一个被 DBCP 代理的连接,调用close()不是真正关掉数据库连接,而是归还给连接池。所以你在 DAO 里可以放心写 try-with-resources 或 finally close。这里把testWhileIdle设为 true,配合SELECT 1每 60 秒探活一次,能避免隔夜后第一次访问白屏。

参数说明:initialSize=5对毕设足够,太小会在启动后第一批请求时冷启动慢,太大浪费内存。maxWaitMillis=3000的含义是,如果连接池全部被占用,最多等 3 秒,超时直接抛CannotGetJdbcConnectionException,页面显示 500,总比无限等待好。如果你想看到更具体的原因,可以在日志里打印异常堆栈。

3. 数据库设计与用户模块:图书表、用户表、购物车表怎么落地

3.1 数据库ER设计:三张主表与订单明细表

网上书店的数据模型不复杂,但表关系容易画错。标准做法是五张表:用户表user、图书表book、购物车表cart_item、订单主表orders、订单项表order_item。cart_item是用户和图书的多对多关联表,order_item是订单和图书的关联表。先看用户表和图书表的 DDL,这是后面所有功能的地基。

CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` CHAR(64) NOT NULL COMMENT 'SHA-256 哈希值', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '个人中心展示昵称', `email` VARCHAR(100) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL, `author` VARCHAR(50) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `cover_url` VARCHAR(255) DEFAULT NULL COMMENT '封面图相对路径', `description` TEXT, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:密码字段用CHAR(64),因为 SHA-256 输出是定长 64 位十六进制字符串。别用VARCHAR(32)存 MD5,面试官看到会直接问"为什么不用更安全的算法"。金额用DECIMAL(10,2),这是钢规,FLOAT/DOUBLE的二进制浮点在累加 0.1 时会出0.30000000000000004,订单总价会让你查到怀疑人生。

参数说明:数据库默认字符集选utf8mb4而不是utf8,因为utf8在 MySQL 里最多存 3 字节,遇到生僻字或 emoji 会报 "Incorrect string value"。表名user是 MySQL 保留字?其实user在 MySQL 里是mysql.user系统表,但作为业务表名只要反引号包裹就能用。为了减少麻烦,我后来习惯用t_user,不过这里为了直观保留了user,记得所有 SQL 都加反引号。

3.2 用JDBC封装BaseDao,避免每页重复写连接

JSP 页面里直接写Class.forName、DriverManager.getConnection是很多毕设的第一版,但到了购物车阶段就会发现每个页面都要复制粘贴 20 行连接代码,改一下密码要全局替换。我一般写一个BaseDao,把连接获取、资源关闭和通用增删改查收进去。这样各个 DAO 只关心自己的 SQL。

package com.bookstore.dao; import com.bookstore.util.DbUtil; import java.sql.*; public abstract class BaseDao { @FunctionalInterface public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; } protected int update(String sql, Object... params) { try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("数据库更新失败", e); } } protected <T> T queryOne(String sql, RowMapper<T> mapper, Object... params) { try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { return rs.next() ? mapper.map(rs) : null; } } catch (SQLException e) { throw new RuntimeException("数据库查询失败", e); } } }

逻辑说明:Java 7 的 try-with-resources 会自动关闭Connection、PreparedStatement、ResultSet,顺序是逆序关闭。这里conn.close()调用的是 DBCP 代理连接的 close,归还连接池而不是断连,所以不会浪费连接。queryOne的RowMapper让你在子类里写rs.getInt("id")之类的映射,和 MyBatis 的 ResultHandler 是一个思路。

参数说明:update方法第一个参数是 SQL,后面的可变参数是占位符值。如果你在 SQL 里写了 3 个?,但参数只传了 2 个,PreparedStatement 会抛Parameter index out of range。这个错误很常见,排查时先数问号个数。另外,所有 DAO 方法里都不要自己 new 连接,统一走DbUtil.getConnection(),否则连接池形同虚设。

3.3 注册与登录:会话管理与密码加密的常见做法

用户模块是 JSP 页面里"个人信息展示页面"的数据来源。注册流程我放在RegisterServlet,密码哈希后落库;登录成功后把User对象放入 session,JSP 页面通过 JSTL 判断 session 里有没有用户,有就显示"欢迎 xxx",没有就显示"登录/注册"链接。

@WebServlet("/register") public class RegisterServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); String nickname = req.getParameter("nickname"); if (username == null || username.trim().isEmpty() || password == null || password.length() < 6) { req.setAttribute("error", "用户名不能为空,密码长度至少 6 位"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } // 密码哈希后再入库,绝不明文存储 String hashed = HashUtil.sha256(password); int result = userDao.insert(username, hashed, nickname); if (result > 0) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); } else { req.setAttribute("error", "注册失败,用户名可能已存在"); req.getRequestDispatcher("/register.jsp").forward(req, resp); } } }

逻辑说明:密码哈希这里强调一下,不要只做一次SHA-256(password),因为彩虹表里全是现成结果。毕设项目可以加一个全局固定的盐,比如SHA-256(salt + password),然后在HashUtil里写清楚盐值。答辩时评委问"怎么防彩虹表",你能说出"加固定盐"就已经超过大部分同学了。

参数说明:注册成功用sendRedirect而不是forward,因为 redirect 会让浏览器发起新请求,刷新页面时不会重复提交注册表单。失败时用forward保留error属性,register.jsp里用${error}展示。这里有个细节:req.getParameter("username")拿到的值已经被编码过滤器转成 UTF-8 了,如果你没配过滤器,中文用户名会直接乱码。

4. 在线购书核心链路:展示、搜索、购物车、订单

4.1 图书列表分页与条件搜索

图书展示页是 JSP 网上书店的门面,分页和搜索通常绑在一起。我来用一个BookServlet接收pageNo和keyword,在BookDao里分两次查询:一次COUNT(*)求总数,一次LIMIT取当前页数据。这样做的原因是不同数据库对LIMIT的写法不一样,MySQL 用LIMIT offset, size,SQL Server 用OFFSET ... FETCH,所以 DAO 层必须和数据库绑定。

public Page<Book> findByPage(int pageNo, int pageSize, String keyword) { StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM book WHERE 1=1"); StringBuilder dataSql = new StringBuilder("SELECT * FROM book WHERE 1=1"); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { String like = "%" + keyword.trim() + "%"; countSql.append(" AND title LIKE ?"); dataSql.append(" AND title LIKE ?"); params.add(like); } // 先查总数,再查当前页数据 int total = queryScalar(countSql.toString(), params.toArray()); dataSql.append(" ORDER BY id DESC LIMIT ?, ?"); params.add((pageNo - 1) * pageSize); params.add(pageSize); List<Book> list = queryList(dataSql.toString(), mapper, params.toArray()); return new Page<>(pageNo, pageSize, total, list); }

逻辑说明:WHERE 1=1是个老技巧,动态加搜索条件时不用判断"我是不是第一个条件",直接append(" AND ...")就行。total用queryScalar返回Long再intValue(),因为COUNT(*)在 MySQL 返回的是 BIGINT。偏移量计算公式是(pageNo - 1) * pageSize,如果前端页码从 0 开始,公式要改成pageNo * pageSize,这个约定必须前后端一致,否则最后一页会出现重复数据或漏数据。

参数说明:分页条上的"总页数"要自己算,int totalPages = (total + pageSize - 1) / pageSize,用整数除法向上取整。很多同学直接写total / pageSize,当总数是 12、每页 12 时得到 1,但实际应该是 1 页,还好;当总数是 13 时得到 1,就会少显示一页。这个细节在答辩时很加分的。

4.2 购物车设计:用Session还是用数据库表

购物车是网上书店系统里最容易引战的设计点。Session 购物车代码少,适合纯演示;数据库购物车能持久化,也更像一个完整的"设计与实现"项目。我给出的建议是:既然题目已经标了"设计与实现",就用数据库表方案,这样 ER 图里多一张表,答辩时能讲清楚cart_item的外键和唯一索引。

对比维度Session 购物车数据库购物车
持久化关闭浏览器即丢失存放 MySQL,随时可读
多设备同步不支持支持
表设计工作量0一张关联表
适合场景原型演示毕业设计/真实系统
public int add(int userId, int bookId, int count) { // 先查找该用户购物车里是否已存在这本书 CartItem existing = findCartItem(userId, bookId); if (existing == null) { String sql = "INSERT INTO cart_item (user_id, book_id, count, create_time) " + "VALUES (?, ?, ?, NOW())"; return update(sql, userId, bookId, count); } else { // 已存在则数量累加,SQL 直接做算术,避免先查后改的并发问题 String sql = "UPDATE cart_item SET count = count + ? WHERE id = ?"; return update(sql, count, existing.getId()); } }

逻辑说明:累加操作写成count = count + ?,这一步叫"原子更新"。如果先SELECT count再算好新值去UPDATE,两条并发请求同时下单时可能把数量覆盖成同一个值,丢了一次加购。数据库的行锁会保证这条 UPDATE 是串行的。购物车表对(user_id, book_id)建唯一索引,防止用户重复添加时产生两行记录,这样findCartItem才能稳定返回单条数据。

参数说明:cart_item表结构里,count我建议用SMALLINT(6),最大值 32767,已经足够。如果你做秒杀类设计需要更大数量,再改成INT。另外,create_time用DATETIME DEFAULT CURRENT_TIMESTAMP,这样插入时不传时间也能自动记录,个人中心展示加购时间很自然。

4.3 订单提交与库存扣减的事务处理

下单是整个系统里"含金量最高"的一环,因为它要同时处理订单主表、订单项表、库存、购物车清理,任何一个动作失败都不能留下半成品数据。所有 SQL 要包在同一个数据库事务里,手动控制commit和rollback。在 JSP 技术栈里没有 Spring 的@Transactional,只能手写 Connection 级别的事务。

public int checkout(int userId, int[] bookIds, int[] counts) { Connection conn = null; try { conn = DbUtil.getConnection(); // 关闭自动提交,开始事务 conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 1. 向 orders 插入一条订单,拿到自增 orderId // 2. 根据 bookIds 查询图书单价,逐条插入 order_item // 3. 扣减库存,SQL 必须带库存条件: // UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ? // 影响行数为 0 说明库存不够,抛异常触发回滚 // 4. 清空 cart_item 中该用户的记录 conn.commit(); // 全部成功才提交 return orderId; } catch (Exception e) { try { conn.rollback(); // 任一步失败,整个订单作废 } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException("下单失败,已回滚", e); } finally { if (conn != null) { try { conn.setAutoCommit(true); // 恢复默认,方便连接池复用 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

逻辑说明:把setAutoCommit(true)放在 finally 里,是为了归还连接池时它的状态是干净的。如果你忘了重置,DBCP 拿到的下一个连接可能还带着未提交的事务,然后出现莫名其妙的锁等待超时。REPEATABLE_READ隔离级别保证在事务期间,其他会话对库存的修改不会读到,防止超卖。但光靠隔离级别不够,扣库存 SQL 必须带stock >= ?,这是最后一道保险。

参数说明:int[] bookIds和int[] counts从前端传来时,一定要校验两个数组长度相等,并且每个 count 都大于 0。前端恶意请求传一个超大的 count,stock >= count会挡住,但如果传负数,stock - (-5)会变成加库存,这是安全漏洞。所以 Service 层入口的第一行就检查:if (bookIds.length != counts.length) throw new IllegalArgumentException();。

5. 避坑与常见问题:JSP开发翻车现场的三条血泪经验

5.1 乱码问题:POST、GET、数据库三处编码要统一

现象:表单提交"中国青年出版社",列表页显示成"中å½";或者注册中文用户名后,个人中心里全是问号。

原因:Tomcat 对 POST 请求默认按 ISO-8859-1 解码,对 GET 请求从 Tomcat 8 开始默认 UTF-8,但前端没设置页面编码也会导致乱。数据库连接 URL 没指定characterEncoding=UTF-8,存进去时就已经是错的。

解决:三处统一。项目里加一个过滤器,强制请求和响应都使用 UTF-8;db.properties 的 URL 写成:

jdbc.url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8&useSSL=false

然后 JSP 页面第一行写<%@ page pageEncoding="UTF-8" %>。这三处缺一不可,少一处就会在不同环境间跳乱码。过滤器实现代码如下:

package com.bookstore.filter; import javax.servlet.*; import java.io.IOException; public class CharacterEncodingFilter implements Filter { private String encoding = "UTF-8"; @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(encoding); resp.setCharacterEncoding(encoding); chain.doFilter(req, resp); } }

5.2 路径问题:相对路径与绝对路径导致CSS图片失效

现象:首页样式正常,点进/book/detail.jsp后,页面变回纯 HTML,图片全部裂开,控制台报 404。

原因:JSP 里你写的是<link rel="stylesheet" href="css/style.css">,这是相对路径。浏览器看到的 URL 是http://localhost:8080/bookstore/book/detail.jsp,它会去/bookstore/book/css/找资源,当然找不到。

解决:把所有静态资源路径改成以上下文路径开头的绝对路径。常见做法是在 JSP 头部分别写:

<c:set var="ctx" value="${pageContext.request.contextPath}" /> <link rel="stylesheet" href="${ctx}/css/style.css">

如果是<img>标签,图片 src 也用${ctx}/upload/cover.jpg。这样不管请求落到哪一层,都能正确拼出/bookstore/css/style.css。注意${pageContext.request.contextPath}在 JSP 中返回的是部署上下文根,默认是 war 包名。

5.3 数据库连接池空闲超时:页面白屏和连接关闭

现象:项目部署在两台考试机器上,上午还能访问,下午第一次点页面直接报Communications link failure,重启 Tomcat 后恢复正常,过一会儿又挂。

原因:MySQL 的wait_timeout默认 8 小时,但很多实验室 MySQL 被改成 120 秒;空闲连接超过这个时间后,服务端主动断开,而 DBCP 池里的连接对象还以为自己活着,真正发送 SQL 时才拿到一个已经关闭的 socket。

解决:连接池开启探活。在 DbUtil 里补两个参数:

dataSource.setTestWhileIdle(true); dataSource.setValidationQuery("SELECT 1"); dataSource.setTimeBetweenEvictionRunsMillis(60000); dataSource.setMinEvictableIdleTimeMillis(300000);

参数说明:timeBetweenEvictionRunsMillis=60000表示回收线程每 60 秒跑一次;minEvictableIdleTimeMillis=300000表示空闲超过 5 分钟才会被回收,否则探活频率太高会浪费 CPU。如果你用的是 c3p0,对应参数是idleConnectionTestPeriod和automaticTestTable,效果相同。

5.4 JSP里写大量Java源码:答辩容易翻车

现象:翻开list.jsp,从Connection conn = null到while(rs.next())一坨黑压压的 Java 代码,改一个字段要动十个页面。

原因:很多同学照着 JSP 时代的旧博客写,把页面当成了万能壳,业务逻辑和视图完全耦合。

解决:严格分层。Servlet 只做参数接收和跳转,DAO 只做 SQL,JSP 只做数据显示。用 JSTL 重写列表展示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach var="b" items="${bookList}"> <div class="book-card"> <h3>${b.title}</h3> <p>${b.author} 著,单价 ${b.price} 元</p> </div> </c:forEach>

说明:${b.price}会自动调用Book模型的getPrice()方法,items="${bookList}"在 Servlet 里用req.setAttribute("bookList", list)设置。这样改页面布局时 Java 代码一行都不用动。这条坑看起来不严重,但答辩时评委最常翻的就是 JSP 页面源码,看到满屏<%会直接质疑你的设计能力。

6. 部署验证与进阶改造:把毕业设计变成可维护的系统

6.1 上线前自测清单与跨浏览器检查

答辩或部署前,我习惯按一张清单快速过一遍。前几次做网上书店时,我总在跨浏览器支持的设计与实现上翻车:Chrome 没问题,到了 Firefox 按钮不换行;或者 JSP 页面里的图片位置偏移,背景错位。后来我统一在 head 里加了 viewport meta 和基础重置,图片都设置固定宽高和object-fit: cover,这个问题基本绝迹。

检查项验证方法常见失败
浏览器兼容Chrome/Edge/Firefox 打开核心页面CSS 错位、JS 报错
中文乱码注册中文昵称后刷新个人中心昵称变问号
购物车持久化登录加购后重启浏览器购物车清空
库存边界库存设 1,两个账号同时下单库存变负数
连接池回收闲置 10 分钟后再访问页面白屏

这个清单里最容易被忽略的是连接池回收,原因就是 5.3 节的空闲连接被 MySQL 断开。把自测项加进去,能提前暴露问题。

6.2 从JSP到Spring Boot:下一步的最小迁移

如果答辩完还想继续优化,不建议推翻重写。把 Servlet 层换成 Spring Boot 的 Controller,DAO 层保持原样或者逐步换成 MyBatis,JSP 暂时保留——Spring Boot 对 JSP 的支持虽然麻烦,但能跑。这里给一个最直观的对照:原来的BookServlet的doGet方法里req.setAttribute("list", dao.find(...))然后forward,迁移后变成:

@Controller @RequestMapping("/book") public class BookController { private final BookDao bookDao = new BookDao(); @GetMapping("/list") public String list(@RequestParam(defaultValue = "1") int pageNo, @RequestParam(required = false) String keyword, Model model) { Page<Book> page = bookDao.findByPage(pageNo, 12, keyword); model.addAttribute("page", page); return "book/list"; } }

逻辑说明:@RequestParam(defaultValue = "1")自动完成了原来request.getParameter("pageNo")和"空值给默认值"的工作;Model model相当于 request 域;返回字符串是视图名,对应/WEB-INF/views/book/list.jsp。你会发现业务逻辑和 DAO 完全没变,变的是入口层,迁移成本很低。

我自己第一次做网上书店的时候,就是没想清楚连接池心跳这回事,答辩演示前十分钟页面白屏,最后靠重启 Tomcat 救场。后来我把 6.1 的自测清单固定下来,每次部署先跑一遍,再也没在关键时候翻过车。技术选型这件事,稳定比新鲜重要得多,JSP 虽然老,但把你的 Servlet、JSP、JDBC、MySQL 基础打得越实,后面学 Spring Boot 越轻松。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:44:15

Wintel架构深度解析:Windows与Intel组合的硬件搭配与系统安装指南

1. 从“Wintel”这个词说起&#xff1a;一个时代的硬件与软件共生样本第一次听到“Wintel”这个说法&#xff0c;还是在一次老设备翻新的讨论里。当时有人提到“微特爾架构的老机器还能不能救”&#xff0c;我愣了一下才反应过来&#xff0c;这说的是Windows 操作系统加 Intel …

作者头像 李华
网站建设 2026/10/22 3:44:16

用航空规范治理AI废话病:Agent工程化的可靠实践

最近圈子里有个说法特别扎心&#xff1a;AI什么都会&#xff0c;就是不会少说。你让它总结三句话&#xff0c;它能给你铺垫三段背景&#xff1b;你让它查个报错&#xff0c;它先共情你加班辛苦&#xff0c;最后才把错误码夹在中间。Andrej Karpathy最近反复在提“software is c…

作者头像 李华
网站建设 2026/10/22 5:37:16

C++容器底层原理与选型:从vector到unordered_map

CS106L 第五讲的 PPT&#xff0c;我对着屏幕断断续续整理了两遍&#xff0c;最后得出的结论很直接&#xff1a;Containers 这一节是整个 C 标准库真正开始显山露水的地方&#xff0c;也是从“会用 C 写程序”迈向“理解 C 为什么这样设计”的分水岭。第五讲没有绕弯子&#xff…

作者头像 李华
网站建设 2026/10/22 14:14:07

OpenClaw接入MCP后token暴涨?三步优化省70%成本

给 OpenClaw 接上 MCP 之后&#xff0c;我发现自己和它的每一次对话&#xff0c;其实都在偷偷烧 token。原本以为只是加了几个工具接口&#xff0c;没想到月底一查 API 账单&#xff0c;数字比想象中高出好几倍。这篇文章不聊高大上的架构&#xff0c;就聊聊我踩过的坑、怎么分…

作者头像 李华
网站建设 2026/10/22 5:37:16

Vibe Coding实战:从AI辅助到人机协作的工程化工作流与避坑指南

Vibe Coding 这个词&#xff0c;一年前我第一次看到的时候&#xff0c;第一反应是“这不就是偷懒写代码吗”&#xff0c;后来自己被朋友拖着试了两周&#xff0c;直接上头。到现在一年半过去&#xff0c;我经手了大小项目十来个&#xff0c;从最初只会拿 AI 写点胶水脚本&#…

作者头像 李华
网站建设 2026/10/22 5:37:15

Cisco与Brocade光纤交换机异构级联实战配置指南

简介&#xff1a;本资源是一份面向网络工程师与存储系统运维人员的实战型技术指南&#xff0c;聚焦Brocade与Cisco两大主流光纤交换机平台的异构级联配置难题&#xff0c;解决跨厂商设备协同组网、VSAN互通及Domain ID冲突等关键问题。文档以Cisco MDS 9120交换机为实操对象&am…

作者头像 李华