☰
SSM图书管理系统源码实战:从Spring MVC到MyBatis完整整合教程
2026/10/23 16:48:48 网站建设 项目流程

简介:这套基于SSM(Spring MVC + Spring + MyBatis)的图书信息管理系统项目包,面向正在学习Java Web框架整合、需要完成课程设计或毕业设计的开发者。项目围绕图书信息管理实现了控制层、业务层和持久层代码,可在MyEclipse或Eclipse中结合Tomcat与MySQL运行,技术栈既有SSM整合方式,也保留了JSP+Servlet+JavaBean+JDBC的实现思路,适合作为SSM入门到进阶的参考模板。

压缩包共包含188个文件,整体大小约55.89MB。其中103个jar依赖包提供了SSM框架运行所需的第三方库;28个xml配置文件覆盖Web部署描述、Spring容器装配和MyBatis映射等关键配置;12个Java源码与12个class文件展示控制层、业务层、持久层的分层写法;7个JSP页面构成前端交互界面;另有2个SQL脚本用于初始化数据库表结构,1个docx文档可作为项目说明参考。目前已有1119人学习下载,对框架初学者有较强实用性。

通过这套资源,读者能够获得一套结构完整的SSM项目模板,学习请求流转、接口映射、事务整合等核心知识点,同时可参考配置文件与SQL脚本快速搭建自己的后台管理系统,节省从零搭建环境的时间。

1. 图书信息管理系统(二):一个能把 SSM 流程完整串起来的项目包

A同学前几天拿着一份图书管理系统源码来找我调 bug,登录页面一点提交就 500。我看了下控制台,是 Spring MVC 包扫描路径写错了,Controller 根本没被注册。这种问题在刚接触 SSM 组合的开发手里太常见了。这个基于 Spring MVC + Spring + MyBatis 的图书信息管理系统(二)源码包,是把三者协作关系理清的一套完整范例:登录、读者管理、图书管理、借还记录都齐全。适合刚学完框架想找个真实项目练手的人,也适合要交课程设计的在校生拿来改造成自己的系统。下面我会从架构分工讲到配置、数据库、复现步骤,最后落到一个加分项的改造。

2. SSM 三件套怎么协作:从 URL 到数据库的完整链路

很多人在单独学 Spring、Spring MVC、MyBatis 时觉得都懂,一合起来就晕。这个项目的价值就在于它把三者分工摆得很清楚。先看职责,再看调用链,之后配置文件才能看懂。

2.1 谁在管请求、谁在管业务、谁在管 SQL

这套系统是标准的 SSM 三层结构。Spring MVC 只管 Web 层:接收 HTTP 请求,根据 URL 找到对应的 Controller 方法,把前端提交的参数绑定成 Java 对象,最后把返回的 ModelAndView 交给视图解析器渲染成 JSP。Spring 容器管的是 Service 层的 Bean:Controller 里用 @Autowired 注入 Service 接口,实际对象由容器创建,事务也在 Service 层生效。MyBatis 管的是数据访问:Mapper 接口定义方法,XML 文件写 SQL,运行时由 MyBatis 生成实现类,把 ResultSet 映射成实体。

一次完整的登录请求链路是这样的:

前端提交表单 → DispatcherServlet 截获 → HandlerMapping 匹配 /login 到 LoginController 的 login() 方法 → Controller 调用 UserService.findUserByUsername() → UserService 注入 UserMapper → UserMapper 对应 UserMapper.xml 里的 select 语句执行 → MyBatis 将数据库记录映射成 User 对象 → 返回给 Service → Controller 判断密码后跳转页面。

这个链路理清了,后面看项目代码就不会迷路。

2.2 项目包结构:一张图看懂代码放哪

打开源码包,先看包结构。这个项目没有用 Maven 多模块,就是单模块 Web 工程,包名是 com.bookms,下面分成五个子包:

