引言
在 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
<?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 默认是接口名首字母小写(如
UserMapper→userMapper),可通过@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_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<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);
}
}
核心流程:
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;
@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;
}
}
核心流程:
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;
@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),避免频繁创建连接;- 关闭无效检测:
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 后端开发者的必备技能之一。