尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb图书订阅管理系统实战:Servlet+JSP+JDBC+MVC全流程解析

JavaWeb图书订阅管理系统实战:Servlet+JSP+JDBC+MVC全流程解析 简介这是一套面向JavaWeb初学者与课程设计者的简易图书订阅管理系统实战项目基于ServletJSPMySQL技术栈构建聚焦用户登录注册、图书浏览/购买、后台图书CRUD及基础安全防护等核心功能适用于高校JavaWeb实验课、毕业设计原型开发或自学练手。资源包共76个文件含37个Java源码涵盖Servlet控制层与JDBC数据访问逻辑、20个JSP页面实现前后端交互与动态展示、9个运行依赖JAR包、1个book.sql数据库脚本含建表与初始数据以及CSS/JS/图片等静态资源整体5.16MB结构清晰模块划分明确。已有676人学习下载提供开箱即用的完整工程结构含src、web、WEB-INF、pages等标准目录配套jdbc.properties配置与Tomcat部署说明便于快速导入IDE调试运行是理解JavaWeb分层开发与MVC实践的典型入门案例。1. 项目概述与核心价值最近在带新人或者自己回顾基础的时候发现一个现象很多朋友学完了Java基础、Servlet、JSP甚至框架也接触了一些但一提到“做个完整的项目”脑子里还是乱的不知道从哪里下手各个模块怎么串起来。这其实挺正常的因为学习是点状的而项目是线性和网状的。今天我就以一个非常经典且实用的练手项目——“简易的JavaWeb图书订阅管理系统”为例给大家彻底拆解一遍。这个项目麻雀虽小五脏俱全涵盖了从需求分析、数据库设计、后端逻辑到前端展示的完整流程特别适合用来巩固JavaWeb核心技术和理解MVC模式的实际应用。简单说这个系统就是模拟一个图书馆或者个人对图书订阅的管理。核心功能包括用户可能是读者或管理员的注册登录、浏览图书信息、订阅借阅图书、查看自己的订阅记录、管理员对图书和用户信息进行增删改查等。你别看它“简易”里面涉及的技术点非常密集JDBC操作数据库、Servlet处理请求、JSP展示页面、会话管理、简单的权限控制、还有前后端数据交互。通过亲手实现它你能把之前学的那一堆零散的知识点像拼图一样完整地拼接起来真正理解一个Web应用是怎么跑起来的。无论你是正在找工作的应届生还是想夯实基础的在职开发者这个项目都是一个绝佳的起点。2. 项目整体设计与技术栈选型2.1 为什么选择这个技术组合提到JavaWeb现在可能大家第一反应是Spring Boot。但对于深入理解底层原理从Servlet和JSP入手是无可替代的。我们这个“简易”系统就采用最经典、最纯粹的JavaWeb技术栈Servlet JSP JDBC MySQL Tomcat开发工具选用目前最主流的IntelliJ IDEA。这么选型的核心考量是聚焦核心原理Spring Boot做了大量封装虽然开发效率高但容易让人变成“配置工程师”不清楚底层发生了什么。直接用Servlet你能清晰地看到HTTP请求如何被接收、如何被分发、如何被处理这对理解Web容器如Tomcat的工作机制至关重要。强化基础能力手写JDBC能让你深刻理解数据库连接池的意义、SQL注入的风险与防范比如使用PreparedStatement、事务管理的基本概念。这些是无论用任何ORM框架如MyBatis, Hibernate都绕不开的根基。理解MVC的雏形虽然我们没有用Spring MVC但我们可以自己模拟MVC架构。Servlet充当Controller控制器负责处理请求和业务逻辑JSP充当View视图负责展示数据JavaBean实体类和Service/DAO层充当Model模型负责数据和业务规则。自己动手实现一遍对MVC的理解会比直接使用框架深刻十倍。环境简单排除干扰这套环境搭建简单几乎不会在环境配置上卡住可以让我们把全部精力集中在业务逻辑和代码编写上。注意有同学可能会问为什么不用Thymeleaf或FreeMarker而用JSPJSP虽然看起来“老”但它将Java代码嵌入HTML的模式对于初学者理解服务器端渲染SSR和EL表达式、JSTL标签库来说非常直观。先理解JSP再看现代模板引擎你会明白它们解决了JSP的哪些痛点。2.2 系统功能模块拆解在动手写代码之前我们必须把系统要做什么想清楚。基于“图书订阅管理”这个核心我们可以拆解出以下核心功能模块1. 用户模块核心功能用户注册、登录、退出。细节登录时需要验证用户名和密码密码绝不能明文存储需要MD5或SHA等哈希算法加密后存库使用Session来记录用户的登录状态和基本信息如用户ID、角色。权限雏形我们可以设计一个role字段如0-普通用户1-管理员为后续的权限控制打下基础。2. 图书模块核心功能图书信息的增、删、改、查CRUD。细节图书属性至少包括图书ID、ISBN、书名、作者、出版社、出版日期、简介、总数量、可订阅数量。注意“总数量”和“可订阅数量”的区别这是实现订阅业务的关键。3. 订阅借阅模块核心功能用户订阅图书、归还图书、查看个人订阅记录。细节这是系统的业务核心。用户订阅时需要检查该书“可订阅数量”是否大于0然后生成一条订阅记录并减少该书的“可订阅数量”。归还时更新订阅记录状态为“已归还”并增加书的“可订阅数量”。这里涉及到数据库事务要保证“更新库存”和“生成记录”两步要么都成功要么都失败。4. 后台管理模块基于权限核心功能管理员可以管理所有图书和用户信息。细节在Servlet中可以通过判断Session中的用户角色来决定是否允许访问某些特定的管理页面如/admin/*路径下的请求。根据以上模块我们可以规划出大致的页面流首页 - (登录/注册) - 图书列表页 - 图书详情页 - (订阅操作) - 个人中心我的订阅。管理员则有额外的入口进入管理后台。3. 数据库设计与核心表结构数据库是项目的基石设计得好后面编程事半功倍。我们使用MySQL至少需要三张核心表。3.1 表结构详解与字段设计用户表 (user)这张表存储系统用户信息。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID主键, username varchar(50) NOT NULL UNIQUE COMMENT 用户名唯一, password varchar(255) NOT NULL COMMENT 密码加密后存储, email varchar(100) DEFAULT NULL COMMENT 邮箱, role int(11) DEFAULT 0 COMMENT 角色0-普通用户1-管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;设计要点password字段长度要给足因为经过哈希加密如MD5产生32位字符串后的密码会比明文长。role字段用整数表示角色便于程序判断。扩展时也可以使用字符串如‘USER‘’ADMIN‘。create_time设置默认值避免插入时遗漏。图书表 (book)这张表存储图书信息。CREATE TABLE book ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 图书ID主键, isbn varchar(20) DEFAULT NULL COMMENT ISBN号, name varchar(200) NOT NULL COMMENT 书名, author varchar(100) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, publish_date date DEFAULT NULL COMMENT 出版日期, description text COMMENT 简介, total_quantity int(11) NOT NULL DEFAULT 0 COMMENT 总数量, available_quantity int(11) NOT NULL DEFAULT 0 COMMENT 可订阅数量, PRIMARY KEY (id), KEY idx_name (name) COMMENT 书名索引方便搜索 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;设计要点total_quantity和available_quantity是核心字段。初始时两者相等。每当成功订阅一次available_quantity减1归还时加1。total_quantity一般不变除非图书新增或销毁。为name字段添加普通索引是因为我们很可能需要根据书名进行模糊查询。订阅记录表 (subscription)这张表记录每一次订阅行为。CREATE TABLE subscription ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 记录ID主键, user_id int(11) NOT NULL COMMENT 用户ID外键关联user.id, book_id int(11) NOT NULL COMMENT 图书ID外键关联book.id, borrow_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 订阅时间, expected_return_time datetime DEFAULT NULL COMMENT 应归还时间, actual_return_time datetime DEFAULT NULL COMMENT 实际归还时间, status int(11) DEFAULT 0 COMMENT 状态0-订阅中1-已归还2-超期未还, 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订阅记录表;设计要点使用了外键约束(FOREIGN KEY)确保user_id和book_id的引用完整性。ON DELETE CASCADE表示当用户或图书被删除时其相关的订阅记录也会自动删除这符合业务逻辑。status字段用整数表示状态便于程序处理。通过expected_return_time和actual_return_time可以计算是否超期。为user_id和book_id建立了索引因为这是高频查询条件如“查询我的订阅”、“查询某本书的订阅记录”。3.2 数据库连接与工具类封装在Java中操作数据库我们使用JDBC。但直接在每个Servlet里写连接代码会导致大量重复和难以维护。最佳实践是封装一个数据库工具类。DBUtil.java (数据库连接与关闭工具类)import java.sql.*; import javax.sql.DataSource; import com.mchange.v2.c3p0.ComboPooledDataSource; // 使用C3P0连接池 public class DBUtil { private static DataSource dataSource new ComboPooledDataSource(); // 静态数据源 // 获取数据库连接从连接池 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace();} try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace();} try { if (conn ! null) conn.close(); } // 注意这里是归还连接到池不是真正关闭 } }为什么要用连接池(C3P0/DBCP/HikariCP)每次操作数据库都新建一个物理连接开销巨大TCP三次握手、数据库认证等。连接池预先创建一些连接放在“池”里程序使用时从池中取用完后归还避免了频繁创建和销毁的开销极大提升性能。这是企业级应用的标配。实操心得在src目录下创建一个c3p0-config.xml配置文件在里面配置连接池的初始连接数、最大连接数、超时时间等参数。这样ComboPooledDataSource()会自动加载该配置。把数据库的URL、用户名、密码等敏感信息放在配置文件中而不是硬编码在Java代码里是更安全、更灵活的做法。4. 后端核心Servlet设计与业务逻辑实现Servlet是项目的控制器是大脑。我们按照功能模块来创建Servlet。4.1 用户相关Servlet登录、注册与会话管理LoginServlet.java (处理登录请求)WebServlet(/login) // 使用注解配置Servlet映射路径替代web.xml public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); // 依赖业务层 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 基本参数校验 if(username null || password null || username.trim().isEmpty()){ request.setAttribute(msg, 用户名或密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 2. 调用业务层验证用户 User user userService.login(username, password); if(user ! null){ // 3. 登录成功将用户信息存入Session HttpSession session request.getSession(); session.setAttribute(user, user); // 4. 重定向到首页防止表单重复提交 response.sendRedirect(request.getContextPath() /index.jsp); } else { // 5. 登录失败返回错误信息 request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }关键点解析WebServlet注解简化配置/login对应表单的actionlogin。请求转发(forward)vs重定向(redirect)这是初学者易混淆点。登录失败时我们用forward将请求和request对象包含错误信息msg一起转发到login.jsp页面显示。登录成功时我们用redirect发送302响应让浏览器重新请求index.jsp这样浏览器地址栏会变且刷新页面不会重复提交登录表单。Session的使用HttpSession是服务器为每个用户创建的会话对象。登录成功后我们将整个User对象存入Session。在其他页面可以通过session.getAttribute(user)来判断用户是否登录以及获取用户信息。UserService接口及其实现类是典型的面向接口编程将业务逻辑与Servlet解耦。// UserService.java public interface UserService { User login(String username, String password); boolean register(User user); } // UserServiceImpl.java public class UserServiceImpl implements UserService { private UserDao userDao new UserDaoImpl(); Override public User login(String username, String password) { // 1. 对传入的密码进行同样的加密 String encryptedPwd MD5Util.encrypt(password); // 2. 调用DAO层查询用户 User user userDao.findUserByUsernameAndPassword(username, encryptedPwd); return user; } }密码加密绝对不要在数据库存明文密码我们使用MD5工具类进行单向加密。// MD5Util.java import java.security.MessageDigest; public class MD5Util { public static String encrypt(String str) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(str.getBytes(UTF-8)); // 将byte数组转换为16进制字符串 StringBuilder result new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xFF); if (hex.length() 1) { result.append(0); } result.append(hex); } return result.toString(); } catch (Exception e) { throw new RuntimeException(e); } } }重要安全提示单纯MD5现在已不够安全可通过彩虹表破解。生产环境应使用加盐Salt的哈希算法如BCrypt或PBKDF2。但对于学习项目理解加密的必要性和基本流程更为关键。4.2 图书管理ServletCRUD与分页查询BookListServlet.java (展示图书列表通常支持分页)WebServlet(/book/list) public class BookListServlet extends HttpServlet { private BookService bookService new BookServiceImpl(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取分页参数 String pageNumStr request.getParameter(pageNum); String pageSizeStr request.getParameter(pageSize); int pageNum 1; // 默认第一页 int pageSize 10; // 默认每页10条 try { pageNum Integer.parseInt(pageNumStr); } catch (NumberFormatException e) { /* 使用默认值 */ } try { pageSize Integer.parseInt(pageSizeStr); } catch (NumberFormatException e) { /* 使用默认值 */ } // 2. 获取查询条件如按书名搜索 String keyword request.getParameter(keyword); // 3. 调用业务层获取分页数据 PageInfoBook pageInfo bookService.getBooksByPage(keyword, pageNum, pageSize); // 4. 将数据放入request域转发给JSP展示 request.setAttribute(pageInfo, pageInfo); request.getRequestDispatcher(/book_list.jsp).forward(request, response); } }分页逻辑的实现分页是Web应用的必备技能。核心是计算limit start, pageSize中的start。start (pageNum - 1) * pageSize。我们需要查询两次数据库一次用count(*)查总记录数用于计算总页数一次用limit查当前页的数据。这些逻辑封装在BookService和BookDao中。PageInfo.java (分页信息封装类)这是一个通用的分页包装类包含当前页数据列表、当前页码、每页条数、总记录数、总页数等属性方便在JSP页面中展示分页导航栏。4.3 订阅业务Servlet事务控制的典型场景BorrowServlet.java (处理订阅图书请求)这是整个系统业务逻辑最复杂的地方因为它涉及多个表的更新必须保证数据一致性。WebServlet(/book/borrow) public class BorrowServlet extends HttpServlet { private SubscriptionService subService new SubscriptionServiceImpl(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 检查用户是否登录 HttpSession session request.getSession(); User user (User) session.getAttribute(user); if(user null){ response.sendRedirect(request.getContextPath() /login.jsp); return; } // 2. 获取图书ID String bookIdStr request.getParameter(bookId); int bookId 0; try { bookId Integer.parseInt(bookIdStr); } catch (NumberFormatException e) { // 参数错误处理 response.sendRedirect(request.getContextPath() /error.jsp); return; } // 3. 调用业务层执行订阅 boolean success false; try { success subService.borrowBook(user.getId(), bookId); } catch (Exception e) { e.printStackTrace(); // 记录日志 } // 4. 根据结果跳转 if(success){ // 订阅成功重定向到“我的订阅”页面 response.sendRedirect(request.getContextPath() /subscription/my); } else { // 订阅失败可能库存不足返回错误信息 request.setAttribute(msg, 订阅失败图书可能已被借完或不存在); request.getRequestDispatcher(/book_detail.jsp).forward(request, response); } } }SubscriptionServiceImpl.borrowBook 方法事务核心Override public boolean borrowBook(int userId, int bookId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 开启事务 // 2. 检查图书库存使用行级锁 FOR UPDATE防止超订 BookDao bookDao new BookDaoImpl(); Book book bookDao.getBookByIdForUpdate(conn, bookId); if(book null || book.getAvailableQuantity() 0){ conn.rollback(); return false; } // 3. 减少图书可订阅数量 bookDao.updateAvailableQuantity(conn, bookId, book.getAvailableQuantity() - 1); // 4. 创建订阅记录 SubscriptionDao subDao new SubscriptionDaoImpl(); Subscription sub new Subscription(); sub.setUserId(userId); sub.setBookId(bookId); sub.setBorrowTime(new Date()); // 计算应归还时间例如30天后 Calendar calendar Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, 30); sub.setExpectedReturnTime(calendar.getTime()); sub.setStatus(0); subDao.insert(conn, sub); conn.commit(); // 5. 提交事务 return true; } catch (SQLException e) { try { if(conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace();} e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } }事务与锁的深度解析conn.setAutoCommit(false)这是手动管理事务的开始。默认情况下JDBC每条SQL都是一个独立事务。这里我们将多条SQL检查库存、更新库存、插入记录捆绑成一个原子操作。FOR UPDATE在getBookByIdForUpdate方法对应的SQL中我们使用SELECT ... FROM book WHERE id ? FOR UPDATE。这会在查询时对这条记录加上行级排他锁。在高并发场景下如果没有这个锁两个用户可能同时查询到库存为1然后都成功下单导致超订库存变为-1。FOR UPDATE锁确保在第一个事务提交或回滚前其他事务无法读取或修改这条记录。commit()与rollback()所有操作成功则提交使更改永久生效。任何一步失败则回滚所有更改撤销数据库恢复到事务开始前的状态。资源关闭在finally块中关闭连接确保无论成功失败连接都能归还到连接池。踩坑记录务必在catch块中进行回滚(rollback)并在finally块中关闭连接。我曾因为忘记回滚导致在更新库存失败后订阅记录却插入了数据严重不一致。另外关闭连接前最好将连接的自动提交状态恢复为true避免影响连接池中其他代码的使用。5. 前端视图JSP页面与数据展示JSP负责将后端处理好的数据渲染成HTML。我们应尽量让JSP只做展示逻辑交给Servlet和JavaBean。5.1 使用JSTL与EL表达式简化JSP在JSP中直接写Java代码% ... %是过时且难以维护的做法。我们应该使用EL表达式${}来获取数据使用JSTL标签库来进行循环、判断等逻辑控制。引入JSTL在JSP页面头部添加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %book_list.jsp (图书列表页示例)% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle图书列表/title/head body h1图书列表/h1 !-- 搜索框 -- form action${pageContext.request.contextPath}/book/list methodget input typetext namekeyword value${param.keyword} placeholder输入书名搜索 button typesubmit搜索/button /form !-- 图书表格 -- table border1 tr thID/thth书名/thth作者/thth出版社/thth可订阅数量/thth操作/th /tr c:forEach items${pageInfo.list} varbook tr td${book.id}/td tda href${pageContext.request.contextPath}/book/detail?id${book.id}${book.name}/a/td td${book.author}/td td${book.publisher}/td td c:choose c:when test${book.availableQuantity 0} ${book.availableQuantity} 可订阅 /c:when c:otherwise span stylecolor:red;已售罄/span /c:otherwise /c:choose /td td c:if test${sessionScope.user ! null book.availableQuantity 0} form action${pageContext.request.contextPath}/book/borrow methodpost styledisplay:inline; input typehidden namebookId value${book.id} button typesubmit订阅/button /form /c:if c:if test${sessionScope.user.role 1} !-- 管理员才显示编辑删除 -- a href${pageContext.request.contextPath}/admin/book/edit?id${book.id}编辑/a a href# onclickdeleteBook(${book.id})删除/a /c:if /td /tr /c:forEach /table !-- 分页导航 -- div c:if test${pageInfo.hasPreviousPage} a href${pageContext.request.contextPath}/book/list?pageNum${pageInfo.prePage}keyword${param.keyword}上一页/a /c:if c:forEach begin${pageInfo.navigateFirstPage} end${pageInfo.navigateLastPage} varpn c:choose c:when test${pageInfo.pageNum pn} strong[${pn}]/strong /c:when c:otherwise a href${pageContext.request.contextPath}/book/list?pageNum${pn}keyword${param.keyword}${pn}/a /c:otherwise /c:choose /c:forEach c:if test${pageInfo.hasNextPage} a href${pageContext.request.contextPath}/book/list?pageNum${pageInfo.nextPage}keyword${param.keyword}下一页/a /c:if /div script function deleteBook(id) { if(confirm(确定要删除这本图书吗)){ location.href ${pageContext.request.contextPath}/admin/book/delete?id id; } } /script /body /html关键点解析${pageContext.request.contextPath}动态获取项目的上下文路径这样无论项目部署在什么路径下链接都不会错。这是写JSP链接的最佳实践。c:forEach循环遍历pageInfo.list这个图书列表。c:choose / c:when / c:otherwise相当于Java的switch-case这里用于根据库存数量显示不同文本。c:if条件判断。这里用于判断用户是否登录、是否是管理员从而控制“订阅”、“编辑”、“删除”按钮的显示。${sessionScope.user}就是从Session中取出的用户对象。${param.keyword}获取请求参数中的keyword值并回填到搜索框提升用户体验。分页逻辑使用pageInfo对象中的各种属性如hasPreviousPage,navigateFirstPage等来动态生成分页链接并将搜索关键词keyword也作为参数传递确保翻页后搜索条件不丢失。5.2 页面复用使用静态包含分离公共部分每个页面都有共同的头部导航栏、尾部版权信息。我们可以把它们抽成独立的JSP文件然后在其他页面中包含进来避免重复代码。common/header.jspdiv stylebackground: #eee; padding: 10px; a href${pageContext.request.contextPath}/index.jsp首页/a | c:choose c:when test${empty sessionScope.user} a href${pageContext.request.contextPath}/login.jsp登录/a | a href${pageContext.request.contextPath}/register.jsp注册/a /c:when c:otherwise 欢迎你${sessionScope.user.username} c:if test${sessionScope.user.role 1}(管理员)/c:if | a href${pageContext.request.contextPath}/subscription/my我的订阅/a | a href${pageContext.request.contextPath}/logout退出/a /c:otherwise /c:choose /div在其他页面中通过% include filecommon/header.jsp%静态包含进来。这样修改导航栏只需要改一个文件。6. 项目部署、调试与常见问题排查6.1 在IntelliJ IDEA中配置与运行创建项目新建一个Java Enterprise项目选择Web Application模板确保勾选Create web.xml虽然我们用注解但保留它更规范。配置Tomcat点击Run - Edit Configurations添加Tomcat Server - Local。在Deployment选项卡中添加Artifact你的项目war包。添加依赖库将MySQL驱动jar包、JSTL的jar包jstl.jar和standard.jar复制到WEB-INF/lib目录下。IDEA会自动将其添加到项目库中。运行点击绿色三角运行IDEA会自动启动Tomcat并部署你的应用。访问http://localhost:8080/你的项目名/即可。6.2 常见问题与解决方案实录在开发这个项目的过程中我遇到了不少典型问题这里总结一下希望能帮你提前避坑。问题1点击提交按钮后出现404错误提示“The requested resource is not available”。可能原因AServlet映射路径错误。检查WebServlet(/login)中的路径是否与表单actionlogin或链接hreflogin完全一致。注意路径是相对于项目上下文根目录的。使用${pageContext.request.contextPath}/login是更稳妥的写法。可能原因BServlet类没有被Tomcat加载。确保你的Servlet类在src目录下并且有正确的WebServlet注解或web.xml配置。重启Tomcat有时能解决。排查技巧在IDEA的Tomcat控制台查看启动日志看是否有“Servlet [XxxServlet] mapped to [/xxx]”这样的信息确认Servlet已成功注册。问题2JSP页面显示乱码。解决方案这是一个经典问题需要多管齐下。确保JSP文件本身保存为UTF-8编码IDEA中File - File Properties - File Encoding。在JSP页面顶部添加% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8%。对于POST请求如表单提交在对应的Servlet的doPost方法最前面添加request.setCharacterEncoding(UTF-8);。对于GET请求的乱码参数在URL中需要修改Tomcat的server.xml配置文件在Connector标签中添加URIEncodingUTF-8属性。或者在后端用new String(param.getBytes(ISO-8859-1), UTF-8)进行转换不推荐麻烦。设置数据库连接字符串时指定编码jdbc:mysql://localhost:3306/db_name?useUnicodetruecharacterEncodingUTF-8。问题3登录成功后Session中存了用户但刷新页面后用户信息就没了。可能原因浏览器禁用了Cookie。Session ID默认依靠CookieJSESSIONID在浏览器和服务器间传递。如果Cookie被禁用每次请求都会被认为是新会话。解决方案使用URL重写。在所有需要保持会话的链接和表单action中使用response.encodeURL(url)或response.encodeRedirectURL(url)。但这样会让URL变得很长且难看。对于学习项目确保浏览器启用Cookie即可。在实际项目中这也是需要考虑的兼容性问题。问题4订阅图书时出现“库存超卖”即库存减为负数。根本原因高并发下的线程安全问题。两个线程同时查询到库存为1然后都去执行减1操作。解决方案如4.3节所述在查询库存的SQL语句中使用SELECT ... FOR UPDATE进行加锁将“查询”和“更新”放在同一个数据库事务中。这是最根本的解决方案。仅仅在Java代码中用synchronized关键字是不够的因为你的应用可能部署在多台服务器上。问题5使用JSTL标签库页面报错“The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved”。原因缺少JSTL的jar包或者jar包没有放在正确的位置。解决从Maven仓库或官网下载jstl.jar和standard.jar或使用javax.servlet.jsp.jstl-api和javax.servlet.jsp.jstl将其放入WEB-INF/lib目录下并右键 - Add as Library。同时确保web.xml头部的版本是2.4或以上支持JSTL。6.3 项目扩展思路完成这个基础版本后你可以尝试添加以下功能让项目更丰满写到简历里也更有分量前端美化引入Bootstrap、Layui等前端UI框架替换原始的表格和按钮让界面现代化。异步交互使用jQuery或原生JavaScript的AJAX技术实现“订阅”按钮的无刷新操作提升用户体验。过滤器Filter编写一个AuthFilter过滤所有需要登录才能访问的路径如/subscription/*,/user/center统一检查Session实现权限控制的解耦。文件上传实现图书封面图片的上传功能。邮件提醒在图书到期前发送邮件提醒用户归还可以使用JavaMail API。图表统计使用ECharts等库为管理员后台增加数据统计图表如“热门图书借阅榜”、“月度借阅趋势”。这个“简易”的图书订阅管理系统就像一把钥匙帮你打开了JavaWeb应用开发的大门。它串联起了JavaSE、数据库、前端、网络协议等多个领域的知识。我强烈建议你不要只看而是打开IDEA从头到尾敲一遍。过程中遇到的每一个错误都是你进步的阶梯。当你最终看到浏览器上跑起自己写的完整应用时那种成就感是无与伦比的。编程终究是一门实践的技艺。本文还有配套的精品资源点击获取
返回列表