SSM框架实现图书借阅系统:可审计强事务设计
2026/9/16 23:03:23 网站建设 项目流程

简介:本资源是一套基于SSM(Spring+Spring MVC+MyBatis)框架开发的完整图书借阅系统源码,面向Java Web初学者与中级开发者,助力掌握企业级Web应用的分层架构设计与整合开发实践。压缩包共61个文件,含14个Java业务类(涵盖Controller、Service、Mapper及实体类)、9个JSP页面(实现用户登录、图书检索、借阅管理等核心交互)、9个XML配置文件(Spring容器、MyBatis映射及SQL定义)、4个SQL建表脚本(books、users、borrow、admins),辅以CSS、JS和图片资源,整体813KB,结构清晰、模块完整。已有2003人学习下载,资源开箱即用,包含可直接导入运行的数据库脚本、规范的MVC分层代码结构、事务控制与权限管理基础实现,是理解SSM整合原理、练手真实业务场景的优质教学案例。

1. 图书借阅系统为什么选 SSM 框架?不是 Spring Boot 就不行吗?

很多刚接手图书馆信息化改造的开发同学第一反应是:“现在都 2024 年了,还用 SSM?直接上 Spring Boot 不香吗?”——这问题很实在。但现实是:大量高校图书馆、区县级公共图书馆的存量业务系统仍基于 Java EE 体系运行,数据库多为 Oracle 或 SQL Server,中间件常为 WebLogic 或 Tomcat 7/8,运维团队熟悉传统 WAR 包部署流程,且对“自动配置”“Starter 依赖爆炸”存在安全审计顾虑。SSM(Spring + Spring MVC + MyBatis)恰恰卡在这个平衡点上:它不强制约定目录结构、不隐藏 JDBC 连接细节、允许对 SQL 执行粒度做精准控制,特别适合图书借阅这类强事务、多状态、需审计留痕的场景——比如“读者 A 借书 B”必须原子性完成:库存减 1、借阅记录插入、逾期规则校验、日志写入四步操作,缺一不可。本文不讲 Spring Boot 的快捷,专讲 SSM 如何把图书借阅的“借、还、续、查、罚”五类核心动作,用可落地、可审计、可交接的方式扎进生产环境。


2. 搭建 SSM 环境:从 Maven 依赖到 DispatcherServlet 注册

SSM 不是框架,而是三层协作模型。Spring 负责 IoC 和事务管理,Spring MVC 处理 HTTP 请求路由与参数绑定,MyBatis 实现对象与关系映射。三者版本兼容性是第一个硬门槛,尤其在 JDK 8 环境下,常见踩坑点是 MyBatis 3.4.x 与 Spring 4.3.x 的SqlSessionFactoryBean初始化失败。以下为经生产验证的最小可行依赖组合(pom.xml 片段):

