
简介本资源是一套基于SSMSpringSpring MVCMyBatis框架开发的完整图书借阅系统实战项目面向Java Web初学者与中级开发者旨在帮助学习者掌握企业级Web应用的分层架构设计、前后端交互及数据库操作全流程。压缩包共61个文件含14个Java业务类涵盖Controller、Service、Mapper层、9个JSP页面实现用户登录、图书检索、借阅管理等核心功能、9个XML配置文件Spring与MyBatis整合关键配置、4个SQL建表脚本books、users、borrow、admins以及CSS、JS、图片等前端资源整体813KB结构清晰、模块解耦。已有2003人学习下载提供开箱即用的可运行工程包含完整数据库初始化脚本、事务控制实现、权限基础逻辑及Tomcat部署说明是理解SSM整合原理与落地实践的典型教学案例。1. 这不是又一个“学生管理系统”SSM图书借阅系统里藏着企业级事务边界与三层解耦的真实切口很多刚学完 Spring、Spring MVC、MyBatis 单独章节的人一上手整合项目就卡在「Controller 调不到 Service」「MyBatis 报Invalid bound statement」「登录成功却跳转到 404」——不是框架不熟而是没看清 SSM 三者之间那几条关键的“胶水线”。这个基于 SSM 的图书借阅系统恰恰把这三条线全暴露在明处Spring 的Transactional如何精准控制借书/还书的原子性Spring MVC 的RequestMapping与InternalResourceViewResolver怎样协同完成/borrow?bookId123到borrow_success.jsp的路径映射MyBatis 的if动态 SQL 如何让“按书名模糊查按分类精确筛按状态过滤”共用一个 Mapper 方法。它不追求炫技的前端或高并发压测但完整覆盖了 Java Web 工程师从开发、调试到部署上线的全部真实链路数据库建表脚本books.sql等四份 SQL已按角色拆分pom.xml中spring-webmvc与mybatis-spring版本严格对齐4.3.28.RELEASE 1.3.2连 Tomcat 8.5 的context.xml里 JNDI 数据源配置都预留了占位符。适合正在啃《Spring 实战》第 5 章、刚写完 MyBatis 单元测试、但还没跑通第一个整合项目的 Java 开发者也适合需要快速验证 SSM 各层职责边界的中级工程师——你改一行Service注解就能立刻看到 Controller 层报NoSuchBeanDefinitionException这种“错误即文档”的设计比任何教程都直击要害。2. Spring 容器初始化与 Bean 生命周期管理从applicationContext.xml到SqlSessionFactoryBean的依赖注入链SSM 整合的本质是让 Spring 容器成为整个应用的“中枢神经系统”。它不仅要创建 Controller、Service、Mapper 对象更要确保这些对象之间的依赖关系在启动时就正确装配。本项目中这一过程始于web.xml中对ContextLoaderListener的声明其加载applicationContext.xml触发 Spring 容器初始化。2.1 核心配置文件结构与加载顺序applicationContext.xml并非大杂烩而是按职责分层组织context:component-scan扫描com.book.service和com.book.dao包自动注册Service和Repository类为 Beanbean iddataSource定义 HikariCP 连接池项目使用com.zaxxer.hikari.HikariDataSource其中jdbcUrl指向本地 MySQL 实例username/password默认为root/123456需按实际环境修改bean idsqlSessionFactory是 MyBatis 与 Spring 集成的关键桥梁它将dataSource注入并通过configLocation指向mybatis-config.xml该文件仅启用驼峰命名转换无其他复杂配置bean idtransactionManager声明DataSourceTransactionManager为后续Transactional提供事务管理能力tx:annotation-driven /启用基于注解的事务这是借阅操作强一致性的底层保障。提示若启动时报Cannot load driver class: com.mysql.cj.jdbc.Driver请确认pom.xml中mysql-connector-java版本为8.0.28与mybatis-spring1.3.2 兼容且jdbcUrl中必须包含serverTimezoneUTCuseSSLfalse参数否则 MySQL 8 会拒绝连接。2.2 依赖注入的显式与隐式路径观察BorrowService类其构造函数接收BorrowMapper和BookMapperService public class BorrowService { private final BorrowMapper borrowMapper; private final BookMapper bookMapper; public BorrowService(BorrowMapper borrowMapper, BookMapper bookMapper) { this.borrowMapper borrowMapper; this.bookMapper bookMapper; } }Spring 容器在实例化BorrowService时会按以下路径解析依赖查找BorrowMapper接口的代理实现类由MapperScannerConfigurer自动注册该代理类内部持有SqlSessionTemplate而SqlSessionTemplate又依赖SqlSessionFactorySqlSessionFactory依赖dataSourcedataSource依赖数据库连接参数整个链条最终指向applicationContext.xml中定义的bean iddataSource。这种层层依赖的显式声明避免了Autowired的“黑盒感”也让调试时能清晰定位若BorrowService初始化失败先检查BorrowMapper.xml是否被MapperScannerConfigurer扫描到路径是否为classpath*:mapper/*.xml再确认SqlSessionFactoryBean的mapperLocations属性是否正确配置。2.3 事务传播行为在借阅场景中的精准控制图书借阅的核心业务逻辑——“检查库存 → 扣减库存 → 插入借阅记录”——必须在一个数据库事务中完成。本项目在BorrowService.borrowBook()方法上标注TransactionalTransactional(rollbackFor Exception.class) public boolean borrowBook(Long bookId, Long userId) { // 1. 查询图书当前库存 Book book bookMapper.selectById(bookId); if (book.getStock() 0) { throw new RuntimeException(图书库存不足); } // 2. 更新库存乐观锁where stock 0 int stockUpdated bookMapper.updateStock(bookId, -1); if (stockUpdated 0) { throw new RuntimeException(库存更新失败请重试); } // 3. 插入借阅记录 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setUserId(userId); record.setBorrowDate(new Date()); borrowMapper.insert(record); return true; }此处rollbackFor Exception.class显式声明只要抛出RuntimeException或其子类如上面的new RuntimeException(图书库存不足)整个方法内所有数据库操作将回滚。注意updateStock的 SQL 使用了WHERE stock 0条件这是典型的乐观锁实践避免超卖。若两个用户同时借同一本书第二个UPDATE将因WHERE不成立而返回0行影响触发异常并回滚保证数据一致性。3. Spring MVC 请求处理流程从 DispatcherServlet 到视图解析器的全链路追踪Spring MVC 的核心是DispatcherServlet它作为前端控制器Front Controller拦截所有匹配/的请求并将请求分发给合适的Controller。本项目的web.xml中明确配置了其加载顺序和初始化参数servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-value/WEB-INF/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-startup1/load-on-startup确保DispatcherServlet在容器启动时立即初始化早于其他 Servlet。3.1 Controller 层的请求映射与参数绑定BookController是典型的 RESTful 风格实现其RequestMapping注解定义了资源路径与 HTTP 方法Controller RequestMapping(/book) public class BookController { Autowired private BookService bookService; // GET /book/list?categoryJavakeywordSpring RequestMapping(value /list, method RequestMethod.GET) public String listBooks(RequestParam(required false) String category, RequestParam(required false) String keyword, Model model) { ListBook books bookService.findBooksByCategoryAndKeyword(category, keyword); model.addAttribute(bookList, books); return book/list; // 逻辑视图名 } // POST /book/add RequestMapping(value /add, method RequestMethod.POST) public String addBook(Valid ModelAttribute(book) Book book, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return book/add; // 返回表单页显示校验错误 } bookService.addBook(book); redirectAttributes.addFlashAttribute(message, 图书添加成功); return redirect:/book/list; } }关键点在于RequestParam将 URL 查询参数如?categoryJava自动绑定到方法参数ModelAttribute(book)将表单字段input namename映射到Book对象的name属性Valid触发 JSR-303 校验Book类中NotBlank等注解BindingResult必须紧跟其后才能捕获错误RedirectAttributes.addFlashAttribute()将消息存入 session供重定向后的页面读取避免重复提交。3.2 视图解析器配置与 JSP 渲染机制spring-mvc.xml中定义了InternalResourceViewResolverbean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp/ / property namesuffix value.jsp / /bean当BookController.listBooks()返回book/list时该解析器将其拼接为/WEB-INF/jsp/book/list.jsp并交由容器内置的 JSP 引擎如 Tomcat 的 Jasper编译执行。JSP 页面中通过${bookList}访问Model中的数据其底层是HttpServletRequest.setAttribute()的封装。注意若访问/book/list返回 404请检查spring-mvc.xml是否被DispatcherServlet正确加载init-param路径是否准确以及book/list.jsp文件是否确实位于/WEB-INF/jsp/book/目录下。JSP 文件不能放在webapp根目录否则可被直接访问存在安全风险。3.3 静态资源放行与拦截器配置默认情况下DispatcherServlet会拦截所有请求包括/css/style.css这类静态资源。本项目在spring-mvc.xml中通过mvc:resources放行mvc:resources mapping/static/** location/static/ / mvc:resources mapping/images/** location/images/ /这意味着/static/js/app.js将由容器直接返回不经过 Spring MVC 流程大幅提升性能。此外项目还配置了LoginInterceptor用于拦截未登录用户的/borrow/*请求public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; // 中断请求 } return true; // 放行 } }该拦截器在spring-mvc.xml中注册作用于/borrow/**路径确保只有登录用户才能执行借阅操作。这是权限控制的第一道防线比在每个 Controller 方法里写if (session null)更优雅。4. MyBatis 数据持久层实现从 Mapper 接口到动态 SQL 的数据库交互细节MyBatis 在本项目中承担着“SQL 编写者”与“结果集映射器”的双重角色。它不隐藏 SQL而是将 SQL 从 Java 代码中剥离放入 XML 文件使数据库操作更易维护、更易复用。4.1 Mapper 接口与 XML 文件的严格对应BookMapper.java是一个纯接口Repository public interface BookMapper { Book selectById(Long id); ListBook selectByCategory(String category); int updateStock(Param(bookId) Long bookId, Param(delta) int delta); ListBook findBooks(Param(category) String category, Param(keyword) String keyword); }其对应的BookMapper.xml必须满足namespace属性值为com.book.dao.BookMapper即接口全限定名每个select、update标签的id与接口方法名完全一致resultMap定义Book实体与数据库字段的映射关系id字段映射到bookId属性。mapper namespacecom.book.dao.BookMapper resultMap idBookResultMap typecom.book.entity.Book id propertybookId columnid/ result propertyname columnname/ result propertyauthor columnauthor/ result propertycategory columncategory/ result propertystock columnstock/ result propertypublishDate columnpublish_date/ /resultMap select idselectById resultMapBookResultMap SELECT * FROM books WHERE id #{id} /select /mapper#{id}是预编译参数占位符防止 SQL 注入resultMap中columnpublish_date与propertypublishDate的映射依赖mybatis-config.xml中setting namemapUnderscoreToCamelCase valuetrue/的全局配置否则需手动写columnpublish_date→propertypublishDate。4.2 动态 SQL 实现多条件组合查询图书检索功能需支持“按分类筛选”、“按关键词搜索”或两者兼有。硬编码多个 SQL 语句会导致冗余MyBatis 的if标签完美解决此问题select idfindBooks resultMapBookResultMap SELECT * FROM books where if testcategory ! null and category ! AND category #{category} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY publish_date DESC /selectwhere标签会智能处理 SQL 中的AND关键字若所有if条件都不成立则不生成WHERE子句若只成立一个if则自动添加WHERE若成立多个则用AND连接且不会出现多余的AND。CONCAT(%, #{keyword}, %)实现模糊匹配test表达式中的! 防止空字符串导致的意外查询。4.3 数据库脚本分析与实体关系映射项目提供的四份 SQL 脚本揭示了核心实体关系books.sql定义books表主键id字段含name,author,category,stock,publish_dateusers.sql定义users表主键id字段含username,password,real_name,role区分普通用户与管理员borrow.sql定义borrow_records表主键id外键book_id关联books.id、user_id关联users.id字段含borrow_date,return_date,statusBORROWED, RETURNEDadmins.sql定义admins表主键id字段含username,password用于后台登录。这种设计符合图书馆业务一本图书可被多人借阅一对多一个用户可借多本书一对多borrow_records是典型的关联表。Book和User实体类中未定义对方集合说明项目采用“按需查询”策略而非 MyBatis 的association或collection嵌套查询降低了首次加载的开销。5. 项目启动、调试与常见故障排查从 Tomcat 部署到 SQL 语法错误的逐层定位将项目成功运行起来是理解 SSM 整合的第一步。本节提供一套可复现的启动流程并针对高频报错给出精准定位方法。5.1 标准化部署步骤与环境检查清单数据库准备在 MySQL 中创建bookdb数据库依次执行books.sql、users.sql、borrow.sql、admins.sql四个脚本修改数据源配置打开src/main/resources/jdbc.properties更新jdbc.url、jdbc.username、jdbc.passwordIDE 导入项目使用 IntelliJ IDEA 或 Eclipse选择Import Project→Maven等待依赖下载完成配置 Tomcat在 IDE 中添加 Tomcat ServerDeployment选项卡中Application context设为/book启动服务点击Run观察控制台日志关键成功标志为INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet dispatcher: initialization completed in 1234 ms INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 2345 ms提示若启动卡在Initializing Spring root WebApplicationContext...大概率是applicationContext.xml中dataSource配置错误URL、用户名、密码或 MySQL 服务未启动。此时应检查catalina.out日志末尾的Caused by:堆栈。5.2 五类高频报错的根因与修复命令报错现象根本原因修复命令/操作java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServletpom.xml中spring-webmvc依赖缺失或版本冲突检查pom.xml确保artifactIdspring-webmvc/artifactId存在且version4.3.28.RELEASE/version与spring-core一致执行mvn clean compile重新编译org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.book.dao.BookMapper.selectByIdBookMapper.xml未被MapperScannerConfigurer扫描到检查applicationContext.xml中bean classorg.mybatis.spring.mapper.MapperScannerConfigurer的basePackage是否为com.book.dao确认BookMapper.xml文件在src/main/resources/mapper/目录下非java/下HTTP Status 404 – /book/login.jspDispatcherServlet拦截了.jsp请求且未配置mvc:resources放行在spring-mvc.xml中添加mvc:resources mapping/login.jsp location/login.jsp /或更规范地将登录页改为login.html并放行/login.htmlorg.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [INSERT INTO borrow_records ...]; Column user_id cannot be nullBorrowRecord对象的userId字段未赋值或前端表单未提交userId在BorrowController.borrowBook()方法中打印System.out.println(userId: userId);确认RequestParam是否正确获取检查borrow.jsp表单中是否有input typehidden nameuserId value${sessionScope.user.id}java.sql.SQLException: The server time zone value XXX is unrecognizedMySQL 8 时区配置与 JDBC URL 不匹配修改jdbc.properties中jdbc.url在末尾添加serverTimezoneAsia/ShanghaiuseSSLfalse5.3 验证事务生效的实操技巧要确认Transactional是否真正起作用可进行如下压力测试在BorrowService.borrowBook()方法开头添加Thread.sleep(5000);模拟长事务启动两个浏览器窗口同时访问/book/borrow?bookId1userId1观察数据库books表中stock字段若第一个请求成功扣减为9第二个请求应因WHERE stock 0失败而回滚stock保持9若stock变为8说明事务未生效需检查BorrowService类是否被Service注解且被component-scan扫描到以及Transactional是否在 public 方法上。这种“人为制造竞争”的验证方式比阅读文档更能建立对事务边界的肌肉记忆。本文还有配套的精品资源点击获取