SSM图书管理系统实战:从三层架构到借还书自动化

发布时间:2026/10/23 3:09:37
SSM图书管理系统实战:从三层架构到借还书自动化
简介这是一套基于SSM框架开发的图书管理系统项目文件对于正在学习Java Web整合开发的初中级开发者可用它理解Spring管理对象、SpringMVC处理请求、MyBatis操作数据库的完整流程。资源内共包含三个文件其中一个RAR压缩包存放项目源码与相关配置另一个SQL文件是数据库脚本另一个文本文件为使用手册整体打包后约九十一MBRAR压缩包内包括完整项目目录、前端页面与后端代码SQL脚本提供了数据表结构及初始数据文本手册则对环境配置、启动方式和常见排错作了说明。目前已有三千二百五十四人学习下载。通过阅读源码和手册可以掌握权限登录、图书借阅、邮件通知、答题互动等模块的实现思路同时学习模块化分层设计及防SQL注入等安全细节适合作为课程设计或SSM框架综合练习的参考样例。1. 一个让图书管理员少加班的 SSM 图书管理系统从手工台账到借还书全流程自动化见过太多图书馆还把借还书记录写在 Excel 甚至纸质台账上逾期费靠人工掰着指头算图书被借走以后没人记得什么时候该催还。这个用 SSM 框架搭起来的图书管理系统解决的就是这套乱账图书信息维护、读者档案、借书、还书、逾期天数计算全部搬到 Web 页面上管理员点几下鼠标就能完成一次完整的借还业务。适合两类人一类是正在做课程设计、需要一套结构清楚的 SSM 整合项目当参考的在校生另一类是刚学完 Spring、SpringMVC、MyBatis 三个框架想知道它们怎么在一个真实业务里配合干活的新手开发者。项目不花哨但该有的分层、事务、动态 SQL 都有跑起来以后你能看到一条清晰的请求处理链路。2. 技术栈与架构设计SSM 整合 JSP 视图 MySQL三层架构怎么落地2.1 技术选型为什么是这个组合先说结论这套组合在中小型管理系统里属于“怎么选都不会错”的搭配技术栈如下Spring 负责对象管理和事务控制Service 层的 Bean 全部交给容器事务边界也用它声明式管理。SpringMVC 负责请求分发Controller 接收页面参数、调用 Service、把结果塞进 ModelAndView 返回 JSP 页面。MyBatis 负责数据访问SQL 写在 Mapper XML 里动态查询、结果映射都由它处理。JSP JSTL 做视图层配合 EL 表达式渲染列表和表单。MySQL 做数据存储建三张核心表图书表、读者表、借阅记录表。这套选型最大的好处是“好招人、好排查”。数据源的连接池用阿里 Druid 来配监控页面还能直接看 SQL 执行情况。比起 Spring Boot 那种自动装配的省心SSM 需要手动把三个框架拧在一起这个过程反过来能帮你把 Bean 生命周期、DispatcherServlet 初始化这些概念全部过一遍。2.2 三层架构一个请求从进入到返回的完整链路项目严格按三层结构组织我在本地跑通以后画了一条请求链路大概是这样走的浏览器发起请求 - DispatcherServlet 拦截 *.do 路径 - 根据 HandlerMapping 找到对应的 Controller 方法 - Controller 调用 Service 接口 - ServiceImpl 里加事务调用 Mapper 接口 - Mapper 执行 XML 里的 SQL 操作 MySQL - 结果原路返回Controller 把数据放 ModelAndView - JSP 渲染出 HTML浏览器展示链路本身不复杂但每一层都做了一件事Controller 层只做参数接收和视图跳转不碰 SQLService 层只做业务逻辑比如借书时要同时更新图书库存和插入借阅记录Mapper 层只做数据交换每个方法对应一条或一组 SQL。这么拆的好处是出了问题不用全盘翻代码页面数据不对查 Controller业务规则不对查 ServiceSQL 报错直接看 Mapper 里的 XML。2.3 数据库设计三张表撑起核心业务数据库设计是这个项目的骨架一共三张表。图书表存图书基础信息读者表存借阅人档案借阅记录表存每一笔“谁在什么时候借了哪本书”。表名关键字段说明bookid, book_name, author, publisher, total_stock, current_stockcurrent_stock 每次借书减一、还书加一readerid, reader_name, phone, id_cardid_card 做唯一索引防止重复建档borrow_recordid, book_id, reader_id, borrow_date, return_date, statusstatus 用 0/1 区分未还和已还三张表的关系也简单直接borrow_record 通过 book_id 和 reader_id 分别关联图书表和读者表。逾期费没有做成单独字段而是用“当前时间 - 应还日期”动态算这样不会因为长期不还书导致逾期费字段越攒越大。需要理解的一点是这本书的库存数是冗余存到 book 表里的借还书时必须在同一个事务里更新它这也是后面代码的中心逻辑。2.4 项目目录结构分包分包一眼看清src/main/java ├─ com.example.library │ ├─ controller // BookController, ReaderController, BorrowController │ ├─ service // 接口定义 │ │ └─ impl // 业务实现事务注解在这层加 │ ├─ mapper // MyBatis Mapper 接口 │ ├─ entity // Book, Reader, BorrowRecord 实体类 │ └─ common // 分页工具类、统一返回结果 src/main/resources ├─ applicationContext.xml // Spring 容器配置数据源、事务、扫描 ├─ springMVC.xml // 视图解析器、静态资源放行 ├─ mybatis-config.xml // 下划线转驼峰、日志、别名 └─ mapper // BookMapper.xml, ReaderMapper.xml, BorrowMapper.xml └─ jdbc.properties // 数据库连接参数 src/main/webapp ├─ WEB-INF │ └─ jsp // index.jsp, bookList.jsp, readerList.jsp... └─ static // css/js页面样式资源初看这个结构可能会觉得“类怎么这么少”因为项目没有做复杂的权限框架登录用的是过滤器判断 session 里有没有用户信息省掉了 Spring Security 那一整套配置。如果想把它改成更复杂的管理系统后面往这里加模块就行分层边界是清楚的不会出现“加一个功能要改五个层还不一定对得上”的尴尬。3. 本地跑通全流程环境准备、数据库初始化和 Tomcat 启动3.1 环境版本搭配先对齐再动手SSM 项目对环境版本比较敏感新手最容易在这里翻车。项目本身基于 JDK 1.8 编写Tomcat 用 8.5 或 9.0 都可以MySQL 建议 5.7连接驱动用 mysql-connector-java 5.1.49 版本。Maven 用 3.6 以上即可。如果你是近两年才装的环境机器上大概率是 JDK 8 起步没问题但注意一点不要用 JDK 11 以上直接跑某些旧版 Tomcat 在更高版本 JDK 下会报模块访问错误折腾起来没必要。IDEA 里导入项目时选择 Maven 方式让 IDEA 下载依赖。如果公司网络或校园网导致 Maven 下载很慢先确认本地的 settings.xml 里有没有配阿里云镜像这是老生常谈但十次里有八次慢都出在这。3.2 初始化数据库执行脚本建库建表项目压缩包里一般带一个 sql 文件直接用命令行或者 Navicat 执行。如果包里没有现成的则用下面这段建库脚本先手动建好命名要用英文避免字符集问题CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARSET utf8mb4; USE library_db; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), total_stock INT DEFAULT 0, current_stock INT DEFAULT 0 ); CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20), id_card VARCHAR(30) ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT, reader_id INT, borrow_date DATETIME, return_date DATETIME, status TINYINT DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) );建完表以后至少插入两条图书数据和一条读者数据不然页面打开是空的容易误以为项目没跑起来。外键约束要理解一下如果读者有未归还的借阅记录直接删读者会失败这是数据库在保护数据完整性不是程序写错了。3.3 修改 jdbc.properties三处配置别写错项目里连接数据库的配置集中在 jdbc.properties 里启动之前必须改成本机的账号密码jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf-8 jdbc.usernameroot jdbc.password123456最容易写错的是 URL 里的参数。useUnicodetrue 和 characterEncodingutf-8 必须带上否则中文会乱码如果你本机 MySQL 是 8.x驱动类要改成 com.mysql.cj.jdbc.Driver还要在 URL 后面加 serverTimezoneAsia/Shanghai否则会报时区错误。改完配置以后可以先用 Navicat 或命令行确认账号密码能连上这一步能排除一半的启动失败问题。3.4 IDEA 配置 Tomcatwar 包部署方式配置 Tomcat 这一步有几个关键动作Run - Edit Configurations - 左上角加号 - Tomcat Server - Local。在 Deployment 标签页点加号选择 Artifact把项目的 war exploded 加进去Application context 建议填 /library。点开 Artifact 展开看有没有遗漏 lib 目录没有的话整个项目跑起来会报 ClassNotFoundException。启动以后访问地址就是 http://localhost:8080/library/。需要注意不要用 JSP 文件的路径直接访问会被拦截器挡掉必须走 Controller 的请求映射。如果启动失败分两步看日志先看 IDEA 控制台有没有 Tomcat 的启动日志确认端口有没有被占用再看 catalina 日志里抛的是什么异常。别一上来就怀疑代码绝大多数情况是数据库没连上或者依赖没拉全。4. 核心实现拆解借书、还书、逾期计算这条业务链路的代码细节4.1 借书流程一个操作同时完成库存扣减和记录新增借书的业务规则是检查读者存在、检查图书有库存、扣减库存、创建借阅记录。这是典型的“要么全成功、要么全失败”的场景所以事务必须加在这一层。看 BorrowServiceImpl 的核心代码Override Transactional(rollbackFor Exception.class) public void borrowBook(Integer bookId, Integer readerId) { // 1. 查图书判断在架库存 Book book bookMapper.selectById(bookId); if (book null) { throw new RuntimeException(图书不存在); } if (book.getCurrentStock() 0) { throw new RuntimeException(该图书暂无库存); } // 2. 扣减库存注意这里用的是 SQL 里的 stock - 1 bookMapper.decreaseStock(bookId); // 3. 创建借阅记录默认归还时间为借出后 30 天 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowDate(new Date()); record.setStatus(0); borrowRecordMapper.insert(record); }这段代码有三个细节值得说道。第一Transactional 注解加在 Service 实现类上而不要加在 Controller 上事务才能保证步骤 2 和 3 要么全部提交、要么全部回滚不会出现库存减了但记录没生成的情况。第二扣减库存用的 SQL 是 update book set current_stock current_stock - 1 where id ? and current_stock 0这种写法天然防止超卖比“先查再改”的方式安全。第三查询和扣减不是同一个操作中间理论上存在其他请求并发修改的窗口但因为扣减时带了库存大于 0 的条件即使并发也不会出现负数库存。4.2 还书与逾期计算动态计算避免脏数据还书比借书多了一个环节算读者有没有逾期。实现方式很直接还书时把记录的 return_date 更新为当前时间然后和应还日期借出日 30 天做比较Override Transactional(rollbackFor Exception.class) public BigDecimal returnBook(Integer recordId) { // 1. 查借阅记录 BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() 1) { throw new RuntimeException(记录不存在或已归还); } // 2. 更新归还时间和状态 record.setReturnDate(new Date()); record.setStatus(1); borrowRecordMapper.update(record); // 3. 恢复图书库存 bookMapper.increaseStock(record.getBookId()); // 4. 逾期费用 逾期天数 * 单日费用不足一天按一天算 Date dueDate DateUtils.addDays(record.getBorrowDate(), 30); long overdueDays DateUtils.passDaysOf(record.getReturnDate(), dueDate); if (overdueDays 0) { return BigDecimal.ZERO; } return new BigDecimal(overdueDays).multiply(new BigDecimal(0.5)); }这里还书日期直接取自系统当前时间而不是让管理员手选从源头上堵住“把日期改回来规避逾期费”的路子。逾期天数用 DateUtils 工具类里自定义的方法算它内部会转成毫秒数相减再除以一天的毫秒数。需要注意一点逾期费用是在事务提交后才返回给 Controller 展示的不会被写进数据库所以每次查看同一笔历史记录时算出来的过期费可能不一样——原因不是程序有 bug而是“查询时重新计算”这种设计。4.3 MyBatis 动态 SQL查询列表的模糊搜索列表页常见的需求是书名模糊搜索 状态筛选MyBatis 的where标签正好解决“条件拼 SQL 时多了个 AND”的尴尬select idselectByCondition parameterTypemap resultTypecom.example.library.entity.Book SELECT * FROM book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testpublisher ! null and publisher ! AND publisher #{publisher} /if /where ORDER BY id DESC /selectwhere标签会自动判断如果没有任何条件成立生成的 SQL 就是 SELECT * FROM book不会出现“WHERE AND”这种语法错误。这里的 #{bookName} 是预编译参数MyBatis 会把它转成 ? 占位符从根上防住了 SQL 注入。我之前见过有人贪省事直接拼字符串进 SQL结果书名里带个单引号页面就崩换成这种方式以后再没出过事。4.4 Controller 层怎么接参数、跳页面Controller 层的代码风格比较传统用 RequestParam 接收参数然后返回视图名数据用 Model 传给 JSP。以还书操作为例Controller RequestMapping(/borrow) public class BorrowController { Autowired private BorrowService borrowService; RequestMapping(/return) public String returnBook(RequestParam Integer recordId, Model model) { try { BigDecimal overdueFee borrowService.returnBook(recordId); model.addAttribute(fee, overdueFee); return returnSuccess; } catch (RuntimeException e) { model.addAttribute(errorMsg, e.getMessage()); return error; } } }Controller 里没有业务代码只有参数接收、调用、结果放模型、返回视图名这是比较规范的做法。需要留意 RequestParam 的 required 属性默认是 true也就是说浏览器没带 recordId 直接访问这个地址会直接 400不会给你任何业务提示。想要更友好的体验可以在参数前面加一个 required false再在方法里判断空值。页面跳转用的字符串对应 JSP 文件名解析器会自动拼上前缀 /WEB-INF/jsp/ 和后缀 .jsp。5. 常见问题排查与避坑指南五个踩过的坑从 404 到事务不生效5.1 启动后访问地址直接 404现象Tomcat 启动成功IDEA 控制台没有报错但浏览器打开 http://localhost:8080/library/ 显示 404。原因Deployment 里没有把 Artifact 添加进去或者 Application context 路径和实际访问路径不一致。IDEA 识别不到你的 Web 模块时启动的其实是一个空 Tomcat自然找不到项目。解决Run - Edit Configurations - Deployment 标签页点加号选 Artifact把项目 war exploded 添加进去然后在 Application context 填 /library之后重启。如果是 Maven 多模块工程检查 Artifact 有没有带依赖库展开以后看 Available Elements 里有没有需要手动右键添加的 jar 包。5.2 JSP 页面中文全部变成乱码现象页面上的书名、读者名显示成问号或一串乱码但数据库里查看数据是正常的。原因分了三层问题。MySQL 表或库的字符集不是 utf8JDBC 连接 URL 里没加 characterEncodingutf-8SpringMVC 配置里缺少字符编码过滤器导致 POST 请求的参数解析乱码。解决在 web.xml 里加 Spring 提供的 CharacterEncodingFilter强制请求和响应都走 UTF-8。同时把 JDBC 连接串换成带 utf-8 参数的那一版。改完这三处一般能解决。注意 JSP 页面文件本身的编码最好在文件头显式写 pageEncodingUTF-8。5.3 加了 Transactional 但事务没生效现象只要代码里“先减库存再插入记录”的第二步骤故意执行失败跑完发现库存还是被扣了说明事务没有回滚。原因事务注解没被扫描到或方法被同类内部调用导致 Spring AOP 拦截不到还有一种情况是 applicationContext.xml 里事务管理器没有指定 build 数据源。解决先在事务管理器配置里确认 dataSource 属性指向的是你配好的数据源 Bean然后检查 Service 实现类有没有被context:component-scan扫到最后确认你调 service 方法时用的是注入的代理对象。同类内部 this 调方法是不经过代理的事务管不到。5.4 MySQL 8.x 环境连接报错现象启动时提示 ClassNotFoundException 或者 Communications link failure看堆栈里有 SSL 或 timezone 相关字样。原因驱动版本和数据库版本不匹配。MySQL 8.x 需要 8.0 以上的驱动驱动类名变成了 com.mysql.cj.jdbc.Driver且连接 URL 必须带 serverTimezone用了旧版驱动就会报找不到类或者安全连接失败。解决升级 mysql-connector-java 依赖版本同时把 jdbc.properties 里的 driver 和 URL 改成 8.x 写法。推荐在 pom.xml 里使用 8.0.11 以上版本这套写法对 5.7 和 8.0 都兼容。5.5 修改了 JSP 或 Java 代码刷新不生效现象改完代码重新启动 Tomcat页面还是老样子甚至改了 Controller 跳转逻辑也无效。原因IDEA 里 Tomcat 热部署有两个开关一个是 on frame deactivation 设置另一个是 build project automatically 选项。如果没配置好修改的 class 或资源不会被自动部署到 Tomcat 的工作目录。解决把 Tomcat 配置里的 On frame deactivation 选成 Update classes and resources想更稳妥就直接用 Run 模式重启。如果你的项目用了 war exploded 方式部署IDEA 的自动更新一般会生效但偶尔还是会出现 JSP 没有被复制过去的玄学情况碰到就手动点一下 Build - Recompile或者干脆完整重启。6. 二次开发改造思路从“能跑”到“好用”的三个切入点这套项目跑通以后想往里面加东西有三条比较顺手的路按性价比从高到低排第一条是给图书表加封面图和分类字段。数据库加列之后在 Book 实体类补属性Mapper XML 的 resultMap 加上映射关系然后 JSP 页面在列表头加一个“分类筛选”下拉框。动态 SQL 的if条件只需要多写一个判断整体改动量在一个小时内。第二条是把逾期提醒做成一个定时任务。借阅记录里现在有 borrow_date 和 status 字段写一个方法查出所有 status0 且 return_date 超过当前时间的记录用 Spring 的 Scheduled 注解周期执行把提醒消息写到一张新表里管理员登录后在首页看到待办列表。这个改造会经历“写 SQL 查数据、写 Service、写 Controller 读取、页面渲染”完整链路改完你对这套架构的掌握程度会上一个台阶。第三条是把明文密码改成加密存储。现在登录模块大多直接把密码存数据库先引入一个简单散列工具对密码加密再入库登录时用同样方式算哈希比对。这个改造虽然看起来小但会迫你把“读者注册”“登录拦截器”“session 保存用户信息”整条链路走一遍。一个改造时的常见失误是只改代码不改数据库。加封面字段只改实体类不写 ALTER TABLE启动以后就会直接报 column not found而出错位置往往在 MyBatis 映射那一层。从我被坑过以后每次动表结构都强制按“先写 SQL 变更脚本、再改实体、再改 XML、最后改页面”的顺序来一条一条走完再启动。想做一个真正能用起来的图书管理系统这个项目是个够用的地基按上面三个方向往里填功能会比重新选型来得快得多。希望这个拆解能帮你顺利跑起来也少走两步弯路。本文还有配套的精品资源点击获取