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. 整合的核心组件与依赖关系

整合后核心组件的依赖链:DataSourceSqlSessionFactorySqlSessionMapper代理对象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

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hnu.it</groupId>
    <artifactId>spring-mybatis-integration</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <!-- 版本统一管理 -->
        <spring.version>5.3.28</spring.version>
        <mybatis.version>3.5.13</mybatis.version>
        <mybatis-spring.version>2.0.10</mybatis.version>
        <mysql.version>8.0.36</mysql.version>
        <druid.version>1.2.20</druid.version>
        <junit.version>4.13.2</junit.version>
        <logback.version>1.4.11</logback.version>
        <slf4j.version>2.0.7</slf4j.version>
    </properties>

    <dependencies>
        <!-- 1. Spring核心依赖 -->
        <!-- Spring IOC核心 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring JDBC(事务管理依赖) -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring AOP(声明式事务依赖) -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- 2. MyBatis核心依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>

        <!-- 3. Spring整合MyBatis桥梁 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>${mybatis-spring.version}</version>
        </dependency>

        <!-- 4. 数据库驱动(MySQL8) -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 5. 连接池(Druid,企业首选) -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!-- 6. 日志框架(SLF4J+Logback,打印SQL日志) -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!-- 7. 单元测试(Spring整合JUnit) -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- 8. Lombok(简化实体类getter/setter,可选) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <!-- 编译配置:指定JDK版本、资源文件目录 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>

        <!-- 资源文件过滤:确保resources目录下的配置文件被正确加载 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include> <!-- 扫描java目录下的Mapper映射文件(注解方案可选) -->
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

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

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 1. 全局设置(核心优化项) -->
    <settings>
        <!-- 驼峰命名自动映射:数据库字段user_name → 实体类属性userName -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启延迟加载(关联查询时提升性能) -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 按需加载:仅查询需要的属性时才触发关联查询 -->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 开启二级缓存(全局默认关闭,可在Mapper中单独开启) -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 打印SQL日志(依赖SLF4J+Logback) -->
        <setting name="logImpl" value="SLF4J"/>
        <!-- 允许SQL中使用多结果集 -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        <!-- 允许使用列别名 -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 关闭JDBC自动生成主键(如需自动生成,在SQL中配置) -->
        <setting name="useGeneratedKeys" value="false"/>
    </settings>

    <!-- 2. 类型别名配置(简化Mapper映射文件中的全类名) -->
    <typeAliases>
        <!-- 扫描指定包下的实体类,别名默认是类名首字母小写(User→user) -->
        <package name="com.hnu.it.ssm.pojo"/>
        <!-- 也可单独为类配置别名(优先级高于包扫描) -->
        <!-- <typeAlias type="com.hnu.it.ssm.pojo.User" alias="user"/> -->
    </typeAliases>

    <!-- 3. 插件配置(可选,如分页插件、SQL拦截器) -->
    <plugins>
        <!-- 示例:PageHelper分页插件(需额外引入依赖) -->
        <!-- <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mysql"/>
            <property name="reasonable" value="true"/>
        </plugin> -->
    </plugins>

    <!-- 注意:整合后,environments(数据源)、mappers(Mapper扫描)由Spring管理,此处无需配置 -->
</configuration>
(3)日志配置(logback.xml)

通过日志框架打印 SQL 执行细节,便于开发调试:

xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <!-- 控制台输出配置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 文件输出配置(按天滚动) -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/ssm-log.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/ssm-log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory> <!-- 保留30天日志 -->
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 日志级别配置:root为全局级别,可单独为包配置级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

    <!-- MyBatis SQL日志级别:DEBUG级别可打印SQL语句和参数 -->
    <logger name="com.hnu.it.ssm.mapper" level="DEBUG"/>
    <!-- Spring框架日志级别:INFO即可,避免冗余 -->
    <logger name="org.springframework" level="INFO"/>
</configuration>

二、XML 配置方案:传统整合流程(企业主流)

XML 配置方案的核心是通过applicationContext.xml文件,将 Spring 与 MyBatis 的组件逐一整合,步骤清晰、易于维护,是企业开发的主流选择。

步骤 1:配置 Spring 读取外部属性文件

通过context:property-placeholder标签加载jdbc.properties,便于在配置中通过${key}引用数据库连接信息:

xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 1. 加载外部属性文件(jdbc.properties) -->
    <context:property-placeholder location="classpath:jdbc.properties" ignore-unresolvable="true"/>

步骤 2:配置数据源(Druid 连接池)

