ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb图书订阅管理系统实战:从Servlet/JSP到MVC架构完整实现

JavaWeb图书订阅管理系统实战:从Servlet/JSP到MVC架构完整实现 简介这是一套面向JavaWeb初学者与课程设计者的简易图书订阅管理系统实战项目聚焦Servlet、JSP与MySQL协同开发解决Web端用户注册登录、图书浏览、订阅下单及后台管理等典型业务场景。资源包共76个文件包含37个Java源码涵盖Servlet控制层与DAO数据访问逻辑、20个JSP页面实现前后端交互与动态展示、9个核心Jar依赖库如JDBC驱动、JSTL标签库以及book.sql数据库脚本、CSS/JS静态资源和配置文件整体压缩后仅5.16MB结构清晰、模块分明便于快速部署到Tomcat服务器运行调试。已有676人学习下载配套完整可运行代码覆盖从环境搭建、数据库初始化、功能测试到基础安全防护如预编译防SQL注入的全流程实践是理解JavaWeb三层架构与MVC模式落地的理想入门范例。1. 项目概述与核心价值最近在整理过去的项目资料翻到了这个“简易的JavaWeb图书订阅管理系统”。这算是我带新人入门JavaWeb开发时最常拿出来讲解的一个经典案例。它麻雀虽小五脏俱全几乎涵盖了从数据库设计、后端逻辑处理到前端页面交互的全流程。对于刚学完Java基础、Servlet/JSP想找个项目练手却无从下手的同学来说这个项目再合适不过了。这个系统要解决的核心问题很明确模拟一个简化版的图书馆订阅服务。用户读者可以浏览图书、订阅借阅图书、查看自己的订阅记录管理员则负责管理图书信息和用户订阅状态。别看功能简单里面涉及的技术点非常密集MVC分层思想、JDBC数据库操作、会话管理Session、简单的权限控制、以及前后端数据交互。通过亲手实现它你能把课本上零散的知识点像拼图一样串联起来真正理解一个Web应用是如何跑起来的。我之所以推荐它是因为它的“简易”并不意味着粗糙。相反为了达到“简易”且可维护的目标我们需要在架构上做出清晰的设计选择比如是否引入DAO模式、如何组织Servlet、怎样避免JSP页面里塞满Java代码即避免过度使用Scriptlet。这些思考正是初级开发者迈向中级的必经之路。下面我就把这个项目的完整实现思路、技术细节以及我踩过的坑毫无保留地分享出来。2. 系统整体设计与技术选型考量2.1 需求分析与功能模块拆解做任何项目第一步永远是厘清需求。我们把这个图书订阅系统的需求细化一下角色划分系统主要涉及两种角色——普通用户读者和管理员。核心功能用户端注册与登录用户可注册账号并使用账号密码登录。图书浏览与检索查看图书列表支持按书名、作者等关键词搜索。图书订阅对可订阅的图书进行订阅操作类似于借阅。我的订阅查看个人当前订阅的图书列表及历史记录。个人信息管理查看和修改个人基本信息。管理端图书管理对图书进行增删改查CRUD操作。用户订阅管理查看所有用户的订阅记录处理订阅如确认借出、归还状态。用户管理管理用户账号信息本简易系统可能将用户注册与管理合并或管理员仅有查看权限。基于以上功能我们可以规划出几个核心的数据库表用户表(user)、图书表(book)、订阅记录表(subscription)。subscription表是连接用户和图书的桥梁它会记录用户ID、图书ID、订阅时间、应归还时间、实际归还时间以及状态如“订阅中”、“已归还”、“超期”。2.2 技术栈选择与架构设计这是一个典型的JavaWeb项目技术选型遵循经典、易学、资源丰富的原则。后端Servlet JSP这是JavaWeb的基石。Servlet作为控制器Controller接收请求、处理业务、转发或重定向JSP作为视图View负责展示数据。虽然现在流行Spring Boot但直接从Servlet/JSP入手能帮你深刻理解Web底层机制以后学任何框架都会事半功倍。JDBC用于数据库连接和操作。我们会封装一个DBUtil工具类来管理数据库连接池如使用Druid或基础的DataSource避免频繁创建和关闭连接。MVC模式这是本项目的架构灵魂。虽然Servlet和JSP本身可以混写但我们必须强制实施分层Model模型对应JavaBean实体类如User,Book,Subscription和业务逻辑类/DAO类。View视图即JSP页面只负责显示尽量不写Java业务代码。Controller控制器即Servlet作为调度中心。前端HTML/CSS/JavaScript基础三件套。为了简化我们可以使用纯原生技术或者引入轻量级的Bootstrap框架来快速搭建美观的界面。JSTL EL表达式这是关键优化点务必在JSP中使用JSTL标签库和EL表达式来替代% ... %脚本片段Scriptlet。这能使JSP页面更干净、更易维护也是企业开发的基本要求。数据库MySQL最流行的开源关系型数据库与Java生态结合紧密。我们将使用MySQL 5.7或8.0版本。开发工具IntelliJ IDEA当前Java开发的主流IDE对Web项目支持极好。我们将创建Maven项目来管理依赖。Maven管理项目依赖如MySQL驱动、Servlet API、JSTL、Druid连接池等。Tomcat作为Servlet容器运行和调试我们的Web应用。注意很多新手会纠结要不要用框架。我的建议是第一个实战项目不要用Spring Boot。用最基础的Servlet/JSP实现一遍你才能遇到并真正理解诸如“为什么需要事务管理”、“为什么要分层”、“Web.xml配置的作用”等问题。有了这个基础你再学习Spring Boot会发现它帮你解决了多少麻烦理解也会更加深刻。3. 数据库设计与核心代码实现解析3.1 数据库表结构详细设计建表语句是项目的基石设计的好坏直接影响后续编码复杂度。-- 用户表 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL UNIQUE COMMENT 用户名唯一, password varchar(255) NOT NULL COMMENT 密码存储密文, email varchar(100) DEFAULT NULL, role varchar(20) DEFAULT user COMMENT 角色user/admin, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 图书表 CREATE TABLE book ( id int(11) NOT NULL AUTO_INCREMENT, isbn varchar(20) DEFAULT NULL COMMENT 国际标准书号, name varchar(200) NOT NULL COMMENT 书名, author varchar(100) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, price decimal(10,2) DEFAULT NULL COMMENT 价格, total_count int(11) NOT NULL DEFAULT 0 COMMENT 总库存, available_count int(11) NOT NULL DEFAULT 0 COMMENT 可订阅数量, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 订阅记录表核心业务表 CREATE TABLE subscription ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, book_id int(11) NOT NULL COMMENT 图书ID, subscribe_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 订阅时间, due_time datetime DEFAULT NULL COMMENT 应归还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, status varchar(20) DEFAULT subscribed COMMENT 状态subscribed(订阅中)/returned(已归还)/overdue(超期未还), PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_book_id (book_id), CONSTRAINT fk_sub_user FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, CONSTRAINT fk_sub_book FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订阅记录表;设计要点解析密码存储password字段长度设为255是为了预留使用BCrypt等强哈希算法加密后的密文长度。绝对不要在数据库中明文存储密码库存管理book表设计了total_count总库存和available_count可订阅数。当用户订阅时需要原子性地减少available_count归还时再增加。这避免了仅用一个数量字段时可能出现的并发问题虽然简易系统可能不涉及高并发但养成好习惯很重要。外键约束subscription表通过外键关联user和book表并设置ON DELETE CASCADE确保数据完整性。在实际大型应用中可能会因为分库分表或性能考虑而放弃外键由程序保证逻辑一致性但学习阶段使用外键有助于理解关系。索引为user_id,book_id,book.name等查询频繁的字段添加索引能极大提升查询效率。3.2 核心JavaBean与DBUtil工具类首先创建与数据库表对应的JavaBean实体类这里以Book为例package com.library.model; import java.math.BigDecimal; import java.util.Date; public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private BigDecimal price; private Integer totalCount; private Integer availableCount; private Date createTime; // 标准的Getter和Setter方法省略... // 务必生成 toString() 方法便于调试 }接着是至关重要的数据库工具类DBUtil。我们使用Druid连接池它在pom.xml中的依赖是com.alibaba:druid。package com.library.util; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static DruidDataSource dataSource; static { // 静态代码块初始化连接池 dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(your_username); dataSource.setPassword(your_password); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMinIdle(5); // 最小空闲连接 dataSource.setMaxActive(20); // 最大活跃连接 // ... 其他配置如检测SQL等 } // 获取数据源 public static DataSource getDataSource() { return dataSource; } // 获取连接 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }实操心得数据库连接参数中的serverTimezoneAsia/Shanghai非常重要可以避免因时区不一致导致的日期时间问题。useSSLfalse在本地开发环境常用生产环境应启用SSL。3.3 DAO层设计以BookDao为例DAOData Access Object模式将数据库操作封装起来是MVC中Model层的重要组成部分。package com.library.dao; import com.library.model.Book; import com.library.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { // 新增图书 public boolean addBook(Book book) { String sql INSERT INTO book(isbn, name, author, publisher, price, total_count, available_count) VALUES(?,?,?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, book.getIsbn()); pstmt.setString(2, book.getName()); pstmt.setString(3, book.getAuthor()); pstmt.setString(4, book.getPublisher()); pstmt.setBigDecimal(5, book.getPrice()); pstmt.setInt(6, book.getTotalCount()); pstmt.setInt(7, book.getAvailableCount()); // 新增时可订阅数通常等于总库存 return pstmt.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 根据ID查询图书 public Book getBookById(int id) { String sql SELECT * FROM book WHERE id ?; Book book null; // ... 使用PreparedStatement防止SQL注入并映射结果集到Book对象 return book; } // 分页查询图书列表 public ListBook getBooksByPage(int pageNum, int pageSize, String keyword) { ListBook list new ArrayList(); String sql SELECT * FROM book WHERE name LIKE ? OR author LIKE ? LIMIT ?, ?; // ... 实现模糊查询和分页逻辑 return list; } // 更新图书信息特别是库存 public boolean updateBook(Book book) { // ... 实现更新逻辑注意更新available_count时的并发考量 } // 删除图书需要检查是否有未归还的订阅记录 public boolean deleteBook(int id) { // ... 实现删除逻辑可能需要先检查外键约束 } }关键点使用PreparedStatement所有动态拼接SQL的地方都必须使用PreparedStatement这是防止SQL注入攻击的生命线。资源管理使用try-with-resources语法Java 7确保Connection、Statement、ResultSet等资源被自动关闭避免内存泄漏。分页查询使用MySQL的LIMIT offset, pageSize实现。offset的计算公式是(pageNum - 1) * pageSize。3.4 Service层与业务逻辑Service层处理核心业务逻辑是Controller和DAO之间的桥梁。例如用户订阅图书不是一个简单的插入记录操作它涉及多个步骤和事务。package com.library.service; import com.library.dao.BookDao; import com.library.dao.SubscriptionDao; import com.library.model.Subscription; import java.sql.Connection; import java.sql.SQLException; import com.library.util.DBUtil; public class SubscriptionService { private BookDao bookDao new BookDao(); private SubscriptionDao subscriptionDao new SubscriptionDao(); public boolean subscribeBook(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查图书是否可订阅 (available_count 0) if (!bookDao.checkAvailable(conn, bookId)) { return false; } // 2. 减少图书可订阅数量 if (!bookDao.decreaseAvailableCount(conn, bookId)) { conn.rollback(); return false; } // 3. 创建订阅记录 Subscription sub new Subscription(); sub.setUserId(userId); sub.setBookId(bookId); // 计算应归还时间例如30天后 // ... if (!subscriptionDao.addSubscription(conn, sub)) { conn.rollback(); return false; } // 所有步骤成功提交事务 conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }重要提示这是手动管理事务的经典场景。在Service层的一个业务方法中如果涉及多个DAO操作如检查库存、扣减库存、生成订单这些操作必须在一个数据库事务中完成要么全部成功要么全部失败回滚。我们通过conn.setAutoCommit(false)、conn.commit()和conn.rollback()来控制。这是理解数据库事务ACID特性的绝佳实践。在Spring框架中可以通过Transactional注解轻松实现但底层原理与此一致。3.5 Controller层Servlet设计与实现Servlet作为控制器负责接收HTTP请求、调用Service处理业务、并将结果传递给JSP视图。以处理图书列表查询的Servlet为例package com.library.controller; import com.library.model.Book; import com.library.service.BookService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/book/list) // 使用注解配置Servlet路径替代web.xml配置 public class BookListServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取请求参数页码、关键词等 String pageNumStr req.getParameter(pageNum); String keyword req.getParameter(keyword); int pageNum 1; if (pageNumStr ! null !pageNumStr.isEmpty()) { pageNum Integer.parseInt(pageNumStr); } // 2. 调用Service处理业务 ListBook bookList bookService.getBooksByPage(pageNum, 10, keyword); int totalCount bookService.getBookCount(keyword); // 3. 将数据存入请求属性供JSP使用 req.setAttribute(bookList, bookList); req.setAttribute(pageNum, pageNum); req.setAttribute(totalPages, (int) Math.ceil((double) totalCount / 10)); req.setAttribute(keyword, keyword); // 4. 转发请求到JSP页面 req.getRequestDispatcher(/WEB-INF/jsp/book/list.jsp).forward(req, resp); } }设计要点使用WebServlet注解简化配置使Servlet与URL映射关系一目了然。doGet与doPost根据操作类型查询用GET提交数据用POST重写对应方法。请求转发Forward使用req.getRequestDispatcher().forward()在服务器端将请求和响应对象传递给JSPURL不变适合内部跳转。数据传递使用req.setAttribute()将数据放入请求作用域在JSP中通过EL表达式${bookList}获取。路径安全将JSP页面放在/WEB-INF/目录下是一种安全最佳实践。因为/WEB-INF/下的文件不能被客户端直接访问必须通过Servlet转发这确保了所有请求都经过Controller层处理。3.6 视图层JSP与EL/JSTL实战JSP页面应尽可能简洁。以下是book/list.jsp的示例片段% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head title图书列表/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap4.6.0/dist/css/bootstrap.min.css /head body div classcontainer mt-4 h2图书列表/h2 form classform-inline mb-3 action${pageContext.request.contextPath}/book/list methodget input typetext classform-control mr-2 namekeyword placeholder输入书名或作者 value${param.keyword} button typesubmit classbtn btn-primary搜索/button /form table classtable table-striped table-bordered theadtrthID/thth书名/thth作者/thth出版社/thth库存/thth操作/th/tr/thead tbody %-- 使用JSTL的c:forEach遍历列表 --% c:forEach varbook items${bookList} tr td${book.id}/td td${book.name}/td td${book.author}/td td${book.publisher}/td td span classbadge badge-success可借: ${book.availableCount}/span span classbadge badge-secondary总数: ${book.totalCount}/span /td td c:choose c:when test${book.availableCount 0} a href${pageContext.request.contextPath}/subscription/do?actionsubscribebookId${book.id} classbtn btn-sm btn-outline-primary onclickreturn confirm(确认订阅)订阅/a /c:when c:otherwise button classbtn btn-sm btn-secondary disabled已无库存/button /c:otherwise /c:choose %-- 管理员才显示编辑删除按钮 --% c:if test${sessionScope.user.role admin} a href# classbtn btn-sm btn-warning编辑/a a href# classbtn btn-sm btn-danger onclickreturn confirm(确认删除)删除/a /c:if /td /tr /c:forEach /tbody /table %-- 分页组件 --% nav ul classpagination c:if test${pageNum 1} li classpage-itema classpage-link href?pageNum${pageNum-1}keyword${param.keyword}上一页/a/li /c:if c:forEach begin1 end${totalPages} vari li classpage-item ${i pageNum ? active : } a classpage-link href?pageNum${i}keyword${param.keyword}${i}/a /li /c:forEach c:if test${pageNum totalPages} li classpage-itema classpage-link href?pageNum${pageNum1}keyword${param.keyword}下一页/a/li /c:if /ul /nav /div /body /html技术要点JSTL核心标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %引入。c:forEach用于循环c:if和c:choose/c:when/c:otherwise用于条件判断完全取代了Scriptlet。EL表达式${bookList}、${book.name}、${pageContext.request.contextPath}等用于从请求、会话、应用作用域中取值语法简洁。pageContext.request.contextPath动态获取应用的上下文路径这样写链接和表单action时无论应用部署在什么路径下都能正确访问。Bootstrap集成通过CDN引入Bootstrap快速构建美观、响应式的界面。权限控制通过${sessionScope.user.role admin}在页面上控制按钮的显示这是视图层的权限控制。注意后端Servlet中必须进行二次校验防止用户直接通过URL访问管理功能。4. 关键功能实现与避坑指南4.1 用户登录、会话管理与权限控制用户登录是系统的门户涉及密码校验和会话Session管理。登录Servlet核心逻辑protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 前端传来的明文密码 UserService userService new UserService(); User user userService.login(username, password); if (user ! null) { // 登录成功将用户对象存入Session HttpSession session req.getSession(); session.setAttribute(user, user); // 根据角色跳转不同首页 if (admin.equals(user.getRole())) { resp.sendRedirect(req.getContextPath() /admin/index.jsp); } else { resp.sendRedirect(req.getContextPath() /user/index.jsp); } } else { // 登录失败返回错误信息 req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } }密码加密与验证 绝对不要用MD5使用BCryptPasswordEncoder需要引入Spring Security的bcrypt包或使用其他安全的BCrypt实现库。// 注册时加密密码 String encodedPwd BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 存入数据库的是encodedPwd // 登录时验证密码 boolean isMatch BCrypt.checkpw(inputPassword, storedHashedPasswordFromDB);权限拦截器Filter 对于需要登录才能访问的页面如/user/*,/admin/*我们使用Servlet Filter进行统一拦截。WebFilter(/*) // 拦截所有请求 public class AuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); String contextPath req.getContextPath(); // 放行登录、注册、静态资源等请求 if (uri.contains(/login) || uri.contains(/register) || uri.contains(.css) || uri.contains(.js) || uri.contains(.jpg)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); // 不创建新session // 检查管理员路径 if (uri.contains(/admin/)) { if (session null || session.getAttribute(user) null) { resp.sendRedirect(contextPath /login.jsp); return; } User user (User) session.getAttribute(user); if (!admin.equals(user.getRole())) { resp.sendRedirect(contextPath /error/403.jsp); // 无权限页面 return; } } // 检查普通用户路径逻辑类似略 chain.doFilter(request, response); } }踩坑实录Filter的放行规则chain.doFilter一定要小心。我曾因为忘记放行CSS/JS文件导致页面样式全无排查了半天。另外req.getSession(false)很重要如果传true当用户访问公开页面时也会创建一个新Session浪费服务器资源。4.2 图书订阅的业务逻辑与事务处理订阅功能是核心业务其Service层代码在3.4节已展示。这里补充几个关键细节并发控制多个用户同时订阅最后一本书时会出现“超卖”。在decreaseAvailableCount的SQL中可以加上条件WHERE id? AND available_count 0。这样在事务中第一个执行成功的用户会扣减库存后续用户的更新语句会因为available_count0而影响行数为0导致事务回滚或订阅失败。更高级的方案是使用数据库的悲观锁SELECT ... FOR UPDATE或乐观锁版本号。应归还时间计算在Java 8中使用新的时间API更安全。LocalDateTime now LocalDateTime.now(); LocalDateTime dueTime now.plusDays(30); // 假设借阅30天 // 转换为java.sql.Timestamp存入数据库 Timestamp dueTimestamp Timestamp.valueOf(dueTime);订阅状态流转status字段的状态机需要清晰定义。可以通过一个定时任务如Quartz Scheduler或每天运行一次的批处理程序扫描due_time NOW() AND statussubscribed的记录将其状态更新为overdue。4.3 前端表单验证与异步交互Ajax虽然是一个“简易”系统但良好的用户体验离不开前端交互。表单验证使用HTML5原生属性结合JavaScript。form onsubmitreturn validateForm() input typetext nameusername required minlength3 maxlength50 input typeemail nameemail required input typepassword namepassword idpwd required pattern^(?.*[A-Za-z])(?.*\d)[A-Za-z\d]{6,}$ title密码至少6位包含字母和数字 input typepassword nameconfirmPwd idconfirmPwd required button typesubmit注册/button /form script function validateForm() { var pwd document.getElementById(pwd).value; var confirmPwd document.getElementById(confirmPwd).value; if (pwd ! confirmPwd) { alert(两次输入的密码不一致); return false; } return true; } /script使用Ajax检查用户名是否重复在用户注册时提升体验。// 使用原生JavaScript或jQuery document.getElementById(username).addEventListener(blur, function() { var username this.value; var xhr new XMLHttpRequest(); xhr.open(GET, ${pageContext.request.contextPath}/user/check?username encodeURIComponent(username)); xhr.onload function() { if (xhr.status 200) { var result JSON.parse(xhr.responseText); if (!result.available) { alert(用户名已存在); } } }; xhr.send(); });后端对应一个UserCheckServlet查询数据库后返回JSON数据{available: false}。5. 项目部署、调试与常见问题排查5.1 在IDEA中配置与运行项目创建Maven Web项目在IDEA中选择New Project - Maven - Create from archetype选择maven-archetype-webapp。配置pom.xml添加依赖Servlet API, JSTL, MySQL Driver, Druid, JUnit等。注意Servlet API的scope应为provided因为Tomcat服务器自带。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency配置TomcatRun - Edit Configurations - Add New Configuration - Tomcat Server - Local。在Deployment选项卡中添加Artifact选择war exploded格式。解决JSTL依赖问题这是一个经典坑点。除了引入jstl依赖还需要确保你的Tomcat版本和JSTL版本兼容。如果JSP页面报The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved错误很可能是依赖没下载好或版本不对。可以尝试在pom.xml中明确指定版本并检查IDEA的Maven仓库。5.2 常见问题与解决方案速查表问题现象可能原因解决方案访问JSP页面报404错误1. JSP文件未放在webapp或其子目录下。2. 通过浏览器直接访问了/WEB-INF/下的JSP。1. 检查JSP文件路径。2. 确保JSP是通过Servlet转发访问的或将其移到WEB-INF目录外不推荐。页面中文乱码1. JSP页面编码未设置。2. Servlet未设置请求/响应编码。3. 数据库连接字符集不对。1. JSP头部加% page pageEncodingUTF-8%和contentTypetext/html;charsetUTF-8。2. Servlet中req.setCharacterEncoding(UTF-8)和resp.setContentType(text/html;charsetUTF-8)。3. 数据库连接URL加characterEncodingUTF-8。ClassNotFoundException: com.mysql.cj.jdbc.Driver1. MySQL驱动jar包未放入WEB-INF/lib或Maven依赖未下载。2. Tomcat未加载到驱动。1. 检查pom.xml依赖并ReimportMaven项目。2. 对于传统Web项目确保jar包在WEB-INF/lib下。插入数据库的时间比系统时间慢8小时数据库时区与系统时区不一致。在JDBC连接URL中指定时区serverTimezoneAsia/Shanghai。点击链接或提交表单后URL变得很长且包含Servlet路径Servlet中处理完请求后使用了forward而不是redirect。完成数据更新操作如登录、注册、删除后应使用resp.sendRedirect()进行重定向避免用户刷新页面导致重复提交Post/Redirect/Get模式。JSP页面无法解析EL表达式显示为${xxx}1. 页面是旧格式默认禁用了EL。2. web.xml中配置了禁用EL。1. 确保JSP文件头是% page isELIgnoredfalse %新版本默认是false。2. 检查web.xml中是否有el-ignoredtrue/el-ignored配置。事务不起作用部分SQL执行失败但数据仍被修改1. 没有正确开启事务setAutoCommit(false)。2. 使用了不同的Connection对象执行多个DAO操作。1. 确保在Service方法开始时获取同一个Connection并setAutoCommit(false)。2. 将Connection对象通过参数传递给各个DAO方法确保它们在同一个事务中。5.3 项目打包与部署到独立Tomcat开发完成后需要打包成WAR文件部署到独立的Tomcat服务器。打包在IDEA右侧Maven工具栏中执行package命令或在命令行进入项目目录执行mvn clean package。成功后会在target目录下生成项目名.war文件。部署将WAR文件复制到Tomcat的webapps目录下。启动Tomcat运行bin/startup.batWindows或bin/startup.shLinux/Mac。访问打开浏览器访问http://localhost:8080/你的项目名/。部署心得生产环境部署前务必在web.xml中配置数据库连接参数为生产环境的值不要使用硬编码。可以将配置写在properties文件中通过ServletContext读取。另外记得关闭Tomcat的目录浏览功能在conf/web.xml中设置param-valuefalse/param-value并考虑配置HTTPS。实现这个“简易”的图书订阅管理系统就像完成了一次JavaWeb开发的完整徒步穿越。你可能会遇到乱码的泥沼、事务管理的陡坡、以及前端交互的岔路。但每解决一个问题你对HTTP协议、Servlet生命周期、数据库连接、MVC模式的理解就会加深一层。这个项目代码完全可以作为你未来更复杂项目如使用Spring Boot重构的基石。当你再看到那些配置繁琐的框架时你会明白它们究竟在帮你简化什么这才是从基础做起的最大价值。本文还有配套的精品资源点击获取
返回列表