简介:这是一套基于SSM框架开发的图书管理系统项目文件,对于正在学习Java Web整合开发的初中级开发者,可用它理解Spring管理对象、SpringMVC处理请求、MyBatis操作数据库的完整流程。资源内共包含三个文件,其中一个RAR压缩包存放项目源码与相关配置,另一个SQL文件是数据库脚本,另一个文本文件为使用手册,整体打包后约九十一MB;RAR压缩包内包括完整项目目录、前端页面与后端代码,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 层只做参数接收和视图跳转,不碰 SQL;Service 层只做业务逻辑,比如借书时要同时更新图书库存和插入借阅记录;Mapper 层只做数据交换,每个方法对应一条或一组 SQL。这么拆的好处是出了问题不用全盘翻代码:页面数据不对查 Controller,业务规则不对查 Service,SQL 报错直接看 Mapper 里的 XML。
2.3 数据库设计:三张表撑起核心业务
数据库设计是这个项目的骨架,一共三张表。图书表存图书基础信息,读者表存借阅人档案,借阅记录表存每一笔“谁在什么时候借了哪本书”。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| book | id, book_name, author, publisher, total_stock, current_stock | current_stock 每次借书减一、还书加一 |
| reader | id, reader_name, phone, id_card | id_card 做唯一索引防止重复建档 |
| borrow_record | id, book_id, reader_id, borrow_date, return_date, status | status 用 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.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf-8 jdbc.username=root jdbc.password=123456最容易写错的是 URL 里的参数。useUnicode=true 和 characterEncoding=utf-8 必须带上,否则中文会乱码;如果你本机 MySQL 是 8.x,驱动类要改成 com.mysql.cj.jdbc.Driver,还要在 URL 后面加 serverTimezone=Asia/Shanghai,否则会报时区错误。改完配置以后,可以先用 Navicat 或命令行确认账号密码能连上,这一步能排除一半的启动失败问题。
3.4 IDEA 配置 Tomcat:war 包部署方式
配置 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 id="selectByCondition" parameterType="map" resultType="com.example.library.entity.Book"> SELECT * FROM book <where> <if test="bookName != null and bookName != ''"> AND book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="publisher != null and publisher != ''"> AND publisher = #{publisher} </if> </where> ORDER BY id DESC </select><where>标签会自动判断:如果没有任何条件成立,生成的 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 表或库的字符集不是 utf8;JDBC 连接 URL 里没加 characterEncoding=utf-8;SpringMVC 配置里缺少字符编码过滤器,导致 POST 请求的参数解析乱码。
解决:在 web.xml 里加 Spring 提供的 CharacterEncodingFilter,强制请求和响应都走 UTF-8。同时把 JDBC 连接串换成带 utf-8 参数的那一版。改完这三处一般能解决。注意 JSP 页面文件本身的编码,最好在文件头显式写 pageEncoding="UTF-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 字段,写一个方法查出所有 status=0 且 return_date 超过当前时间的记录,用 Spring 的 @Scheduled 注解周期执行,把提醒消息写到一张新表里,管理员登录后在首页看到待办列表。这个改造会经历“写 SQL 查数据、写 Service、写 Controller 读取、页面渲染”完整链路,改完你对这套架构的掌握程度会上一个台阶。
第三条是把明文密码改成加密存储。现在登录模块大多直接把密码存数据库,先引入一个简单散列工具对密码加密再入库,登录时用同样方式算哈希比对。这个改造虽然看起来小,但会迫你把“读者注册”“登录拦截器”“session 保存用户信息”整条链路走一遍。
一个改造时的常见失误是只改代码不改数据库。加封面字段只改实体类不写 ALTER TABLE,启动以后就会直接报 column not found,而出错位置往往在 MyBatis 映射那一层。从我被坑过以后,每次动表结构都强制按“先写 SQL 变更脚本、再改实体、再改 XML、最后改页面”的顺序来,一条一条走完再启动。想做一个真正能用起来的图书管理系统,这个项目是个够用的地基,按上面三个方向往里填功能,会比重新选型来得快得多。希望这个拆解能帮你顺利跑起来,也少走两步弯路。
本文还有配套的精品资源,点击获取