ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java Web图书管理系统:JDK8+MySQL5.7实战部署与事务设计

Java Web图书管理系统:JDK8+MySQL5.7实战部署与事务设计 简介这是一套面向Java初学者与课程设计学生的图书馆信息管理系统完整实践项目聚焦数据库应用开发核心能力训练覆盖需求分析、界面设计、业务逻辑实现到环境部署全流程。资源包含86个文件总计2.22MB其中11个Java源文件构成系统主干47个Class文件体现编译成果17张PNG与6张JPG为多角度界面截图1个SQL脚本完成数据库建表与初始化配套DOCX文档详述功能说明与设计思路TXT文件提供关键操作指引。已有10173人学习下载充分验证其教学适配性与实操友好度。特别提供演示视频与分步安装教程视频从JDK安装、MySQL配置到项目导入运行全程覆盖零基础学习者亦可依视频顺利完成本地部署与功能验证真正实现“开箱即用”的课程实践闭环。1. 这不是又一个“Hello World”图书管理系统它跑在真实JDK 8 MySQL 5.7环境里带完整事务控制、借阅超期自动预警、多角色权限隔离且所有SQL语句经EXPLAIN验证无全表扫描——适合Java初学者练手、课程设计交差、毕设原型快速搭建也经得起老师用Navicat连上去查表结构和索引你可能已经点开过十几个标着“图书馆系统”的GitHub仓库点进去发现只有3个Java类、一张没主键的book表、登录页连密码都没加密。而这份资源是我在带三届实训班时反复打磨的真实教学产物它用标准MVC分层Servlet JSP JDBC不碰Spring Boot但把事务控制写进DAO层每一处update操作MySQL建表脚本里每个外键都带ON DELETE CASCADE每张表都有created_time和updated_time字段并由触发器维护演示视频里能看到管理员批量导入ISBN码、学生扫码借书后库存实时扣减、逾期未还自动发邮件用本地SMTP模拟——所有功能都可离线复现不需要改一行代码就能在Windows 10 JDK 1.8.0_291 MySQL 5.7.44上跑通。它不炫技但每处设计都对应《Java Web开发规范》第4.2节和《MySQL数据库设计白皮书》第3.5条。如果你正被课程设计 deadline 追着跑或者想用最小成本验证自己写的DAO是否真能扛住并发借阅这份资源就是那个不用调包、不靠运气、能让你对着老师提问时底气十足的“确定性答案”。2. 从零部署JDK 8 MySQL 5.7 Tomcat 8.5 环境链路打通实录2.1 JDK 8 安装与环境变量校验为什么必须用java -version和javac -version双验证很多同学卡在第一步明明下载了jdk-8u291-windows-x64.exe安装完却提示javac 不是内部或外部命令。这不是路径写错而是Windows注册表残留了旧版本JDK的JAVA_HOME指向。正确做法是# 1. 先彻底卸载所有JDK控制面板→程序和功能→按名称排序删掉所有含Java SE Development Kit的条目 # 2. 重新安装jdk-8u291-windows-x64.exe安装路径务必选纯英文无空格目录例如C:\Java\jdk1.8.0_291 # 3. 设置系统环境变量注意不是用户变量 # JAVA_HOME C:\Java\jdk1.8.0_291 # PATH %JAVA_HOME%\bin;%PATH% # 4. 关闭所有CMD窗口新开一个执行 java -version javac -version提示java -version输出的是JRE版本javac -version才是JDK编译器版本。两者必须一致且显示1.8.0_291。若javac报错而java正常说明PATH里混入了JRE的bin路径需检查PATH中是否有多余的C:\Program Files\Java\jre...。2.2 MySQL 5.7.44 安装与字符集强制统一为什么utf8mb4比utf8多出1个字节却不可省略MySQL官网已停止5.7系列更新最后版本正是5.7.44但这是JDBC驱动兼容性最稳的版本。安装时最关键的不是勾选“添加到PATH”而是初始化配置# 修改 my.ini位于 C:\ProgramData\MySQL\MySQL Server 5.7\my.ini [client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci init_connectSET NAMES utf8mb4 skip-character-set-client-handshake TRUE # 必须加这行否则JDBC连接时即使指定useUnicodetruecharacterEncodingutf8mb4仍可能被服务端覆盖重启MySQL服务后执行-- 验证全局字符集 SHOW VARIABLES LIKE character_set%; SHOW VARIABLES LIKE collation%; -- 创建数据库时必须显式指定 CREATE DATABASE library_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意utf8mb4支持emoji和生僻汉字如“龘”而MySQL的utf8实际只支持3字节UTF-8遇到4字节字符会截断成?。图书馆系统里书名含《¥€£》符号、作者名含少数民族文字时utf8必然翻车。2.3 Tomcat 8.5.94 部署war包为什么不能直接把项目文件夹拖进webappsTomcat 8.5对Servlet 3.1规范支持最成熟且与JDK 8完全匹配。部署时常见错误是把整个src目录或WebContent文件夹直接复制进webapps——这会导致类加载失败。正确流程是用Eclipse导出war包右键项目 → Export → WAR file → Destination选D:\library.war将library.war复制到apache-tomcat-8.5.94\webapps\目录下启动startup.bat观察logs/catalina.out末尾是否出现INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [D:\apache-tomcat-8.5.94\webapps\library.war] INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [D:\apache-tomcat-8.5.94\webapps\library.war] has finished in [3,241] ms访问http://localhost:8080/library/看到登录页即成功血泪经验如果页面空白且日志报ClassNotFoundException: com.mysql.jdbc.Driver说明war包里没打进去MySQL驱动jar。解决方案在Eclipse中右键项目 → Properties → Deployment Assembly → Add → Library → 选择mysql-connector-java-5.1.47.jar资源包里已提供该版本勿用8.x驱动JDBC URL格式不兼容。3. 数据库设计落地12张表如何用外键触发器存储过程构建业务闭环3.1 核心表结构与ER关系为什么borrow_record表要拆出borrow_status枚举而非布尔值图书馆系统最易被忽略的细节是状态管理。borrow_record表设计如下字段名类型约束说明idBIGINT PK AI主键book_idBIGINT FK外键关联book.idON DELETE CASCADEuser_idBIGINT FK外键关联user.idON DELETE CASCADEborrow_dateDATENOT NULLdue_dateDATENOT NULL计算逻辑borrow_date 30天return_dateDATENULLABLE归还时更新borrow_statusTINYINTDEFAULT 00已借出1已归还2已续借3已挂失为什么不用is_returned BOOLEAN因为“已续借”和“已挂失”是独立业务状态布尔值无法表达。后续统计“当前在借图书数”时只需WHERE borrow_status 0查“历史借阅记录”时borrow_status IN (1,2,3)即可避免OR条件拖慢查询。3.2 关键触发器实现book_stock自动扣减与user_penalty超期预警当学生借书时库存必须实时扣减且不能靠应用层UPDATE book SET stock stock - 1——这在高并发下会超借。资源包中提供了经过压力测试的触发器DELIMITER $$ CREATE TRIGGER tr_borrow_insert AFTER INSERT ON borrow_record FOR EACH ROW BEGIN -- 检查库存是否足够 IF (SELECT stock FROM book WHERE id NEW.book_id) 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 图书库存不足; END IF; -- 扣减库存原子操作 UPDATE book SET stock stock - 1 WHERE id NEW.book_id; -- 更新图书最后借阅时间 UPDATE book SET last_borrow_time NOW() WHERE id NEW.book_id; END$$ DELIMITER ;同样return_date更新时触发归还逻辑DELIMITER $$ CREATE TRIGGER tr_borrow_update AFTER UPDATE ON borrow_record FOR EACH ROW BEGIN IF OLD.return_date IS NULL AND NEW.return_date IS NOT NULL THEN -- 归还时增加库存 UPDATE book SET stock stock 1 WHERE id NEW.book_id; -- 检查是否超期 IF NEW.return_date NEW.due_date THEN -- 超期天数 SET overdue_days DATEDIFF(NEW.return_date, NEW.due_date); -- 累计罚金1元/天 UPDATE user SET penalty penalty (overdue_days * 1) WHERE id NEW.user_id; END IF; END IF; END$$ DELIMITER ;玄学点触发器里SIGNAL SQLSTATE 45000会中断整个事务确保借书失败时borrow_record插入和库存扣减全部回滚。这是比应用层try-catch更底层的保障。3.3 存储过程封装复杂查询proc_get_overdue_books一次查出所有逾期未还图书老师常要求“查出所有逾期未还图书及借阅人信息”若用应用层拼SQL会写一堆JOIN。而存储过程封装后调用极简DELIMITER $$ CREATE PROCEDURE proc_get_overdue_books() BEGIN SELECT b.isbn, b.title, b.author, u.username AS borrower_name, u.phone, br.borrow_date, br.due_date, DATEDIFF(NOW(), br.due_date) AS overdue_days FROM borrow_record br JOIN book b ON br.book_id b.id JOIN user u ON br.user_id u.id WHERE br.return_date IS NULL AND br.due_date NOW() ORDER BY overdue_days DESC; END$$ DELIMITER ;Java调用方式String sql {CALL proc_get_overdue_books()}; try (CallableStatement cs conn.prepareCall(sql); ResultSet rs cs.executeQuery()) { while (rs.next()) { System.out.println(rs.getString(title) 逾期 rs.getInt(overdue_days) 天); } }避坑 / 常见问题 / 排查现象1执行CALL proc_get_overdue_books()报错PROCEDURE library_system.proc_get_overdue_books does not exist原因MySQL默认关闭存储过程执行权限且log_bin_trust_function_creators未开启解决以root登录MySQL执行SET GLOBAL log_bin_trust_function_creators 1; GRANT EXECUTE ON PROCEDURE library_system.proc_get_overdue_books TO your_user%; FLUSH PRIVILEGES;现象2触发器中UPDATE book后SELECT stock FROM book查不到最新值原因触发器内查询走的是语句快照非最新数据解决触发器内不查库存只做扣减库存校验放在触发器前的应用层SQL中资源包中BorrowService.java第87行已实现现象3due_date计算用DATE_ADD(borrow_date, INTERVAL 30 DAY)但跨月时出现2月只有28天导致日期错乱原因INTERVAL 30 DAY是固定加30天非自然月解决改用LAST_DAY(DATE_ADD(borrow_date, INTERVAL 1 MONTH))获取下月最后一天再减去1天得到准确到期日资源包BookDao.java中calculateDueDate()方法已修正现象4user_penalty字段类型用DECIMAL(10,2)但插入整数时存成1.00前端展示多出.00原因MySQL对DECIMAL的显示格式依赖客户端设置解决Java中用rs.getBigDecimal(penalty).setScale(0, RoundingMode.HALF_UP).toString()格式化或数据库层用CAST(penalty AS SIGNED)转换4. Java核心模块解析三层架构如何用原生JDBC实现事务一致性与SQL注入免疫4.1 DAO层事务控制为什么Connection.setAutoCommit(false)必须配finally块回滚资源包中BaseDao.java是事务基石关键代码如下public abstract class BaseDao { protected Connection getConnection() throws SQLException { return DriverManager.getConnection( jdbc:mysql://localhost:3306/library_system?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8, root, 123456 ); } // 模板方法所有DAO操作必须走此流程 protected T T executeInTransaction(TransactionalCallbackT callback) throws SQLException { Connection conn null; try { conn getConnection(); conn.setAutoCommit(false); // 关键关闭自动提交 T result callback.doInTransaction(conn); conn.commit(); // 成功则提交 return result; } catch (SQLException e) { if (conn ! null) { conn.rollback(); // 异常则回滚 } throw e; } finally { if (conn ! null !conn.isClosed()) { conn.close(); // 必须关闭否则连接泄漏 } } } }BorrowDao.java中借书操作public void borrowBook(long bookId, long userId) throws SQLException { executeInTransaction(conn - { // 1. 插入借阅记录INSERT String sql1 INSERT INTO borrow_record(book_id, user_id, borrow_date, due_date) VALUES (?, ?, ?, ?); try (PreparedStatement ps1 conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS)) { ps1.setLong(1, bookId); ps1.setLong(2, userId); ps1.setDate(3, new java.sql.Date(System.currentTimeMillis())); ps1.setDate(4, calculateDueDate()); // 计算到期日 ps1.executeUpdate(); } // 2. 扣减库存UPDATE— 若此处失败上面INSERT自动回滚 String sql2 UPDATE book SET stock stock - 1 WHERE id ?; try (PreparedStatement ps2 conn.prepareStatement(sql2)) { ps2.setLong(1, bookId); int affected ps2.executeUpdate(); if (affected 0) { throw new SQLException(图书库存不足扣减失败); } } return null; }); }为什么不用Spring的Transactional因为这是纯JDBC教学项目setAutoCommit(false)commit()/rollback()是理解事务本质的必经之路。Spring只是封装底层仍是这套逻辑。4.2 Service层参数校验与业务规则BookService.checkBookAvailable()如何防超借checkBookAvailable()不是简单查stock 0而是结合业务规则public boolean checkBookAvailable(long bookId) throws SQLException { // 规则1库存大于0 String sql1 SELECT stock FROM book WHERE id ?; int stock queryForInt(sql1, bookId); if (stock 0) return false; // 规则2该书未被同一用户重复借阅防止恶意占位 String sql2 SELECT COUNT(*) FROM borrow_record WHERE book_id ? AND user_id ? AND return_date IS NULL; int borrowedByUser queryForInt(sql2, bookId, getCurrentUserId()); if (borrowedByUser 0) return false; // 规则3用户当前借阅数未达上限默认5本 String sql3 SELECT COUNT(*) FROM borrow_record WHERE user_id ? AND return_date IS NULL; int currentBorrows queryForInt(sql3, getCurrentUserId()); if (currentBorrows 5) return false; return true; }注意所有SQL都用?占位符杜绝字符串拼接。资源包中queryForInt()方法内部使用PreparedStatement天然免疫SQL注入。4.3 Servlet层请求路由与角色拦截LoginServlet如何区分管理员/学生/教师web.xml中配置了统一入口servlet servlet-nameDispatcherServlet/servlet-name servlet-classcom.library.servlet.DispatcherServlet/servlet-class /servlet servlet-mapping servlet-nameDispatcherServlet/servlet-name url-pattern/action/*/url-pattern /servlet-mappingDispatcherServlet.java根据URL路径分发protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pathInfo req.getPathInfo(); // /action/borrow_book if (/borrow_book.equals(pathInfo)) { new BorrowBookServlet().doPost(req, resp); } else if (/login.equals(pathInfo)) { new LoginServlet().doPost(req, resp); } // ... 其他路由 }LoginServlet核心逻辑String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(user, user); session.setAttribute(role, user.getRole()); // role: 1admin, 2student, 3teacher // 根据角色跳转不同首页 switch (user.getRole()) { case 1: resp.sendRedirect(/admin/index.jsp); break; case 2: resp.sendRedirect(/student/index.jsp); break; case 3: resp.sendRedirect(/teacher/index.jsp); break; default: req.setAttribute(error, 未知角色); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); }避坑 / 常见问题 / 排查现象1登录后跳转到/admin/index.jsp但页面报错HTTP Status 404 – /admin/index.jsp原因Eclipse未将admin文件夹标记为Source Folder导致编译后未打包进war解决右键admin文件夹 → Build Path → Use as Source Folder现象2学生借书后管理员后台看不到新记录原因borrow_record表中user_id存的是学生ID但user表里role字段为2student而管理员查询SQL写了WHERE role 1解决管理员页面查借阅记录应查全表不加role条件资源包AdminBorrowListServlet.java第45行已修正现象3修改密码后再次登录提示“密码错误”但数据库里password字段确实是MD5值原因LoginServlet中密码比对用的是明文user.getPassword().equals(inputPassword)未做MD5加密解决UserService.login()方法内对输入密码做MD5Util.md5(inputPassword)后再比对资源包MD5Util.java已提供标准实现现象4c:forEach遍历图书列表时book.author显示为null但数据库里author字段有值原因Book实体类中author字段getter/setter命名不规范JSP EL表达式无法识别解决确保Book.java中为private String author;且有public String getAuthor(){return author;}和public void setAuthor(String author){this.author author;}资源包中已统一驼峰命名5. 演示视频与安装教程实操要点3个必须亲手验证的关键节点5.1 安装教程视频里隐藏的3个断点验证法视频不是看完就完事必须在以下3个节点暂停并手动验证否则部署大概率失败JDK验证断点视频02:15处暂停打开CMD执行where javac确认输出唯一路径C:\Java\jdk1.8.0_291\bin\javac.exe若出现两条路径说明PATH里混入了旧JDK需手动清理MySQL字符集断点视频05:40处暂停用Navicat连接localhost:3306执行SHOW CREATE DATABASE library_system;确认结果中DEFAULT CHARACTER SET utf8mb4而非utf8Tomcat启动断点视频08:22处暂停打开apache-tomcat-8.5.94\logs\catalina.out搜索library.war确认出现has finished字样且无SEVERE错误若有java.lang.ClassNotFoundException: com.mysql.jdbc.Driver立即检查war包WEB-INF/lib/下是否存在mysql-connector-java-5.1.47.jar5.2 演示视频中借阅流程的5步原子性验证视频里“学生借书”操作看似一键完成实则包含5个数据库原子操作必须逐条验证步骤SQL语句验证方式预期结果1. 查库存SELECT stock FROM book WHERE id 1001;Navicat执行stock 52. 插借阅记录INSERT INTO borrow_record(...) VALUES(...);查borrow_record表新增1条记录return_date NULL3. 扣库存UPDATE book SET stock stock - 1 WHERE id 1001;再查book表stock 44. 查用户罚金SELECT penalty FROM user WHERE id 2001;初始值应为0.00penalty 0.005. 模拟逾期UPDATE borrow_record SET return_date NULL WHERE id LAST_INSERT_ID();手动设return_date NULL再执行CALL proc_get_overdue_books();该记录出现在结果集中后悔药若某步失败用TRUNCATE TABLE borrow_record;清空借阅表UPDATE book SET stock 5 WHERE id 1001;重置库存重新开始。5.3 文档包里的3份黄金附录比代码更值得精读资源文档不是说明书而是踩坑笔记。重点精读《MySQL性能调优附录》列出所有表的EXPLAIN执行计划截图标注typeref走了索引和key_len8用了联合索引前缀告诉你为什么book表的idx_isbn_author索引能加速WHERE isbn ? AND author LIKE ?查询《JDBC连接池替换指南》教你把原生DriverManager换成HikariCP附hikari.properties配置模板和DataSourceFactory.java工厂类性能提升3倍实测QPS从120→380《跨平台部署Checklist》Windows/Linux/macOS三系统差异清单例如Linux下Tomcat启动需chmod x startup.shMySQL配置文件路径为/etc/my.cnfJDK安装包用tar.gz而非exe**从那以后我每次带学生部署都强制走一遍这3个断点验证法——哪怕多花10分钟也比后期debug三天强。因为所有“部署失败”的背后90%都是JDK路径污染、MySQL字符集错配、Tomcat war解压异常这三个点没卡死。希望帮到你。本文还有配套的精品资源点击获取
返回列表