com.bookms ├── common │ ├── Page.java // 分页对象 │ └── Result.java // 统一返回结果封装 ├── controller │ ├── BookController.java │ ├── ReaderController.java │ ├── BorrowController.java │ └── LoginController.java ├── mapper │ ├── BookMapper.java │ ├── ReaderMapper.java │ └── BorrowMapper.java ├── pojo │ ├── Book.java │ ├── Reader.java │ └── Borrow.java └── service ├── BookService.java ├── ReaderService.java ├── BorrowService.java └── impl ├── BookServiceImpl.java └── ...

这里的 pojo 对应数据库表,每个字段对应表里的列,MyBatis 通过 resultType 自动映射。controller 层只做参数接收和逻辑转发,不写 SQL;service 接口和 impl 分离,事务注解写在 impl 上。按这个分层去加功能,会顺手很多。

我一般拿到分包不清晰的 SSM 项目,第一件事就是按这个骨架重新归类,否则后面 MyBatis 的 mapper 扫描会出问题。

2.3 核心配置文件:web.xml、spring-mvc.xml、spring-mybatis.xml

SSM 项目里配置文件是重灾区。这个项目一共有三个核心 XML,各自管一件事。先看 web.xml:

<web-app version="3.0"> <display-name>bookms</display-name> <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> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>springMvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>

这里的关键是 DispatcherServlet 的 url-pattern 配成/,表示所有请求都交给 Spring MVC 处理,但这样静态资源(css、js、图片)也会被拦截,所以等会儿在 spring-mvc.xml 里必须放行静态资源。CharacterEncodingFilter 必须在最前面,不然中文乱码。load-on-startup 设为 1,让容器启动时就初始化 Spring 容器。

接着看 spring-mvc.xml:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="..."> <context:component-scan base-package="com.bookms.controller"/> <mvc:annotation-driven/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>

component-scan 只扫描 controller 包,Service 和 Mapper 不在这里注册,它们归 Spring 根容器管。annotation-driven 是开启 @ResponseBody、@RequestBody 等注解的必要项,这个漏了接口会全部报 415 或 404。default-servlet-handler 就是放行静态资源的,加上之后浏览器才能加载登录页的 css。

然后是管数据源的 spring-mybatis.xml:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="..."> <context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <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> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.bookms.pojo"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.bookms.mapper"/> </bean> </beans>

mapperLocations 把 mapper 包下的 XML 都加载进去;MapperScannerConfigurer 扫描 Mapper 接口,生成的实现类由 Spring 管理。这一步很容易漏,漏了就会在 Service 注入时直接启动失败。mapUnderscoreToCamelCase 设为 true 后,数据库里的 publish_date 就能自动映射成 publishDate,少写很多 resultMap。

注意:这个项目用的是 Druid 连接池,如果你本机没配 Druid 依赖,也可以换成 CommonsDbcp2,但要注意 driverClassName 要跟着换,否则数据源初始化会报 ClassNotFoundException。

3. 数据库设计与 MyBatis 映射:图书、读者、借阅记录三张表的联动

一个图书管理系统最核心的不是页面,是数据表之间怎么约束。这个项目一共有三张主表:图书表、读者表、借阅记录表。理解了这三张表,整个系统的业务逻辑就透了一半。

3.1 建表语句与字段设计

打开资源包里的 db/bookms.sql,里面是完整的建表和测试数据。核心建表语句长这样:

