简介这是一套基于Java与MySQL实现的图书管理系统完整项目代码清晰、结构规范适合Java Web初学者及课程设计者学习参考。系统围绕图书添加、查询、借阅、归还及用户管理等核心功能展开运用Servlet、JSP与MVC设计模式并通过JDBC连接MySQL数据库充分展示了后端业务处理与关系型数据库交互的完整链路。资源包共222个文件其中160个Java源码文件构成主要业务逻辑另含12个JAR依赖库、SQL脚本、配置文件及少量文档整体压缩包约8.58MB方便直接导入开发环境进行调试。目前已有1468人学习下载实用性和完成度得到验证。通过该项目可掌握数据库表设计、JDBC连接配置、MVC分层开发等实战要点并借助内置脚本快速初始化数据是一份难度适中、可运行性强的实践项目。1. 图书管理系统Java MySQL不是增删改查那么简单图书管理系统Java MySQL这个题目第一眼看是标准的增删改查管理员登录、录入图书、维护读者、借书还书。但真正动手做的人都知道前面这些功能撑不过三天后期时间全耗在“同一本书被两个人同时借走”“还书日期算错”“页面打开乱码一片”这类事情上。这个题目适合正在做课程设计、毕业设计的同学也适合给某学院图书室、公司内部资料室搭一套能用的内部系统。我按自己实战的做法把这条链路串起来技术栈怎么选、六张表怎么定、SSM 工程怎么起、借还书事务怎么写、部署后常见问题怎么查、上线前做哪几个增强。看完你能照着搭出一套能跑的系统也知道翻车时往哪排查。这套做法不依赖某份现成源码单台服务器加单库单表足够可靠文中的项目统一叫模拟项目X代码里用 library 作为包名方便替换成自己的工程名。2. 技术选型与数据库设计表结构决定了后面改代码的代价2.1 SSM、Spring Boot 还是 Servlet JSP先想清楚给谁看先回答最常被问的这个题目用不用框架常见做法是课程设计和答辩场景用 SSMSpring SpringMVC MyBatis最稳妥因为每一层都能单独讲清楚老师或面试官问“请求怎么进来的、SQL 从哪来”时都能指着代码说。如果目标是快速把系统跑起来Spring Boot 加 MyBatis-Plus 更省事但原理不熟的人会被自动配置这个黑匣子卡住排起错来反而更慢。Servlet JSP 是另一个可控的备选适合只做简单录入和查询、且不允许用框架的场合。我的建议分两条追求“做出来且能讲”就选 SSM追求“上线最快”就选 Spring Boot。下面的内容按 SSM 讲原因很简单网上已有的源码包、模板和排错资料SSM 比例最高你拿到别人的模拟项目X也好自己从零写也好都更容易参考。这里不把 JPA 列为备选是因为 MyBatis 对这个题更贴合图书列表、借阅流水都是偏查询的场景SQL 自己控制更直观尤其在分页和统计报表上MyBatis 不用处理实体关系映射带来的性能问题。后面所有 Mapper XML 也都是 MyBatis 风格。选型定下来之后表结构设计才谈得上“为后面的事兜底”。2.2 从借书还书的数据流反推六张核心表表结构是整个系统改动成本最高的部分。常见错误是一上来只建两张表图书和读者借阅记录用“图书表里加一个是否借出字段”代替结果查不了历史、统计不了超期最后只能重写。我做这个题目时习惯先画一条数据流管理员录书 → 读者查书 → 借书 → 生成借阅记录 → 还书 → 更新图书和借阅记录状态。顺着这条流最少需要六张表。下面是核心的建表 SQL先看图书表和借阅表这两张最关键。CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT COMMENT 图书id, isbn varchar(32) NOT NULL COMMENT ISBN号, title varchar(128) NOT NULL COMMENT 书名, author varchar(64) NOT NULL COMMENT 作者, category_id bigint DEFAULT NULL COMMENT 分类id, status tinyint NOT NULL DEFAULT 0 COMMENT 0在馆 1借出 2预约中, location varchar(64) DEFAULT NULL COMMENT 馆藏位置, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE borrow ( id bigint NOT NULL AUTO_INCREMENT COMMENT 流水id, book_id bigint NOT NULL COMMENT 图书id, reader_id bigint NOT NULL COMMENT 读者id, borrow_time datetime NOT NULL COMMENT 借出时间, due_time datetime NOT NULL COMMENT 应还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, fine decimal(10,2) DEFAULT 0.00 COMMENT 超期罚金, status tinyint NOT NULL DEFAULT 0 COMMENT 0借阅中 1已还 2超期未还, PRIMARY KEY (id), KEY idx_book (book_id), KEY idx_reader (reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅表;book 表的 status 用 tinyint 而不是布尔值是因为后面要加“预约中”这个第三态布尔字段改起来要动索引和所有代码。borrow 表是借阅流水一个读者可以有多条记录图书表不记录“当前借给谁”这就是不做成“图书表加两个字段”的原因并发场景下后借的人会覆盖前一个人的信息历史就断了。其余四张表字段相对简单category 是分类表id、name、sortreader 是读者表id、reader_no 唯一、name、phone、可借数量admin 是管理员表id、username 唯一、password 加密存储reserve 是预约表第 4 章讲预约流程时再展开。注意 password 不要明文存用 BCrypt 或加盐的 SHA-256 都行这是很多源码包密码泄露的直接原因。2.3 借阅状态机借阅表里的 status 是给谁看的借阅表里的 status 字段经常被忽略常见做法是还完书直接把记录删掉。删记录意味着所有历史统计比如“谁的书超期最多”“哪些书借阅次数高”全部丢失。这里约定一个简单的状态机0 表示借阅中1 表示已归还2 表示超期未还。还书时判断 return_time 和 due_time 的关系如果当前时间已经超过 due_time查询时把状态置为 2真正归还后置为 1同时计算罚金。这个状态字段的最大意义是让统计查询变简单select count(*) from borrow where status 2 就是当前超期未还的人数。如果没有这个字段每次统计都要比较两个时间字段索引用不上数据量上来后查询会明显变慢。图书表的状态则负责“这本书现在能不能借”0 在馆可借1 已借出2 预约中。注意预约中的书实际还在上一个读者手里只是被“预定了”所以在 4.3 里会把预约和借出拆成两步操作。两张表的状态不要互相替代否则边界一混业务流程就说不清了。3. 搭起 SSM 工程配置、分层和第一个分页查询3.1 Maven 依赖与四层分包先把骨架立住用 Maven 管理依赖IDE 里建一个普通的 war 工程。关键依赖就这几样spring-webmvc、spring-jdbc、spring-tx、mybatis、mybatis-spring、mysql-connector-java、druid、pagehelper。不要一开始引一堆全家桶依赖多了新手排错非常痛苦。以下是 pom.xml 的关键片段。properties spring.version5.3.27/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.3/version /dependency /dependenciesmybatis 和 mybatis-spring 两个都要加前者是 ORM 核心后者负责把 SqlSessionFactory 交给 Spring 管理。mysql-connector-java 用 8.x因为 MySQL 8 的默认认证插件是 caching_sha2_password旧驱动连不上本地即使装 MySQL 5.78.x 驱动也向下兼容。spring-jdbc 和 spring-tx 是 MyBatis-Spring 和事务注解的底层依赖少了会在启动时出现 BeanCreationException。分层按四层走controller 负责接收参数和返回结果service 处理事务和业务规则mapper 接口加 mapper XML 访问数据库entity 对应表字段。别把 SQL 写在 controller 里也别在 service 里拼原生 JDBC这两点是课程设计答辩时的重灾区。3.2 第一个分页查询从 Mapper 到 Controller 的最小闭环先跑通“图书列表分页”后面每个功能都照这个套路。分页用 PageHelper它通过 MyBatis 拦截器自动拼接 limit避免手写 limit (pageNum-1)*pageSize。Mapper 接口和 XML 如下。public interface BookMapper { ListBook pageBooks(String title); }select idpageBooks resultTypecom.library.entity.Book select id, isbn, title, author, status from book where if testtitle ! null and title ! and title like concat(%, #{title}, %) /if /where order by id desc /selectService 和 ControllerService public class BookService { Autowired private BookMapper bookMapper; public PageInfoBook page(int pageNum, int pageSize, String title) { PageHelper.startPage(pageNum, pageSize); ListBook list bookMapper.pageBooks(title); return new PageInfo(list); } } RestController RequestMapping(/book) public class BookController { Autowired private BookService bookService; GetMapping(/list) public PageInfoBook list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String title) { return bookService.page(pageNum, pageSize, title); } }逻辑说明PageHelper.startPage 必须在执行查询 SQL 之前调用PageInfo 里封装了 total、pages、list 等字段前端翻页直接取。条件查询用 where 加 iftitle 为空时跳过这个条件参数从 #{title} 传入而不是拼字符串防止 SQL 注入。pageSize 建议在 Service 里做一层限制超过 100 就按 100 算防止前端传一个超大值把数据库拖垮。配置齐全后启动 Tomcat访问 http://localhost:8080/library/book/list?pageNum1pageSize10能返回 JSON 就说明闭环通了。如果返回 406 或空白通常是少了 jackson-databind 依赖或者 spring-mvc.xml 里没有启用 mvc:annotation-driven加上即可。注意PageHelper.startPage 只对紧接的下一条查询生效调用一次就失效不需要手动 clearPage同一方法里如果插入了别的 SQL分页就可能失效。3.3 Druid 连接池和 PageHelper 的两个必调参数数据源配置一般写在 applicationContext.xml 里核心参数看下面这段配置。bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property nameurl valuejdbc:mysql://localhost:3306/library?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ property nameinitialSize value2/ property namemaxActive value20/ property namemaxWait value10000/ property namevalidationQuery valueSELECT 1/ property nametestWhileIdle valuetrue/ /beaninitialSize 是启动时初始连接数maxActive 是最大连接数maxWait 是拿不到连接时的等待毫秒数。maxActive 不要贪大单库场景下设到几百只会堆积空闲连接拖慢建连和释放20 对一个图书管理系统绰绰有余。validationQuery 配合 testWhileIdle让连接池定期检测空闲连接是否还可用否则 MySQL 默认 8 小时断开空闲连接后第二天第一次访问会报连接超时。PageHelper 的参数也要调见 SqlSessionFactoryBean 的 plugins 配置bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties value helperDialectmysql reasonabletrue /value /property /bean /array /property /beanhelperDialect 必须写成 mysqlPageHelper 按它选择分页方言reasonabletrue 表示页码小于 1 时自动转成 1大于最大页时自动转成最后一页避免前端翻出空白页。这两个参数在网上的源码包里经常被删掉结果换数据库方言时分页语法报错或者页码越界直接查到空列表。4. 借书与还书的核心业务事务、行锁和超期费用4.1 借书接口怎么防止同一本书被两个人同时借走借书的代码写起来简单查出书的状态判断可借updateinsert 一条借阅记录。但这三步之间有个时间窗口两个请求同时查到的都是可借然后都去 update最后出现一本书被借了两次。常见做法是在 service 方法上加 Transactional然后把 update 语句变成条件更新用“受影响行数等于 1”代替状态判断。下面是借书的核心代码。Transactional(rollbackFor Exception.class) public boolean borrow(Long bookId, Long readerId) { Book book bookMapper.selectByIdForUpdate(bookId); if (book null || book.getStatus() ! 0) { return false; } int updated bookMapper.updateStatus(bookId, 0, 1); if (updated ! 1) { throw new IllegalStateException(这本书刚被别人借走); } LocalDateTime now LocalDateTime.now(); borrowMapper.insert(new Borrow(bookId, readerId, now, now.plusDays(30))); return true; }配套 XMLselect idselectByIdForUpdate resultTypecom.library.entity.Book select id, status from book where id #{id} for update /select update idupdateStatus update book set status #{toStatus} where id #{bookId} and status #{fromStatus} /update逻辑说明select ... for update 先在这条 book 记录上加行锁后面的事务必须等它提交才能继续updateStatus 里的 and status #{fromStatus} 是第二道保险即使有人绕过锁直接改表也只会影响 0 行。两步都在同一个事务里事务提交后锁才释放。这里有个血泪经验加锁顺序不一致是死锁主因批量借书时必须按 book_id 排序后逐本加锁不能按读者传入的列表顺序加。提示Transactional 只对 public 方法生效如果借书方法在同一个类内部被另一个方法调用事务会失效。这是“加了注解还是超借”的最常见原因。4.2 还书接口超期天数、罚金和状态同步还书时要做三件事更新借阅记录、把书的状态改回 0、如果有超期则计算罚金。最常见的问题是超期天数用毫秒差值除以 86400000得到 2.5 天这种数罚金没法跟用户解释。正确做法是只算日期忽略时分秒用 LocalDate 计算。Transactional(rollbackFor Exception.class) public boolean returnBook(Long borrowId) { Borrow borrow borrowMapper.selectById(borrowId); if (borrow null || borrow.getStatus() 1) { return false; } LocalDate due borrow.getDueTime().toLocalDate(); LocalDate now LocalDate.now(); BigDecimal fine BigDecimal.ZERO; if (now.isAfter(due)) { long days ChronoUnit.DAYS.between(due, now); fine BigDecimal.valueOf(days).multiply(FINE_PER_DAY); } borrowMapper.returnBook(borrowId, new Date(), fine); bookMapper.updateStatus(borrow.getBookId(), 1, 0); return true; }参数说明FINE_PER_DAY 建议做成常量或数据库配置比如 0.5 元一天别写死在 SQL 里。归还后 book 表状态从 1 改回 0这条更新放在借阅记录写入之后保证“记录已还、状态已改”要么都成功要么都失败。罚金不用 double是因为 0.1 元乘天数会出现 0.30000000000000004 这样的数账面没法解释。超期状态不需要定时任务去批量更新还书时实时算就好。只有当系统需要给超期读者发提醒时再引入一个每天凌晨跑的定时任务把超期记录的状态统一标记为 2然后按状态发消息。4.3 预约第三态一本书被预约时状态怎么流转预约功能不是必选但“多人想借同一本热门书”几乎是答辩必问题。核心是 book.status 的三态和 reserve 表配合状态机这样走新书入库 status0。被借出时 status1借阅记录 status0。另一个读者发起预约只往 reserve 表插一条记录不动 book.status因为书还在别人手里。原读者还书时先查 reserve 有没有排队的预约有则把 book.status 改为 2 表示“已被预约、等人来借”没有则直接改为 0。预约人来借书时把 reserve 记录置为已完成book.status 从 2 改为 1借阅记录照常生成。-- 还书时判断是否有人预约 select reader_id from reserve where book_id #{bookId} and status 0 order by create_time limit 1; -- 状态流转 update book set status 2 where id #{bookId} and status 1;这么设计的理由如果预约状态直接画在 book 表上会出现“预约中”和“借出”语义混淆比如书在馆但被预约此时到底能不能被其他人直接借三态把边界切清楚了status2 表示这本书不能从书架直接借出只能由预约人办理。reserve 表记得加唯一约束保证同一本书只能有一条活跃预约防止一本书被多个人同时排队。5. 常见问题排查乱码、部署、分页与空指针集中营5.1 MySQL 中文乱码不是把数据库改成 utf8 就完事现象插入中文书名后数据库客户端显示正常网页上打开是问号或者反过来数据库里是 ??页面正常。原因通常是三处不一致建库字符集、JDBC 连接参数、Tomcat 对 URL 参数的编码。常见做法是一处都不放过。建库时指定create database library default charset utf8mb4;JDBC URL 带参数jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiTomcat 的 server.xml 里给 Connector 加 URIEncodingUTF-8解决地址栏传中文参数乱码还有一个容易忽略的点MySQL 驱动收到 characterEncodingutf8 时会自动按 utf8mb4 处理所以连接串写 utf8 没毛病但建库如果用了 latin1后续字段字符集要逐列改。检查方法很简单执行 show create table book看 DEFAULT CHARSET 是什么。改完连接参数必须重启 Tomcat连接池里已经缓存了旧连接只改配置文件不重启页面继续乱码。这是“我改了怎么没用”的最常见原因排错先想这个。5.2 war 包部署到 Tomcat 后 404 或 ClassNotFoundException现象本地 IDEA 启动一点问题没有打成 war 放到 Tomcat 后访问界面 404或者日志里报 java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener。原因war 包里的 WEB-INF/lib 没有依赖 jar。IDEA 的 Artifacts 打包默认有时候不把 Maven 依赖带进去或者把 provided 作用域的 jar 当成了运行依赖。解决方式很直接用 Maven 打包而不是 IDE 的 Artifacts。mvn clean package -DskipTests打完打开 target 目录下的 war确认 WEB-INF/lib 里有 druid、mybatis、spring 等 jar数量不齐就检查 maven-war-plugin 配置和依赖 scope。另一个隐蔽原因是两个不同版本的 servlet-api 同时进了 libTomcat 自己也带一份类加载器先加载到旧版导致方法签名对不上。处理方式是把 pom 里 servlet-api、tomcat-api 这类依赖的 scope 改为 provided。一条教训部署前先在干净的 Tomcat 上跑一次不要拿装了半辈子插件的开发 Tomcat 验证 war。开发环境能跑不代表部署环境能跑带着“我以为没问题”的想法上线排查成本是最高的。5.3 分页总数不对PageHelper 的自动 count 遇到 group by现象列表翻页正常但 PageInfo 里的 total 和实际记录数对不上或者查询带 group by 后 total 变成了分组数而不是记录数。原因PageHelper 默认会拦截查询自动包一层 select count(*) from (...)当 SQL 里有 distinct、group by 时这个 count 统计的是分组后的行数甚至可能因为子查询字段不完整直接报错。常见做法是不要依赖自动 count自己写一个符合业务口径的统计查询。我习惯先查总数再分页查列表两次查询的条件必须完全一致。long total bookMapper.countBooks(title); PageHelper.startPage(pageNum, pageSize); ListBook list bookMapper.pageBooks(title); PageInfoBook page new PageInfo(list); page.setTotal(total);countBooks 写一条 select count(*) from book where title like concat(%, #{title}, %)条件里和 pageBooks 保持一致。这样即使列表 SQL 里带了 group by总数也能按业务口径手动控制不会翻车。如果坚持用自动 count至少保证 PageHelper 拦截到的是一条不包含分组聚合的简单查询。5.4 数据库连不上驱动类名、时区和 SSL 警告三连现象启动时报 Communications link failure、Unable to load authentication plugin caching_sha2_password或控制台刷一堆 SSL 警告后连接超时。原因不同 MySQL 版本驱动类名不一样。MySQL 8 的驱动类名是 com.mysql.cj.jdbc.Driver5.x 是 com.mysql.jdbc.Driver数据库 8.0 默认认证插件 caching_sha2_password旧驱动不认识连接参数缺少 serverTimezone 时驱动和服务器时区不一致报 CST 相关异常。解决驱动类名换成新的URL 加上 serverTimezoneAsia/Shanghai本地调试不需要加密连接时可以加 useSSLfalse减少握手耗时。jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse注意 useSSLfalse 只建议本地调试用。部署到有安全要求的网络环境时要先解决证书问题而不是关闭校验。这个参数在论坛里被到处复制很多人不加思考就照搬把数据直接暴露在网络上。另一个关联问题是连接池老化MySQL 默认 8 小时断开空闲连接Druid 如果不做空闲检测第二天第一次访问就会报连接超时所以 3.3 里 validationQuery 和 testWhileIdle 要配。5.5 空指针查询结果没判空就直接取字段现象接口偶发 java.lang.NullPointerException集中在按 id 查详情、按编号查读者这类接口。原因很直接MyBatis 的 select 查不到记录时返回 null代码里没判空就直接 book.getStatus()。解决方式是在入口处做统一判断查不到就直接抛业务异常让全局异常处理器把“记录不存在”返回给前端而不是把堆栈打给用户。Book book bookMapper.selectById(bookId); if (book null) { throw new NotFoundException(图书不存在); }这个习惯比写一堆 if (xx ! null) 的嵌套更干净。空指针不是玄学它就是“你假设了数据一定存在但现实没有”。凡是按主键查询的接口都默认查不到是常态反而能少踩很多坑。6. 图书管理系统上线前三个值得做的增强与自测习惯课程设计做到能增删改查很容易但答辩和实际用起来是两回事。这一章说三个性价比高的增强导出报表、热门图书榜、交付前自测。不追求大架构只补实用缺口。6.1 借阅报表导出 Excel把历史数据变成可用数据导出借阅记录是图书管理系统里最常被要求加的功能也最能让系统从“作业”变成“工具”。用 EasyExcel 实现先定义一个带 ExcelProperty 注解的 VO再一行代码写文件。ExcelProperty(书名) private String title; EasyExcel.write(fileName, BorrowExcelVO.class) .sheet(借阅记录) .doWrite(list);导出大数据量时用异步下载别在请求线程里一次性加载全表。几百条看不出问题几万条会把请求拖到超时。导出前加一个时间范围参数比全量导出更符合实际使用。6.2 Redis 缓存热门图书榜只加一个读写点也能用如果只是做热门榜不必专门搞定时任务。借书接口里顺手把 bookId 的计数加一页面查榜单时直接从 Redis 取 top 10每天凌晨把计数落库归档。这个方案的一致性要求不高Redis 丢了数据第二天重新累计也能接受。stringRedisTemplate.opsForValue().increment(hot:book: bookId);用 StringRedisTemplate 的 increment 加计数reverseRangeWithScores 取榜单不要引分布式锁等重型设计。图书管理系统还没到需要那层的规模简单方案反而最好维护。6.3 交付前自测清单先把自己当用户走一遍我交付前的习惯是不只看接口通不通而是把用户会做的操作从头到尾走一遍新读者注册、登录、录入一本中文书名的图书、搜索、借出、还书、超期一天再还、并发借同一本书、部署到干净 Tomcat 后页面访问。这八步里只要有一步断就说明不是某个接口的问题而是流程没串起来。这个自测在我自己做的模拟项目X上抓出过不少问题比如还书后状态没更新、罚金重复计算。做完这轮再交付被退回的概率会低很多。希望帮到你。本文还有配套的精品资源点击获取