<!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.30.RELEASE</version> </dependency> <!-- Spring MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.30.RELEASE</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- MyBatis-Spring 集成桥 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!-- 数据库驱动(以 MySQL 5.7 为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>

提示:spring-webmvc必须显式声明,否则@Controller类不会被扫描;mybatis-spring版本必须与 MyBatis 主版本严格匹配,1.3.2 对应 MyBatis 3.4.x,若误用 2.x 版本会导致SqlSessionTemplate构造失败。

2.1 web.xml 中注册 DispatcherServlet 与 ContextLoaderListener

SSM 项目启动入口是 Servlet 容器(如 Tomcat),而非 Spring Boot 的main()方法。web.xml是配置中枢,必须明确划分“根上下文”(Spring)和“Web 上下文”(Spring MVC):

<!-- 加载 Spring 根上下文(Service、DAO 层) --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <!-- 注册 Spring MVC 前端控制器 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/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>
2.1.1 为什么必须分离两个配置文件?
  • applicationContext.xml:定义数据源、事务管理器、Service Bean、MyBatis 的SqlSessionFactoryBeanMapperScannerConfigurer。这些组件与 Web 层无关,应由ContextLoaderListener加载。
  • spring-mvc.xml:仅定义@Controller扫描、视图解析器、静态资源处理、@RequestBody解析器等。若混入 Service Bean,会导致 Controller 被重复实例化,引发事务失效。

2.2 MyBatis 核心配置:SqlSessionFactoryBean 与 Mapper 扫描

applicationContext.xml中,MyBatis 的集成关键在于SqlSessionFactoryBean的属性设置。以下是最小必要配置(含注释说明):

<!-- 数据源(使用 Druid 连接池,比 C3P0 更稳定) --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/library?useUnicode=true&amp;characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> </bean> <!-- MyBatis SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 指向 MyBatis 全局配置文件 --> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 指向 Mapper XML 文件路径 --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 自动扫描 Mapper 接口(替代手动定义每个 Mapper Bean) --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.library.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>

注意:mapperLocations使用classpath:mapper/*.xml而非classpath:mapper/**/*Mapper.xml,因 AntPathMatcher 在 Spring 4.3 中对双星号支持不稳定,易导致 XML 文件未加载;sqlSessionFactoryBeanName必须用字符串引用,不能用ref,否则会触发提前初始化异常。

2.3 Spring MVC 配置:解决静态资源拦截与 JSON 支持

spring-mvc.xml中,常见错误是<mvc:default-servlet-handler/>缺失,导致/js/app.js返回 404。完整配置如下:

<mvc:annotation-driven> <!-- 启用 @ResponseBody 支持 JSON --> <mvc:message-converters register-defaults="true"> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>application/json;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven> <!-- 扫描 Controller --> <context:component-scan base-package="com.library.controller" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 静态资源放行(CSS/JS/IMG) --> <mvc:default-servlet-handler/> <!-- 或更精确写法(Tomcat 下推荐) --> <mvc:resources mapping="/static/**" location="/static/"/>
2.3.1 JSON 乱码的根本解法

即使配置了MappingJackson2HttpMessageConverter,中文仍可能乱码。必须在web.xml中添加字符编码过滤器:

<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> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

3. 图书借阅核心业务建模:ER 图落地与 MyBatis 映射实现

图书借阅系统不是 CRUD 堆砌,其业务逻辑集中在“借阅状态机”上:一本书的状态在在馆→已借出→已归还→已丢失→已报废间流转,而读者状态则关联正常→挂失→冻结→注销。ER 图设计必须体现这种双向约束,而非简单画出“读者-借阅-图书”三张表。

3.1 关键实体与关系定义(基于实际图书馆业务)

实体主键关键字段业务约束
reader(读者)reader_idcard_no,name,status(0=正常,1=挂失,2=冻结),borrow_limitcard_no唯一,status影响借书权限
book(图书)isbntitle,author,press,stock,status(0=在馆,1=借出,2=丢失)isbn为国际标准书号,主键兼唯一标识
borrow_record(借阅记录)record_idreader_id,isbn,borrow_date,due_date,return_date,status(0=未还,1=已还,2=逾期)reader_id+isbn组合在未还状态下必须唯一

提示:borrow_record表不设外键约束(MySQL InnoDB 可设,但生产环境常禁用),因高并发下外键检查拖慢插入性能;一致性靠 Service 层事务+SQL 显式校验保障。

3.2 MyBatis Mapper XML 实现借书核心逻辑

借书操作需同时更新三处:book.stock减 1、borrow_record插入新记录、reader的当前借阅数校验。MyBatis 通过<selectKey><update>链式执行保证原子性:

<!-- mapper/BorrowRecordMapper.xml --> <insert id="borrowBook" parameterType="com.library.entity.BorrowRecord"> <!-- 1. 校验读者是否可借(状态正常 & 未超限) --> SELECT 1 FROM reader WHERE reader_id = #{readerId} AND status = 0 AND (SELECT COUNT(*) FROM borrow_record WHERE reader_id = #{readerId} AND status = 0) < ( SELECT borrow_limit FROM reader WHERE reader_id = #{readerId} ) <!-- 2. 校验图书是否可借(库存 > 0 且状态为在馆) --> AND EXISTS (SELECT 1 FROM book WHERE isbn = #{isbn} AND stock > 0 AND status = 0) <!-- 3. 执行借书(MyBatis 会将此 SQL 作为 insert 前置校验,失败则抛异常) --> <selectKey keyProperty="recordId" resultType="java.lang.Long" order="BEFORE"> SELECT ROUND(UNIX_TIMESTAMP(NOW(3)) * 1000) AS recordId FROM DUAL </selectKey> INSERT INTO borrow_record (record_id, reader_id, isbn, borrow_date, due_date, status) VALUES (#{recordId}, #{readerId}, #{isbn}, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0) <!-- 4. 更新图书库存与状态 --> UPDATE book SET stock = stock - 1, status = 1 WHERE isbn = #{isbn} </insert>
3.2.1 为什么用ROUND(UNIX_TIMESTAMP(NOW(3)) * 1000)生成主键?
  • 避免 UUID 字符串主键导致索引碎片;
  • NOW(3)提供毫秒级时间戳,乘 1000 后取整,保证单机每秒 1000 条不重复;
  • SELECT ... FROM DUAL是 MySQL 兼容写法,Oracle 中需改为SELECT ... FROM DUAL,此处为简化示例。

3.3 Service 层事务控制:@Transactional 的精确作用域

借书操作必须包裹在事务中,但事务边界不能过大。以下为典型反例与正例对比:

// ❌ 错误:事务范围过大,包含非 DB 操作 @Transactional public void borrowBook(BorrowRecord record) { sendSmsNotification(); // 发短信耗时操作,不应在事务内 borrowRecordMapper.borrowBook(record); // 实际 DB 操作 } // ✅ 正确:事务仅包裹 DB 操作,通知异步化 @Transactional public void borrowBook(BorrowRecord record) { borrowRecordMapper.borrowBook(record); // 仅此一行在事务内 // 发短信改用 MQ 或线程池异步触发 }

注意:@Transactional默认只对RuntimeException回滚。若借书失败需回滚(如库存不足),必须确保校验失败时抛出RuntimeException子类(如IllegalStateException),而非Exception


4. 图书借阅状态机驱动:基于枚举与 SQL 的状态流转控制

借阅系统最易出错的是状态不一致:比如borrow_record.status=0(未还),但book.status=0(在馆)。SSM 项目中,状态流转不能靠应用层 if-else 判断,而应由 SQL 和数据库约束兜底。

4.1 定义状态枚举并映射到数据库字段

Java 枚举与数据库 tinyint 字段一一对应,避免魔法数字:

public enum BorrowStatus { NOT_RETURNED(0, "未还"), RETURNED(1, "已还"), OVERDUE(2, "逾期"); private final int code; private final String desc; BorrowStatus(int code, String desc) { this.code = code; this.desc = desc; } public static BorrowStatus fromCode(int code) { for (BorrowStatus s : values()) { if (s.code == code) return s; } throw new IllegalArgumentException("Unknown borrow status: " + code); } }

MyBatis TypeHandler 自动转换(BorrowRecordMapper.xml中无需手动CAST):

<!-- 在 resultMap 中声明类型处理器 --> <resultMap id="BorrowRecordMap" type="com.library.entity.BorrowRecord"> <id property="recordId" column="record_id"/> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> </resultMap>

4.2 归还操作的 SQL 状态校验

归还时必须确保:该记录确实未还、对应图书确为“已借出”状态、且读者状态正常。MyBatis XML 中用UPDATE ... WHERE的多条件校验实现:

<update id="returnBook" parameterType="map"> UPDATE borrow_record br JOIN book b ON br.isbn = b.isbn JOIN reader r ON br.reader_id = r.reader_id SET br.return_date = NOW(), br.status = 1, b.stock = b.stock + 1, b.status = 0 WHERE br.record_id = #{recordId} AND br.status = 0 <!-- 必须是未还状态 --> AND b.status = 1 <!-- 图书必须是已借出 --> AND r.status = 0 <!-- 读者必须正常 --> AND NOT EXISTS ( <!-- 防止重复归还 --> SELECT 1 FROM borrow_record WHERE record_id = #{recordId} AND status = 1 ) </update>
4.2.1 执行后校验影响行数

MyBatis 的update方法返回int,表示影响行数。Service 层必须检查:

int rows = borrowRecordMapper.returnBook(params); if (rows != 1) { throw new BusinessException("归还失败:记录不存在或状态不合法(影响行数:" + rows + ")"); }

这是比 try-catch 更轻量、更精准的业务异常捕获方式。

4.3 逾期自动检测:定时任务与状态批量更新

逾期不是实时计算,而是每日凌晨扫描due_date < NOW() AND status = 0的记录并批量更新:

<update id="batchUpdateOverdue"> UPDATE borrow_record SET status = 2 WHERE due_date < NOW() AND status = 0 </update>

Spring 配置定时任务(applicationContext.xml):

<task:scheduler id="scheduler" pool-size="5"/> <task:scheduled-tasks scheduler="scheduler"> <task:scheduled ref="borrowService" method="checkOverdue" cron="0 0 2 * * ?"/> </task:scheduled-tasks>

提示:cron="0 0 2 * * ?"表示每天凌晨 2 点执行,避开业务高峰;pool-size="5"防止任务堆积,因checkOverdue是纯 SQL 批量更新,几乎无 CPU 消耗。


5. SSM 图书借阅系统的上线前必检清单:从 SQL 优化到日志审计

SSM 项目交付前,不能只测功能通不通,更要验证其在真实图书馆场景下的鲁棒性。以下 5 项是高频故障点,必须逐项验证。

5.1 数据库连接池监控:Druid 的 SQL 慢查询告警

Druid 内置监控页面(http://localhost:8080/druid)需开启,并配置慢 SQL 阈值:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- ... 其他属性 --> <property name="filters" value="stat,wall,log4j"/> <property name="connectionProperties" value="druid.stat.slowSqlMillis=500"/> </bean>
  • slowSqlMillis=500:执行超 500ms 的 SQL 记录到日志;
  • wall:防火墙插件,拦截DELETE FROM book WHERE 1=1类危险语句;
  • log4j:将监控日志输出到 log4j,便于 ELK 收集。

5.2 借阅并发安全:乐观锁 vs 悲观锁选型

高并发借书场景下,UPDATE book SET stock = stock - 1可能导致超借。SSM 中推荐数据库层面乐观锁,而非 Java 层 synchronized:

-- 在 book 表增加 version 字段 ALTER TABLE book ADD COLUMN version INT DEFAULT 0; -- 借书 SQL 改为: UPDATE book SET stock = stock - 1, version = version + 1 WHERE isbn = #{isbn} AND stock > 0 AND status = 0 AND version = #{version};

MyBatis 返回rows == 1才成功,否则重试(最多 3 次)。比悲观锁(SELECT ... FOR UPDATE)吞吐量高 3 倍以上。

5.3 日志审计字段标准化

所有关键操作(借、还、续、罚)必须记录operator_id(操作员)、ip_address(来源 IP)、client_info(浏览器 UA)。在 Controller 层统一注入:

@Controller public class BorrowController { @RequestMapping("/borrow") public String borrowBook(@RequestBody BorrowRequest req, HttpServletRequest request) { // 提取操作员(从 Session 或 Token 解析) Long operatorId = getCurrentOperatorId(); // 提取 IP(考虑代理转发) String ip = getClientIp(request); BorrowRecord record = new BorrowRecord(); record.setOperatorId(operatorId); record.setIpAddress(ip); record.setClientInfo(request.getHeader("User-Agent")); borrowService.borrowBook(record); return "success"; } }

5.4 JSP 页面防 XSS:JSTL 的fn:escapeXml强制启用

图书标题、作者名等用户输入内容,必须在 JSP 中转义:

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <!-- 错误写法 --> <td>${book.title}</td> <!-- 正确写法 --> <td>${fn:escapeXml(book.title)}</td>

Spring MVC 默认不开启 HTML 转义,必须手动调用fn:escapeXml,否则《黑客帝国<script>alert(1)</script>》会执行脚本。

5.5 WAR 包部署验证:Tomcat 的 context.xml 配置要点

生产环境 Tomcat 的conf/context.xml必须包含:

<!-- 禁用 session 序列化(SSM 项目通常无状态) --> <Manager pathname="" /> <!-- 设置 JNDI 数据源(可选,比 Spring 内置 DataSource 更易运维) --> <Resource name="jdbc/library" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://db-server:3306/library" username="prod_user" password="prod_pass" maxActive="50" minIdle="10"/>

提示:若使用 JNDI,applicationContext.xml中的dataSourcebean 应改为 JNDI 查找:

<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/library"/>

最后一步,用 curl 模拟真实借书请求,观察日志中 SQL 执行时间、事务提交标记、Druid 监控页慢 SQL 数量——全部达标,方可交付。

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

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

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

立即咨询