ARTICLE DETAIL

建站实战干货

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

Java Web原生开发:Servlet+JSP+JDBC图书系统实战

2026/9/17 1:28:58 拓冰建站 浏览量
Java Web原生开发:Servlet+JSP+JDBC图书系统实战 简介这是一套面向Java初学者与课程设计学生的完整图书管理系统实战项目资源基于Java Web技术栈开发覆盖数据库设计、前后端交互与业务逻辑实现全流程。资源包含源码、MySQL数据库脚本及配套课程设计报告可直接部署运行并用于软件工程实践、毕业设计参考或Web开发能力训练。压缩包共851个文件主体为16个Java源文件含Servlet与业务类、10个JSP页面、8个CSS与HTML前端资源、24个编译后class文件、18个jar依赖库以及大量XML配置、DAT数据文件和项目元数据总大小12.73MB结构完整符合Eclipse/IDEA标准Web项目布局。已有2538人学习下载读者可获得可运行的全功能系统、规范的数据库建表语句、清晰的MVC分层代码结构及详实的设计报告涵盖需求分析、表关系设计、借阅流程逻辑与测试用例助其快速掌握Java Web开发核心技能。1. 这不是又一个“Hello World”项目它用标准 Java Web 技术栈跑通了真实业务闭环你可能见过几十个“图书管理系统”Demo——JSP 页面堆满表格、Servlet 里硬编码 SQL、数据库字段名直接当变量名用。但这份源码包含完整数据库脚本与课程设计报告不同它在 Tomcat 8 环境下可直接部署用户注册后能真实完成「借书→库存扣减→借阅记录生成→归还→库存回填」全链路且所有外键约束、事务边界、密码加密BCrypt、会话超时控制全部启用。它不依赖 Spring Boot 自动装配而是用原生 Servlet JSP JDBC Filter 实现分层解耦每个模块BookServlet、UserServlet、BorrowServlet职责清晰DAO 层封装了 PreparedStatement 防注入Service 层显式标注了 Transactional 语义通过手动 commit/rollback 模拟。适合 Java 初学者理解 Web 容器生命周期也足够让有 3 年经验的开发者反向推演如果把其中某个 Filter 改成 JWT 验证或把 JSP 替换为 Thymeleaf改动点在哪它不是教学玩具是能放进简历“项目经验”栏、经得起面试官深挖三层调用栈的真实工程切片。2. 从零启动环境准备、数据库初始化与 WAR 包部署路径2.1 环境依赖与版本对齐该系统基于 Java EE 规范实现不兼容 JDK 17 的模块化特性实测稳定运行于以下组合JDK 8u291必须使用JAVA_HOME指向此版本java -version输出含1.8.0_291Apache Tomcat 8.5.99注意Tomcat 9 默认禁用org.apache.jasper.servlet.JspServlet会导致 JSP 编译失败MySQL 5.7.36InnoDB 引擎字符集utf8mb4排序规则utf8mb4_unicode_ci提示若使用 MySQL 8.0需在 JDBC URL 后追加?serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse否则DriverManager.getConnection()会抛出java.sql.SQLException: The server time zone value XXX is unrecognized。2.2 数据库初始化执行建表与基础数据注入解压 ZIP 后进入database/目录找到book_system.sql文件。该脚本包含三张核心表表名主要字段关键约束t_bookid(PK),title,author,publisher,stock,isbnstock默认值0isbn唯一索引t_userid(PK),username,password,role,emailusername唯一password长度 ≥64BCrypt 加密后长度t_borrow_recordid(PK),book_id(FK),user_id(FK),borrow_time,return_time,status外键级联 ON DELETE CASCADEstatus枚举值BORROWED,RETURNED执行建库与导入命令Linux/macOSmysql -u root -p -e CREATE DATABASE IF NOT EXISTS book_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p book_system ./database/book_system.sql验证数据-- 检查是否成功插入管理员账号默认用户名 admin密码 123456 SELECT username, password FROM t_user WHERE username admin; -- 返回结果应为admin, $2a$10$...BCrypt 密文非明文2.3 WAR 包构建与 Tomcat 部署源码结构遵循标准 Java Web 工程规范src/ ├── main/ │ ├── java/ # Servlet、DAO、Service 类 │ │ └── com/book/system/ │ │ ├── servlet/ # BookServlet.java, UserServlet.java... │ │ ├── dao/ # BookDao.java, UserDao.java... │ │ └── service/ # BookService.java, BorrowService.java... │ ├── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml # 配置 Servlet 映射、Filter、Listener │ │ │ └── lib/ # 包含 mysql-connector-java-5.1.47.jar │ │ └── index.jsp # 登录入口页构建 WAR 包步骤在src/main/webapp/WEB-INF/web.xml中确认servlet-mapping路径servlet-mapping servlet-nameUserServlet/servlet-name url-pattern/user/*/url-pattern /servlet-mapping使用 Maven 打包需确保pom.xml存在若无则手动创建!-- pom.xml 关键片段 -- packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties执行打包cd /path/to/project mvn clean package -Dmaven.test.skiptrue # 输出 target/book-system.war部署到 Tomcatcp target/book-system.war $CATALINA_HOME/webapps/ # 启动 Tomcat$CATALINA_HOME/bin/startup.sh # 访问 http://localhost:8080/book-system/2.4 首次访问验证登录流程与会话状态检查打开浏览器访问http://localhost:8080/book-system/页面跳转至login.jsp。输入默认账号用户名admin密码123456成功登录后URL 变为/book-system/home.jsp页面顶部显示「欢迎admin」。此时检查 Tomcat 会话# 查看当前活跃会话需开启 Manager App curl -u admin:admin http://localhost:8080/manager/html # 或在代码中打印 session ID // 在 HomeServlet.java 中添加 System.out.println(Session ID: request.getSession().getId());注意web.xml中配置了session-configsession-timeout30/session-timeout/session-config30 分钟无操作自动失效。若修改为 5 分钟测试需重启 Tomcat 生效。3. 核心业务逻辑拆解借阅流程中的事务控制与状态机设计3.1 借阅请求的完整调用链从 HTTP 请求到数据库写入用户点击「借阅」按钮触发 AJAX 请求// borrow.js $.post(/book-system/borrow, { bookId: 101, userId: 1 }, function(data) { if (data.success) alert(借阅成功); });后端BorrowServlet处理流程// BorrowServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int bookId Integer.parseInt(request.getParameter(bookId)); int userId Integer.parseInt(request.getParameter(userId)); BorrowService borrowService new BorrowService(); // 关键事务开始前检查库存 boolean canBorrow borrowService.checkStock(bookId); // 查询 t_book.stock 0 if (!canBorrow) { response.getWriter().write({\success\:false,\msg\:\库存不足\}); return; } // 手动控制事务非 Spring需显式 try-catch Connection conn null; try { conn DBUtil.getConnection(); // 获取连接 conn.setAutoCommit(false); // 关闭自动提交 // 步骤1扣减库存 BookDao bookDao new BookDao(conn); bookDao.updateStock(bookId, -1); // stock stock - 1 // 步骤2插入借阅记录 BorrowDao borrowDao new BorrowDao(conn); borrowDao.insertRecord(bookId, userId); conn.commit(); // 全部成功才提交 response.getWriter().write({\success\:true}); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } response.getWriter().write({\success\:false,\msg\:\系统错误\}); } finally { DBUtil.closeConnection(conn); } }3.1.1 为什么不用Transactional因为项目未引入 Spring 框架事务控制完全由 JDBCConnection对象管理。conn.setAutoCommit(false)是关键开关它使后续所有executeUpdate()操作处于同一事务上下文。若bookDao.updateStock()成功但borrowDao.insertRecord()失败如外键约束冲突conn.rollback()会将库存变更回滚保证数据一致性。这是原生 Java Web 开发中必须掌握的底层机制。3.2 状态机驱动的归还逻辑status字段的双重校验归还操作不仅更新t_borrow_record.return_time更需校验业务状态// BorrowService.java public boolean returnBook(int recordId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 校验1记录是否存在且状态为 BORROWED String checkSql SELECT status FROM t_borrow_record WHERE id ? AND status BORROWED; PreparedStatement ps conn.prepareStatement(checkSql); ps.setInt(1, recordId); ResultSet rs ps.executeQuery(); if (!rs.next()) return false; // 不存在或已归还 // 校验2对应图书库存是否被正确扣减防并发超借 String getBookIdSql SELECT book_id FROM t_borrow_record WHERE id ?; ps conn.prepareStatement(getBookIdSql); ps.setInt(1, recordId); rs ps.executeQuery(); rs.next(); int bookId rs.getInt(book_id); // 更新记录状态并回填库存 String updateSql UPDATE t_borrow_record SET statusRETURNED, return_time? WHERE id?; ps conn.prepareStatement(updateSql); ps.setTimestamp(1, new Timestamp(System.currentTimeMillis())); ps.setInt(2, recordId); ps.executeUpdate(); BookDao bookDao new BookDao(conn); bookDao.updateStock(bookId, 1); // stock stock 1 conn.commit(); return true; } catch (SQLException e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) {} return false; } finally { DBUtil.closeConnection(conn); } }3.2.1status字段为何不能仅靠应用层校验若只在 Java 代码中if (record.getStatus().equals(BORROWED))高并发场景下可能出现「ABA 问题」线程 A 读取到BORROWED线程 B 同时归还并更新为RETURNED线程 A 仍执行归还逻辑。因此必须在 SQL 中嵌入AND status BORROWED条件利用数据库行级锁InnoDB 的SELECT ... FOR UPDATE或UPDATE ... WHERE保证原子性。本项目虽未显式加锁但UPDATE语句天然持有行锁已满足要求。3.3 用户密码安全BCrypt 加密与验证流程UserServlet注册时调用String rawPassword request.getParameter(password); String encodedPassword BCrypt.hashpw(rawPassword, BCrypt.gensalt(12)); // 插入 t_user.password 字段登录验证时String inputPassword request.getParameter(password); String storedHash userDao.getPasswordByUsername(username); // 从数据库查出 $2a$12$... if (BCrypt.checkpw(inputPassword, storedHash)) { // 密码正确 }提示BCrypt.gensalt(12)中12是 log rounds值越大越安全但耗时越长。生产环境建议10~12开发环境4即可加速测试。4. 源码级调试技巧定位常见报错与性能瓶颈4.1 五类高频报错的根因与修复方案报错现象根本原因修复命令/配置HTTP Status 404 – /book-system/login.jspWAR 包未解压或web.xml中welcome-file-list未配置login.jsp检查webapp/WEB-INF/web.xml是否含welcome-filelogin.jsp/welcome-filejava.lang.ClassNotFoundException: com.mysql.jdbc.DriverMySQL 驱动 JAR 未放入WEB-INF/lib/下载mysql-connector-java-5.1.47.jar复制到src/main/webapp/WEB-INF/lib/javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequestTomcat 版本与 Servlet API 不匹配确认pom.xml中scopeprovided/scope避免打包重复 Servlet APIcom.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failureMySQL 服务未启动或 JDBC URL 错误执行systemctl status mysqld检查DBUtil.java中 URL 是否为jdbc:mysql://localhost:3306/book_systemHTTP Status 500 – java.sql.SQLException: Column xxx cannot be null表单提交字段缺失或数据库字段NOT NULL约束未满足在t_book表中执行ALTER TABLE t_book MODIFY COLUMN isbn VARCHAR(20) NULL;若 ISBN 非必填4.2 性能瓶颈定位慢查询与连接泄漏检测系统在高并发借阅时可能出现响应延迟需排查慢查询日志开启MySQLSET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; -- 超过1秒记为慢查询 SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;检查连接泄漏在DBUtil.java的closeConnection()方法中添加日志public static void closeConnection(Connection conn) { if (conn ! null) { try { System.out.println(Closing connection: conn.hashCode()); // 关键日志 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }若日志中出现大量Closing connection: xxx但getConnection()调用次数远少于关闭次数说明连接未被正确释放。4.3 JSP 页面调试EL 表达式失效的三种场景当home.jsp中${user.username}显示为空白而非预期用户名场景1% page isELIgnoredtrue %被误设→ 删除该指令或改为false场景2web.xml中web-app版本过低→ 确保声明为xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1场景3Servlet 未将对象存入 request scope→ 检查HomeServlet.java是否调用request.setAttribute(user, user)5. 进阶改造将原生 Filter 替换为基于角色的细粒度权限控制5.1 理解现有 Filter 的局限性当前AuthFilter.java仅做粗粒度拦截// AuthFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); }它无法区分「普通用户只能借阅」和「管理员可删除图书」所有受保护路径/book/*,/user/*权限相同。5.2 实现 RBAC基于角色的访问控制Filter新建RoleBasedFilter.java替换web.xml中的AuthFilter声明filter filter-nameRoleBasedFilter/filter-name filter-classcom.book.system.filter.RoleBasedFilter/filter-class /filter filter-mapping filter-nameRoleBasedFilter/filter-name url-pattern/admin/*/url-pattern !-- 仅管理员可访问 -- /filter-mapping filter-mapping filter-nameRoleBasedFilter/filter-name url-pattern/user/borrow/url-pattern !-- 所有登录用户可访问 -- /filter-mappingRoleBasedFilter.java核心逻辑public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 未登录); return; } User user (User) session.getAttribute(user); String requestURI request.getRequestURI(); // 规则/admin/ 开头的路径仅允许 roleADMIN if (requestURI.startsWith(request.getContextPath() /admin/) !ADMIN.equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 权限不足); return; } // 规则/user/delete 路径仅允许管理员 if (requestURI.contains(/user/delete) !ADMIN.equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 仅管理员可删除用户); return; } chain.doFilter(req, resp); }5.2.1 如何动态加载权限规则避免硬编码路径可将规则存入数据库CREATE TABLE t_permission ( id INT PRIMARY KEY AUTO_INCREMENT, url_pattern VARCHAR(255) NOT NULL, -- 如 /admin/book required_role VARCHAR(20) NOT NULL -- ADMIN, USER ); INSERT INTO t_permission VALUES (1, /admin/book, ADMIN), (2, /user/borrow, USER);在 Filter 初始化时init()方法查询该表缓存到static MapString, String提升性能。5.3 验证权限控制生效启动应用后用普通用户roleUSER尝试访问http://localhost:8080/book-system/admin/book应返回 HTTP 403 错误页而非空白或重定向。查看 Tomcat 日志[ERROR] RoleBasedFilter - 权限不足: USER 尝试访问 /admin/book这证明细粒度控制已介入无需修改任何 Servlet 代码即可扩展权限体系。本文还有配套的精品资源点击获取