简介:面向Java学习者和毕业设计学生,基于SSM框架、JSP技术与MySQL数据库实现的图书管理系统资料包,同步配套毕业论文、开题报告与任务书,覆盖毕业设计的主要环节。压缩包共870个文件,整体大小约9.3MB,包含Java后端源码、JSP动态页面、CSS与JavaScript前端资源、SQL建库脚本、XML配置文件等,文件类型分布清晰,便于按模块查阅。已有92人学习使用。借助该资源可深入理解SSM三大框架的整合方式,以及B/S三层结构的设计思路;管理员功能涵盖用户信息、图书信息、借阅信息和留言信息的管理,具体包括用户信息的增删改查、图书信息的查看修改与作废、借阅信息按借阅人及书名等条件的查询、留言信息的回复删除与新增。适合毕业设计、课程设计或Java Web项目实战参考,也可作为学习后端开发的完整案例,通过项目练习可快速搭建可运行的管理系统,掌握从数据库设计到页面展示的常见开发流程,提升综合开发能力。
1. 为什么毕业设计还选 SSM + JSP 的图书管理系统
图书管理系统是 Java 后端开发里最典型的“增删改查”业务场景,它覆盖了用户管理、图书信息维护、借阅归还、逾期记录这些图书馆日常运转的必备功能。很多人在选题时觉得“SSM 太过时了”,但实际上 SSM(Spring + 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.RELEASE,MyBatis 用 3.4.6,mybatis-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 和 DAO,Controller 交给 spring-mvc.xml --> <context:component-scan base-package="com.example.book"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 读取数据库配置文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- Druid 连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- SqlSessionFactory 与 Mapper 扫描 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.book.dao"/> </bean>这里需要说明三个容易踩的坑:MapperScannerConfigurer的basePackage必须与 DAO 接口包名一致,而且它会自动注册所有 Mapper 接口,不需要再手写<bean>;mapperLocations指向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`) ) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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。接口方法的返回值设计要符合分页场景:查询列表返回List<Book>,按 ID 查询返回单个实体,新增返回影响行数。XML 映射文件里,SQL 参数用#{...}占位符,可以防止 SQL 注入。
public interface BookDao { List<Book> 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 id="selectBookList" parameterType="com.example.book.entity.Book" resultType="com.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 test="bookName != null and bookName != ''"> AND book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="isbn != null and isbn != ''"> AND isbn LIKE CONCAT('%', #{isbn}, '%') </if> </where> ORDER BY book_id DESC </select><where>标签会自动去掉第一个AND,不需要在条件前手动拼WHERE 1=1。模糊查询用CONCAT('%', #{bookName}, '%'),避免直接写'%${bookName}%',后者会把值原样拼进 SQL,存在注入风险且不好维护。开发调试时,在mybatis-config.xml里加<setting name="logImpl" value="STDOUT_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); PageInfo<Book> 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.jsp;toAdd是纯页面跳转;save在操作成功后重定向,避免刷新页面时表单重复提交。RedirectAttributes的addFlashAttribute会在重定向后立即消失,适合携带一次性提示信息。
JSP 页面上用c:forEach循环渲染表格,这是 JSP 相比在 Controller 里拼 HTML 更清晰的体现:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>图书列表</title></head> <body> <table border="1"> <tr> <th>编号</th><th>书名</th><th>作者</th> <th>库存</th><th>可借</th><th>操作</th> </tr> <c:forEach items="${pageInfo.list}" var="book"> <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 href="javascript:void(0)" onclick="confirmDelete('${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 有五个常用参数,实际项目中我会这样设置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
helperDialect | mysql | 指定方言,不设置会自动从连接 URL 推断 |
reasonable | true | 页码超出范围时自动回退到第一页或最后一页 |
supportMethodsArguments | true | 支持从 Controller 方法参数里读pageNum/pageSize |
params | count=countSql | 自动生成 COUNT 查询 |
pageSizeZero | false | 为 true 时 pageSize=0 查全部数据,生产中不建议开 |
分页必须在 SQL 查询执行前一行调用PageHelper.startPage(pageNum, pageSize)。它基于 ThreadLocal 实现,如果startPage之后没有执行 Mapper 查询,或者执行了两次 Mapper 查询,分页参数会被第二次查询消费掉,导致数据错乱。这个细节在论文的“系统测试与问题分析”章节也是一个很好的素材。
4.2 JSP 页面提交表单时的中文乱码和 404 排查
SSM 项目里 JSP 乱码属于高频问题,根源在于三个环节的编码不一致:浏览器提交、POST 请求体解码、MySQL 存储。JSP 页面第一行已经声明了charset=UTF-8,但 POST 请求还需要在web.xml里加过滤器:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>forceEncoding设为true是让请求和响应的编码都强制为 UTF-8。MySQL 连接串里也必须追加characterEncoding=utf8和useSSL=false。如果表单提交后在数据库里看到???,多半是连接串少了编码参数;如果 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 id="selectBorrowRecordVOList" parameterType="com.example.book.entity.BorrowQuery" resultType="com.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 test="status != null"> AND br.status = #{status} </if> </where> ORDER BY br.borrow_time DESC </select>LEFT 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 秒内找到方向。
本文还有配套的精品资源,点击获取