
简介这是一份基于Spring MVC Spring MyBatis的图书信息管理系统完整源码包面向正在学习SSM框架整合或需要完成课程设计的Java学习者。项目采用经典三层架构包含TomcatMySQL环境下的完整可运行工程也可参考JSPServletJavaBeanJDBC的实现思路。压缩包共188个文件约55.89MB其中包含103个jar依赖库、28个xml配置与映射文件、12个Java源文件及对应class文件、7个JSP页面、2个SQL脚本等目录结构清晰便于直接导入MyEclipse/Eclipse运行。SQL脚本提供建库建表语句配置文件和jar包齐全适合快速着手。已有1119人参与学习下载。资源还附带docx说明文档可帮助理解项目模块划分与业务逻辑从内容预览可看到控制器、服务层与持久层相关的典型SSM类覆盖Controller-Service-Mapper完整调用链对掌握注解开发、MyBatis映射及Spring事务管理有直接参考价值。1. 这套SSM图书管理系统能跑通就算赢能改明白才算懂如果你正在找一套能直接跑起来的 SSMSpring Spring MVC MyBatis整合项目又不想被各种版本冲突和配置报错耗掉一晚上这套以图书信息管理为业务载体的源码包基本就是为你准备的。它解决的问题很具体把三大框架的整合步骤落成一整套可运行的工程从数据库脚本、Maven 依赖到后端三层代码一次给齐适合刚学完框架但还没独立整合过、或在课程设计里需要一个完整 Web 项目的从业者。我把这套项目从头到尾拆了一遍配置、代码、坑都写在下面照着跑一遍你对 SSM 的理解会比只看教程深得多。2. 工程骨架与整合原理先把三个框架的“职责边界”理清楚2.1 Maven 工程结构与依赖版本先看 pom.xml 再动手打开这套资源里的工程目录首先见到的就是标准的 Maven 结构。src/main/java下面按controller、service、mapper、entity分包src/main/resources里放着 Spring 和 MyBatis 的配置文件src/main/webapp下面是 JSP 页面和 web.xml。这个分包结构本身就是 SSM 的标准范式你以后做任何企业级项目看到的都会是这个影子。依赖这块pom.xml 里最核心的就是这几个坐标spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java以及 Jackson 用于 JSON 转换。这里有个容易被忽略的点Spring 的版本必须统一比如都用 5.x 的某一个具体版本最怕的就是spring-webmvc是 5.2 而spring-context是 5.3这种不统一会导致运行时出现NoSuchMethodError排查起来极其痛苦。MyBatis 和 mybatis-spring 的兼容性也要注意MyBatis 3.5 之后搭配 mybatis-spring 2.x 是主流选择。!-- pom.xml 核心依赖片段仅列出关键坐标 -- properties spring.version5.2.22.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies !-- Spring 核心 MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- Spring JDBC 事务 -- dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合桥接 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.26/version /dependency /dependencies上面这段依赖里spring-jdbc不是可有可无的它提供了DataSourceTransactionManager是声明式事务的基础。如果你只加了spring-webmvc而漏了它事务注解Transactional会直接失效而且不报错——这种静默失败比报错还难查。版本统一是这套项目能跑起来的第一道门槛新手最容易在这里翻车。2.2 web.xml 与 Spring 容器关系谁先启动谁管谁web.xml是整个 Web 应用的入口它决定了一个请求进来之后先经过谁。这套项目里配置了两个核心组件ContextLoaderListener和DispatcherServlet。ContextLoaderListener负责启动 Spring 根容器加载applicationContext.xml或这项目里的spring-mybatis.xml管的是 service、mapper、数据源这些业务层组件DispatcherServlet是 Spring MVC 的前端控制器它加载的是spring-mvc.xml管的是 controller 和视图解析器。两个容器的父子关系决定了 controller 能注入 service但反过来不行。!-- web.xml 关键配置 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup设为 1意思是 Tomcat 启动时就初始化 DispatcherServlet而不是等第一个请求进来才初始化。很多人会遇到“第一次访问接口特别慢甚至超时”的问题多半就是这里没配。url-pattern配成/意味着所有请求都交给 Spring MVC 处理包括静态资源所以后面必须在 spring-mvc.xml 里额外放行静态资源否则 CSS、JS、图片全部 404。2.3 Spring 配置文件的拆分逻辑为什么必须有两份 XML这套项目把配置文件按职责拆成了两份spring-mybatis.xml管数据源、SqlSessionFactory、Mapper 扫描和事务管理器spring-mvc.xml管注解驱动、静态资源放行和视图解析器。这个拆分不是随便分的——根容器加载的组件不能重复扫描 controller否则会出现一个 controller 被两个容器各实例化一次的问题表现为Transactional失效或注入的 service 为 null。!-- spring-mybatis.xml 核心配置 -- context:component-scan base-packagecom.xxx.service context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/library_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueroot/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.xxx.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.xxx.mapper/ /beancomponent-scan里的exclude-filter是新手最容易漏的如果根容器把 controller 也扫进去了Spring MVC 的子容器就会因为找不到对应的 Handler 而报 404或者出现“一个 Bean 两个实例”的诡异问题。SqlSessionFactoryBean的mapperLocations指定了 SQL 映射 XML 的位置typeAliasesPackage可以让你在 mapper XML 里直接写Book而不是全限定名com.xxx.entity.Book代码会清爽很多。数据源用的是 Druid 而不是 Spring 自带的基础配置这个选择很务实Druid 自带监控面板和连接池统计调试时能直接在浏览器看 SQL 执行情况比 C3P0 直观得多。URL 里的serverTimezoneAsia/Shanghai是在 MySQL 8.x 下必须加的不加会直接报时区异常这是很多人拿着旧项目的配置跑新数据库时的典型翻车点。3. 图书管理功能从表到页面一条请求的完整生命周期3.1 数据库脚本与实体映射先把 Book 这张表设计明白这套资源里带了一份library_db.sql直接导入 MySQL 就能用。图书表的核心字段无外乎这几个id、book_name、author、publisher、isbn、category、stock、create_time。如果你拿到的 SQL 里没有create_time我一般会建议你自己补上没别的原因——后续做“最新入库图书”这种需求时没有时间字段你连排序都做不了。CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 图书ID, book_name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT NULL COMMENT 作者, publisher VARCHAR(80) DEFAULT NULL COMMENT 出版社, isbn VARCHAR(20) DEFAULT NULL COMMENT ISBN号, category VARCHAR(30) DEFAULT NULL COMMENT 分类, stock INT DEFAULT 0 COMMENT 库存, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;对应的实体类Book.java就是一个标准的 POJO字段、getter、setter。这里有个细节create_time在数据库里是DATETIMEJava 侧用java.util.Date或LocalDateTime都行。如果用LocalDateTime需要在 MyBatis 的配置里确认没有启用mapUnderscoreToCamelCase的兼容问题——实际上 MyBatis 3.5 对这个支持得很好下划线转驼峰会帮你把create_time自动映射到createTime不需要手写 resultMap。3.2 Mapper 接口与 XML动态 SQL 是查询的底气Mapper 层直接和数据库打交道是这套系统里最有含金量的部分。BookMapper是一个接口SQL 写在BookMapper.xml里。核心方法无非几个selectAll、selectById、insert、update、delete再加一个带条件的分页查询。// BookMapper.java public interface BookMapper { ListBook selectAll(); Book selectById(Integer id); int insert(Book book); int update(Book book); int deleteById(Integer id); ListBook selectByCondition(Param(bookName) String bookName, Param(category) String category); }!-- BookMapper.xml 动态 SQL 示例 -- select idselectByCondition resultTypeBook SELECT id, book_name, author, publisher, isbn, category, stock, create_time FROM book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY id DESC /selectwhere标签是 MyBatis 动态 SQL 里最常用的一个它会自动去掉第一项多余的AND。如果你不用where而是写死WHERE 11也不是不行但where标签的语义更干净。#{bookName}是预编译占位符能防 SQL 注入任何时候都不要用${}拼接用户输入。resultType直接写Book是因为前面在SqlSessionFactoryBean里配置了typeAliasesPackage否则这里要写全限定名。3.3 Service 层与事务边界一个方法一个事务Service 层是业务规则落地的地方。这套项目里BookService和BookServiceImpl的职责很单一——直接调 Mapper没有太复杂的业务逻辑。但它示范了一个关键姿势事务边界放在 Service 方法上而不是 Controller 或 Mapper 层。// BookServiceImpl.java Service Transactional public class BookServiceImpl implements BookService { Autowired private BookMapper bookMapper; Override public ListBook getBookList(String bookName, String category) { return bookMapper.selectByCondition(bookName, category); } Override Transactional(rollbackFor Exception.class) public void addBook(Book book) { if (book.getStock() null || book.getStock() 0) { throw new RuntimeException(库存不能为负数); } bookMapper.insert(book); } }类上标注Transactional表示这个类里所有 public 方法都纳入事务管理方法上再标注则是局部覆盖。注意rollbackFor Exception.class这个属性——默认情况下 Spring 事务只在遇到RuntimeException时才回滚如果你抛的是一个自定义的Exception子类不指定rollbackFor的话事务会继续提交这是很多人在“明明抛了异常但数据还是写进去了”这个现象上浪费几个小时的原因。3.4 Controller 与视图渲染URL 是怎么一步步走到 JSP 的Controller 层负责接收前端请求、调用 service、把数据塞进 Model然后返回视图名。这套项目里的图书管理 Controller 主要处理三个请求列表页、新增、删除。// BookController.java Controller RequestMapping(/book) public class BookController { Autowired private BookService bookService; RequestMapping(/list) public String list(String bookName, String category, Model model) { ListBook books bookService.getBookList(bookName, category); model.addAttribute(books, books); return book/list; // 对应 /WEB-INF/views/book/list.jsp } RequestMapping(/add) public String add(Book book) { bookService.addBook(book); return redirect:/book/list; } RequestMapping(/delete) public String delete(Integer id) { bookService.deleteById(id); return redirect:/book/list; } }RequestMapping(/book)是在类级别定义前缀方法上再补具体路径这样 URL 是/book/list、/book/add语义清晰。list方法的参数bookName、category是直接绑定到请求参数的Spring MVC 会自动把?bookNamexxxcategoryyyy映射到方法参数上不需要手写request.getParameter()。返回redirect:/book/list是重定向用来避免表单重复提交——用户按一次 F5 就重复插入一条记录的问题靠的就是这个重定向而不是 forward。视图解析器在 spring-mvc.xml 里配好前缀/WEB-INF/views/和后缀.jsp所以返回值book/list会被解析到book/list.jsp。4. SSM 整合避坑指南六个高频报错对应的真实原因4.1 启动报Invalid bound statement (not found)——mapper XML 没被扫描到现象Tomcat 启动不报错但一访问/book/list就抛Invalid bound statement (not found): com.xxx.mapper.BookMapper.selectByCondition。原因最常见的有两个。一是spring-mybatis.xml里的mapperLocations配的路径不对比如写成classpath:mapper/*.xml但实际 XML 在mapper/book子目录下二是你的 mapper XML 里的namespace写错了没有和接口的全限定名保持一致。解决先检查mapperLocations的路径是否和实际资源目录一致再用 IDE 打开编译后的 target/classes 目录确认 XML 有没有被复制进去——很多时候是 Maven 默认不把 src/main/java 下的 XML 打进 classes这时候要在 pom.xml 里加resources配置把 XML 目录显式包含进去。4.2 请求能进 Controller 但静态资源 404——拦截器把 JS/CSS 吞了现象页面 JSP 能显示但 CSS 完全没有样式Chrome 控制台一堆 404。原因DispatcherServlet 的url-pattern是/所有请求都走 Spring MVC你没有在spring-mvc.xml里放行静态资源。解决加mvc:resources mapping/static/** location/static//并且 JSP 页面里的引用路径要写成${pageContext.request.contextPath}/static/css/style.css带上 contextPath否则部署到子路径时全部失效。4.3 页面能出数据但createTime是 null——驼峰映射没开现象列表页其他字段都有值只有createTime是 null数据库里明明有数据。原因数据库字段是create_timeJava 属性是createTime默认是不匹配的。有些人会手写 resultMap但漏了映射关系。解决在spring-mybatis.xml的 SqlSessionFactory 配置里加一个属性property nameconfigurationProperties props prop keymapUnderscoreToCamelCasetrue/prop /props /property或者直接用property namemapUnderscoreToCamelCase valuetrue/MyBatis 3.x 支持驼峰自动映射。加完这个所有下划线字段都能自动匹配不用再手写几十行 resultMap。4.4 事务不生效——同一个类内部调用打成死结现象addBook方法里先调另一个带Transactional的方法数据库照常插入异常不回滚。原因Spring 事务是基于 AOP 代理实现的同类内部的this调用走的是 this 本身而不是代理对象所以事务注解失效。解决按依赖方向拆类——把需要事务的方法单独放在一个 Service 类里需要调用时注入另一个 Service。如果一定要留在同类里可以用AopContext.currentProxy()或者直接在方法内部用SqlSessionTemplate手动控事务但后者代码很难看。血的教训就是同类内部自调用事务就是纸糊的。4.5 MySQL 8 连接报Public Key Retrieval is not allowed现象数据源连接报Public Key Retrieval is not allowed或者Connection refused。原因MySQL 8 默认用 caching_sha2_password 认证驱动 8.x 首次连接需要获取 RSA 公钥。解决在 JDBC URL 后面加allowPublicKeyRetrievaltrueamp;useSSLfalse。完整的 URL 应该是jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse这一串参数看着啰嗦但没有绕过去的空间缺一个就会在某些环境下翻车。4.6 JSP 页面 EL 表达式原样输出——web.xml 版本问题现象页面上直接显示${book.bookName}而不是具体书名。原因web.xml 用的是 2.3 版本的低版本声明该版本默认不开启 EL 表达式。解决把 web.xml 头部改成 3.1 或 4.0 的 schema。以 3.1 为例web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 /web-app如果项目里没有 web.xmlServlet 3.0 可以用注解和代码配置代替但你这套项目是带 web.xml 的老式结构就直接把版本升上去。5. 让这套代码从“能跑”变成“能扛”三层优化与验证习惯5.1 优化一Controller 瘦身引入统一的返回结构把现有的BookController改造一下让它不要直接操作数据库和返回 ModelAndView 混着来而是抽取一个Result统一返回 JSON。这套项目虽然前端是 JSP 渲染但以后接小程序或前后端分离时你的 controller 能直接复用。public class ResultT { private Integer code; // 200 成功500 失败 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg ok; r.data data; return r; } }改造之后BookController里加一个/listJson方法返回ResultListBook配合 Jackson 自动转 JSON调试前端联调时直接用 Postman 打这个接口根本不需要启动页面。5.2 优化二分页查询接 PageHelper不要手写 LIMIT现在的代码是selectByCondition一把梭数据量一上来就卡。最省力的方案是引 PageHelper只改两行// 分页查询改造 public ListBook getBookList(int pageNum, int pageSize, String bookName, String category) { PageHelper.startPage(pageNum, pageSize); ListBook books bookMapper.selectByCondition(bookName, category); PageInfoBook pageInfo new PageInfo(books); return pageInfo.getList(); }PageHelper.startPage(pageNum, pageSize)是静态方法必须在查询语句执行前的最近一行调用中间不能有任何其他代码否则分页不生效——这属于该框架的设计限制用久了就习惯了。返回的PageInfo里还带着total、pageNum、pages这些分页元数据可以直接塞给前端做分页条。5.3 优化三参数校验前置到 Controller现在addBook的库存校验写在 Service 里但如果所有参数校验都堆在 Service 里一个方法会越写越胖。建议在 Controller 层先做基础校验不在业务层做RequestMapping(/add) public String add(Book book) { if (book.getBookName() null || book.getBookName().trim().isEmpty()) { return redirect:/book/list?errorbookNameEmpty; } if (book.getStock() null || book.getStock() 0) { return redirect:/book/list?errorstockInvalid; } bookService.addBook(book); return redirect:/book/list; }这不是摆架子——基础格式问题在入口挡掉业务层只处理真正的业务规则可以少写很多防御式代码。5.4 验证这套项目的正确姿势从控制台日志到数据库落库每次改完配置或代码不要只盯着页面看。我会强制自己走这么一遍先看 Tomcat 启动日志有没有Mapped URL path和Root WebApplicationContext的初始化记录再打开接口直接跑一次查询观察 MyBatis 打印的 SQL 是否符合预期最后去数据库SELECT * FROM book ORDER BY id DESC LIMIT 5看落库数据。这三步里任何一步不对就停下来先修不带着疑问往下走。有一次我把 Druid 的maxActive调太大把数据库连接数打满表现就是页面偶发 500看日志才发现是连接泄漏——从那以后我每次改连接池参数都强制查一遍SHOW PROCESSLIST看连接释放正不正常。这套流程看着笨但能挡掉八成的隐性故障。希望帮到你。本文还有配套的精品资源点击获取