数据源是连接数据库的基础,整合后由 Spring 管理,替代 MyBatis 原生的environments配置。此处选用 Druid 连接池(企业级首选,性能优于 Spring 自带的 BasicDataSource):

xml

复制代码
    <!-- 2. 配置Druid数据源(核心组件:管理数据库连接) -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据库连接核心参数 -->
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- Druid连接池优化参数 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="maxActive" value="${druid.maxActive}"/>
        <property name="maxWait" value="${druid.maxWait}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/>
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/>
        <property name="validationQuery" value="${druid.validationQuery}"/>
        <property name="testWhileIdle" value="${druid.testWhileIdle}"/>
        <property name="testOnBorrow" value="${druid.testOnBorrow}"/>
        <property name="testOnReturn" value="${druid.testOnReturn}"/>
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/>
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}"/>
        <property name="connectionProperties" value="${druid.connectionProperties}"/>

        <!-- 配置监控统计拦截的filters(可选:开启Druid监控) -->
        <property name="filters" value="stat,wall"/>
    </bean>

原理解析 :DruidDataSource 通过init-method="init"初始化连接池,destroy-method="close"销毁时释放资源,连接池参数(如 maxActive、minIdle)通过 Spring 的依赖注入赋值,确保连接池的高效运行。

步骤 3:配置 SqlSessionFactory(MyBatis 核心工厂)

SqlSessionFactory是 MyBatis 的核心工厂,负责创建SqlSession(数据库操作会话)。整合后,通过mybatis-spring提供的SqlSessionFactoryBean由 Spring 管理,无需手动调用SqlSessionFactoryBuilder创建:

xml

复制代码
    <!-- 3. 配置SqlSessionFactory(MyBatis核心工厂:创建SqlSession) -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- ① 关联Spring管理的数据源(核心依赖) -->
        <property name="dataSource" ref="dataSource"/>

        <!-- ② 关联MyBatis全局配置文件(mybatis-config.xml) -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>

        <!-- ③ 配置Mapper映射文件路径(SQL语句所在文件) -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>

        <!-- ④ 配置实体类别名包(与mybatis-config.xml的typeAliases功能一致,二选一即可) -->
        <!-- <property name="typeAliasesPackage" value="com.hnu.it.ssm.pojo"/> -->

        <!-- ⑤ 配置MyBatis插件(如分页插件,与mybatis-config.xml的plugins功能一致) -->
        <!-- <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <value>
                            helperDialect=mysql
                            reasonable=true
                        </value>
                    </property>
                </bean>
            </array>
        </property> -->

        <!-- ⑥ 配置全局参数(覆盖mybatis-config.xml的settings,优先级更高) -->
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration">
                <property name="mapUnderscoreToCamelCase" value="true"/>
                <property name="lazyLoadingEnabled" value="true"/>
            </bean>
        </property>
    </bean>

核心原理

  • 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

复制代码
    <!-- 4. 配置Mapper接口扫描(自动生成Mapper代理对象并注入IOC) -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- ① 扫描Mapper接口所在的包(多个包用逗号分隔) -->
        <property name="basePackage" value="com.hnu.it.ssm.mapper"/>

        <!-- ② 关联SqlSessionFactory(若IOC中只有一个SqlSessionFactory,可省略) -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

        <!-- ③ 配置MarkerInterface(可选:仅扫描继承该接口的Mapper) -->
        <!-- <property name="markerInterface" value="com.hnu.it.ssm.mapper.BaseMapper"/> -->

        <!-- ④ 配置Mapper代理对象的作用域(默认singleton,可选prototype) -->
        <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"/>
    </bean>

    <!-- 5. 配置SqlSessionTemplate(线程安全的SqlSession实现) -->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <!-- 依赖SqlSessionFactory创建SqlSession -->
        <constructor-arg index="0" ref="sqlSessionFactory"/>
        <!-- 配置事务管理模式(默认BATCH,可选SIMPLE/REUSE) -->
        <constructor-arg index="1" value="BATCH"/>
    </bean>

深度解析

  • 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. 配置Spring组件扫描(扫描Service、Controller等注解类) -->
    <context:component-scan base-package="com.hnu.it.ssm">
        <!-- 排除Controller层(若使用SpringMVC,需在SpringMVC配置文件中单独扫描) -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

步骤 6:配置声明式事务(AOP 实现)

数据库操作需事务保障(如转账、批量操作),Spring 通过 AOP 实现声明式事务,无需手动控制SqlSession的提交 / 回滚。核心是配置事务管理器和事务规则:

xml

