详细介绍:Spring 整合 MyBatis 深度详解(原理 + 实操 + 源码级解析)

引言

在 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 容器接管DataSourceSqlSessionFactoryMapper代理对象,自动管理对象创建、依赖注入与销毁;
  • 事务统一管控:基于 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.10
        8.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.8
                    1.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 默认是接口名首字母小写(如UserMapperuserMapper),可通过@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; // 主键ID
    private 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_user
        SET 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_user
        WHERE id IN
        
            #{id}
        
    

Mapper 映射文件核心规则

  • namespace必须与 Mapper 接口全类名完全一致;
  • 标签id必须与接口方法名完全一致;
  • parameterType(输入参数类型)、resultType/resultMap(输出参数类型)需与方法参数、返回值类型匹配;
  • resultMap用于解决数据库字段名与实体类属性名不一致问题(如user_nameuserName),优先级高于驼峰命名映射。
(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容器中获取)
    @Autowired
    private 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);
    }
    @Override
    public Integer addUser(User user) {
        // 调用Mapper新增方法
        userMapper.insert(user);
        // 新增成功后,实体类id会被MyBatis自动赋值(useGeneratedKeys="true")
        return user.getId();
    }
    @Override
    public 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容器管理)
    @Autowired
    private UserService userService;
    /**
     * 测试根据ID查询用户
     */
    @Test
    public void testFindUserById() {
        User user = userService.getUserById(1);
        System.out.println("查询到的用户:" + user);
        // 断言:验证查询结果不为空
        assert user != null;
    }
    /**
     * 测试分页查询所有用户
     */
    @Test
    public void testFindAllUsers() {
        List userList = userService.getAllUsers(1, 5);
        System.out.println("第1页用户列表(5条):" + userList);
        assert userList.size() <= 5;
    }
    /**
     * 测试新增用户
     */
    @Test
    public 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)
     */
    @Bean
    public 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)
     */
    @Bean
    public 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 {
    @Autowired
    private UserAnnotationMapper userAnnotationMapper;
    @Test
    public void testSelectById() {
        User user = userAnnotationMapper.selectById(1);
        System.out.println("注解式查询用户:" + user);
        assert user != null;
    }
    @Test
    public 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;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 校验数据源是否配置
        if (this.dataSource == null) {
            throw new IllegalArgumentException("Property 'dataSource' is required");
        }
        // 构建SqlSessionFactory
        this.sqlSessionFactory = buildSqlSessionFactory();
    }
    @Override
    public 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. 创建并返回SqlSessionFactory
        return new DefaultSqlSessionFactory(configuration);
    }
}

核心流程

  1. afterPropertiesSet()方法在 Bean 初始化时调用,校验数据源等必填参数;
  2. buildSqlSessionFactory()方法加载 MyBatis 配置、整合数据源、扫描 Mapper 映射文件,构建Configuration对象;
  3. 通过DefaultSqlSessionFactory创建SqlSessionFactory实例,最终通过getObject()方法提供给 Spring IOC 容器。

2. MapperScannerConfigurer 的扫描机制

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,在 Spring 容器初始化时扫描 Mapper 接口并注册 BeanDefinition:

java

运行

// 简化源码逻辑
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {
    private String basePackage;
    private String sqlSessionFactoryBeanName;
    @Override
    public 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();
        // 扫描接口并注册BeanDefinition
        doScan(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);
            // 设置SqlSessionFactoryBeanName
            definition.getPropertyValues().add("sqlSessionFactoryBeanName", this.sqlSessionFactoryBeanName);
        }
        return beanDefinitions;
    }
}

核心流程

  1. postProcessBeanDefinitionRegistry()在 Spring BeanDefinition 注册后调用,启动 Mapper 扫描;
  2. ClassPathMapperScanner扫描basePackage下的所有接口,排除非接口类;
  3. 为每个 Mapper 接口创建GenericBeanDefinition,并将 BeanClass 设置为MapperFactoryBean
  4. MapperFactoryBeanFactoryBean的实现类,Spring 初始化时会调用其getObject()方法,通过SqlSession.getMapper()生成 Mapper 代理对象。

3. 声明式事务的 AOP 实现原理

Spring 声明式事务基于 AOP 动态代理,核心是TransactionInterceptor(事务拦截器)和TransactionAttributeSource(事务属性源):

  1. 事务属性解析TransactionAttributeSource解析@Transactional注解或 XML 中的事务规则(传播行为、隔离级别等);
  2. 事务拦截TransactionInterceptor作为 AOP 切面,拦截 Service 层方法调用,在方法执行前后进行事务控制:
    • 方法执行前:获取数据库连接,设置事务隔离级别,开启事务;
    • 方法执行后:若无异常,提交事务;若抛出指定异常,回滚事务;
    • 最终:释放数据库连接到连接池。

简化源码逻辑

java

运行

public class TransactionInterceptor implements MethodInterceptor {
    private PlatformTransactionManager transactionManager;
    private TransactionAttributeSource transactionAttributeSource;
    @Override
    public 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),避免频繁创建连接;
    • 关闭无效检测:testOnBorrowtestOnReturn设为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_nameid)创建索引,提升查询效率。

六、常见问题排查与解决方案

1. Mapper 代理对象注入失败(No qualifying bean of type)

原因

  • MapperScannerConfigurerbasePackage配置错误,未扫描到 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注解缺失,或参数类型不匹配)。

解决方案

  • 严格校验namespaceid与接口的一致性;
  • 检查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 后端开发的核心技能,其本质是 “组件托管 + 依赖注入 + 事务统一管控”。通过本文的详细拆解,从环境搭建、配置实操到源码解析、性能优化,完整覆盖了整合的全流程,既符合湖南大学计算机科学与技术专业的课程实践要求,也适配企业级应用的开发标准。

核心要点回顾:

  1. 整合的核心是让 Spring 接管 MyBatis 的DataSourceSqlSessionFactoryMapper代理对象,减少手动编码;
  2. XML 配置方案步骤清晰、易于维护,是企业主流选择;注解配置方案无 XML 依赖,是 Spring Boot 的基础;
  3. 声明式事务是整合后的核心优势,需正确配置事务管理器和事务规则;
  4. 性能优化的关键在于连接池配置、SQL 优化、事务细粒度控制;
  5. 常见问题多源于配置不一致(如namespaceid、包路径),需严格遵循规范。

掌握 Spring 与 MyBatis 的整合,不仅能应对高校课程的实践任务,更能为后续学习 Spring Boot、Spring Cloud 等微服务技术打下坚实基础,是 Java 后端开发者的必备技能之一。

posted @ 2025-12-25 11:18  yangykaifa  阅读(20)  评论(0)    收藏  举报