引言
在 Java 后端开发领域,Spring 与 MyBatis 的整合(SSM 架构核心组成)是企业级应用的标准技术方案,也是湖南大学计算机科学与技术专业 “Java EE 框架开发”“企业级应用设计” 等核心课程的重点实践内容。MyBatis 专注于数据访问层(DAO)的 SQL 映射与执行,Spring 则擅长依赖注入(DI)、面向切面编程(AOP)、声明式事务等企业级特性,二者的整合本质是让 Spring 的 IOC 容器接管 MyBatis 的核心组件生命周期,通过标准化配置实现 “数据访问层与业务层解耦、开发效率与可维护性双提升” 。
本文将从 “整合原理→环境搭建→分步实操(XML / 注解双方案)→源码解析→性能优化→问题排查” 六个维度,进行 7000 字以上的深度拆解,不仅覆盖企业开发全流程,还融入源码级原理分析,适配高校课程深度学习与职场技术沉淀需求。
一、整合核心原理与设计思想
1. 整合的核心矛盾与解决思路
MyBatis 原生开发的痛点的:
- 组件创建繁琐:需手动加载配置文件、创建
SqlSessionFactory、获取SqlSession、生成 Mapper 代理对象,代码冗余且耦合度高; - 事务管理混乱:需手动控制
SqlSession的提交 / 回滚,多数据源场景下一致性难以保障; - 资源管理低效:数据库连接需手动维护,无连接池复用机制,性能瓶颈明显。
Spring 的解决方案:
- 组件托管:通过 IOC 容器接管
DataSource、SqlSessionFactory、Mapper代理对象,自动管理对象创建、依赖注入与销毁; - 事务统一管控:基于 AOP 实现声明式事务,通过注解或 XML 配置即可完成事务规则定义,无需侵入业务代码;
- 资源池化:整合第三方连接池(Druid/C3P0),优化数据库连接的创建与复用,提升系统并发能力。
2. 整合的核心组件与依赖关系
整合后核心组件的依赖链:DataSource → SqlSessionFactory → SqlSession → Mapper代理对象 → Service,各组件职责与 Spring 接管逻辑如下:
| 组件 | 核心职责 | MyBatis 原生方式 | Spring 整合方式 |
|---|---|---|---|
| DataSource | 管理数据库连接(连接池核心) | 手动配置environments标签 | Spring IOC 容器管理,支持连接池参数优化 |
| SqlSessionFactory | 创建 SqlSession(MyBatis 核心工厂) | 手动加载mybatis-config.xml创建 | 通过SqlSessionFactoryBean自动创建,依赖 DataSource |
| SqlSession | 数据库操作会话(执行 SQL、管理事务) | 手动调用openSession()获取 | Spring 通过SqlSessionTemplate管理,线程安全 |
| Mapper 代理对象 | 映射 SQL 语句到接口方法 | 手动通过SqlSession.getMapper()获取 | MapperScannerConfigurer自动扫描接口,生成代理对象并注入 IOC |
| PlatformTransactionManager | 事务管理器(提交 / 回滚) | 无原生支持,需手动控制 | Spring 提供DataSourceTransactionManager,基于 AOP 实现声明式事务 |
3. 整合的关键桥梁:mybatis-spring.jar
mybatis-spring是官方提供的整合中间件,核心作用是 “打通 Spring 与 MyBatis 的组件通信”,其核心类如下:
SqlSessionFactoryBean:替代 MyBatis 原生的SqlSessionFactoryBuilder,将DataSource、MyBatis 配置、Mapper 映射文件等参数封装,由 Spring IOC 容器初始化时创建SqlSessionFactory;SqlSessionTemplate:Spring 管理的SqlSession实现类,线程安全(原生SqlSession非线程安全),通过SqlSessionFactory获取SqlSession,并整合事务管理逻辑;MapperScannerConfigurer:基于 Spring 的BeanDefinitionRegistryPostProcessor,扫描指定包下的 Mapper 接口,为每个接口生成 MyBatis 动态代理对象,并注册到 IOC 容器;MapperFactoryBean:针对单个 Mapper 接口的工厂类,可手动配置单个 Mapper 的代理对象(适用于少量 Mapper 场景)。
二、环境准备(Maven 依赖 + 项目结构)
1. 项目结构设计(标准 Maven 工程)
plaintext
src/
├── main/
│ ├── java/
│ │ └── com/hnu/it/ssm/
│ │ ├── config/ // 配置类(注解方案)
│ │ ├── controller/ // 控制层(接收请求)
│ │ ├── service/ // 业务层(接口+实现)
│ │ ├── mapper/ // Mapper接口(数据访问层)
│ │ ├── pojo/ // 实体类(与数据库表映射)
│ │ ├── exception/ // 全局异常处理
│ │ └── util/ // 工具类
│ └── resources/
│ ├── applicationContext.xml // Spring核心配置(XML方案)
│ ├── mybatis-config.xml // MyBatis全局配置
│ ├── mapper/ // Mapper映射文件(XML方案)
│ ├── jdbc.properties // 数据库连接配置
│ └── logback.xml // 日志配置(SLF4J+Logback)
└── test/└── java/└── com/hnu/it/ssm/└── MapperTest.java // 整合测试类
2. 完整 Maven 依赖配置(pom.xml)
需引入 Spring 核心、MyBatis 核心、整合中间件、数据库驱动、连接池、日志等依赖,版本需保持兼容(Spring 5.x 搭配 MyBatis 3.5.x、mybatis-spring 2.0.x):
xml
4.0.0 com.hnu.it spring-mybatis-integration 1.0-SNAPSHOT jar 5.3.28 3.5.13 2.0.108.0.36 1.2.20 4.13.2 1.4.11 2.0.7 org.springframework spring-context ${spring.version} org.springframework spring-jdbc ${spring.version} org.springframework spring-aop ${spring.version} org.springframework spring-aspects ${spring.version} org.mybatis mybatis ${mybatis.version} org.mybatis mybatis-spring ${mybatis-spring.version} mysql mysql-connector-java ${mysql.version} runtime com.alibaba druid ${druid.version} org.slf4j slf4j-api ${slf4j.version} ch.qos.logback logback-classic ${logback.version} ch.qos.logback logback-core ${logback.version} org.springframework spring-test ${spring.version} test junit junit ${junit.version} test org.projectlombok lombok 1.18.30 provided org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.81.8 UTF-8 src/main/resources **/*.xml **/*.properties true src/main/java **/*.xml true
3. 基础配置文件准备
(1)数据库连接配置(jdbc.properties)
将数据库连接信息抽离为独立配置文件,便于维护:
properties
# 数据库驱动(MySQL8必须使用com.mysql.cj.jdbc.Driver)
jdbc.driver=com.mysql.cj.jdbc.Driver
# 数据库URL(指定时区、编码、SSL配置)
jdbc.url=jdbc:mysql://localhost:3306/ssm_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowPublicKeyRetrieval=true
# 数据库用户名
jdbc.username=root
# 数据库密码
jdbc.password=123456
# Druid连接池参数优化
# 初始连接数
druid.initialSize=5
# 最大活跃连接数
druid.maxActive=20
# 最大等待时间(毫秒)
druid.maxWait=60000
# 最小空闲连接数
druid.minIdle=3
# 连接检测间隔(毫秒)
druid.timeBetweenEvictionRunsMillis=60000
# 连接最小生存时间(毫秒)
druid.minEvictableIdleTimeMillis=300000
# 连接有效性检测SQL
druid.validationQuery=SELECT 1 FROM DUAL
# 空闲时检测连接有效性
druid.testWhileIdle=true
# 申请连接时检测有效性(建议关闭,影响性能)
druid.testOnBorrow=false
# 归还连接时检测有效性(建议关闭,影响性能)
druid.testOnReturn=false
# 支持PSCache(提升查询性能)
druid.poolPreparedStatements=true
# PSCache最大缓存数
druid.maxPoolPreparedStatementPerConnectionSize=20
# 连接属性配置
druid.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
(2)MyBatis 全局配置(mybatis-config.xml)
MyBatis 全局配置文件仅保留 “非数据源相关” 的全局设置(数据源由 Spring 管理),核心配置包括驼峰命名映射、日志实现、别名配置等:
xml
(3)日志配置(logback.xml)
通过日志框架打印 SQL 执行细节,便于开发调试:
xml
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 logs/ssm-log.log logs/ssm-log.%d{yyyy-MM-dd}.log 30 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8
二、XML 配置方案:传统整合流程(企业主流)
XML 配置方案的核心是通过applicationContext.xml文件,将 Spring 与 MyBatis 的组件逐一整合,步骤清晰、易于维护,是企业开发的主流选择。
步骤 1:配置 Spring 读取外部属性文件
通过context:property-placeholder标签加载jdbc.properties,便于在配置中通过${key}引用数据库连接信息:
xml
步骤 2:配置数据源(Druid 连接池)
数据源是连接数据库的基础,整合后由 Spring 管理,替代 MyBatis 原生的environments配置。此处选用 Druid 连接池(企业级首选,性能优于 Spring 自带的 BasicDataSource):
xml
原理解析:DruidDataSource 通过init-method="init"初始化连接池,destroy-method="close"销毁时释放资源,连接池参数(如 maxActive、minIdle)通过 Spring 的依赖注入赋值,确保连接池的高效运行。
步骤 3:配置 SqlSessionFactory(MyBatis 核心工厂)
SqlSessionFactory是 MyBatis 的核心工厂,负责创建SqlSession(数据库操作会话)。整合后,通过mybatis-spring提供的SqlSessionFactoryBean由 Spring 管理,无需手动调用SqlSessionFactoryBuilder创建:
xml
核心原理:
SqlSessionFactoryBean实现了 Spring 的FactoryBean接口,Spring 初始化时会调用其getObject()方法创建SqlSessionFactory实例;dataSource是必填依赖,SqlSessionFactory通过数据源获取数据库连接;mapperLocations指定 Mapper 映射文件路径,Spring 会自动扫描并加载这些文件,无需在 MyBatis 配置中重复配置mappers标签;configuration属性可直接配置 MyBatis 的全局参数,优先级高于mybatis-config.xml中的settings。
步骤 4:配置 Mapper 接口扫描(自动生成代理对象)
MyBatis 原生开发中,需通过SqlSession.getMapper(XXXMapper.class)获取 Mapper 代理对象,整合后可通过MapperScannerConfigurer让 Spring 自动扫描 Mapper 接口,生成代理对象并注册到 IOC 容器,后续可通过依赖注入直接使用:
xml
深度解析:
MapperScannerConfigurer实现了 Spring 的BeanDefinitionRegistryPostProcessor接口,在 Spring 容器初始化时,会扫描basePackage下的所有接口,为每个接口创建BeanDefinition;- 扫描到的 Mapper 接口不会直接实例化,而是通过
SqlSessionTemplate创建 MyBatis 的动态代理对象(底层基于 JDK 动态代理); SqlSessionTemplate是线程安全的SqlSession包装类,Spring 通过它管理SqlSession的生命周期,确保每个线程使用独立的SqlSession,避免线程安全问题;- Mapper 代理对象在 IOC 容器中的 beanId 默认是接口名首字母小写(如
UserMapper→userMapper),可通过@Repository注解自定义 beanId。
步骤 5:配置 Spring 组件扫描(Service/Controller 层)
开启 Spring 的组件扫描,自动扫描@Service、@Controller、@Component等注解标记的类,将其注册到 IOC 容器:
xml
步骤 6:配置声明式事务(AOP 实现)
数据库操作需事务保障(如转账、批量操作),Spring 通过 AOP 实现声明式事务,无需手动控制SqlSession的提交 / 回滚。核心是配置事务管理器和事务规则:
xml
事务配置核心参数解析:
- 事务传播行为(propagation):
REQUIRED:如果当前存在事务,加入事务;否则创建新事务(增删改默认);SUPPORTS:如果当前存在事务,加入事务;否则以非事务方式执行(查询默认);- 其他传播行为:
REQUIRES_NEW(创建新事务,暂停当前事务)、NOT_SUPPORTED(非事务方式执行)等。
- 事务隔离级别(isolation):
READ_COMMITTED:读取已提交的数据,避免脏读,是大多数数据库默认隔离级别(如 MySQL);- 其他隔离级别:
READ_UNCOMMITTED(读取未提交数据)、REPEATABLE_READ(可重复读)、SERIALIZABLE(串行化)。
rollback-for:指定触发事务回滚的异常类型(默认仅回滚 RuntimeException 及其子类,需显式指定 Exception 确保所有异常都回滚)。read-only:查询操作设为true,Spring 会优化事务性能(关闭事务写入能力)。
步骤 7:编写核心业务组件(Pojo→Mapper→Service)
(1)实体类(Pojo):与数据库表映射
假设数据库存在t_user表,创建对应的实体类User(使用 Lombok 简化代码):
java
运行
package com.hnu.it.ssm.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/*** 用户实体类(与t_user表映射)*/
@Data // Lombok注解:自动生成getter/setter/toString等方法
public class User implements Serializable {private Integer id; // 主键IDprivate String userName; // 用户名(对应数据库字段user_name)private String password; // 密码private Integer age; // 年龄private String email; // 邮箱private Date createTime; // 创建时间(对应数据库字段create_time)private Date updateTime; // 更新时间(对应数据库字段update_time)
}
(2)Mapper 接口:数据访问层接口
定义UserMapper接口,声明数据访问方法(无需实现类,由 MyBatis 动态代理生成):
java
运行
package com.hnu.it.ssm.mapper;
import com.hnu.it.ssm.pojo.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/*** 用户Mapper接口(数据访问层)*/
public interface UserMapper {/*** 根据ID查询用户* @param id 用户ID* @return 用户实体*/User selectById(@Param("id") Integer id);/*** 根据用户名查询用户* @param userName 用户名* @return 用户实体*/User selectByUserName(@Param("userName") String userName);/*** 查询所有用户(支持分页)* @param startIndex 起始索引* @param pageSize 每页条数* @return 用户列表*/List selectAll(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);/*** 新增用户* @param user 用户实体* @return 影响行数*/int insert(User user);/*** 更新用户信息* @param user 用户实体* @return 影响行数*/int update(User user);/*** 根据ID删除用户* @param id 用户ID* @return 影响行数*/int deleteById(@Param("id") Integer id);/*** 批量删除用户* @param ids 用户ID数组* @return 影响行数*/int batchDelete(@Param("ids") Integer[] ids);
}
(3)Mapper 映射文件:SQL 语句定义
创建UserMapper.xml文件(放在resources/mapper目录下),编写与 Mapper 接口方法对应的 SQL 语句:
xml
INSERT INTO t_user (user_name, password, age, email, create_time, update_time)VALUES (#{userName}, #{password}, #{age}, #{email}, NOW(), NOW()) UPDATE t_userSET user_name = #{userName},password = #{password},age = #{age},email = #{email},update_time = NOW()WHERE id = #{id} DELETE FROM t_user WHERE id = #{id} DELETE FROM t_userWHERE id IN#{id}
Mapper 映射文件核心规则:
namespace必须与 Mapper 接口全类名完全一致;- 标签
id必须与接口方法名完全一致; parameterType(输入参数类型)、resultType/resultMap(输出参数类型)需与方法参数、返回值类型匹配;resultMap用于解决数据库字段名与实体类属性名不一致问题(如user_name→userName),优先级高于驼峰命名映射。
(4)Service 层:业务逻辑封装
Service 层负责封装业务逻辑,通过依赖注入(@Autowired)获取 Mapper 代理对象,调用数据访问方法。
① Service 接口:
java
运行
package com.hnu.it.ssm.service;
import com.hnu.it.ssm.pojo.User;
import java.util.List;
/*** 用户Service接口(业务层)*/
public interface UserService {/*** 根据ID查询用户* @param id 用户ID* @return 用户实体*/User getUserById(Integer id);/*** 查询所有用户(分页)* @param pageNum 页码(从1开始)* @param pageSize 每页条数* @return 用户列表*/List getAllUsers(Integer pageNum, Integer pageSize);/*** 新增用户* @param user 用户实体* @return 新增成功的用户ID*/Integer addUser(User user);/*** 更新用户信息* @param user 用户实体* @return 是否更新成功(true/false)*/Boolean updateUser(User user);/*** 批量删除用户* @param ids 用户ID数组* @return 删除成功的条数*/Integer batchDeleteUsers(Integer[] ids);
}
② Service 实现类:
java
运行
package com.hnu.it.ssm.service.impl;
import com.hnu.it.ssm.mapper.UserMapper;
import com.hnu.it.ssm.pojo.User;
import com.hnu.it.ssm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/*** 用户Service实现类(业务层实现)*/
@Service // 标记为Spring服务组件,被组件扫描到并注册到IOC
@Transactional // 类级事务注解:所有方法默认应用事务规则
public class UserServiceImpl implements UserService {// 依赖注入Mapper代理对象(Spring自动从IOC容器中获取)@Autowiredprivate UserMapper userMapper;@Override@Transactional(readOnly = true) // 方法级事务注解:覆盖类级配置,查询设为只读public User getUserById(Integer id) {// 直接调用Mapper接口方法(代理对象自动执行SQL)return userMapper.selectById(id);}@Override@Transactional(readOnly = true)public List getAllUsers(Integer pageNum, Integer pageSize) {// 计算分页起始索引(pageNum从1开始)Integer startIndex = (pageNum - 1) * pageSize;return userMapper.selectAll(startIndex, pageSize);}@Overridepublic Integer addUser(User user) {// 调用Mapper新增方法userMapper.insert(user);// 新增成功后,实体类id会被MyBatis自动赋值(useGeneratedKeys="true")return user.getId();}@Overridepublic Boolean updateUser(User user) {// 调用Mapper更新方法,返回影响行数int rows = userMapper.update(user);return rows > 0;}@Override@Transactional(rollbackFor = Exception.class) // 显式指定回滚异常类型public Integer batchDeleteUsers(Integer[] ids) {// 模拟业务异常:若ids为空,抛出异常,事务回滚if (ids == null || ids.length == 0) {throw new IllegalArgumentException("删除ID数组不能为空");}// 调用Mapper批量删除方法return userMapper.batchDelete(ids);}
}
Service 层核心要点:
- 用
@Service注解标记实现类,确保被 Spring 组件扫描到; - 用
@Autowired注入 Mapper 代理对象,无需手动创建; - 事务注解可加在类上(所有方法默认应用)或方法上(覆盖类级配置),查询方法建议设为
readOnly=true提升性能; - 业务逻辑中可加入参数校验、异常处理等逻辑,异常抛出后事务会自动回滚(需配置
rollbackFor)。
步骤 8:整合测试(JUnit+Spring Test)
使用 Spring Test 整合 JUnit,直接从 IOC 容器中获取 Service 对象,测试整合效果:
java
运行
package com.hnu.it.ssm;
import com.hnu.it.ssm.pojo.User;
import com.hnu.it.ssm.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
/*** Spring整合MyBatis测试类*/
// 指定Spring测试运行器
@RunWith(SpringJUnit4ClassRunner.class)
// 加载Spring核心配置文件
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class UserServiceTest {// 注入UserService对象(Spring IOC容器管理)@Autowiredprivate UserService userService;/*** 测试根据ID查询用户*/@Testpublic void testFindUserById() {User user = userService.getUserById(1);System.out.println("查询到的用户:" + user);// 断言:验证查询结果不为空assert user != null;}/*** 测试分页查询所有用户*/@Testpublic void testFindAllUsers() {List userList = userService.getAllUsers(1, 5);System.out.println("第1页用户列表(5条):" + userList);assert userList.size() <= 5;}/*** 测试新增用户*/@Testpublic void testAddUser() {User user = new User();user.setUserName("test_user");user.setPassword("123456");user.setAge(25);user.setEmail("test@hnu.edu.cn");Integer userId = userService.addUser(user);System.out.println("新增用户ID:" + userId);assert userId != null;}/*** 测试批量删除用户(含事务回滚测试)*/@Test(expected = IllegalArgumentException.class)public void testBatchDeleteUsers() {// 测试1:正常删除(IDs为[2,3])Integer[] ids = {2, 3};Integer deleteCount = userService.batchDeleteUsers(ids);System.out.println("批量删除成功条数:" + deleteCount);assert deleteCount == 2;// 测试2:传入空IDs,触发异常,事务回滚userService.batchDeleteUsers(null);}
}
测试结果验证:
- 运行测试方法,控制台会打印 SQL 执行日志(如
DEBUG com.hnu.it.ssm.mapper.UserMapper.selectById - ==> Preparing: SELECT id, user_name, password, age, email, create_time, update_time FROM t_user WHERE id = ?); - 正常情况下,新增、更新、删除操作会提交事务,数据库数据发生变化;
- 当抛出
IllegalArgumentException时,事务回滚,数据库数据不变,验证事务配置生效。
三、注解配置方案:无 XML 全注解整合(Spring Boot 前奏)
随着 Spring Boot 的普及,无 XML 的注解配置方案逐渐成为趋势。该方案通过@Configuration、@Bean等注解替代 XML 配置,核心逻辑与 XML 方案一致,仅配置方式不同。
步骤 1:编写 Spring 核心配置类(SpringConfig)
java
运行
package com.hnu.it.ssm.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
/*** Spring核心配置类(替代applicationContext.xml)*/
@Configuration // 标记为Spring配置类
@ComponentScan("com.hnu.it.ssm") // 组件扫描(扫描Service、Controller等)
@PropertySource("classpath:jdbc.properties") // 加载外部属性文件
@MapperScan("com.hnu.it.ssm.mapper") // Mapper接口扫描(替代MapperScannerConfigurer)
@EnableTransactionManagement // 开启声明式事务(替代tx:annotation-driven)
public class SpringConfig {// 从jdbc.properties中读取配置(@Value注解注入)@Value("${jdbc.driver}")private String driverClassName;@Value("${jdbc.url}")private String url;@Value("${jdbc.username}")private String username;@Value("${jdbc.password}")private String password;@Value("${druid.initialSize}")private Integer initialSize;@Value("${druid.maxActive}")private Integer maxActive;@Value("${druid.maxWait}")private Long maxWait;@Value("${druid.minIdle}")private Integer minIdle;/*** 1. 配置Druid数据源(替代XML中的dataSource bean)*/@Bean(destroyMethod = "close")public DataSource dataSource() {DruidDataSource dataSource = new DruidDataSource();// 核心连接参数dataSource.setDriverClassName(driverClassName);dataSource.setUrl(url);dataSource.setUsername(username);dataSource.setPassword(password);// 连接池优化参数dataSource.setInitialSize(initialSize);dataSource.setMaxActive(maxActive);dataSource.setMaxWait(maxWait);dataSource.setMinIdle(minIdle);// 其他参数可按需配置dataSource.setTestWhileIdle(true);dataSource.setValidationQuery("SELECT 1 FROM DUAL");return dataSource;}/*** 2. 配置SqlSessionFactory(替代XML中的sqlSessionFactory bean)*/@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();// 关联数据源factoryBean.setDataSource(dataSource);// 关联MyBatis全局配置文件factoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));// 配置Mapper映射文件路径factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));// 配置事务工厂(Spring管理事务)factoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return factoryBean;}/*** 3. 配置事务管理器(替代XML中的transactionManager bean)*/@Beanpublic DataSourceTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
步骤 2:编写 MyBatis 注解式 Mapper(无需 XML 映射文件)
MyBatis 支持通过@Select、@Insert等注解直接在 Mapper 接口中编写 SQL,无需单独的 XML 映射文件,简化配置:
java
运行
package com.hnu.it.ssm.mapper;
import com.hnu.it.ssm.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
/*** 注解式Mapper接口(无需XML映射文件)*/
@Mapper // 标记为MyBatis Mapper接口(与@MapperScan配合使用)
public interface UserAnnotationMapper {/*** 根据ID查询用户(@Select注解)*/@Select("SELECT id, user_name AS userName, password, age, email, create_time AS createTime, update_time AS updateTime " +"FROM t_user WHERE id = #{id}")User selectById(Integer id);/*** 新增用户(@Insert注解)*/@Insert("INSERT INTO t_user (user_name, password, age, email, create_time, update_time) " +"VALUES (#{userName}, #{password}, #{age}, #{email}, NOW(), NOW())")@Options(useGeneratedKeys = true, keyProperty = "id") // 返回自增主键int insert(User user);/*** 更新用户(@Update注解)*/@Update("UPDATE t_user SET user_name = #{userName}, password = #{password}, age = #{age}, " +"email = #{email}, update_time = NOW() WHERE id = #{id}")int update(User user);/*** 删除用户(@Delete注解)*/@Delete("DELETE FROM t_user WHERE id = #{id}")int deleteById(Integer id);/*** 分页查询所有用户(@Results注解定义结果映射)*/@Select("SELECT id, user_name, password, age, email, create_time, update_time " +"FROM t_user LIMIT #{startIndex}, #{pageSize}")@Results({@Result(column = "id", property = "id", id = true),@Result(column = "user_name", property = "userName"),@Result(column = "create_time", property = "createTime"),@Result(column = "update_time", property = "updateTime")})List selectAll(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
}
步骤 3:注解式配置测试
java
运行
package com.hnu.it.ssm;
import com.hnu.it.ssm.config.SpringConfig;
import com.hnu.it.ssm.mapper.UserAnnotationMapper;
import com.hnu.it.ssm.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/*** 注解式配置测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class) // 加载注解配置类
public class AnnotationConfigTest {@Autowiredprivate UserAnnotationMapper userAnnotationMapper;@Testpublic void testSelectById() {User user = userAnnotationMapper.selectById(1);System.out.println("注解式查询用户:" + user);assert user != null;}@Testpublic void testInsert() {User user = new User();user.setUserName("annotation_user");user.setPassword("654321");user.setAge(30);user.setEmail("annotation@hnu.edu.cn");int rows = userAnnotationMapper.insert(user);System.out.println("注解式新增用户影响行数:" + rows + ",用户ID:" + user.getId());assert rows == 1;}
}
四、源码级解析:整合的核心底层逻辑
1. SqlSessionFactoryBean 的工作原理
SqlSessionFactoryBean是整合的核心桥梁,其实现了FactoryBean<SqlSessionFactory>和InitializingBean接口,核心逻辑在afterPropertiesSet()和getObject()方法中:
java
运行
// 简化源码逻辑
public class SqlSessionFactoryBean implements FactoryBean, InitializingBean {private DataSource dataSource;private Resource configLocation;private Resource[] mapperLocations;@Overridepublic void afterPropertiesSet() throws Exception {// 校验数据源是否配置if (this.dataSource == null) {throw new IllegalArgumentException("Property 'dataSource' is required");}// 构建SqlSessionFactorythis.sqlSessionFactory = buildSqlSessionFactory();}@Overridepublic SqlSessionFactory getObject() throws Exception {if (this.sqlSessionFactory == null) {afterPropertiesSet();}return this.sqlSessionFactory;}private SqlSessionFactory buildSqlSessionFactory() throws Exception {// 1. 加载MyBatis配置文件(mybatis-config.xml)Configuration configuration = new Configuration();if (this.configLocation != null) {XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(configLocation.getInputStream());configuration = xmlConfigBuilder.parse();}// 2. 配置数据源和事务工厂configuration.setEnvironment(new Environment("default",new SpringManagedTransactionFactory(), // Spring管理的事务工厂this.dataSource));// 3. 扫描并加载Mapper映射文件if (this.mapperLocations != null) {for (Resource mapperLocation : this.mapperLocations) {XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration,mapperLocation.toString(), configuration.getMapperRegistry());xmlMapperBuilder.parse(); // 解析Mapper映射文件,注册SQL语句}}// 4. 创建并返回SqlSessionFactoryreturn new DefaultSqlSessionFactory(configuration);}
}
核心流程:
afterPropertiesSet()方法在 Bean 初始化时调用,校验数据源等必填参数;buildSqlSessionFactory()方法加载 MyBatis 配置、整合数据源、扫描 Mapper 映射文件,构建Configuration对象;- 通过
DefaultSqlSessionFactory创建SqlSessionFactory实例,最终通过getObject()方法提供给 Spring IOC 容器。
2. MapperScannerConfigurer 的扫描机制
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,在 Spring 容器初始化时扫描 Mapper 接口并注册 BeanDefinition:
java
运行
// 简化源码逻辑
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {private String basePackage;private String sqlSessionFactoryBeanName;@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {// 创建Mapper扫描器ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);// 扫描basePackage下的接口scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));}// ClassPathMapperScanner的scan方法核心逻辑public int scan(String... basePackages) {int beanCountAtScanStart = this.registry.getBeanDefinitionCount();// 扫描接口并注册BeanDefinitiondoScan(basePackages);return this.registry.getBeanDefinitionCount() - beanCountAtScanStart;}protected Set doScan(String... basePackages) {Set beanDefinitions = super.doScan(basePackages);for (BeanDefinitionHolder holder : beanDefinitions) {GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();// 设置Bean的构造函数参数:Mapper接口全类名definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());// 设置Bean的类型为MapperFactoryBean(通过FactoryBean创建代理对象)definition.setBeanClass(MapperFactoryBean.class);// 设置SqlSessionFactoryBeanNamedefinition.getPropertyValues().add("sqlSessionFactoryBeanName", this.sqlSessionFactoryBeanName);}return beanDefinitions;}
}
核心流程:
postProcessBeanDefinitionRegistry()在 Spring BeanDefinition 注册后调用,启动 Mapper 扫描;ClassPathMapperScanner扫描basePackage下的所有接口,排除非接口类;- 为每个 Mapper 接口创建
GenericBeanDefinition,并将 BeanClass 设置为MapperFactoryBean; MapperFactoryBean是FactoryBean的实现类,Spring 初始化时会调用其getObject()方法,通过SqlSession.getMapper()生成 Mapper 代理对象。
3. 声明式事务的 AOP 实现原理
Spring 声明式事务基于 AOP 动态代理,核心是TransactionInterceptor(事务拦截器)和TransactionAttributeSource(事务属性源):
- 事务属性解析:
TransactionAttributeSource解析@Transactional注解或 XML 中的事务规则(传播行为、隔离级别等); - 事务拦截:
TransactionInterceptor作为 AOP 切面,拦截 Service 层方法调用,在方法执行前后进行事务控制:- 方法执行前:获取数据库连接,设置事务隔离级别,开启事务;
- 方法执行后:若无异常,提交事务;若抛出指定异常,回滚事务;
- 最终:释放数据库连接到连接池。
简化源码逻辑:
java
运行
public class TransactionInterceptor implements MethodInterceptor {private PlatformTransactionManager transactionManager;private TransactionAttributeSource transactionAttributeSource;@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {// 1. 获取目标方法的事务属性TransactionAttribute txAttr = transactionAttributeSource.getTransactionAttribute(invocation.getMethod(), targetClass);// 2. 获取事务管理器PlatformTransactionManager tm = determineTransactionManager(txAttr);// 3. 生成事务名称String joinpointIdentification = methodIdentification(invocation.getMethod(), targetClass, txAttr);// 4. 事务执行模板TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);Object retVal = null;try {// 5. 执行目标方法(Service层业务逻辑)retVal = invocation.proceed();} catch (Throwable ex) {// 6. 异常回滚completeTransactionAfterThrowing(txInfo, ex);throw ex;} finally {// 7. 清理事务信息cleanupTransactionInfo(txInfo);}// 8. 事务提交commitTransactionAfterReturning(txInfo);return retVal;}
}
五、整合后的性能优化方案
1. 连接池优化
- 选择高性能连接池:优先使用 Druid 或 HikariCP(Spring Boot 默认),避免使用 Spring 自带的 BasicDataSource;
- 合理配置连接池参数:
initialSize:根据系统初始并发量设置(建议 5-10);maxActive:根据数据库最大连接数设置(建议不超过数据库max_connections的 80%);minIdle:保证最小空闲连接数(建议 3-5),避免频繁创建连接;- 关闭无效检测:
testOnBorrow和testOnReturn设为false,通过testWhileIdle定期检测空闲连接。
2. MyBatis 性能优化
- 开启二级缓存:在 Mapper 接口上添加
@CacheNamespace注解,缓存查询结果(适用于查询频繁、修改少的数据); - 优化 SQL 语句:避免
SELECT *,只查询需要的字段;使用索引优化查询条件; - 批量操作优化:使用 MyBatis 的
BatchExecutor(通过SqlSessionTemplate配置ExecutorType.BATCH),减少 SQL 执行次数; - 延迟加载:开启
lazyLoadingEnabled=true,关联查询时仅在需要时加载关联数据,减少不必要的查询。
3. 事务优化
- 细粒度事务控制:避免在类上添加
@Transactional,仅在需要事务的方法上添加,减少事务开销; - 查询操作设为只读:
@Transactional(readOnly=true),Spring 会优化事务性能,关闭事务写入能力; - 合理设置事务隔离级别:默认使用
READ_COMMITTED,避免使用SERIALIZABLE(性能极低); - 避免长事务:事务中不包含耗时操作(如 IO、网络请求),减少事务持有时间,降低锁竞争。
4. 其他优化
- 开启 MyBatis 二级缓存:在
mybatis-config.xml中设置cacheEnabled=true,并在 Mapper 接口添加@CacheNamespace; - 使用分页插件:集成 PageHelper 或 MyBatis-Plus 分页插件,避免手动编写分页 SQL;
- 日志优化:生产环境关闭 SQL 日志打印,避免日志 IO 开销;
- 数据库索引优化:为查询频繁的字段(如
user_name、id)创建索引,提升查询效率。
六、常见问题排查与解决方案
1. Mapper 代理对象注入失败(No qualifying bean of type)
原因:
MapperScannerConfigurer的basePackage配置错误,未扫描到 Mapper 接口;- Mapper 接口未放在指定包下,或接口未被 Spring 扫描到;
sqlSessionFactoryBeanName配置错误,未关联正确的SqlSessionFactory。
解决方案:
- 检查
basePackage是否准确(如com.hnu.it.ssm.mapper); - 确保 Mapper 接口在
basePackage下,且无@Service等冲突注解; - 若 IOC 中只有一个
SqlSessionFactory,可省略sqlSessionFactoryBeanName配置。
2. SQL 语句执行失败(BindingException/Invalid bound statement)
原因:
- Mapper 映射文件的
namespace与 Mapper 接口全类名不一致; - 映射文件中
id与接口方法名不一致; - 映射文件路径未被
mapperLocations正确配置,导致 MyBatis 未加载; - 参数绑定错误(如
@Param注解缺失,或参数类型不匹配)。
解决方案:
- 严格校验
namespace和id与接口的一致性; - 检查
mapperLocations配置(如classpath:mapper/*.xml),确保映射文件在指定路径下; - 多参数方法需添加
@Param注解,明确参数名; - 查看日志中的 SQL 语句,验证参数绑定是否正确。
3. 事务不生效(未提交 / 回滚)
原因:
@Transactional注解加在非 public 方法上(Spring 事务仅对 public 方法生效);- 注解加在 Controller 层或 Mapper 接口上(事务应加在 Service 层);
- 未配置
transactionManager,或transactionManager未关联数据源; - 异常类型未被
rollbackFor指定(默认仅回滚 RuntimeException); - 方法内部捕获了异常,未抛出到外层。
解决方案:
- 将
@Transactional注解加在 Service 层的 public 方法上; - 确保配置了
DataSourceTransactionManager,且关联正确的dataSource; - 显式配置
rollbackFor=Exception.class,确保所有异常都回滚; - 业务逻辑中不要捕获异常,或捕获后重新抛出(
throw new RuntimeException(ex))。
4. 数据库连接失败(CommunicationsException)
原因:
- 数据库驱动类名错误(MySQL8 应为
com.mysql.cj.jdbc.Driver,而非com.mysql.jdbc.Driver); - 数据库 URL 配置错误(如时区未指定、端口错误、数据库名不存在);
- 数据库用户名 / 密码错误;
- 数据库服务未启动,或防火墙拦截了连接。
解决方案:
- 校验驱动类名和 URL 格式(如
jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai); - 确认数据库服务已启动,且用户名 / 密码正确;
- 测试数据库连接(如使用 Navicat 连接 URL,验证可用性)。
七、总结
Spring 整合 MyBatis 是 Java 后端开发的核心技能,其本质是 “组件托管 + 依赖注入 + 事务统一管控”。通过本文的详细拆解,从环境搭建、配置实操到源码解析、性能优化,完整覆盖了整合的全流程,既符合湖南大学计算机科学与技术专业的课程实践要求,也适配企业级应用的开发标准。
核心要点回顾:
- 整合的核心是让 Spring 接管 MyBatis 的
DataSource、SqlSessionFactory、Mapper代理对象,减少手动编码; - XML 配置方案步骤清晰、易于维护,是企业主流选择;注解配置方案无 XML 依赖,是 Spring Boot 的基础;
- 声明式事务是整合后的核心优势,需正确配置事务管理器和事务规则;
- 性能优化的关键在于连接池配置、SQL 优化、事务细粒度控制;
- 常见问题多源于配置不一致(如
namespace、id、包路径),需严格遵循规范。
掌握 Spring 与 MyBatis 的整合,不仅能应对高校课程的实践任务,更能为后续学习 Spring Boot、Spring Cloud 等微服务技术打下坚实基础,是 Java 后端开发者的必备技能之一。