复制代码
    <!-- 7. 配置事务管理器(DataSourceTransactionManager) -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 依赖数据源,通过数据源获取数据库连接,控制事务 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 8. 配置事务通知(定义事务规则) -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- ① 增删改操作:需要事务,默认传播行为REQUIRED -->
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" rollback-for="Exception"/>
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" rollback-for="Exception"/>
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" rollback-for="Exception"/>
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" rollback-for="Exception"/>
            <tx:method name="batch*" propagation="REQUIRED" isolation="READ_COMMITTED" rollback-for="Exception"/>

            <!-- ② 查询操作:只读事务,提升性能 -->
            <tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>

            <!-- ③ 其他操作:默认规则 -->
            <tx:method name="*" propagation="REQUIRED" isolation="READ_COMMITTED"/>
        </tx:attributes>
    </tx:advice>

    <!-- 9. 配置AOP切面(将事务通知织入Service层) -->
    <aop:config>
        <!-- 定义切入点:匹配Service层所有方法 -->
        <aop:pointcut id="txPointcut" expression="execution(* com.hnu.it.ssm.service..*(..))"/>
        <!-- 切面:将事务通知应用到切入点 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

</beans>

事务配置核心参数解析

  • 事务传播行为(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<User> 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

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace必须与Mapper接口全类名一致 -->
<mapper namespace="com.hnu.it.ssm.mapper.UserMapper">
    <!-- 定义ResultMap(解决字段名与属性名不一致问题,优先级高于驼峰命名) -->
    <resultMap id="UserResultMap" type="User">
        <id column="id" property="id"/>
        <result column="user_name" property="userName"/>
        <result column="password" property="password"/>
        <result column="age" property="age"/>
        <result column="email" property="email"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <!-- 1. 根据ID查询用户 -->
    <select id="selectById" parameterType="Integer" resultMap="UserResultMap">
        SELECT id, user_name, password, age, email, create_time, update_time
        FROM t_user
        WHERE id = #{id}
    </select>

    <!-- 2. 根据用户名查询用户 -->
    <select id="selectByUserName" parameterType="String" resultMap="UserResultMap">
        SELECT id, user_name, password, age, email, create_time, update_time
        FROM t_user
        WHERE user_name = #{userName}
    </select>

    <!-- 3. 查询所有用户(分页) -->
    <select id="selectAll" resultMap="UserResultMap">
        SELECT id, user_name, password, age, email, create_time, update_time
        FROM t_user
        LIMIT #{startIndex}, #{pageSize}
    </select>

    <!-- 4. 新增用户(返回自增主键) -->
    <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_user (user_name, password, age, email, create_time, update_time)
        VALUES (#{userName}, #{password}, #{age}, #{email}, NOW(), NOW())
    </insert>

    <!-- 5. 更新用户信息 -->
    <update id="update" parameterType="User">
        UPDATE t_user
        SET user_name = #{userName},
            password = #{password},
            age = #{age},
            email = #{email},
            update_time = NOW()
        WHERE id = #{id}
    </update>

    <!-- 6. 根据ID删除用户 -->
    <delete id="deleteById" parameterType="Integer">
        DELETE FROM t_user WHERE id = #{id}
    </delete>

    <!-- 7. 批量删除用户(foreach标签遍历数组) -->
    <delete id="batchDelete" parameterType="Integer[]">
        DELETE FROM t_user
        WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

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<User> 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<User> 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<User> 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<User> 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<SqlSessionFactory>, 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<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> 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 后端开发者的必备技能之一。

相关推荐
华仔啊1 小时前
SpringBoot 动态菜单权限系统设计的企业级解决方案
java·后端
S***q3771 小时前
Java进阶-在Ubuntu上部署SpringBoot应用
java·spring boot·ubuntu
棋啊_Rachel1 小时前
Spring Boot深度解析:从零开始构建企业级应用
java·spring boot·后端
小王不爱笑1321 小时前
代码生成器
java·mybatis
Slow菜鸟1 小时前
Java开发规范(五)| 接口设计规范—前后端/跨服务协作的“架构级契约”
java·状态模式·设计规范
草原印象1 小时前
Spring Boot Spring MVC MyBatis MyBatis Plus框架编写项目实战案例
spring boot·spring·mybatis·springmvc·mybatisplus
Slow菜鸟1 小时前
SpringBoot教程(三十五)| SpringBoot集成TraceId(追踪ID)
java·spring boot·后端
__万波__2 小时前
二十三种设计模式(二)--工厂方法模式
java·设计模式·工厂方法模式
汤姆yu2 小时前
基于SpringBoot的餐饮财务管理系统的设计与实现
java·spring boot·后端