资讯动态

SSM图书管理系统实战解析:从三层架构到Spring Boot改造

发布时间:2026/10/24 1:54:10 来源:尧图企业网站定制
简介基于SSM框架实现的图书管理系统项目面向正在学习SSM整合开发的Java后端初学者或需要完成课程设计的学生用于快速掌握企业级Web应用从配置到业务落地的完整过程。资源包内共有三个文件包括项目源码压缩包、使用手册文本和数据库脚本压缩包体积约91.12兆其中手册能引导环境部署SQL文件可初始化库表。目前已有3254人学习参考。项目展示了Spring容器管理、SpringMVC请求处理以及MyBatis数据访问的协作流程通过注解与XML配置结合的方式实现业务逻辑并完成了权限登录、图书借还、邮件提醒和答题互动等功能模块同时兼顾了防SQL注入等安全细节。使用说明书与数据库脚本有助于快速搭建开发环境并初始化数据用户可在现有结构上继续扩展图书预约、借阅统计等功能适合作为毕业设计或实训项目的参考起点。1. 为什么选SSM做图书管理课程设计常客背后的工程价值“图书管理系统”和SSM框架的组合在 Java Web 领域几乎成了某种默认选项页面不花哨技术不超前但Spring管理对象、SpringMVC处理请求、MyBatis读写数据库这三件事被一个完整业务串得明明白白。这套代码最典型的用途是高校课程设计或毕业设计的起点——你拿到的不是一堆零散示例而是一个能跑通「登录 → 录入图书 → 借书 → 还书 → 查询统计」全流程的真实工程。对刚学完 Spring 基础但没见过完整项目的开发者来说它恰好补上“框架概念怎么落到实际业务”这一课事务配在哪个文件、拦截器怎么拦请求、SQL和Java对象的映射在哪里做、一个请求从浏览器到数据库再返回总共经过几层。本文按这套经典 SSM 图书管理系统逐步拆解从架构、核心代码到启动部署最后聊聊怎么把它改造成 Spring Boot 风格的项目。2. 技术栈与目录结构三层架构如何用一个请求串起来2.1 SSM 三层的职责边界SSM 不是三个框架的简单叠加而是各管一段Spring 用 IoC 容器管理 Service 层对象和数据库连接池解决解耦问题SpringMVC 负责 Web 层的路由分发把 URL 映射到 Java 方法上MyBatis 把 SQL 操作从业务代码里抽出去集中在 Mapper 接口和 XML 中。三者通过配置文件互相咬合形成了 Controller → Service → Dao 的经典分层。这里有个很多新手会混淆的点三层架构和 SSM 三个框架不是一一对应关系。Controller 层一般只做参数接收和视图跳转Service 层承载业务规则和事务边界Dao 层只负责单表或多表查询。真正的框架分工是——SpringMVC 主要影响 Controller 层和请求流程MyBatis 主要影响 Dao 层而 Spring 用 Bean 装配把所有层串起来。理解了这个错位后面读代码就不会觉得“Controller 里为什么没有 SQL、Mapper 里为什么没有 if else”是奇怪的事了。2.2 目录结构与文件清单这套系统的标准 Maven 工程布局如下src/main/java/com.library ├── controller # 控制层登录Controller、图书Controller、借阅Controller ├── service # 业务层接口 实现类事务注解在这里 ├── dao # 数据访问层Mapper接口 ├── pojo # 实体类Book、Reader、LoanRecord等 └── interceptor # 登录拦截器 src/main/resources ├── applicationContext.xml # Spring 根配置数据源、事务、扫描包 ├── spring-mvc.xml # SpringMVC 配置视图解析器、静态资源 ├── mybatis-config.xml # MyBatis 全局配置别名、驼峰映射 ├── jdbc.properties # 数据库连接参数 └── mapper # MyBatis 的 SQL 映射 XML 文件 src/main/webapp ├── WEB-INF/web.xml # Web 应用入口启动时装载 Spring 容器 └── jsp # 登录页、图书列表、借阅管理页面 pom.xml # Maven 依赖清单文件 / 目录职责controller 包接收请求参数、调用 Service、返回 ModelAndView 或字符串视图名service 包业务校验、事务控制、跨表操作编排dao 包 mapper 目录接口定义方法XML 提供 SQL 实现pojo 包与数据库表对应的 JavaBean含 getter/setterapplicationContext.xmlSpring 的 Bean 定义连接池、事务管理器、组件扫描spring-mvc.xmlSpringMVC 的控制器扫描、注解驱动、视图解析器web.xmlServlet 容器加载配置绑定 Spring 和 SpringMVC第一次打开工程时别急着跑先按上面这张表找到对应文件翻一遍。我一般习惯从 web.xml 入手因为它决定了容器启动时先加载哪个配置文件、哪些 URL 交给哪个 Servlet 处理等于整个应用的启动地图。2.3 配置文件如何串起来web.xml 是入口它做了两件关键的事配置 ContextLoaderListener 让 Spring 容器随应用启动而创建配置 DispatcherServlet 拦截所有 HTTP 请求。核心片段如下web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.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-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-appcontextConfigLocation 参数指定了 Spring 根容器的配置文件路径DispatcherServlet 的 init-param 单独指定 spring-mvc.xml形成父子容器。子容器能访问父容器的 Bean反之不行所以 Service 通常配在 applicationContext.xml 中Controller 则放在 spring-mvc.xml 的扫描范围内。这样分层后Controller 可以直接注入 Service而 Service 不会反向依赖 Controller。这里有个细节值得注意servlet-mapping 的 url-pattern 是/而不是/*这意味着 DispatcherServlet 会处理除 JSP 以外的所有请求。静态资源如果没在 spring-mvc.xml 里放行会出现 CSS、JS 加载不出来的问题。常见的解决方式是在 spring-mvc.xml 中加mvc:default-servlet-handler/让容器默认 Servlet 兜底处理静态文件。3. 核心模块拆解借一本书在 SSM 中的完整调用链3.1 登录拦截Interceptor 如何守住页面入口图书管理系统不可能让未登录用户直接访问借阅接口所以工程里通常会写一个 LoginInterceptor 实现 HandlerInterceptor 接口。这套拦截器在 SpringMVC 中比 Filter 更贴近业务它只拦截经过 DispatcherServlet 的请求可以拿到 HandlerMethod 从而精确判断控制器方法。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object admin session.getAttribute(admin); if (admin null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }这段逻辑非常简单从 Session 中取管理员对象没有就重定向到登录页。但它能拦住多少东西取决于在配置中的拦截路径通常在 spring-mvc.xml 里这样注册mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/login.jsp/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:bean classcom.library.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptorspath 是/**表示所有路径都进拦截器exclude-mapping 放行了登录接口、登录页面和静态资源。参数里最难调的就是 exclude-mapping 的粒度——放多了等于没拦放少了登录页 CSS 都加载不出来。工程实践的通用做法是只放行“登录相关 静态资源”其余业务路径一律过拦截器。3.2 借书主流程Controller 层的参数处理借书是这个系统最核心的业务它的调用链也是三层架构的缩影。Controller 层拿到的是页面表单传过来的 bookId 和 readerId 两个参数先做基础校验再交 Service。Controller RequestMapping(/loan) public class LoanController { Autowired private LoanService loanService; RequestMapping(/borrow) public String borrow(RequestParam(bookId) Integer bookId, RequestParam(readerId) Integer readerId, Model model) { boolean result loanService.borrow(bookId, readerId); if (result) { model.addAttribute(message, 借书成功); } else { model.addAttribute(message, 借书失败库存不足或读者状态异常); } return loan/result; } }RequestParam 把请求参数绑定到方法入参Model 用于向视图传递提示信息。返回值 loan/result 是视图逻辑名由 InternalResourceViewResolver 拼成/WEB-INF/jsp/loan/result.jsp。注意这里 borrow 方法的返回是 String不是 ModelAndView——这是 SpringMVC 很常见的写法字符串代表视图名Model 作为数据载体两种方式功能等价写字符串更轻。借书失败和成功的提示没有用抛异常的方式而是返回布尔值。这是中小型系统的典型取舍业务失败属于正常流程分支用返回值传递比异常捕获更直观只有系统级异常比如数据库连接失败才交给全局异常处理器。3.3 Service 层事务边界在那里生效Service 是借书流程的“大脑”。一次借阅既要扣减图书库存又要新增一条借阅记录这两步不能一成一败所以必须放在同一个事务里。Service public class LoanServiceImpl implements LoanService { Autowired private BookDao bookDao; Autowired private LoanRecordDao loanRecordDao; Override Transactional public boolean borrow(Integer bookId, Integer readerId) { Book book bookDao.selectById(bookId); if (book null || book.getStock() 0) { return false; } bookDao.decreaseStock(bookId); LoanRecord record new LoanRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setLoanTime(new Date()); record.setStatus(1); loanRecordDao.insert(record); return true; } }Transactional 标注在接口实现类的 public 方法上Spring 通过动态代理为这个方法生成事务代理。这里有几个关键参数没写全但默认值很值得注意默认传播行为是 REQUIRED即当前没有事务就新建一个默认回滚条件是 RuntimeException如果代码中抛出受检异常而没配置 rollbackFor事务不会回滚。所以严谨的写法是Transactional(rollbackFor Exception.class)。把事务放在 Service 层而不是 Controller 层原因是 Controller 只负责协议转换不该知道“步骤一、步骤二”这种业务编排。如果事务放在 Controller一个 Controller 调用两个 Service 方法时事务边界会变得模糊后期维护很难受。这个设计原则在小项目里看不出差别但一旦业务复杂Service 层的事务边界就是系统的安全线。3.4 Mapper 层SQL 与结果映射的协同借书流程里两个核心 SQL 分别是减库存和新增记录。MyBatis 有两种写法注解 SQL 和 XML SQL。图书管理系统这类需要多表关联、动态条件的项目XML 是主流选择。update iddecreaseStock UPDATE book SET stock stock - 1 WHERE id #{id} AND stock 0 /update insert idinsert parameterTypecom.library.pojo.LoanRecord INSERT INTO loan_record (book_id, reader_id, loan_time, status) VALUES (#{bookId}, #{readerId}, #{loanTime}, #{status}) /insertdecreaseStock 里 WHERE 条件带stock 0是一个典型的防并发手段即使两个请求同时读到库存为 1数据库层面也只会让一条 UPDATE 成功因为第二条执行时 stock 已经变成 0满足不了条件影响行数为 0。这个 trick 比“先查再判再更新”可靠得多是并发扣减库存的常见实现思路。Mapper 接口和 XML 的绑定关系靠的是接口全限定名与 XML 的 namespace 一致方法名与语句 id 一致。同时 MyBatis 的全局配置里建议开启驼峰映射这样数据库字段loan_time就能自动映射到 JavaBean 的loanTime属性省掉大量 resultMap 手工配置。4. 数据库设计与实体映射五张表如何支撑借阅闭环4.1 核心表结构与建表 SQL图书管理系统的数据库设计并不复杂但五张表之间关联关系清晰category 图书分类、book 图书、reader 读者、loan_record 借阅记录、admin 管理员。其中 loan_record 是关联 book 和 reader 的中枢表一次借阅就是在这张表插入一条数据同时给 book 表减库存。CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) UNIQUE, name VARCHAR(100) NOT NULL, category_id INT, stock INT DEFAULT 0, publish_date DATE, FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE reader ( id INT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 -- 1 正常0 冻结 ); CREATE TABLE loan_record ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, loan_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 1, -- 1 借出0 已还 FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) );注意 loan_record 里没有写UNIQUE(book_id)因为同一本书可以有多条历史借阅记录。如果要限制一本书同时只能被借一次应在业务层查询未归还记录时做判断——更好的做法是给 book 表加一个status字段表示在架/借出但多数课程设计项目会选择直接用库存数模拟这个状态属于实用主义的简化。4.2 实体类与 MyBatis 映射配置POJO 层的 Book 类与表字段严格对应类型使用包装类而不是基本类型这是 MyBatis 映射的硬性要求——如果使用 int 而数据库某行为 NULL映射时会因无法装箱而报错。public class Book { private Integer id; private String isbn; private String name; private Integer categoryId; private Integer stock; // 省略 getter / setter }查询图书列表时页面上通常要显示分类名称而不是分类 ID这就涉及多表关联查询。MyBatis 中的标准做法是定义一个扩展查询对象或用 resultMap 的 association 标签。resultMap idBookWithCategoryMap typecom.library.pojo.Book id propertyid columnid/ result propertyname columnname/ result propertyisbn columnisbn/ result propertystock columnstock/ association propertycategory javaTypecom.library.pojo.Category id propertyid columncategory_id/ result propertyname columncategory_name/ /association /resultMap select idselectBookWithCategory resultMapBookWithCategoryMap SELECT b.*, c.name AS category_name FROM book b LEFT JOIN category c ON b.category_id c.id WHERE b.name LIKE CONCAT(%, #{keyword}, %) /select这里把分类名称取别名为 category_name通过 association 映射到 Book 对象里嵌套的 Category 属性。SQL 用 LEFT JOIN 是为了处理图书没有分类时的场景——如果使用 INNER JOIN那些分类 ID 为空的图书会直接从查询结果中消失页面列表就少了数据这种隐藏 bug 在测试数据不全时很难发现。4.3 模糊查询与动态 SQL图书管理系统中列表页的搜索框几乎都会用到动态查询。MyBatis 的if标签就是为这种场景设计的它可以按条件拼接 WHERE 子句。select idsearchBooks parameterTypemap resultTypecom.library.pojo.Book SELECT * FROM book where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC /selectwhere标签会自动处理第一项前面的 AND避免出现WHERE AND name LIKE这种语法错误。使用 CONCAT 拼接百分号而不是直接写%${name}%是为了防止 SQL 注入——${}是字符串拼接#{} 是预编译参数占位后者才是正确姿势。很多新手在这个细节上翻车表面看功能正常参数值传个%也能跑但一旦遇到恶意输入后果是数据库层面被脱裤。MyBatis 的 #{} 对单引号会自动转义这才是动态查询该有的打开方式。5. 环境准备与启动指南六步跑通附现场排查手册5.1 Maven 依赖与 JDK 版本拿到源码第一步是确认环境版本。这套系统是典型的 Java 8 Tomcat 8.5 MySQL 5.7 组合pom.xml 的依赖核心如下dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependenciesMySQL 驱动版本要匹配数据库版本MySQL 5.x 用 5.1.49MySQL 8.x 需要换用 8.0 系列驱动且连接 URL 要加serverTimezoneAsia/Shanghai参数。如果你本机装的是 MySQL 8直接把驱动版本升级、在 jdbc.properties 的连接串里加上时区参数即可否则连接阶段就会报时区错误。5.2 数据库初始化与连接配置把 SQL 脚本导入本地数据库后修改 jdbc.properties 中的连接信息。这套字段是整个系统跑通的第一道关卡jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.passwordyour_password这里最容易踩的坑是 characterEncodingutf8 必须与数据库创建时的字符集一致。如果建库用了 utf8mb4连接串写 utf8 也能连但中文可能在某些特殊字符场景下乱码。建议两边统一用 utf8mb4连接串同步修改。密码不要写在代码里写死这是本地调试项目直接放 properties 文件没问题但提交到公开仓库前一定改掉。5.3 IDEA Tomcat 部署步骤用 IDEA 打开 Maven 工程后依次执行配置 Tomcat Server → 添加 Artifact 为 war exploded → 部署。war exploded 模式比 war 模式更适合调试修改 JSP 或 Java 文件后无需重启容器热部署能直接生效。Run Configuration 中 VM options 一般不用动但 Application context 建议设为/library这样访问路径就是http://localhost:8080/library/。applicationContext.xml 中数据源 Bean 会把 jdbc.properties 的属性通过占位符注入启动时若看到Property url not set这类报错多半是占位符没解析检查 spring 配置文件里有没有配context:property-placeholder locationclasspath:jdbc.properties/。5.4 常见问题排查从 404 到乱码的五种翻车现场现象一启动后所有页面 404但 Tomcat 正常启动。原因DispatcherServlet 的 url-pattern 写成了/*而不是/导致 JSP 请求也被 Servlet 接管而 Servlet 没有对应的 RequestMapping容器返回 404。解决回到 web.xml 把 servlet-mapping 改成url-pattern//url-pattern保存后重启。这个错误隐蔽在“Tomcat 都能启动就是访问啥都 404”排查时第一时间看 servlet-mapping 的配置。现象二JSP 页面中文全部乱码数据库中文也变成问号。原因三层编码不一致。请求到 Controller 需要 CharacterEncodingFilter 设置 UTF-8但该过滤器要放在 web.xml 过滤器链的最前面才生效数据库连接串没带 characterEncoding 参数MySQL 表默认字符集不是 utf8mb4。解决给 web.xml 加 CharacterEncodingFilter 并放在 DispatcherServlet 之前连接串补上字符集参数MySQL 端用ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4修正已有表。现象三点击借书按钮后提示“库存不足”但数据库里 stock 明明大于 0。原因不是库存真的不足而是 Service 层查询到的 Book 对象为 null。常见情况是传入的 bookId 参数没绑定上Controller 参数名和表单字段名不一致导致 ID 为 null查询自然返回空。解决检查 JSP 表单中 input 的 name 属性是否与 Controller 的 RequestParam 一致加上日志打印 bookId 值即可确认。现象四事务没回滚减库存后插入借阅记录失败库存照样少了。原因Transactional 注解标注在 private 方法上或者同类内部通过 this 调用Spring 代理没有介入。解决把注解移到 public 方法且借阅记录插入要放在不同 Service 中通过注入调用不能 this.borrow 内部直接调 this 的其他方法。每次写完事务逻辑用“数据库操作分两步以上的方法”自查一遍注解位置和调用方式。现象五XML 中 SQL 报Invalid bound statement (not found)。原因Mapper 接口找到了但对应的 XML 没被 MyBatis 加载。常见是 mapper 目录没有在 applicationContext.xml 中配置 mapperLocations或者 XML 文件没被 Maven 打包进 classpath。解决在 Spring 配置中给 sqlSessionFactory 加 mapperLocations 指向classpath:mapper/*.xml同时检查 resources 目录结构让 XML 和 jdbc.properties 都在 src/main/resources 下。6. 二次开发思路从 SSM 到 Spring Boot 的三步改造6.1 改造一把三大配置文件收拢进 application.ymlSSM 项目后期的改造方向几乎都是迁往 Spring Boot。第一步不是重写业务代码而是把 web.xml、applicationContext.xml、spring-mvc.xml 三份配置合并成一份 application.yml。依赖替换也很直接spring-webmvc 换成 spring-boot-starter-webmybatis 换成 mybatis-spring-boot-starterDispatcherServlet 由 Boot 自动装配完成过滤器链用 FilterRegistrationBean 注册。Controller、Service、Mapper 代码几乎不用动真正要调整的只有配置和入口类。SpringBootApplication MapperScan(com.library.dao) public class LibraryApplication { public static void main(String[] args) { SpringApplication.run(LibraryApplication.class, args); } }MapperScan 替代了原来 mybatis-spring 的 MapperScannerConfigurer 配置这也是改造中必须加的一行——漏掉它所有 Mapper 接口都无法注入 Service启动即报错。视图层改造可以选择保留 JSP也可以顺手换成 Thymeleaf。保留 JSP 需要额外加 spring-boot-starter-tomcat-jsp 依赖并设置视图前缀后缀换 Thymeleaf 则模板语法需要重写工作量取决于页面数量。6.2 改造二给查询加上 PageHelper 分页图书列表页的分页在很多 SSM 项目里是用手写 LIMIT 实现的改造成 Spring Boot 阶段可以把分页逻辑统一交给 PageHelper 插件。依赖引入pagehelper-spring-boot-starter后Service 层查询只需在返回 List 前调用分页方法不需要改 SQL。public PageInfoBook getBooks(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListBook books bookDao.selectAll(); return new PageInfo(books); }PageHelper.startPage 之后的第一条 MyBatis 查询会自动拼接 LIMITPageInfo 对象里封装了总页数、当前页、总记录数这些分页所需数据。这里有个经典翻车点startPage 和查询之间不能插入其他 SQL 操作否则插件会作用到错误的查询上。所以写这个方法时我习惯把 startPage 紧跟查询语句中间不放任何日志查询或缓存读取这是 PageHelper 文档里反复强调的规则。6.3 改造三借阅热点的本地缓存这种管理系统有一个共同的痛点图书列表页和借阅排行属于高频查询每次都全量查库里非常浪费。改造时我不建议直接上 Redis而是先在 Service 层加一层本地缓存用 ConcurrentHashMap 存储热门图书 ID 列表缓存过期时间设为 5 分钟被借阅时主动失效。这样不需要引入额外服务改动量小跑一段时间观察命中率再决定是否升级为 Redis。从这段改造经历中得到的最大教训是所有代码改动前必须先理清“请求从进入到返回的完整链路”。每拿到一个 SSM 项目我现在的固定顺序是看 web.xml 确认装载配置找 Controller 看路由和参数翻 Service 看事务边界盯 Mapper XML 确认 SQL 和映射逻辑四个文件看完再动手改代码——顺序不对很容易在配置层迷路。这套图书管理系统的价值也正在于此麻雀虽小但 Web 开发从配置到业务的完整链路一条不少。希望这篇梳理能帮你快速跑通它找到可以下手改造的第一行代码。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