简介:本资源是一套基于SSM(Spring+Spring MVC+MyBatis)框架开发的完整图书借阅系统实战项目,面向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?bookId=123到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类为 Bean;<bean id="dataSource">定义 HikariCP 连接池(项目使用com.zaxxer.hikari.HikariDataSource),其中jdbcUrl指向本地 MySQL 实例,username/password默认为root/123456(需按实际环境修改);<bean id="sqlSessionFactory">是 MyBatis 与 Spring 集成的关键桥梁,它将dataSource注入,并通过configLocation指向mybatis-config.xml(该文件仅启用驼峰命名转换,无其他复杂配置);<bean id="transactionManager">声明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中必须包含serverTimezone=UTC&useSSL=false参数,否则 MySQL 8+ 会拒绝连接。
2.2 依赖注入的显式与隐式路径
观察BorrowService类,其构造函数接收BorrowMapper和BookMapper:
@Service 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又依赖SqlSessionFactory; SqlSessionFactory依赖dataSource,dataSource依赖数据库连接参数;- 整个链条最终指向
applicationContext.xml中定义的<bean id="dataSource">。
这种层层依赖的显式声明,避免了@Autowired的“黑盒感”,也让调试时能清晰定位:若BorrowService初始化失败,先检查BorrowMapper.xml是否被MapperScannerConfigurer扫描到(路径是否为classpath*:mapper/*.xml),再确认SqlSessionFactoryBean的mapperLocations属性是否正确配置。
2.3 事务传播行为在借阅场景中的精准控制
图书借阅的核心业务逻辑——“检查库存 → 扣减库存 → 插入借阅记录”——必须在一个数据库事务中完成。本项目在BorrowService.borrowBook()方法上标注@Transactional:
@Transactional(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-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping><load-on-startup>1</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?category=Java&keyword=Spring @RequestMapping(value = "/list", method = RequestMethod.GET) public String listBooks(@RequestParam(required = false) String category, @RequestParam(required = false) String keyword, Model model) { List<Book> 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 查询参数(如?category=Java)自动绑定到方法参数;@ModelAttribute("book")将表单字段(<input name="name">)映射到Book对象的name属性;@Valid触发 JSR-303 校验(Book类中@NotBlank等注解),BindingResult必须紧跟其后才能捕获错误;RedirectAttributes.addFlashAttribute()将消息存入 session,供重定向后的页面读取,避免重复提交。
3.2 视图解析器配置与 JSP 渲染机制
spring-mvc.xml中定义了InternalResourceViewResolver:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" 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); List<Book> selectByCategory(String category); int updateStock(@Param("bookId") Long bookId, @Param("delta") int delta); List<Book> findBooks(@Param("category") String category, @Param("keyword") String keyword); }其对应的BookMapper.xml必须满足:
namespace属性值为com.book.dao.BookMapper(即接口全限定名);- 每个
<select>、<update>标签的id与接口方法名完全一致; <resultMap>定义Book实体与数据库字段的映射关系(id字段映射到bookId属性)。
<mapper namespace="com.book.dao.BookMapper"> <resultMap id="BookResultMap" type="com.book.entity.Book"> <id property="bookId" column="id"/> <result property="name" column="name"/> <result property="author" column="author"/> <result property="category" column="category"/> <result property="stock" column="stock"/> <result property="publishDate" column="publish_date"/> </resultMap> <select id="selectById" resultMap="BookResultMap"> SELECT * FROM books WHERE id = #{id} </select> </mapper>#{id}是预编译参数占位符,防止 SQL 注入;<resultMap>中column="publish_date"与property="publishDate"的映射,依赖mybatis-config.xml中<setting name="mapUnderscoreToCamelCase" value="true"/>的全局配置,否则需手动写column="publish_date"→property="publishDate"。
4.2 动态 SQL 实现多条件组合查询
图书检索功能需支持“按分类筛选”、“按关键词搜索”或两者兼有。硬编码多个 SQL 语句会导致冗余,MyBatis 的<if>标签完美解决此问题:
<select id="findBooks" resultMap="BookResultMap"> SELECT * FROM books <where> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR author LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY publish_date DESC </select><where>标签会智能处理 SQL 中的AND关键字:若所有<if>条件都不成立,则不生成WHERE子句;若只成立一个<if>,则自动添加WHERE;若成立多个,则用AND连接,且不会出现多余的AND。CONCAT('%', #{keyword}, '%')实现模糊匹配,test表达式中的!= ''防止空字符串导致的意外查询。
4.3 数据库脚本分析与实体关系映射
项目提供的四份 SQL 脚本揭示了核心实体关系:
books.sql:定义books表,主键id,字段含name,author,category,stock,publish_date;users.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,status('BORROWED', 'RETURNED');admins.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.password; - IDE 导入项目:使用 IntelliJ IDEA 或 Eclipse,选择
Import Project→Maven,等待依赖下载完成; - 配置 Tomcat:在 IDE 中添加 Tomcat Server,
Deployment选项卡中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.DispatcherServlet | pom.xml中spring-webmvc依赖缺失或版本冲突 | 检查pom.xml,确保<artifactId>spring-webmvc</artifactId>存在,且<version>4.3.28.RELEASE</version>与spring-core一致;执行mvn clean compile重新编译 |
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.book.dao.BookMapper.selectById | BookMapper.xml未被MapperScannerConfigurer扫描到 | 检查applicationContext.xml中<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">的basePackage是否为com.book.dao;确认BookMapper.xml文件在src/main/resources/mapper/目录下(非java/下) |
HTTP Status 404 – /book/login.jsp | DispatcherServlet拦截了.jsp请求,且未配置<mvc:resources>放行 | 在spring-mvc.xml中添加<mvc:resources mapping="/login.jsp" location="/login.jsp" />,或更规范地将登录页改为login.html并放行/login.html |
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [INSERT INTO borrow_records ...]; Column 'user_id' cannot be null | BorrowRecord对象的userId字段未赋值,或前端表单未提交userId | 在BorrowController.borrowBook()方法中,打印System.out.println("userId: " + userId);,确认@RequestParam是否正确获取;检查borrow.jsp表单中是否有<input type="hidden" name="userId" value="${sessionScope.user.id}"> |
java.sql.SQLException: The server time zone value 'XXX' is unrecognized | MySQL 8+ 时区配置与 JDBC URL 不匹配 | 修改jdbc.properties中jdbc.url,在末尾添加&serverTimezone=Asia/Shanghai&useSSL=false |
5.3 验证事务生效的实操技巧
要确认@Transactional是否真正起作用,可进行如下压力测试:
- 在
BorrowService.borrowBook()方法开头添加Thread.sleep(5000);,模拟长事务; - 启动两个浏览器窗口,同时访问
/book/borrow?bookId=1&userId=1; - 观察数据库
books表中stock字段:若第一个请求成功扣减为9,第二个请求应因WHERE stock > 0失败而回滚,stock保持9; - 若
stock变为8,说明事务未生效,需检查BorrowService类是否被@Service注解且被component-scan扫描到,以及@Transactional是否在 public 方法上。
这种“人为制造竞争”的验证方式,比阅读文档更能建立对事务边界的肌肉记忆。
本文还有配套的精品资源,点击获取