ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot 图书管理系统实战:从零搭建生产级应用

Spring Boot 图书管理系统实战:从零搭建生产级应用 简介本资源是一套基于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 用户权限 借阅流程」这个经典业务模型的工程复杂度压到了临界点以下。它天然规避了传统 SSMSpringSpringMVCMyBatis里 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 的 N1 查询和复杂 SQL 生成反而成为瓶颈必须替换为mybatis-spring-boot-startermybatis-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-cachespring-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全局异常处理器、configMyBatis-Plus 分页配置、utilISBN 校验工具类 └── 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所有 DTOData Transfer Object必须与 VOView Object分离BookDTO用于接收前端参数含NotBlank校验BookVO用于返回前端剔除password、create_time等敏感字段。2.3 最小可运行配置application.yml 关键参数解析spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/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参数说明serverTimezoneAsia/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 ResultString 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 是否重复数据库唯一索引 业务层双重保险 QueryWrapperBook 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 QueryWrapperBook().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 UpdateWrapperBook() .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 ResultPageBookVO listBooks( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { PageBookVO page bookService.listBooks(current, size, categoryId, keyword); return Result.success(page); } // Service 实现 Override public PageBookVO listBooks(Integer current, Integer size, Long categoryId, String keyword) { PageBook page new Page(current, size); QueryWrapperBook 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 子句 IPageBook iPage bookMapper.selectPage(page, wrapper); // 转换为 VO去除敏感字段 ListBookVO 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-casetrue并确保数据库表字段为下划线命名如book_title避免反射开销。4. 图书管理系统进阶技巧缓存穿透防护与借阅超时自动归还4.1 Redis 缓存穿透防护布隆过滤器拦截无效 ISBN 查询当恶意请求大量查询不存在的 ISBN如9780000000000时缓存未命中 → 查数据库 → 数据库未命中 → 不写缓存 → 下次继续穿透。解决方案是布隆过滤器Bloom Filter预判Component public class BloomFilterUtil { // 使用 RedisBloom 的 TopK 模块需 Redis 服务器安装 RedisBloom 模块 private final RedisTemplateString, Object redisTemplate; public BloomFilterUtil(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } // 初始化布隆过滤器应用启动时加载所有 ISBN PostConstruct public void initBloomFilter() { // 从数据库批量读取所有 ISBN ListString 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 ResultBookVO 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 QueryWrapperBook().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 判为存在但不会漏判。因此mightExistfalse一定不存在mightExisttrue需继续查缓存/数据库。误判率可通过调整布隆过滤器大小控制默认 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(); QueryWrapperBorrowRecord wrapper new QueryWrapper(); wrapper.eq(status, BorrowStatus.BORROWED.getCode()) .lt(due_date, now); ListBorrowRecord overdueRecords borrowRecordMapper.selectList(wrapper); if (overdueRecords.isEmpty()) { return; } // 批量更新借阅记录状态 ListLong recordIds overdueRecords.stream() .map(BorrowRecord::getId) .collect(Collectors.toList()); UpdateWrapperBorrowRecord updateWrapper new UpdateWrapper(); updateWrapper.in(id, recordIds) .set(status, BorrowStatus.RETURNED.getCode()) .set(return_date, now); borrowRecordMapper.update(null, updateWrapper); // 批量更新图书库存 ListLong bookIds overdueRecords.stream() .map(BorrowRecord::getBookId) .collect(Collectors.toList()); // 使用 MyBatis-Plus 的批量更新语法 UpdateWrapperBook 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 提供依据确保容器健康状态可被基础设施识别。本文还有配套的精品资源点击获取
返回列表