ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Web在线购书系统:JSP+Servlet+MySQL MVC实战指南

Java Web在线购书系统:JSP+Servlet+MySQL MVC实战指南 简介这是一份面向JavaWeb初学者与高校计算机专业学生的期末大作业实战资源完整实现了一个基于JSPServletMySQL的在线购书系统严格遵循MVC三层架构设计覆盖用户注册登录、图书浏览、购物车管理、订单生成与后台图书管理等核心业务流程可直接用于课程设计或期末答辩。资源包共75个文件含14个JSP页面前端交互、12个Java类Controller与DAO层逻辑、12个Class字节码、3个SQL脚本含建库建表及初始化数据、3个XML配置文件web.xml与数据库连接配置、16张界面截图与1个项目演示MP4视频辅以README.md说明文档整体压缩包大小为47.77MB。已有349人学习下载代码注释详尽、结构清晰、部署简易配套数据库脚本与可视化操作录屏显著降低理解门槛特别适合零基础学生快速掌握JavaWeb开发全流程与工程化实践规范。1. 这不是“抄个JSP页面交差”的期末作业一个真正跑得通、改得动、能上线的在线购书系统长什么样你手头那份标着“满分项目”的javaweb期末大作业-在线购书系统 jspServletmysql采用mvc三层架构源码数据库大概率不是压缩包里一堆.jsp文件加个web.xml就完事。我带过三届Java Web课设每年收200份“购书系统”80%在Tomcat启动后首页404、登录跳转空白、购物车数据存不进MySQL——根本不是代码写得丑而是整个MVC骨架没立稳Model层硬编码SQL、View层混着Java逻辑、Controller里塞满数据库连接和HTML拼接。真正的“满分”是能用localhost:8080/bookstore/login.jsp正常登录点击“加入购物车”后刷新页面看到实时数量管理员后台修改图书库存后前端立刻响应且所有业务逻辑比如库存扣减、订单生成都封装在独立的Service类里不沾JSP半行代码。它不追求Spring Boot那种自动装配但必须把JSP当纯模板、Servlet当唯一入口、MySQL当真实数据源、三层之间靠接口解耦——这才是企业级Java Web开发的最小可行范式。适合刚学完Servlet生命周期、能手写JDBC但还没碰过框架的本科生也适合想补足传统Web开发底层逻辑的转行者。2. 搭建可运行环境从零配置IDEA Tomcat MySQL避开90%新手卡点2.1 IDEA中创建标准Java Web Module不是Maven项目期末作业最坑的起点就是用Maven模板新建项目。JSPServlet老项目依赖的是Servlet API的编译时jar而Maven默认拉取的javax.servlet-api版本常与Tomcat内置冲突。正确做法是手动关联Tomcat lib# 在IDEA中 File → Project Structure → Project Settings → Modules → → Web → 点击Create web.xml强制生成标准web.xml → 在Web Resource Directory中指定 src/main/webapp或你实际的web根目录 → 切换到Dependencies标签页 → → JARs or directories → 选择你本地Tomcat安装目录下的 lib/ 目录如 D:\apache-tomcat-9.0.83\lib\ → 勾选 servlet-api.jar、jsp-api.jar、el-api.jar注意不要选tomcat-juli.jar等日志jar提示如果IDEA报错“Cannot resolve symbol HttpServlet”说明servlet-api.jar没正确加载。检查是否勾选了该jar且未被Maven依赖覆盖。右键项目 → Reload project 无效时直接删掉.idea/libraries/下自动生成的Maven库引用。2.2 Tomcat服务器配置端口、上下文路径与热部署关键设置很多同学配完Tomcat访问http://localhost:8080能进管理页但自己的项目死活404。核心问题在Context Path和Deployment在IDEA中Run → Edit Configurations → → Tomcat Server → LocalServer tabApplication server指向你的Tomcat解压目录非bin目录JRE必须用JDK 8JSP 2.3规范要求JDK 11需额外配置JasperDeployment tab点击→Artifact→ 选择bookstore:war exploded不是war包Important右侧Application context必须填/bookstore不能留空或填/否则项目会部署到根路径与Tomcat管理页冲突On Update action选Update classes and resources热更新JSP/JavaOn frame deactivation选Update resources切出IDEA窗口自动更新静态资源参数说明/bookstore是应用上下文路径意味着所有请求必须带此前缀如/bookstore/login.jsp。若填/则项目会覆盖Tomcat默认欢迎页导致管理页/manager/html无法访问。2.3 MySQL 5.7/8.0 安装与字符集终极配置解决中文乱码期末作业最顽固的Bug用户注册时姓名存进数据库变成??图书名称查出来是????。根源不在JSP% page contentTypetext/html;charsetUTF-8%而在MySQL服务端默认字符集。Windows下MySQL 8.0配置my.ini[client] default-character-setutf8mb4 [mysql] default-character-setutf8mb4 [mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci init_connectSET NAMES utf8mb4 skip-character-set-client-handshake FALSE血泪经验utf8mb4而非utf8MySQL的utf8实际是utf8mb3不支持emoji和部分生僻汉字。重启MySQL服务后执行SHOW VARIABLES LIKE character_set%;确认character_set_server和collation_server均为utf8mb4。建库时显式声明CREATE DATABASE bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;3. MVC三层落地每个包做什么、怎么调、为什么不能跨层调用3.1 包结构设计拒绝“src下全是.java”的野蛮生长一个能维护的MVC项目包结构必须像手术刀一样精准。以下是经过20次课设评审验证的最小合理结构src/ ├── controller/ // Servlet入口只做请求分发、参数校验、跳转控制 │ ├── LoginServlet.java │ ├── BookListServlet.java │ └── CartServlet.java ├── service/ // 业务逻辑中枢调用DAO处理事务不碰HTTP对象 │ ├── impl/ │ │ ├── UserServiceImpl.java │ │ └── BookServiceImpl.java │ └── UserService.java // 接口定义契约 ├── dao/ // 数据访问层只管CRUD不处理业务规则 │ ├── impl/ │ │ └── UserDAOImpl.java │ └── UserDAO.java ├── entity/ // POJO与数据库表一一映射无业务方法 │ ├── User.java │ └── Book.java ├── util/ // 工具类DBUtil获取Connection、StringUtil等 │ └── DBUtil.java └── filter/ // 统一编码过滤器解决POST中文乱码 └── EncodingFilter.java关键原则controller只能调serviceservice只能调daodao只能调util。任何跨层调用如Servlet里直接new UserDAOImpl()都会让项目变成意大利面条代码后续加购物车、订单、支付模块时必然崩溃。3.2 Controller层Servlet如何干净地接收参数并转发以用户登录为例LoginServlet不应该包含JDBC代码也不该拼接SQL// controller/LoginServlet.java WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); // 依赖注入雏形 Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取参数注意GET请求用getParameterPOST需先过滤器处理编码 String username req.getParameter(username); String password req.getParameter(password); // 2. 参数校验空值、长度 if (username null || username.trim().isEmpty() || password null || password.length() 6) { req.setAttribute(error, 用户名或密码格式错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 3. 调用Service获取结果 User user userService.login(username, password); // 4. 根据业务结果跳转不写重定向URL由Service决定不Controller掌控流程 if (user ! null) { req.getSession().setAttribute(user, user); // 登录态存Session resp.sendRedirect(req.getContextPath() /book/list); // 重定向防重复提交 } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }逻辑说明doPost方法只做三件事取参、校验、调Service、跳转。所有数据库操作、密码加密、会话管理细节都下沉到UserServiceImpl。resp.sendRedirect()而非forward()避免F5刷新重复提交登录请求。3.3 Service层事务边界与业务规则的唯一战场UserService接口定义契约UserServiceImpl实现具体逻辑。重点在于事务控制和异常处理// service/impl/UserServiceImpl.java public class UserServiceImpl implements UserService { private UserDAO userDAO new UserDAOImpl(); Override public User login(String username, String password) { // 1. 密码加盐哈希简化版实际用BCrypt String encryptedPassword MD5Util.md5(password bookstore_salt); // 2. 调用DAO查询DAO只返回User对象不处理密码逻辑 return userDAO.findByUsernameAndPassword(username, encryptedPassword); } Override public boolean register(User user) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 3. 先插入用户DAO层执行INSERT int userId userDAO.insert(user); // 4. 再初始化用户购物车关联表插入 Cart cart new Cart(); cart.setUserId(userId); cartDAO.insert(cart); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); // 任一环节失败全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(conn); } } }参数说明conn.setAutoCommit(false)是手动事务起点conn.commit()和conn.rollback()必须成对出现DBUtil.close(conn)在finally块确保连接释放。DAO层绝不出现commit/rollback那是Service的职责。4. JSP视图层如何写出不被老师打叉的“纯展示层”4.1 JSP最佳实践EL表达式、JSTL标签、自定义标签三件套很多同学的JSP里充斥着% String name (String)session.getAttribute(user); %和%name%这是倒退十年的写法。现代JSPJSP 2.0必须用EL和JSTL%-- book/list.jsp --% % page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html head title图书列表/title /head body h1欢迎${sessionScope.user.username}/h1 !-- EL表达式取Session -- c:if test${not empty requestScope.books} table border1 trth书名/thth作者/thth价格/thth操作/th/tr c:forEach items${requestScope.books} varbook !-- JSTL遍历 -- tr td${book.title}/td td${book.author}/td tdfmt:formatNumber value${book.price} typecurrency//td !-- 格式化货币 -- td a href${pageContext.request.contextPath}/cart/add?bookId${book.id}加入购物车/a /td /tr /c:forEach /table /c:if c:if test${empty requestScope.books} p暂无图书/p /c:if /body /html避坑指南c:forEach的items属性必须是Collection不能是null${sessionScope.user}比${user}更安全明确作用域fmt:formatNumber需在web.xml中声明JSTL URI或使用Tomcat自带的JSTL实现无需额外jar。4.2 表单提交与CSRF防护为什么你的登录表单总被“忽略”期末作业常被忽略的安全点没有CSRF Token导致恶意网站可伪造登录请求。简单方案是在表单中加入隐藏域%-- login.jsp --% form action${pageContext.request.contextPath}/login methodpost input typehidden namecsrf_token value${sessionScope.csrfToken}/ 用户名input typetext nameusername/br/ 密码input typepassword namepassword/br/ input typesubmit value登录/ /form对应在LoginServlet中生成Token// LoginServlet.java Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { // GET请求显示登录页时生成Token String token UUID.randomUUID().toString(); req.getSession().setAttribute(csrfToken, token); req.getRequestDispatcher(/login.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { String tokenFromForm req.getParameter(csrf_token); String tokenInSession (String) req.getSession().getAttribute(csrfToken); if (!Objects.equals(tokenFromForm, tokenInSession)) { req.setAttribute(error, 非法请求请刷新页面重试); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // ... 后续登录逻辑 }玄学提醒CSRF Token必须每次GET请求时重新生成且仅校验一次。若学生用Burp Suite抓包重放POST无Token校验的系统瞬间沦陷。5. 数据库设计与SQL脚本从ER图到可执行DDL的完整链路5.1 核心表ER关系与字段设计含外键约束一个能支撑购书业务的最小数据库必须包含5张表且外键关系清晰表名主键关键字段外键约束说明userid(INT PK AUTO_INCREMENT)username,password,email,role—role区分普通用户(0)和管理员(1)bookid(INT PK AUTO_INCREMENT)title,author,price,stock,image_path—stock为INT禁止负数用CHECK约束cartid(INT PK AUTO_INCREMENT)user_id,book_id,quantityuser_id → user.id,book_id → book.id购物车项同一用户对同一书只有一条记录orderid(VARCHAR 32 PK)user_id,total_amount,status,create_timeuser_id → user.id订单号用UUID避免自增ID暴露销量order_itemid(INT PK AUTO_INCREMENT)order_id,book_id,quantity,price_at_orderorder_id → order.id,book_id → book.id订单明细记录下单时价格快照设计理由order_item中的price_at_order是关键避免用户下单后图书降价导致财务纠纷。stock字段必须有CHECK (stock 0)否则超卖无法拦截。5.2 初始化SQL脚本带事务的建库建表与测试数据-- 创建数据库UTF8MB4 CREATE DATABASE IF NOT EXISTS bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookstore; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100), role TINYINT DEFAULT 0 COMMENT 0-普通用户,1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 CHECK (stock 0), image_path VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE, UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE order ( id VARCHAR(32) PRIMARY KEY, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待支付,1-已支付,2-已发货,3-已完成,4-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id VARCHAR(32) NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, price_at_order DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES order(id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入测试管理员 INSERT INTO user (username, password, email, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, adminbookstore.com, 1); -- 密码明文123456的MD5值实际项目应加盐执行要点将上述SQL保存为bookstore_init.sql在MySQL命令行中执行source D:/path/to/bookstore_init.sql。注意order是MySQL关键字必须用反引号包裹。6. 避坑指南那些让老师直接给60分的致命错误附现象、原因、解决方案6.1 现象Tomcat启动成功但访问/bookstore/login.jsp返回404原因web.xml中welcome-file-list未配置login.jsp且项目未设置默认首页IDEA中Deployment的Application context填了/导致项目部署到根路径但login.jsp物理路径在/bookstore/login.jsplogin.jsp文件未放在webapp/目录下而是误放src/main/java/。解决检查webapp/WEB-INF/web.xml是否有welcome-file-listwelcome-filelogin.jsp/welcome-file/welcome-file-list确认IDEA Deployment中Application context为/bookstore在webapp/目录下新建login.jsp而非Java源码目录。6.2 现象登录成功后跳转到/book/list页面空白浏览器控制台无报错原因BookListServlet中req.getRequestDispatcher(/book/list.jsp).forward(req, resp)的路径错误list.jsp实际在webapp/book/list.jsp但Servlet中写成了/book/list.jsp多了一个/变成绝对路径BookListServlet未将图书列表存入requestScopeJSP中${requestScope.books}取不到值。解决BookListServlet中使用相对路径req.getRequestDispatcher(book/list.jsp).forward(req, resp)确保BookService.listBooks()返回ListBook并在Servlet中req.setAttribute(books, books)JSP中用c:if test${not empty requestScope.books}判断而非${books ! null}。6.3 现象购物车添加商品后刷新页面数量归零原因购物车数据存在request或page作用域而非sessionCartServlet中未将购物车对象存入session每次请求都是新对象Cart实体类未实现Serializable导致Session序列化失败Tomcat集群场景。解决在CartServlet中req.getSession().setAttribute(cart, cart)Cart类顶部添加implements SerializableCart中的ListCartItem也要确保CartItem可序列化。6.4 现象MySQL插入中文显示??但建库时已设UTF8MB4原因JDBC连接URL未指定字符集参数DBUtil.getConnection()中DriverManager.getConnection()的URL缺少?useUnicodetruecharacterEncodingutf8mb4MySQL服务端配置了但客户端JDBC未同步。解决// util/DBUtil.java private static final String URL jdbc:mysql://localhost:3306/bookstore? useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8;6.5 现象管理员后台修改图书库存前端列表未更新原因BookUpdateServlet执行UPDATE SQL后未重定向到图书列表页而是forward到success.jspforward不刷新浏览器URL用户点击浏览器刷新按钮会重新提交旧的UPDATE请求缺少resp.sendRedirect(req.getContextPath() /book/list)。解决所有涉及数据变更的Servlet增删改必须用sendRedirect()而非forward()。这是RESTful设计铁律。7. 进阶技巧让“期末作业”具备真实项目气质的3个硬核操作7.1 用Filter统一处理请求编码与登录拦截一个合格的Web项目绝不允许每个Servlet都写req.setCharacterEncoding(UTF-8)。用Filter集中处理// filter/EncodingFilter.java WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 1. 统一请求编码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); // 2. 登录拦截除登录、注册、静态资源外其他路径必须登录 String path request.getServletPath(); HttpSession session request.getSession(false); boolean isLoginPage path.startsWith(/login) || path.startsWith(/register); boolean isStaticResource path.endsWith(.jpg) || path.endsWith(.css) || path.endsWith(.js); if (!isLoginPage !isStaticResource (session null || session.getAttribute(user) null)) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }价值点WebFilter(/*)拦截所有请求isStaticResource判断避免CSS/JS被重定向登录校验逻辑从此退出每个Servlet真正实现关注点分离。7.2 用自定义EL函数校验用户权限比JSTLc:if更优雅管理员需要看到“编辑”、“删除”按钮普通用户只能看到“加入购物车”。传统做法是JSP中写一堆c:if test${sessionScope.user.role 1}但角色值散落在各处。封装为EL函数// util/PermissionUtil.java public class PermissionUtil { public static boolean isAdmin(HttpServletRequest request) { User user (User) request.getSession().getAttribute(user); return user ! null user.getRole() 1; } }在WEB-INF/tld/permission.tld中声明?xml version1.0 encodingUTF-8? taglib xmlnshttp://java.sun.com/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd version2.1 tlib-version1.0/tlib-version short-nameperm/short-name urihttp://example.com/permission/uri function nameisAdmin/name function-classutil.PermissionUtil/function-class function-signatureboolean isAdmin(javax.servlet.http.HttpServletRequest)/function-signature /function /taglibJSP中使用% taglib prefixperm urihttp://example.com/permission % c:if test${perm:isAdmin(pageContext.request)} a href${pageContext.request.contextPath}/book/edit?id${book.id}编辑/a /c:if优势权限逻辑集中在Java类JSP只负责展示修改角色规则只需改PermissionUtil无需遍历所有JSP文件。7.3 用Log4j2记录关键业务日志替代System.out.println期末作业交上去老师问“用户下单失败的原因是什么”你总不能说“我Console里看到了”。集成Log4j2pom.xml添加依赖若用Maven或手动复制log4j-api-2.20.0.jar和log4j-core-2.20.0.jar到WEB-INF/lib/src/main/resources/log4j2.xml配置?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders File nameFileAppender fileNamelogs/bookstore.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileAppender/ /Root /Loggers /Configuration在OrderService中记录private static final Logger logger LogManager.getLogger(OrderService.class); public boolean createOrder(int userId, ListCartItem items) { try { // ... 创建订单逻辑 logger.info(订单创建成功用户ID{}订单号{}商品数{}, userId, orderId, items.size()); return true; } catch (Exception e) { logger.error(订单创建失败用户ID{}错误{}, userId, e.getMessage(), e); return false; } }真实价值当老师抽查“订单功能是否健壮”时你直接打开logs/bookstore.log指出某次失败的完整堆栈和上下文参数比口头解释强十倍。这也是企业项目交付的标配。我带课设时总跟学生说“满分”不是代码行数最多而是当老师随机点开一个Servlet、一个JSP、一个SQL文件都能立刻看懂它的职责且找不到一行越界调用的代码。这个项目真正的价值不在于它能卖书而在于它让你亲手把“MVC”三个字母从PPT里的概念变成硬盘里可调试、可扩展、可交付的字节码。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进