SSM+JSP图书管理系统毕业设计:从框架集成到事务实现
简介面向Java学习者和毕业设计学生基于SSM框架、JSP技术与MySQL数据库实现的图书管理系统资料包同步配套毕业论文、开题报告与任务书覆盖毕业设计的主要环节。压缩包共870个文件整体大小约9.3MB包含Java后端源码、JSP动态页面、CSS与JavaScript前端资源、SQL建库脚本、XML配置文件等文件类型分布清晰便于按模块查阅。已有92人学习使用。借助该资源可深入理解SSM三大框架的整合方式以及B/S三层结构的设计思路管理员功能涵盖用户信息、图书信息、借阅信息和留言信息的管理具体包括用户信息的增删改查、图书信息的查看修改与作废、借阅信息按借阅人及书名等条件的查询、留言信息的回复删除与新增。适合毕业设计、课程设计或Java Web项目实战参考也可作为学习后端开发的完整案例通过项目练习可快速搭建可运行的管理系统掌握从数据库设计到页面展示的常见开发流程提升综合开发能力。1. 为什么毕业设计还选 SSM JSP 的图书管理系统图书管理系统是 Java 后端开发里最典型的“增删改查”业务场景它覆盖了用户管理、图书信息维护、借阅归还、逾期记录这些图书馆日常运转的必备功能。很多人在选题时觉得“SSM 太过时了”但实际上 SSMSpring SpringMVC MyBatis依然是国内高校毕业设计和高职实训里出现频率最高的组合Spring 管对象和事务SpringMVC 管请求分发MyBatis 管 SQL 映射JSP 直接承担页面渲染。这套分工清晰、代码结构一眼能看懂特别适合在论文里把“表现层、业务层、持久层”三层架构讲明白。如果你正打算做“基于 SSM 的图书管理系统设计与实现”或者已经拿到这个题目但还在纠结从哪里下手这篇文会按照工程落地的顺序讲一遍从 Maven 工程怎么建、数据表怎么设计到 Mapper 文件、Service 事务、Controller 跳转再到 JSP 页面循环渲染和借阅流程的事务边界。文章里给出的代码基于 Spring 5.0.x 与 MyBatis 3.4.x适配大多数实验室环境你可以直接改包名和表名用在自己的项目里。这套方案也考虑到论文写作实体类对应 ER 图、Mapper 对应数据访问层设计、Service 对应业务流程图每一个环节都能在正文里展开写。2. SSM 框架集成与数据库建模先定好目录和数据表2.1 Maven 工程结构与依赖版本选择创建一个基于 Maven 的 Web 工程推荐用 IntelliJ IDEA 的maven-archetype-webapp骨架。工程建好后核心目录结构按三层架构组织这是论文里的系统架构图可以直接用的分层方式book-manager/ ├── src/main/java │ └── com.example.book │ ├── controller # 控制层接收 JSP 请求 │ ├── service # 业务层接口与实现 │ ├── dao # MyBatis Mapper 接口 │ ├── entity # 数据库实体类 │ └── utils # 分页、日期处理等工具 ├── src/main/resources │ ├── spring # Spring 与 SpringMVC 配置 │ ├── mapper # MyBatis XML 映射文件 │ └── jdbc.properties # 数据库连接配置 ├── src/main/webapp │ └── WEB-INF │ ├── jsp # 存放 JSP 页面 │ └── web.xml └── pom.xmlpom.xml中需要引入的核心依赖包括spring-context、spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、jackson-databind如果做 AJAX 返回 JSON 用以及jstl和standard两个标签库。连接池不要用DriverManagerDataSource选用druid或dbcp2前者自带监控页面写论文时还能截一张 Druid 监控图。依赖版本上不要盲目追求最新Spring 用 5.0.8.RELEASEMyBatis 用 3.4.6mybatis-spring 用 1.3.2这几个版本在 JDK 8 Tomcat 8.5 环境下配合稳定。如果用更高版本需要确认javax.*还是jakarta.*否则会有包路径不兼容的问题。2.2 Spring 与 MyBatis 集成配置文件里最容易出错的三处SSM 集成共有三个核心配置spring-context.xml管理 Service、DAO 和事务spring-mvc.xml管理 Controller 和视图解析器web.xml做全局启动。下面是spring-context.xml里最关键的配置!-- 开启注解扫描只扫 Service 和 DAOController 交给 spring-mvc.xml -- context:component-scan base-packagecom.example.book context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 读取数据库配置文件 -- context:property-placeholder locationclasspath:jdbc.properties/ !-- Druid 连接池 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean !-- SqlSessionFactory 与 Mapper 扫描 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.book.dao/ /bean这里需要说明三个容易踩的坑MapperScannerConfigurer的basePackage必须与 DAO 接口包名一致而且它会自动注册所有 Mapper 接口不需要再手写beanmapperLocations指向classpath:mapper/*.xml对应的映射文件必须与接口方法签名严格匹配context:component-scan里要把Controller排除掉否则 Spring 容器会重复管理 Controller 实例导致注入混乱。事务管理器直接配DataSourceTransactionManager开启注解驱动后在 Service 实现类的方法上标注Transactional即可。2.3 数据表设计图书、读者、借阅三张核心表图书管理系统的表结构不需要复杂四张表足够覆盖论文里的核心功能同时能画出完整 ER 图。图书表book_info存图书基本信息读者表reader_info存读者档案借阅表borrow_record记录每一次借还行为管理员表admin_info负责登录认证。在 Navicat 或命令行执行建表语句时推荐用下面的字段设计CREATE TABLE book_info ( book_id int(11) NOT NULL AUTO_INCREMENT, book_name varchar(100) NOT NULL COMMENT 书名, book_author varchar(50) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, isbn varchar(20) DEFAULT NULL COMMENT ISBN 编号, category varchar(50) DEFAULT NULL COMMENT 分类, total_count int(11) DEFAULT 0 COMMENT 馆藏总量, available_count int(11) DEFAULT 0 COMMENT 可借数量, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (book_id) ) ENGINEInnoDB AUTO_INCREMENT1001 DEFAULT CHARSETutf8mb4 COMMENT图书信息表; CREATE TABLE borrow_record ( record_id int(11) NOT NULL AUTO_INCREMENT, book_id int(11) NOT NULL, reader_id int(11) NOT NULL, borrow_time datetime DEFAULT NULL, return_time datetime DEFAULT NULL, actual_return_time datetime DEFAULT NULL, status tinyint(4) DEFAULT 0 COMMENT 0 借出 1 已还 2 逾期, PRIMARY KEY (record_id), KEY idx_book_id (book_id), KEY idx_reader_id (reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;字段类型有几个细节isbn不要用char(13)因为 ISBN 含横杠会超出available_count需要配合借还事务做加减必须定义为int不能是varchar时间字段统一用datetime不要用timestamp避免 2038 年问题和时区干扰。所有表使用utf8mb4不用utf8否则存生僻字书名会报Incorrect string value错误。实体类可以直接由 MyBatis 的resultMap映射属性名对应表字段的下划线转驼峰。在mybatis-config.xml里开启mapUnderscoreToCamelCase后就可以省去写resultMap的工作量book_name自动映射到bookName。3. 从 DAO 到 JSP 的图书管理功能三层架构的完整链路3.1 DAO 层Mapper 接口与 XML 映射文件图书信息的增删改查是系统的地基。先定义 Mapper 接口接着写 XML。接口方法的返回值设计要符合分页场景查询列表返回ListBook按 ID 查询返回单个实体新增返回影响行数。XML 映射文件里SQL 参数用#{...}占位符可以防止 SQL 注入。public interface BookDao { ListBook selectBookList(Book book); // 条件查询 分页 Book selectBookById(Integer bookId); int insertBook(Book book); int updateBook(Book book); int deleteBook(Integer bookId); int reduceAvailableCount(Integer bookId); // 借书时扣减可借数量 int increaseAvailableCount(Integer bookId); // 还书时增加可借数量 }对应BookDao.xml中动态 SQL 的关键写法select idselectBookList parameterTypecom.example.book.entity.Book resultTypecom.example.book.entity.Book SELECT book_id, book_name, book_author, publisher, isbn, category, total_count, available_count, create_time FROM book_info where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testcategory ! null and category ! AND category #{category} /if if testisbn ! null and isbn ! AND isbn LIKE CONCAT(%, #{isbn}, %) /if /where ORDER BY book_id DESC /selectwhere标签会自动去掉第一个AND不需要在条件前手动拼WHERE 11。模糊查询用CONCAT(%, #{bookName}, %)避免直接写%${bookName}%后者会把值原样拼进 SQL存在注入风险且不好维护。开发调试时在mybatis-config.xml里加setting namelogImpl valueSTDOUT_LOGGING/控制台会打印每条 SQL 和参数对排查问题非常有用。3.2 Service 层业务校验与事务边界Service 层是论文里业务流程图的核心。图书新增需要校验 ISBN 是否重复借书需要校验读者状态和剩余可借数量。类上标注Service事务方法上加Transactional事务只加在写操作上查询用Transactional(readOnly true)让 MySQL 走只读优化。借书操作是单次请求里涉及多张表写入的典型场景Override Transactional(rollbackFor Exception.class) public boolean borrowBook(Integer readerId, Integer bookId) { // 1. 查询图书判断可借数量 Book book bookDao.selectBookById(bookId); if (book null || book.getAvailableCount() 0) { throw new BusinessException(库存不足或图书不存在); } // 2. 查询读者判断是否有未还图书 int unreturned borrowDao.countUnreturned(readerId); if (unreturned 5) { throw new BusinessException(该读者尚有未归还图书); } // 3. 扣减库存 插入借阅记录 int updated bookDao.reduceAvailableCount(bookId); if (updated ! 1) { throw new BusinessException(操作冲突请重试); } BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(new Date()); record.setStatus(0); borrowDao.insertRecord(record); return true; }这里事务的意义体现在第 3 步reduceAvailableCount更新成功但insertRecord抛异常时两个操作会一起回滚不会出现库存减了但借阅记录没有的情况。rollbackFor Exception.class必须显式声明Spring 默认只对RuntimeException回滚自定义的BusinessException如果继承的是Exception不加这个参数就不会回滚。3.3 Controller 层与 JSP 循环渲染Controller 用Controller注解实现页面跳转和参数绑定。一个标准列表页的 Controller 方法如下Controller RequestMapping(/book) public class BookController { Autowired private BookService bookService; RequestMapping(/list) public String list(Book book, Integer pageNum, Model model) { PageHelper.startPage(pageNum null ? 1 : pageNum, 10); PageInfoBook pageInfo new PageInfo( bookDao.selectBookList(book)); model.addAttribute(pageInfo, pageInfo); return /jsp/bookList; } RequestMapping(/toAdd) public String toAdd() { return /jsp/bookAdd; } RequestMapping(/save) public String save(Book book, RedirectAttributes attributes) { bookService.addBook(book); attributes.addFlashAttribute(message, 添加成功); return redirect:/book/list; } }三个方法对应三种典型行为list方法把分页数据放进Model后返回逻辑视图名SpringMVC 通过InternalResourceViewResolver把/jsp/bookList解析成/WEB-INF/jsp/bookList.jsptoAdd是纯页面跳转save在操作成功后重定向避免刷新页面时表单重复提交。RedirectAttributes的addFlashAttribute会在重定向后立即消失适合携带一次性提示信息。JSP 页面上用c:forEach循环渲染表格这是 JSP 相比在 Controller 里拼 HTML 更清晰的体现% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle图书列表/title/head body table border1 tr th编号/thth书名/thth作者/th th库存/thth可借/thth操作/th /tr c:forEach items${pageInfo.list} varbook tr td${book.bookId}/td td${book.bookName}/td td${book.bookAuthor}/td td${book.totalCount}/td td${book.availableCount}/td td a href${pageContext.request.contextPath}/book/toEdit?bookId${book.bookId}编辑/a a hrefjavascript:void(0) onclickconfirmDelete(${pageContext.request.contextPath}/book/delete?bookId${book.bookId})删除/a /td /tr /c:forEach /table /body /html注意${book.bookName}这样的 EL 表达式要求实体类必须有对应的getBookName()方法IDEA 生成的实体类通常满足要求但手写实体类时容易把get方法写漏。删除操作建议用confirmDelete在客户端弹确认框避免误点直接删数据。pageContext.request.contextPath是 JSP 里取 Web 根路径的固定写法不要省。4. 借阅归还的关键路径事务、分页和常见排错4.1 PageHelper 分页原理与参数调优分页是图书管理系统的展示标配。图书列表和借阅记录列表的数据量不大但论文里有“系统测试”章节时分页查询是最好写测试用例的部分。引入 PageHelper 的方式很简单在pom.xml中添加依赖然后在 Spring 配置的sqlSessionFactory下面注入一个PageInterceptor。注意旧版本的bean配置方式在新版本中已经失效。PageHelper 有五个常用参数实际项目中我会这样设置参数推荐值说明helperDialectmysql指定方言不设置会自动从连接 URL 推断reasonabletrue页码超出范围时自动回退到第一页或最后一页supportMethodsArgumentstrue支持从 Controller 方法参数里读pageNum/pageSizeparamscountcountSql自动生成 COUNT 查询pageSizeZerofalse为 true 时 pageSize0 查全部数据生产中不建议开分页必须在 SQL 查询执行前一行调用PageHelper.startPage(pageNum, pageSize)。它基于 ThreadLocal 实现如果startPage之后没有执行 Mapper 查询或者执行了两次 Mapper 查询分页参数会被第二次查询消费掉导致数据错乱。这个细节在论文的“系统测试与问题分析”章节也是一个很好的素材。4.2 JSP 页面提交表单时的中文乱码和 404 排查SSM 项目里 JSP 乱码属于高频问题根源在于三个环节的编码不一致浏览器提交、POST 请求体解码、MySQL 存储。JSP 页面第一行已经声明了charsetUTF-8但 POST 请求还需要在web.xml里加过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncoding设为true是让请求和响应的编码都强制为 UTF-8。MySQL 连接串里也必须追加characterEncodingutf8和useSSLfalse。如果表单提交后在数据库里看到???多半是连接串少了编码参数如果 JSP 页面显示乱码则是 JSP 文件本身的编码不对IDEA 里要确认右下角编码是 UTF-8。JSP 改完不生效也是大家常反馈的问题。这背后是 Tomcat 的 JSP 编译缓存修改 JSP 文件后服务器需要重新编译开发环境中一般会自动检测但如果你用热部署插件或直接把文件拷进 Tomcat 的webapps目录就会遇到旧页面。处理办法是删除 Tomcatwork/Catalina目录下的localhost缓存文件夹或者关闭服务器后重新启动不要勾选 IDEA 里的Build project automatically来追求热更新那只会带来更多不确定性问题。4.3 借阅记录的关联查询与返回时间回填借阅列表页需要显示书名和读者名但borrow_record表里只有 ID没有冗余字段。两种方案一种是在查询时用JOIN关联book_info和reader_info另一种是建一个BorrowRecordVO把关联查询结果封装到 VO 里。毕业设计推荐用 VO因为 VO 在论文里容易画出清晰的类图。select idselectBorrowRecordVOList parameterTypecom.example.book.entity.BorrowQuery resultTypecom.example.book.entity.BorrowRecordVO SELECT br.record_id AS recordId, b.book_name AS bookName, r.reader_name AS readerName, br.borrow_time AS borrowTime, br.return_time AS returnTime, br.actual_return_time AS actualReturnTime, br.status AS status FROM borrow_record br LEFT JOIN book_info b ON br.book_id b.book_id LEFT JOIN reader_info r ON br.reader_id r.reader_id where if teststatus ! null AND br.status #{status} /if /where ORDER BY br.borrow_time DESC /selectLEFT JOIN而不是INNER JOIN是为了保证即使某条借阅记录的图书被删除记录仍然能显示出来。status字段的值用数字 0、1、2 表示借出、已还、逾期在 JSP 页面可以用c:if或三元表达式转成文字。还书操作时return_time往表里填同时把available_count加回来这两个操作也必须在同一个事务里否则会出现数据不一致。5. 答辩前的系统优化缓存、监控与演示路线毕设系统功能跑通以后至少再做两个优化点才能在答辩时展示你的进阶能力。第一个是给图书列表加 Redis 缓存使用spring-data-redis加上RedisTemplate在查询列表前先查缓存命中则直接返回 JSON未命中时从数据库查询后再写入缓存并设置三分钟过期时间。查询量大的系统会把热点数据放在 Redis 里降低数据库压力这个优化点写进论文的“系统优化”章节非常扎实。如果你的环境装不了 Redis最直接的替代方案是给book_info表和borrow_record表的关键查询字段建索引比如book_id、reader_id、status三个字段上加复合索引然后通过EXPLAIN SELECT ...展示type从ALL变ref的对比截图。第二个是给删除操作加日志记录。图书删除是管理员权限操作笔者的做法是用 Spring AOP 写一个切面在BookMapper.deleteBook执行后记录操作人、操作时间和被删除图书的 ID 到operate_log表。这个设计在论文里属于“系统安全性与可维护性”内容答辩时可以说“生产环境中误删数据不可逆日志可以用于审计追溯”。AOP 切面的代码量不大控制在半小时内可以完成。最后强调演示路线的顺序先演示管理员登录再新增一本新书然后借书、还书。每一步操作前先说预期结果比如新增前说明“新增后列表页应出现刚才录入的图书且馆藏数量为 1”操作后对照验证。这样既展示了需求分析的测试用例又体现出你对系统行为的掌控力远比把每个页面点一遍更有说服力。如果答辩现场出现 500 错误或 Kaptcha 验证码刷新不过去立刻检查 Tomcat 日志里的Caused by段大部分问题都能在 30 秒内找到方向。本文还有配套的精品资源点击获取