CREATE TABLE book ( book_id INT AUTO_INCREMENT PRIMARY KEY, book_name VARCHAR(100) NOT NULL, isbn VARCHAR(20) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), publish_date DATE, total_count INT DEFAULT 1, lend_count INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1可借 0不可借' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE reader ( reader_id INT AUTO_INCREMENT PRIMARY KEY, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20), password VARCHAR(64) NOT NULL, max_borrow INT DEFAULT 5 COMMENT '最大借书数量' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE borrow_record ( borrow_id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0借出 1已还 2逾期', KEY idx_book (book_id), KEY idx_reader (reader_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

注意 borrow_record 里有两个外键字段,但没有直接写 FOREIGN KEY 约束。这个项目是在 Service 层手动校验读者是否存在、图书是否可借,外键约束写在 SQL 里反而会让测试数据插入顺序变麻烦。生产系统我会加物理外键,课程项目或者小系统用逻辑外键更灵活。字段类型上,日期用 DATE,借还时间用 DATETIME,不要拿字符串存日期,否则后面统计逾期会很痛。

3.2 Mapper 接口与 XML:动态 SQL 在哪用

MyBatis 的 Mapper 接口只有方法签名,真正的 SQL 写在 mapper/BookMapper.xml 里。看一下 BookMapper.java 和对应 XML:

public interface BookMapper { List<Book> searchBooks(@Param("name") String name, @Param("status") Integer status, @Param("offset") int offset, @Param("limit") int limit); int countBooks(@Param("name") String name, @Param("status") Integer status); int insertBook(Book book); int updateStatus(@Param("bookId") int bookId, @Param("status") int status); }
<select id="searchBooks" resultType="com.bookms.pojo.Book"> SELECT book_id, book_name, isbn, author, publisher, publish_date, total_count, lend_count, status FROM book <where> <if test="name != null and name != ''"> AND book_name LIKE CONCAT('%', #{name}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY book_id DESC LIMIT #{offset}, #{limit} </select>

这里用了<where>和<if>组成动态 SQL。搜索页面允许按书名模糊查,也允许按状态精确查,两个条件可空可填。<where>标签会自动去掉第一个多余的 AND,避免你拼 SQL 时还要做字符串判断。参数用#{},MyBatis 会预编译成?,防止 SQL 注入。这个习惯必须要养成,项目里所有参数传入都用#{},不要用${}直接拼表名或字段名。

countBooks 和 searchBooks 的查询条件必须保持一致。很多翻车现场就是列表查出来 10 条,分页总条数却对不上,原因就是两条 SQL 的 where 条件不一致。我每次写这种查询都会先复制条件再改查询字段,降低漏条件的概率。

3.3 resultType 和 resultMap:什么时候必须用后者

这个项目的 book 表字段是下划线风格,而 Book 实体是驼峰风格。前面配置里开了 mapUnderscoreToCamelCase,所以大部分查询可以直接用 resultType 映射。但如果你的 SQL 里有 JOIN 查询,比如查看借阅记录时要同时显示书名和读者名,就得用 resultMap 或者给查询列起别名:

public class BorrowVO extends Borrow { private String bookName; private String readerName; }
<select id="selectBorrowList" resultType="com.bookms.vo.BorrowVO"> SELECT b.borrow_id, b.borrow_time, b.return_time, b.status, bk.book_name, r.reader_name FROM borrow_record b LEFT JOIN book bk ON b.book_id = bk.book_id LEFT JOIN reader r ON b.reader_id = r.reader_id WHERE b.status = #{status} ORDER BY b.borrow_time DESC </select>

这里 resultType 因为别名刚好和 VO 属性名一致,所以还能直接用。如果名字对不上,比如数据库叫 publishDate 而实体叫 publishTime,就必须写 resultMap 手动映射。我建议在 SSM 项目里先确认别名,实在不方便别名再上 resultMap,可读性会好很多。

4. 复现流程:从空目录到跑通登录要改的四个地方

源码包导入 IDE 后会遇到一堆环境问题。这里按我实际跑通的顺序,说清楚每一步要做什么、为什么要做。

4.1 环境版本选择:别在版本上硬踩坑

这个项目基于 Maven,JDK 1.8 或 11 都行,Tomcat 8.5 或 9 比较稳。MySQL 5.7 和 8.0 都能跑,但连接配置不同。如果本机装的 MySQL 8.0,pom.xml 里驱动版本至少是 8.0.20 以上,并且 db.properties 里要写成:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bookms?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

MySQL 5.7 的话,driver 可以用 com.mysql.jdbc.Driver,但没必要,8.x 驱动兼容 5.7,所以直接用上面这份即可。serverTimezone 是 MySQL 8 强制要用的参数,不写会报时区错误。useSSL=false 是避免驱动默认尝试 SSL 握手时报警告。

4.2 导入源码包并核对 pom 依赖

用 IDEA 或 Eclipse 导入 Maven 项目后,先看 pom.xml 是否包含这几个核心依赖:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

spring-webmvc 会连带引入 spring-context、spring-core 等基础包,所以不用逐个声明。mybatis-spring 是 SSM 整合的关键,没有它 SqlSessionFactory 没法注册成 Spring Bean。servlet-api 必须设为 provided,否则 Tomcat 会和你 war 包里的版本冲突,启动时可能出现多个 ServletContainerInitializer 的报错。

4.3 建库建表并启动 Tomcat

在 MySQL 里执行资源包中的 db/bookms.sql,我用命令行演示:

mysql -uroot -p123456 < db/bookms.sql

执行完确认一下数据:

use bookms; show tables; select * from reader;

然后启动 Tomcat。如果是在 IDEA 里,配置好 Tomcat Server 后,把 project 的 deployment 设为 war exploded,Application context 填/bookms,直接 Run。也可以用命令行方式先打出 war 包再部署:

mvn clean package -DskipTests cp target/bookms.war $CATALINA_HOME/webapps/ sh $CATALINA_HOME/bin/startup.sh

启动日志里看到SpringMVC Servlet已经在/bookms注册完成,就可以访问:

curl -I http://localhost:8080/bookms/login

返回 200 就说明跑通了。如果返回 404,优先检查 spring-mvc.xml 里的视图解析器前缀是不是 /WEB-INF/jsp/,以及 JSP 文件到底在不在那个目录下。

4.4 系统典型操作:登录、查书、借书

打开登录页,输入初始管理员账号 admin / 123456,进入主页。图书管理页会调/book/list?page=1&limit=10,返回当前页图书列表,支持按书名搜索。借书操作流程是:先查读者→查图书→确认借出,实际上会依次调用/borrow/add接口,传入 readerId 和 bookId。

这套代码里没有使用 pagehelper,分页是手写 LIMIT 实现的,所以你在改分页时要记得传 offset 和 limit 两个参数。前端页面上点击下一页,会重新拼 URL 传给后端,后端按当前页码计算 offset。

5. 避坑与排查:复现 SSM 项目时最常见的五个报错

说点血泪经验。我帮 A同学调这个系统时,遇到的几个问题几乎是 SSM 新手必经的坑。下面按现象、原因、解决三步写。

5.1 启动后访问登录页直接 404

现象:Tomcat 启动成功,但访问 http://localhost:8080/bookms/login 返回 404 页面。 原因:DispatcherServlet 的 url-pattern 配成/后,默认把对 JSP 的请求也拦截了,而 Spring MVC 找不到对应的 Handler,也没有配置默认放行静态资源。 解决:在 spring-mvc.xml 里补上<mvc:default-servlet-handler/>,或者显式配置<mvc:resources>映射静态目录。如果是视图解析器路径不对,还要检查 JSP 是否放在 /WEB-INF/jsp 下,且前缀写的是 /WEB-INF/jsp/。

5.2 登录页能打开,提交时报数据库连接失败

现象:填写登录信息后点击登录,页面显示 500 错误,控制台报 Communications link failure 或者 Access denied for user。 原因:第一种是 MySQL 的认证方式与驱动的兼容问题,第二种是密码写错。最常见的是连接 URL 里没写 serverTimezone,导致连接无法建立。 解决:把 db.properties 的 URL 改成上面那串带 serverTimezone 和 useSSL=false 的完整地址;如果账号密码确认无误,就在 MySQL 里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';刷新认证方式。注意这行命令只适用 MySQL 8,MySQL 5.7 一般不需要。

5.3 Service 调用 Mapper 时报 Invalid bound statement not found

现象:系统启动正常,但进入一个查询接口后报 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found),看起来 Mapper 方法存在,XML 也有。 原因:mapperLocations 配置没有匹配到 XML 文件。常见是 XML 文件放在 src/main/java 包下但项目没有把 xml 打包进 target/classes,或者 spring-mybatis.xml 里写的路径是classpath:mapper/*.xml,而 XML 实际在其它目录。 解决:检查 target/classes 下是否有 mapper 目录和 XML;如果没有,在 pom.xml 的 build 里加<resources>把src/main/java/**/*.xml也纳入打包。确认 spring-mybatis.xml 的 mapperLocations 路径与实际包路径一致。

5.4 插入图书后数据库里中文乱码

现象:页面输入中文书名,保存后 MySQL 里显示???或者繁体乱码。 原因:Tomcat 请求编码没设 UTF-8,且连接 URL 里也没有 characterEncoding=utf8。Spring 的 CharacterEncodingFilter 虽然配置了,但只对 POST 请求的表单参数生效,如果 GET 请求携带中文,Tomcat 默认读 ISO-8859-1,照样乱码。 解决:web.xml 里确保 encodingFilter 的 url-pattern 是/*;MySQL 建库时指定 utf8;db.properties 的 URL 里保留 characterEncoding=utf8。修改后重启 Tomcat,再清一次浏览器缓存。

5.5 借书成功但借阅记录看不到读者姓名

现象:借书流程不报错,但借阅列表里读者姓名和书名都是 null。 原因:BorrowVO 里只有 bookName、readerName 两个字段,但 MyBatis 查询结果列名是 reader_name,开启驼峰映射后可以转成 readerName,前提是 Mapper XML 中 select 没有显式指定 resultMap 覆盖这个映射。此外如果 SQL 里 LEFT JOIN 的表别名不对,也可能选不到数据。 解决:检查 Mapper XML 里 SQL 字段别名是否和 VO 属性完全对应,或者直接给查询列写别名如r.reader_name AS readerName。尽量不要在 JOIN 查询里复用 resultType 配 resultMap,容易两边不一致。

6. 进阶:给借阅流程加一个读者借阅上限校验

这个系统读者表里预留了 max_borrow 字段,默认是 5,但代码里没有真正用它做拦截。把它落到 Service 层,就是一个很好的改造点。

先在 BorrowService 接口加一个方法:

int borrowBook(int bookId, int readerId);

然后在 BorrowServiceImpl 里实现校验逻辑:

@Override @Transactional public int borrowBook(int bookId, int readerId) { Reader reader = readerMapper.selectById(readerId); if (reader == null) { throw new RuntimeException("读者不存在"); } int borrowed = borrowMapper.countUnreturned(readerId); if (borrowed >= reader.getMaxBorrow()) { throw new RuntimeException("当前已借 " + borrowed + " 本,已达上限 " + reader.getMaxBorrow() + " 本"); } Book book = bookMapper.selectById(bookId); if (book == null || book.getStatus() == 0) { throw new RuntimeException("图书不存在或不可借"); } int row = borrowMapper.insertBorrow(bookId, readerId); if (row == 1) { bookMapper.increaseLendCount(bookId); } return row; }

注意我用了 @Transactional,这样校验和插入保持在同一事务里,如果后面 bookMapper.increaseLendCount 失败,整个借书操作回滚,不会出现借阅记录插入成功但库存数量没变的脏数据。业务规则放 Service 而不是 Controller,是因为以后如果加了手机端接口,同样可以复用这套校验,不会因为换入口就绕过限制。验证时可以先造一个已借 4 本的读者,再借第 6 本,观察是否正确抛出异常。如果希望更优雅,可以把抛 RuntimeException 的地方改成一个自定义 BusinessException,上层统一捕获后转成 JSON 返回给前端。

我以前做类似功能时习惯把校验塞在 Controller 里,结果后来换了套前端页面,逻辑没法复用,只能加班重构。从那以后,我每次往系统里加规则,都强制先落到 Service 层,再考虑谁调用它。希望这个 SSM 项目的改造思路也能帮你少走一段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询