简介:本资源是一套基于SpringBoot开发的轻量级图书管理系统,面向Java Web初学者与高校课程设计学生,解决图书馆场景下学生借阅与管理员多维度管理的实际需求。系统采用前后端分离架构,包含学生端(借阅统计、图书浏览、个人信息展示)与管理员端(学院/书屋/学生/图书/借阅全生命周期管理),代码结构清晰,适合SpringBoot入门实践与项目实训。压缩包为ZIP格式,大小2.8MB,含完整可运行源码及配置文件,虽未提供具体文件明细,但根据功能描述可推知包含Controller、Service、Mapper层核心逻辑及Thymeleaf前端页面,便于快速部署与二次开发。目前已有1187人学习下载,读者可直接导入IDE运行,获取完整业务流程实现、权限角色划分示例及典型CRUD模块代码,是理解SpringBoot整合MyBatis、Thymeleaf与基础RBAC权限控制的理想教学案例。
1. 为什么用 Spring Boot 做图书管理系统,不是“又一个 demo”,而是真实业务落地的最小可行路径
很多刚接触 Spring Boot 的开发者看到“图书管理系统”第一反应是:这不就是课程设计、毕设模板?但实际在中小型出版社、高校图书馆技术部、甚至社区阅读空间的数字化改造中,它恰恰是最常被选中的第一个生产级 Web 应用——不是因为简单,而是因为 Spring Boot 把「图书 CRUD + 用户权限 + 借阅流程」这个经典业务模型的工程复杂度压到了临界点以下。它天然规避了传统 SSM(Spring+SpringMVC+MyBatis)里 XML 配置爆炸、事务边界模糊、日志链路断裂等老问题;同时又不像 Spring Cloud 那样引入服务发现、熔断、网关等冗余层。真正关键的是:你能用 3 个注解(@RestController、@Service、@Transactional)就串起借书校验、库存扣减、操作日志写入三件事,且每一步都可单元测试、可监控、可灰度发布。适合 Java 初级工程师快速交付可维护系统,也适合资深工程师用它验证新组件(如 MyBatis-Plus 分页插件、Redis 缓存策略、JWT 权限模型)在真实业务流中的表现。这不是练手玩具,而是你简历里“独立负责后端模块”的起点。
2. 从零搭建图书管理系统的 Spring Boot 工程骨架:依赖选型与结构约定
2.1 核心依赖选择逻辑:为什么不是“全选 Starter”
新建项目时,IDEA 或 Spring Initializr 默认勾选spring-boot-starter-web、spring-boot-starter-data-jpa等,但图书管理系统的真实需求决定了必须做减法和加法:
- 必须移除:
spring-boot-starter-data-jpa—— 图书数据涉及多表关联(图书-分类-作者-出版社)、模糊检索(书名/作者/ISBN 混合搜索)、分页性能敏感(首页加载 20 条 vs 后台导出 10 万条),JPA 的 N+1 查询和复杂 SQL 生成反而成为瓶颈; - 必须替换为:
mybatis-spring-boot-starter+mybatis-plus-boot-starter—— MyBatis-Plus 提供的LambdaQueryWrapper能写出类型安全的动态条件(如eq(Book::getIsbn, isbn).like(Book::getTitle, keyword)),其内置分页插件支持 MySQL/Oracle/PostgreSQL 多种方言,且@TableName("t_book")注解直接绑定物理表,避免 JPA 的@Entity映射歧义; - 必须新增:
spring-boot-starter-validation—— 图书 ISBN 校验(13 位数字+校验码)、借阅天数范围(1~30)、用户手机号格式(@Pattern(regexp = "^1[3-9]\\d{9}$"))必须在 Controller 层拦截,而非等入库时触发数据库异常; - 可选但强烈建议:
spring-boot-starter-cache+spring-boot-starter-data-redis—— 图书详情页高频访问(如《深入理解 Java 虚拟机》详情被查看 500 次/小时),用 Redis 缓存book:1001的 JSON 结构,QPS 提升 4 倍以上,且@Cacheable(key = "#isbn")一行注解即可生效。
提示:不要盲目升级到 Spring Boot 3.x。当前主流生产环境(尤其高校、政府类项目)仍以 JDK 8 + Spring Boot 2.7.x 为主。若用 JDK 17,选 2.7.18(最后兼容 JDK 8 的 2.x 版本);若用 JDK 21,才需 Spring Boot 3.2.x。版本错配会导致
javax.*包找不到、@ConfigurationProperties绑定失败等编译期错误。
2.2 项目结构分层:按业务域而非技术层切分
传统 MVC 分层(controller/service/dao)易导致 Service 层膨胀。图书管理系统应按业务域划分包结构:
com.example.library ├── book // 图书核心域:BookController、BookService、BookMapper、BookDTO ├── borrow // 借阅域:BorrowController、BorrowService(含库存校验、逾期计算) ├── user // 用户域:UserController、UserService(密码加密用 BCryptPasswordEncoder) ├── common // 公共模块:exception(全局异常处理器)、config(MyBatis-Plus 分页配置)、util(ISBN 校验工具类) └── LibraryApplication.java关键约束:
BookService只处理图书本身逻辑(增删改查、分类统计),不包含借阅规则;BorrowService负责借阅全流程:检查用户状态(是否欠费)、校验图书库存(SELECT stock FROM t_book WHERE id = ? FOR UPDATE)、生成借阅记录、更新库存(UPDATE t_book SET stock = stock - 1 WHERE id = ? AND stock > 0);- 所有 DTO(Data Transfer Object)必须与 VO(View Object)分离:
BookDTO用于接收前端参数(含@NotBlank校验),BookVO用于返回前端(剔除password、create_time等敏感字段)。
2.3 最小可运行配置:application.yml 关键参数解析
spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # MyBatis-Plus 配置 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印 SQL global-config: db-config: id-type: assign_id # 使用雪花算法生成 ID,避免数据库自增主键暴露数量 logic-delete-field: deleted # 逻辑删除字段名 logic-delete-value: 1 logic-not-delete-value: 0 # Redis 缓存配置 spring: redis: host: localhost port: 6379 database: 0 timeout: 5000 lettuce: pool: max-active: 20 max-idle: 10 min-idle: 0 max-wait: 3000 # 日志级别控制 logging: level: com.example.library: debug org.springframework.web.servlet.DispatcherServlet: warn参数说明:
serverTimezone=Asia/Shanghai必填:MySQL 8.0+ 默认时区为 UTC,不设置会导致java.time.LocalDateTime存储时间偏移 8 小时;id-type: assign_id是关键:图书 ID 若用AUTO_INCREMENT,攻击者可通过/book/12345推测总藏书量;雪花算法生成的 19 位 Long 型 ID(如1823456789012345678)无规律且全局唯一;logic-delete-field启用逻辑删除:删除图书时执行UPDATE t_book SET deleted = 1 WHERE id = ?,后续查询自动追加AND deleted = 0条件,避免误删数据无法追溯。
3. 图书核心业务实现:从 ISBN 校验到库存并发控制的代码落地
3.1 图书新增接口:三层校验链确保数据质量
图书新增需同时满足格式、业务、数据库三重校验:
@RestController @RequestMapping("/api/book") public class BookController { @PostMapping public Result<String> addBook(@Valid @RequestBody BookDTO dto) { // 第一层:DTO 注解校验(ISBN 格式、书名非空、价格正数) // 第二层:Service 业务校验(ISBN 是否已存在、分类是否存在) boolean saved = bookService.saveBook(dto); return saved ? Result.success("添加成功") : Result.fail("ISBN 已存在"); } }BookDTO定义:
public class BookDTO { @NotBlank(message = "ISBN 不能为空") @Pattern(regexp = "^978\\d{10}|979\\d{10}$", message = "ISBN 格式错误(需 13 位数字,以 978 或 979 开头)") private String isbn; @NotBlank(message = "书名不能为空") @Size(max = 100, message = "书名长度不能超过 100 字符") private String title; @Min(value = 0, message = "价格不能为负数") private BigDecimal price; @NotNull(message = "分类 ID 必须指定") private Long categoryId; // getter/setter... }BookService.saveBook()实现:
@Service public class BookServiceImpl implements BookService { @Override @Transactional(rollbackFor = Exception.class) public boolean saveBook(BookDTO dto) { // 校验分类是否存在(防止伪造 categoryId) Category category = categoryMapper.selectById(dto.getCategoryId()); if (category == null) { throw new BusinessException("分类不存在"); } // 校验 ISBN 是否重复(数据库唯一索引 + 业务层双重保险) QueryWrapper<Book> wrapper = new QueryWrapper<>(); wrapper.eq("isbn", dto.getIsbn()).eq("deleted", 0); long count = bookMapper.selectCount(wrapper); if (count > 0) { return false; // 返回 false 由 Controller 统一处理 } // 构建实体并保存 Book book = new Book(); book.setIsbn(dto.getIsbn()); book.setTitle(dto.getTitle()); book.setPrice(dto.getPrice()); book.setCategoryId(dto.getCategoryId()); book.setStock(dto.getStock()); // 库存默认为 0,需人工录入 return bookMapper.insert(book) > 0; } }注意:
@Valid触发 DTO 校验,@Transactional确保新增图书和后续可能的关联操作(如插入作者关系表)原子性。若只校验不抛异常,需在 Controller 中显式调用BindingResult.hasErrors(),否则校验失败会直接返回 400 错误。
3.2 图书借阅接口:基于数据库行锁的库存扣减
借阅操作本质是“库存减 1 + 新增借阅记录”,必须防止超卖(如库存为 1,两个用户同时点击借阅):
@Service public class BorrowService { @Transactional(rollbackFor = Exception.class) public boolean borrowBook(Long userId, Long bookId) { // 1. 查询图书库存(FOR UPDATE 锁住该行) Book book = bookMapper.selectOne( new QueryWrapper<Book>().eq("id", bookId).select("id", "stock", "title") ); if (book == null || book.getStock() <= 0) { throw new BusinessException("图书不可借阅"); } // 2. 扣减库存(UPDATE 语句自带行锁,无需额外 synchronized) int updated = bookMapper.update(null, new UpdateWrapper<Book>() .eq("id", bookId) .gt("stock", 0) // 防止库存为负数 .setSql("stock = stock - 1") ); if (updated == 0) { throw new BusinessException("库存不足,请稍后再试"); } // 3. 创建借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowDate(LocalDateTime.now()); record.setDueDate(LocalDateTime.now().plusDays(30)); // 默认借阅 30 天 record.setStatus(BorrowStatus.BORROWED.getCode()); borrowRecordMapper.insert(record); return true; } }关键点解析:
SELECT ... FOR UPDATE在 InnoDB 中对bookId对应的行加写锁,其他事务读取同一行会被阻塞,直到当前事务提交或回滚;UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock > 0的AND stock > 0条件是二次保险:即使锁失效(极罕见),也能避免库存变为负数;- 不使用
synchronized方法锁:它只在单 JVM 进程内有效,集群部署时无效;数据库行锁是跨进程、跨机器的终极方案。
3.3 图书列表分页:MyBatis-Plus 分页插件实战
首页图书列表需支持按分类、关键词、价格区间筛选,并保证性能:
// Controller @GetMapping("/list") public Result<Page<BookVO>> listBooks( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { Page<BookVO> page = bookService.listBooks(current, size, categoryId, keyword); return Result.success(page); } // Service 实现 @Override public Page<BookVO> listBooks(Integer current, Integer size, Long categoryId, String keyword) { Page<Book> page = new Page<>(current, size); QueryWrapper<Book> wrapper = new QueryWrapper<>(); if (categoryId != null && categoryId > 0) { wrapper.eq("category_id", categoryId); } if (StringUtils.isNotBlank(keyword)) { wrapper.and(qw -> qw.like("title", keyword) .or().like("author", keyword) .or().eq("isbn", keyword)); } wrapper.eq("deleted", 0); // 逻辑删除过滤 // MyBatis-Plus 自动将 Page 对象注入到 SQL 的 LIMIT 子句 IPage<Book> iPage = bookMapper.selectPage(page, wrapper); // 转换为 VO(去除敏感字段) List<BookVO> voList = iPage.getRecords().stream() .map(book -> { BookVO vo = new BookVO(); vo.setId(book.getId()); vo.setTitle(book.getTitle()); vo.setAuthor(book.getAuthor()); vo.setPrice(book.getPrice()); vo.setStock(book.getStock()); return vo; }) .collect(Collectors.toList()); return new Page<>(iPage.getCurrent(), iPage.getSize(), iPage.getTotal()) .setRecords(voList); }性能优化点:
wrapper.and(...)构建 OR 条件时,MyBatis-Plus 会生成(title LIKE ? OR author LIKE ? OR isbn = ?),避免多次查询;- 分页插件在
selectPage时自动追加LIMIT ?, ?,且iPage.getTotal()会执行SELECT COUNT(*)获取总条数,无需手动写两条 SQL; - 若数据量超百万,
COUNT(*)可能变慢,此时应启用 MyBatis-Plus 的count优化:在application.yml中配置mybatis-plus.configuration.map-underscore-to-camel-case=true,并确保数据库表字段为下划线命名(如book_title),避免反射开销。
4. 图书管理系统进阶技巧:缓存穿透防护与借阅超时自动归还
4.1 Redis 缓存穿透防护:布隆过滤器拦截无效 ISBN 查询
当恶意请求大量查询不存在的 ISBN(如9780000000000)时,缓存未命中 → 查数据库 → 数据库未命中 → 不写缓存 → 下次继续穿透。解决方案是布隆过滤器(Bloom Filter)预判:
@Component public class BloomFilterUtil { // 使用 RedisBloom 的 TopK 模块(需 Redis 服务器安装 RedisBloom 模块) private final RedisTemplate<String, Object> redisTemplate; public BloomFilterUtil(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } // 初始化布隆过滤器(应用启动时加载所有 ISBN) @PostConstruct public void initBloomFilter() { // 从数据库批量读取所有 ISBN List<String> allIsbns = bookMapper.selectAllIsbns(); // 自定义 Mapper 方法 // 使用 RedisBloom 的 BF.ADD 命令逐个添加 for (String isbn : allIsbns) { redisTemplate.opsForValue().getOperations().execute( "BF.ADD", "book:isbn:bloom", isbn ); } } // 查询前先判断 ISBN 是否可能存在 public boolean mightExist(String isbn) { Boolean result = (Boolean) redisTemplate.opsForValue().getOperations().execute( "BF.EXISTS", "book:isbn:bloom", isbn ); return result != null && result; } } // Controller 中使用 @GetMapping("/detail/{isbn}") public Result<BookVO> getBookByIsbn(@PathVariable String isbn) { // 1. 布隆过滤器快速拦截 if (!bloomFilterUtil.mightExist(isbn)) { return Result.fail("图书不存在"); } // 2. 查询缓存 String cacheKey = "book:isbn:" + isbn; BookVO cached = (BookVO) redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return Result.success(cached); } // 3. 查询数据库 Book book = bookMapper.selectOne(new QueryWrapper<Book>().eq("isbn", isbn).eq("deleted", 0)); if (book == null) { return Result.fail("图书不存在"); } // 4. 写入缓存(设置过期时间,避免脏数据) BookVO vo = convertToVo(book); redisTemplate.opsForValue().set(cacheKey, vo, 30, TimeUnit.MINUTES); return Result.success(vo); }提示:布隆过滤器有误判率(可能把不存在的 ISBN 判为存在),但不会漏判。因此
mightExist=false一定不存在,mightExist=true需继续查缓存/数据库。误判率可通过调整布隆过滤器大小控制,默认 0.01%。
4.2 借阅超时自动归还:Spring Boot 定时任务精准触发
借阅到期后需自动归还(更新BorrowRecord.status为RETURNED,并增加book.stock),且需精确到分钟级:
@Component public class BorrowScheduler { @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次(避免秒级任务压力过大) public void autoReturnOverdueBooks() { // 查询所有状态为 BORROWED 且 due_date < now 的记录 LocalDateTime now = LocalDateTime.now(); QueryWrapper<BorrowRecord> wrapper = new QueryWrapper<>(); wrapper.eq("status", BorrowStatus.BORROWED.getCode()) .lt("due_date", now); List<BorrowRecord> overdueRecords = borrowRecordMapper.selectList(wrapper); if (overdueRecords.isEmpty()) { return; } // 批量更新借阅记录状态 List<Long> recordIds = overdueRecords.stream() .map(BorrowRecord::getId) .collect(Collectors.toList()); UpdateWrapper<BorrowRecord> updateWrapper = new UpdateWrapper<>(); updateWrapper.in("id", recordIds) .set("status", BorrowStatus.RETURNED.getCode()) .set("return_date", now); borrowRecordMapper.update(null, updateWrapper); // 批量更新图书库存 List<Long> bookIds = overdueRecords.stream() .map(BorrowRecord::getBookId) .collect(Collectors.toList()); // 使用 MyBatis-Plus 的批量更新语法 UpdateWrapper<Book> bookUpdateWrapper = new UpdateWrapper<>(); bookUpdateWrapper.in("id", bookIds) .setSql("stock = stock + 1"); bookMapper.update(null, bookUpdateWrapper); log.info("自动归还 {} 本超期图书", overdueRecords.size()); } }关键配置:
@EnableScheduling必须在启动类上添加,否则@Scheduled不生效;- Cron 表达式
"0 0 * * * ?"表示每小时 0 分 0 秒执行,比"0 */1 * * * ?"(每分钟)更合理,避免高并发场景下定时任务堆积; - 使用
in批量操作而非循环单条更新,减少数据库连接次数; - 更新库存时用
setSql("stock = stock + 1")而非set("stock", stock + 1),避免先查后改的并发问题。
4.3 生产环境必备:Actuator 监控与 Banner 定制
上线前必须暴露健康检查端点,并定制启动 Banner:
# application.yml management: endpoints: web: exposure: include: health,info,metrics,prometheus # 开放 Prometheus 指标 endpoint: health: show-details: when_authorized # 仅授权用户可见详细信息 metrics: export: prometheus: enabled: true # resources/banner.txt ${AnsiColor.BRIGHT_RED} █████╗ ██╗ ██╗████████╗ ██████╗ ██╗ ██╗███╗ ██╗███████╗ ██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██║ ██║████╗ ██║██╔════╝ ███████║██║ ██║ ██║ ██║ ██║██║ ██║██╔██╗ ██║█████╗ ██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║██╔══╝ ██║ ██║╚██████╔╝ ██║ ╚██████╔╝███████╗██║██║ ╚████║███████╗ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ${AnsiColor.DEFAULT} :: Library Management System :: (v2.7.18)启动时输出:
:: Library Management System :: (v2.7.18) 2024-06-15 10:20:30.123 INFO 12345 --- [ main] c.e.l.LibraryApplication : Started LibraryApplication in 3.21 seconds (JVM running for 3.78) 2024-06-15 10:20:30.124 INFO 12345 --- [ main] c.e.l.LibraryApplication : Application endpoints: 2024-06-15 10:20:30.124 INFO 12345 --- [ main] c.e.l.LibraryApplication : Health: http://localhost:8080/actuator/health 2024-06-15 10:20:30.124 INFO 12345 --- [ main] c.e.l.LibraryApplication : Metrics: http://localhost:8080/actuator/metrics通过http://localhost:8080/actuator/health可返回:
{ "status": "UP", "components": { "diskSpace": {"status": "UP", "details": {"total": 500000000000, "free": 200000000000}}, "db": {"status": "UP", "details": {"database": "MySQL", "validationQuery": "isValid()"}} } }这为 Kubernetes 的 Liveness Probe 提供依据,确保容器健康状态可被基础设施识别。
本文还有配套的精品资源,点击获取