Java+MySQL图书管理系统从零搭建:数据库设计与事务实战全解析
发布时间:2026/9/9 15:09:59 作者:尧图编辑部 阅读量:1,286

简介一套面向Java初学者的图书管理系统完整项目基于JavaMySQL实现覆盖图书添加、查询、借阅、归还、用户管理及统计等典型功能。项目采用Servlet、JSP与MVC设计模式组织代码并配有SQL初始化脚本与JDBC连接示例适合以此理解Java Web后端开发、关系型数据库设计与前后端交互流程。压缩包共222个文件、约8.58MB其中以160个Java源文件为主辅以jar依赖、properties配置、SQL脚本及少量说明文档结构清晰便于导入Eclipse等IDE运行调试。目前已有1464人浏览学习可作为课程设计或自学练手的实战参考。通过该项目还可以学习PreparedStatement防注入写法、数据库连接池配置以及分层项目组织方式是一份能快速上手的入门实践资源。 几乎每个学Java的人都被“图书管理系统”这几个字轰炸过。大学课设、培训班期末项目、简历上的个人项目经历JavaMySQL做图书管理系统几乎成了绕不开的标配。但这个项目也是被误解最深的有人拿它当普通的增删改查练手有人直接跑通别人的源码就交差还有人写完连数据库表为什么这么设计、借书为什么必须开事务都说不清楚。这篇博客说白了就是把我自己从零搭一个图书管理系统的完整过程摊开来讲环境怎么配、六张表怎么设计、三层架构每层写什么、核心SQL怎么组织再到实际踩过的乱码、时区、驱动版本这些坑一次说清。不管你是在准备课设、备战面试还是想拿它当JavaWeb的第一个完整项目这篇都可以直接当操作参考。1. 想清楚再动手这个项目到底在练什么1.1 为什么不用Spring Boot反而选JDKServletJSP这套“老组合”先给结论这个项目的定位不是炫技而是训练一个Java后端开发的基本功。它适合三类人——刚学完Java基础和MySQL、想找个完整项目串联知识的新手要交课程设计或毕业设计、需要一个能讲清楚原理的项目的学生还有准备Java实习面试、需要一个扛得住追问的项目经历来撑场面的人。如果你之前只写过控制台程序或者只跟着视频敲过几百行的Demo那把图书管理系统从头到尾做一遍基本是你第一次以“完整项目”的视角去理解后端开发。技术选型上我故意没用Spring Boot很多人不理解现在企业都用Spring Boot为什么还要用ServletJSP这种“老古董”。原因是图书管理系统的核心价值在业务逻辑和数据库设计不在框架。Spring Boot的自动配置帮你解决了大部分问题你反而看不到Servlet怎么处理请求、JSP怎么渲染数据、JDBC怎么跟数据库交互。用最原生的JavaWeb方式走一遍你能对HTTP请求从浏览器到Servlet再到JSP返回的完整链路建立一个不可替代的体感。之后再学Spring Boot你会明白它在帮你做什么而不是蒙头用。1.2 功能清单先列明白做的时候才不跑偏一个“能交差又不丢人”的图书管理系统功能上至少覆盖以下六个方面。图书管理图书增删改查按书名、作者、ISBN做模糊搜索分类管理维护图书分类方便图书归类和统计读者管理读者信息维护以及正常/挂失状态切换借书还书借出时扣库存、插入借阅记录归还时回补库存、更新记录状态借阅记录历史查询能看出哪本书被谁借走、是否逾期、何时归还统计看板图书总量、读者总量、当前借出数量、分类数量统计这六块功能看着不复杂但它们已经决定了后续的表结构、Servlet接口和前端页面怎么划分。建议你动手前先拿张纸把功能清单写下来每设计一张表、每写一个Servlet都回到清单对一下能省掉后面大量返工。我见过太多人做到一半跑去加“感觉有用”但根本用不上的功能最后连核心流程都没跑通。2. 环境准备阶段最容易被卡住的三个位置翻了搜得最多的两类关键词无非是java环境变量配置和mysql安装配置教程说明大部分人真是卡在这了。这三个位置解决掉后面的路基本就顺了。2.1 JDK环境变量配完别急着庆祝先跑三条命令JDK安装本身不复杂容易翻车的是环境变量。需要配三个JAVA_HOME指向JDK安装根目录Path里追加%JAVA_HOME%\binCLASS_PATH按传统习惯配成.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar。虽然JDK 9以后不再强依赖CLASS_PATH但课设环境下配了不会出错很多教材和教程也还在讲面试偶尔也会问。配完别直接开IDE先打开cmd依次执行三条命令验证java -version、javac -version、echo %JAVA_HOME%。如果java命令有输出而javac提示找不到基本就是Path顺序问题——机器上装了多个JDK或JRE靠前的路径覆盖了你的配置。把Path里多余的Java相关路径清理掉只保留你自己的%JAVA_HOME%\bin就解决了。2.2 MySQL 8.0安装密码、时区、认证方式一起说MySQL 8.0的安装向导已经很省心但有两个步骤不能一路Next。第一是Authentication Method页面建议选Legacy Authentication否则老版本驱动或部分图形工具连接时会报Authentication plugin caching_sha2_password cannot be loaded。第二是设置root密码那一步务必设一个自己记得住的密码并顺手写进备忘录后面写JDBC配置全靠它。安装完成后立刻验证三件事cmd里执行mysql -u root -p能进命令行MySQL服务在Windows服务列表里是启动状态Workbench或Navicat能正常连上3306。还有一个容易忽略的是时区问题MySQL 8.0默认时区是UTC后面JDBC连接URL里不写serverTimezoneAsia/Shanghai查询和插入的时间会差8小时等你发现借阅时间对不上时就晚了。如果你想在Docker里起一个MySQL实例当练习环境docker run --name mysql8 -e MYSQL_ROOT_PASSWORD你的密码 -p 3306:3306 -d mysql:8.0也能用不过课设场景本机直接安装更直观少一层网络和端口映射的干扰。2.3 Tomcat、IDEA和依赖这一关过了项目才算真正能跑Tomcat 9对应JDK 8及以上版本兼容表要提前查。IDEA新建项目时选Maven的webapp骨架也就是org.apache.maven.archetypes:maven-archetype-webapp很多新手找不到这个模板是因为建项目时没选Maven Archetype。加完骨架之后pom.xml里要补三个基础依赖mysql-connector-j驱动、javax.servlet-apiServlet、javax.servlet.jsp-apiJSP如果用Druid连接池再补druid。还有一步最容易漏webapp骨架默认没有这些依赖如果你是手工加jar包而不是用Maven记得把jar拷到WEB-INF/lib目录下否则运行时会报ClassNotFoundException。这个异常不像语法错误那样有具体行号排查起来很费劲最好一开始就按标准来。3. 表结构这样设计后面业务才能写得顺手3.1 六张核心表每张的职责要分清楚数据库设计直接决定后面代码好不好写。我见过最典型的反面教材是把借阅记录和图书信息塞进同一张表结果还书更新库存的逻辑绕来绕去状态还容易乱。推荐至少六张表admin管理员表id、username、password、create_timecategory分类表id、name、descriptionbook图书表id、isbn、name、author、publisher、category_id、total、stock、create_timereader读者表id、card_no、name、phone、status、create_timeborrow_record借阅记录表id、reader_id、book_id、borrow_time、due_time、return_time、statusfine罚款表可选id、record_id、amount、status两个设计细节要单独展开。第一book表的total和stock必须分开。total表示图书总量stock表示当前可借库存借书时stock减一还书时stock加一总数减去当前库存就能算出有多少本在外流通。很多人只用一个数量字段最后做统计和库存校验时非常痛苦。第二borrow_record表用status字段标记借阅状态0未还、1已还、2逾期。这个字段在做列表展示和逾期统计时能少写很多条件判断比单纯靠return_time是否为空来判断清晰得多。3.2 建表SQL字符集、外键、默认值一次配好下面是我实际用的建表SQL核心部分字段类型和约束都按MySQL 8.0的标准来CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE, name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), category_id INT, total INT DEFAULT 1, stock INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里必须强调两点。第一字符集统一用utf8mb4不要用utf8。MySQL的utf8只支持3字节存emoji和一些生僻汉字会报错或变成问号utf8mb4是完整的四字节UTF-8早就是行业默认。第二外键约束该加就加。互联网大厂生产环境为了性能会放弃外键但对课设和入门项目来说外键能兜住大量脏数据利大于弊。3.3 索引和常用查询字段的规划这个体量的系统索引不是性能瓶颈但从现在开始养成建索引的习惯是值得的。图书表的name、author建议加普通索引isbn直接在字段上建UNIQUE约束既能防止重复录入又能加速按ISBN精确查找borrow_record表的reader_id、book_id作为外键InnoDB会自动建索引不用重复添加。你还可以顺手验证一下索引是否生效在Workbench里对查询语句执行EXPLAIN SELECT ...看type列是ref还是range说明索引被用上了如果是all说明在全表扫描。把EXPLAIN养成本能后面写复杂SQL会少走很多弯路。4. 从JDBC到DAO再到Servlet代码分层的完整链路4.1 为什么必须分层而不是把代码堆在JSP里新手最容易犯的错是图省事在JSP页面里直接写Java代码查数据库一个页面塞几百行JDBC。这种写法能跑通Demo但改一个SQL要翻页面报错时分不清是业务问题还是渲染问题代码完全没法复用。所以我坚持用经典三层结构DAO层只负责数据库增删改查不掺业务逻辑Service层处理业务规则比如借书前检查读者状态、检查库存Servlet层接收请求、调Service、控制页面跳转或返回JSONJSP层只做展示用EL表达式和JSTL渲染数据分层之后每个类职责单一出问题定位很快。面试被问到项目架构时你能把这一层一层的关系说清楚本身就比大多数人只会跑Demo强很多。4.2 DBUtil连接工具Druid连接池配置和参数含义我不用最原始的DriverManager每次新建连接而是用Druid连接池。原因很简单Web项目是并发请求每次请求都new Connection再close数据库很快会被拖垮连接池预先建好一批连接复用是生产环境的基本操作。druid.properties配置如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot password你的密码 initialSize5 maxActive20 maxWait3000URL里那一长串参数没有一个是多余的。useUnicodetrue和characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决八小时时差useSSLfalse去掉本地开发的SSL握手警告和开销allowPublicKeyRetrievaltrue配合MySQL 8.0的caching_sha2_password认证不加会偶发连不上。DBUtil用静态代码块加载配置创建数据源对外只暴露一个getConnection方法public class DBUtil { private static DataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }4.3 BookDAOPreparedStatement是底线不是可选项DAO层最常见的是查询和更新方法。查询我坚持用PreparedStatement不是Statement。用字符串拼接SQL一旦书名里包含单引号SQL就会语法错误更严重的是存在SQL注入漏洞用户在搜索框输入 OR 11你的查询条件就被改写了。图书搜索的典型写法如下public ListBook searchBooks(String keyword) { String sql SELECT * FROM book WHERE name LIKE ? OR author LIKE ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setName(rs.getString(name)); book.setAuthor(rs.getString(author)); book.setStock(rs.getInt(stock)); list.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这段代码有几个隐藏知识点Connection、PreparedStatement、ResultSet全部写在try-with-resources里方法执行完自动释放不用手写finally既能防止连接泄漏又能少一堆冗余代码。ResultSet的读取逻辑和JDBC驱动的实现有关但你把getString、getInt这些方法用熟了后面换MyBatis时会更容易理解框架在帮你干什么。4.4 借书和还书必须开事务的一个业务场景整个系统里只有借书和还书需要显式开事务。因为一次借书要同时做两件事往borrow_record表插一条记录同时把book表库存减一。这两步必须要么都成功、要么都失败绝不能出现“记录插了但库存没减”的中间状态。借书的核心逻辑如下public boolean borrowBook(int readerId, int bookId) { String insertSql INSERT INTO borrow_record(reader_id, book_id, borrow_time, due_time, status) VALUES(?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),0); String updateSql UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(insertSql); PreparedStatement ps2 conn.prepareStatement(updateSql)) { ps1.setInt(1, readerId); ps1.setInt(2, bookId); int r1 ps1.executeUpdate(); ps2.setInt(1, bookId); int r2 ps2.executeUpdate(); if (r1 0 r2 0) { conn.commit(); return true; } conn.rollback(); } } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } finally { try { if (conn ! null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } return false; }重点看updateSql里的AND stock 0这是用数据库约束兜底防止把库存扣成负数。如果这本书可借库存是0这条update影响行数为0整体回滚借书失败。这个写法比在Java里先SELECT查一次库存再判断更可靠也省了一次查询。手动setAutoCommit之后finally里还要记得把事务状态恢复并把连接归还给连接池否则连接池里的连接状态会被污染这个坑很隐蔽。5. 图书管理与借阅归还里最难啃的SQL5.1 模糊搜索和排序分页LIMIT公式要记牢图书列表页必做分页分页SQL的核心是LIMIT offset, size其中offset(currentPage-1)*pageSize。比如第一页每页10条offset就是0第二页offset就是10。配合模糊搜索和排序的语句长这样SELECT * FROM book WHERE name LIKE CONCAT(%, ?, %) ORDER BY id DESC LIMIT ?, ?;我用CONCAT(%, ?, %)而不是在Java里拼好“%关键词%”再传参是为了让占位符只代表关键词本身避免通配符被拼进SQL导致语义混乱。配套的查总数语句是SELECT COUNT(*) FROM book WHERE name LIKE CONCAT(%, ?, %)前端用总数除以每页条数向上取整算总页数。这里建议用整数运算别用float直接除页数显示容易出问题。5.2 借阅记录的多表联查一次JOIN省掉N1借阅列表页想展示读者姓名、书名、借阅时间、状态这些字段分散在reader、book、borrow_record三张表里。如果先查记录再循环去查读者和图书就叫N1问题数据一多性能立刻崩。正确写法是一条JOIN拿全所有字段SELECT r.id, rd.card_no, rd.name AS reader_name, b.name AS book_name, r.borrow_time, r.due_time, r.return_time, r.status FROM borrow_record r JOIN reader rd ON r.reader_id rd.id JOIN book b ON r.book_id b.id ORDER BY r.borrow_time DESC;这段SQL里borrow_record是事实表reader和book是维度表一对多关系通过外键JOIN起来。DAO层取出ResultSet后把别名字段封装成一个借阅记录VO页面直接展示。面试问“多表查询怎么避免N1”拿这条SQL现场讲比背概念有力得多。想进一步验证SQL有没有走索引就在Workbench里执行EXPLAIN看执行计划type列是ref或range说明索引生效。5.3 统计看板和存储过程GROUP BY的正确打开方式统计页的核心是聚合查询。比如统计每个分类下的图书数量SELECT c.name AS category_name, COUNT(b.id) AS book_count FROM category c LEFT JOIN book b ON c.id b.category_id GROUP BY c.id, c.name;这里必须用LEFT JOIN而不是INNER JOIN因为要保证“一本书都没有的空分类”也要显示出来数量为0。这是一个很小的细节但能看出你对JOIN语义的理解是否扎实。当前借出数量就是SELECT COUNT(*) FROM borrow_record WHERE status 0逾期数量就是status 2。如果你想在统计页做成行列转换的报表MySQL里用SUM(CASE WHEN ... THEN 1 ELSE 0 END)也能实现比如把每月借阅量转成月份列表这是一个值得练手的进阶技巧。如果你对存储过程感兴趣我建议把还书逻辑抽成一个存储过程练手传入record_id内部开启事务、更新记录状态、回补库存一次调用完成三步。存储过程在传统的银行类系统里还在用面试被问到时要能说出“什么时候该用、什么时候不该用”比如跨多个事务的复杂业务不建议塞在存储过程里维护成本太高。6. 上线前必排的雷乱码、时区、驱动的血泪清单6.1 中文乱码的三个入口每个都要堵上中文乱码是JavaWeb里出现频率最高的坑而且往往是三个环节同时出问题。JSP页面顶部要写pageEncodingUTF-8Servlet接收POST参数前要调request.setCharacterEncoding(UTF-8)或者写一个Filter统一处理响应输出前要调response.setContentType(text/html;charsetUTF-8)。最典型的情况是页面显示正常但往数据库插入中文后变成问号这种大多数是连接URL少了characterEncodingutf8或者表字符集不是utf8mb4。排查顺序建议是先看表结构字符集再看连接URL最后看页面编码。按这个顺序基本一次能定位别一上来就改代码。6.2 驱动、时区、SSLMySQL 8.0换驱动引发的连锁问题MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver。类名写错启动直接ClassNotFoundException。换新驱动后不配时区会报The server time zone value...错误URL里加serverTimezoneAsia/Shanghai就解决。控制台出现SSL警告本地开发在URL里加useSSLfalse就会消失。还有一个隐蔽的allowPublicKeyRetrievaltrueMySQL 8.0默认caching_sha2_password认证不加会偶发Public Key Retrieval is not allowed加上就稳了。下面把常见运行期异常整理成一张速查表方便你对着排查异常现象大概率原因处理方式ClassNotFoundException: com.mysql.jdbc.Driver驱动类名写错或jar未放进lib改成com.mysql.cj.jdbc.Driver确认jar在WEB-INF/libThe server time zone value...未配置时区URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0认证方式问题URL加allowPublicKeyRetrievaltruejava.net.BindException: Address already in useTomcat 8080被占用netstat -ano | findstr 8080taskkill或改端口Unknown database数据库名不一致核对连接URL和CREATE DATABASE名称Duplicate entry for key唯一约束冲突例如isbn重复检查唯一索引字段6.3 Navicat连不上的排查顺序别一上来就怀疑密码Navicat连不上本地MySQL第一反应别是改密码。按这个顺序排查先看MySQL服务有没有启动Windows下services.msc里看服务状态再看3306端口有没有被防火墙拦截接着看bind-address是不是只绑了localhost而你要连的是远程最后登录MySQL执行SELECT user, host FROM mysql.user;看root的host权限。最后这条最坑本地明明能连远程就是不行通常就是host字段不允许对应IP访问改成root%授权即可。还有一个小经验项目刚搭好的时候每写一条核心SQL先在Workbench里跑一遍确认结果再往代码里贴。就这一步能帮你省下大半调试时间。很多人出错后反复看DAO代码其实问题根本不在Java而在SQL语句本身。先把SQL在客户端验证通过再排查Java层两个常见问题源就一下排掉了。如果你已经把这个系统跑通并部署到Tomcat你的JavaWeb入门项目就算正式毕业了。接下来想让简历上的项目更有分量可以从三个方向往上长一是给系统加登录鉴权和Session拦截区分管理员和普通读者的权限二是把前端升级成BootstrapAjax搜索改成输入即查三是把DAO换成MyBatis、Controller换成Spring Boot走一遍从原生JavaWeb到企业框架的升级路线。我个人写完这个项目的最大体会是“想清楚再动手”比“多写几行代码”重要得多。表结构没设计好后面所有SQL都要跟着改事务边界没想清楚借还书的数据早晚会乱分层没做好改需求时一个页面翻三天。把数据库设计、三层架构、事务这三件事先想明白后面的代码基本是水到渠成的事。本文还有配套的精品资源点击获取