SSM 源码拆解:从一行 XML 到一次事务提交,全程大白话
SSM 源码拆解:从一行 XML 到一次事务提交,全程大白话
你有没有过这样的经历:照着教程把
applicationContext.xml抄下来,项目能跑,但心里发虚——这几十行 XML 到底在干什么?为什么有的项目要写SqlSessionTemplate,有的项目不用?@Transactional又是怎么"自动"开事务、自动回滚的?今天这篇,我把这些配置背后的源码逻辑拆开揉碎,用"超市 + 机器人 + 储物柜"的比喻,配上一行行能直接复制的代码示例,讲给你听。
目录
- 一个 Bean 的诞生:Spring 怎么"组装"一个 DAO
- 两种 DAO 写法:手写实现类 vs 扫描接口
- SqlSessionTemplate 源码:看不见的"机器人服务员"
- ThreadLocal:人手一个专属"储物柜"
- 声明式事务 XML 版:四段配置各司其职
- 声明式事务注解版:一行配置顶四段
- 事务回滚的四个坑:附正确/错误代码对比
一、一个 Bean 的诞生:Spring 怎么"组装"一个 DAO
先看这段最原始的配置:
<bean id="providerDao6" class="com.kgc.dao.impl.ProviderDaoImpl6">
<property name="sqlSession" ref="sqlSessionTemplate"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
大白话翻译:Spring 说——“我要创建一个 ProviderDaoImpl6 的对象,名字叫 providerDao6。建好之后,把容器里那个叫 sqlSessionTemplate 的对象塞进它的 sqlSession 属性里,把 sqlSessionFactory 塞进它的 sqlSessionFactory 属性里。”
Spring 干活的依据是 JavaBean 规范:属性名叫 sqlSession,容器就去找 setSqlSession(...) 这个 setter 方法,把参数传进去。所以你的 Java 类必须长这样:
public class ProviderDaoImpl6 implements ProviderDao {
private SqlSessionTemplate sqlSession; // 主用:日常 SQL 全靠它
private SqlSessionFactory sqlSessionFactory; // 备用:特殊场景自己开会话
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
}
要点:
class必须写实现类,不能写接口——接口没法new。- 默认是单例,全容器只有一个实例。
- 两个 setter 缺一个,启动直接报错。
二、两种 DAO 写法:手写实现类 vs 扫描接口
上面那种是"老风格",而现在主流是"新风格"。先看对比表:
| 对比项 | 老风格(手写模板) | 新风格(扫描器) |
|---|---|---|
| DAO 长什么样 | 接口 + 实现类 ProviderDaoImpl6 | 只有接口 ProviderDao |
| SQL 执行能力谁提供 | 手动配置 SqlSessionTemplate 注入实现类 | 扫描器内部自动建模板 + 生成代理 |
| 你写不写实现类 | 写 | 不用写 |
| Service 注入谁 | 注入实现类 bean | 注入接口(拿到的是代理) |
老风格配置:
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
新风格配置:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.kgc.dao"/>
</bean>
新风格背后发生了什么:MapperScannerConfigurer 扫描 com.kgc.dao 包,把每个 Mapper 接口(如 ProviderDao)变成一个动态代理(MapperProxy)。每个代理内部偷偷 new 了一个 SqlSessionTemplate——所以你看不见它,但它一直在。Service 里直接注入接口即可:
@Service
public class ProviderServiceImpl implements ProviderService {
@Autowired
private ProviderDao providerDao; // 注入的是代理对象
public void add(Provider p) {
providerDao.insert(p);
}
}
总结一句:新老两种写法殊途同归,最终都走 SqlSessionFactory → DataSource;区别只是"写实现类 + 注册模板"这两步,新风格交给框架自动完成了。
三、SqlSessionTemplate 源码:看不见的"机器人服务员"
SqlSessionTemplate 是 MyBatis-Spring 最核心的类。它本质是一个动态代理:
// 源码简化版
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSessionProxy sqlSessionProxy; // 代理
public SqlSessionTemplate(SqlSessionFactory factory) {
this.sqlSessionFactory = factory;
this.sqlSessionProxy = (SqlSessionProxy) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSessionProxy.class},
new SqlSessionInterceptor() // 拦截器是灵魂
);
}
}
当你调用 sqlSession.selectList(...) 时,实际上执行的是拦截器的 invoke() 方法:
// 源码简化版:核心判断逻辑
class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) {
SqlSession sqlSession = getSqlSession( // 关键:怎么拿到会话
SqlSessionUtils.class, // 先问事务管理器
this.sqlSessionFactory,
TransactionSynchronizationManager.getResource(this.sqlSessionFactory)
);
try {
return method.invoke(sqlSession, args); // 执行真正的 SQL
} catch (Exception e) {
rollbackIfNecessary(sqlSession); // 出错回滚
throw e;
} finally {
closeSqlSession(sqlSession); // 收尾
}
}
}
大白话比喻:SqlSessionTemplate 是一个机器人服务员。你(DAO)跟它说"帮我查一下供应商列表",它不会自己闷头干,而是先扭头问总台(事务管理器):“现在这位顾客是不是在买单(事务)过程中?”
- 在事务里 → 用总台指定的那个连接(复用);
- 不在事务里 → 自己新开一个会话干完拉倒。
这就引出了下一个问题:总台凭什么能保证"同一个连接"?答案藏在 ThreadLocal。
四、ThreadLocal:人手一个专属"储物柜"
DataSourceTransactionManager 开事务时做的事,源码简化版如下:
// DataSourceTransactionManager.doBegin() 简化
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSource dataSource = ((DataSourceTransactionObject) transaction).getDataSourceHolder().getDataSource();
Connection connection = DataSourceUtils.getConnection(dataSource); // 1. 从连接池拿连接
// 2. 关键:把连接绑到 ThreadLocal 上,key 是 dataSource
TransactionSynchronizationManager.bindResource(dataSource, connection);
connection.setAutoCommit(false); // 3. 关闭自动提交
}
提交/回滚后,事务管理器还会清理储物柜:
// doCleanupAfterCompletion() 简化
protected void doCleanupAfterCompletion(Object transaction) {
TransactionSynchronizationManager.unbindResource(dataSource); // 从柜子里拿出来
DataSourceUtils.releaseConnection(connection, dataSource); // 还回连接池
}
ThreadLocal 的原理:每个线程都有一个私有的 ThreadLocals Map,key 是 ThreadLocal 对象,value 是你存的值。TransactionSynchronizationManager 里就放了一个静态的 ThreadLocal,把"当前线程 → 当前事务的连接"存进去。
// 简化理解
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new ThreadLocal<>();
}
大白话比喻:ThreadLocal 是人手一个的专属储物柜。线程 A 打开自己的柜子,放进自己的连接;线程 B 打开自己的柜子,放进自己的连接——谁都不碰谁的。同一个线程里,第一次放进连接后,后面的 DAO 一查柜子:"有!就是这个连接!"于是一个事务里的所有 SQL 就天然共用一个连接。
提醒:柜子用完必须清空(
unbindResource)。如果不清,线程池里的线程被复用,下一个任务可能拿到上一个任务的旧连接——这就是"ThreadLocal 内存泄漏"的来源。
五、声明式事务 XML 版:四段配置各司其职
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="find*" timeout="60"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pointCut" expression="execution(* com.kgc.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
</aop:config>
三块分别是什么:
- transactionManager(总指挥):知道去哪个连接池拿连接、怎么开事务、怎么提交回滚。它不认识 MyBatis,只管理 JDBC Connection,但 SqlSessionTemplate 会自动配合它。
- tx:advice(规则说明书):按方法名匹配事务规则——
find*走事务且超时 60 秒;*全部走事务,默认配置(RuntimeException才回滚)。 - aop:config(自动拦截器):
execution(* com.kgc.service..*.*(..))意思是"com.kgc.service包及子包下所有类的所有方法"。advisor把"规则说明书"和"拦截范围"绑在一起。
执行流程:
Service 方法被代理拦截
↓
事务管理器 doBegin:取连接 → 绑定 ThreadLocal
↓
方法内所有 DAO 调用 → SqlSessionTemplate 查柜子 → 复用同一连接
↓
正常结束 → commit;抛 RuntimeException → rollback
六、声明式事务注解版:一行配置顶四段
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
和 XML 版的对应关系:
| XML 版 | 注解版 |
|---|---|
<tx:method name="find*" timeout="60"/> | 方法上 @Transactional(timeout=60) |
<tx:method name="*"/> | 方法上 @Transactional |
<aop:pointcut expression="..."/> | 不需要——注解标哪,就拦哪 |
advisor 绑定规则 | tx:annotation-driven 自动扫描 |
Service 里的用法:
@Service
public class ProviderServiceImpl implements ProviderService {
@Autowired
private ProviderDao providerDao;
@Transactional(timeout = 60, readOnly = true)
public List<Provider> findProviderList() {
return providerDao.selectList(...);
}
@Transactional
public void addProvider(Provider p) {
providerDao.insert(p);
// 抛 RuntimeException 会自动回滚
}
}
执行流程和 XML 版完全一样,只有一点不同:代理是 tx:annotation-driven 在容器启动时扫描 @Transactional 注解自动生成的,而不是靠 pointcut 表达式指定的。
七、事务回滚的四个坑:附正确/错误代码对比
坑 1:受检异常默认不回滚
默认只对 RuntimeException/Error 回滚。你的方法 throws SQLException 时,事务不会回滚,直接提交了。
// ❌ 错:SQLException 是受检异常,默认不回滚
@Transactional
public void add(Provider p) throws SQLException {
dao.insert(p);
}
// ✅ 对:明确指定回滚条件
@Transactional(rollbackFor = Exception.class)
public void add(Provider p) throws SQLException {
dao.insert(p);
}
坑 2:catch 吞掉异常
把异常 catch 住又不重新抛,事务管理器根本看不到异常,照样提交。
// ❌ 错:异常被吞掉,事务照常提交
@Transactional
public void add(Provider p) {
try {
dao.insert(p);
dao.updateStock(p); // 这里抛异常
} catch (Exception e) {
log.error("出错了", e); // 吞了!
}
}
// ✅ 对:要么重新抛出,要么手动回滚
@Transactional(rollbackFor = Exception.class)
public void add(Provider p) {
try {
dao.insert(p);
dao.updateStock(p);
} catch (Exception e) {
log.error("出错了", e);
throw new RuntimeException("添加失败", e); // 重新抛出
}
}
坑 3:this.xxx() 自调用绕开代理
同一个类里,this.addProvider() 调用另一个 @Transactional 方法,走的是原始对象,不是代理——事务不生效。
// ❌ 错:this 自调用,第二个方法的事务不生效
@Service
public class ProviderServiceImpl {
@Transactional
public void addWithLog(Provider p) {
this.addProvider(p); // 直穿对象,跳过代理
}
@Transactional
public void addProvider(Provider p) { ... }
}
// ✅ 对:注入自己的代理,或把事务方法拆到别的类
@Service
public class ProviderServiceImpl {
@Autowired
private ProviderService self; // 注入代理(自己代理自己)
@Transactional
public void addWithLog(Provider p) {
self.addProvider(p); // 走代理,事务生效
}
@Transactional
public void addProvider(Provider p) { ... }
}
坑 4:方法不是 public / 类不是 Spring Bean
@Transactional只对 public 方法生效,private/protected 会被忽略。- 类上没有
@Service/@Component等注解、没被扫描进容器,tx:annotation-driven根本不认识它。
结尾互动
源码拆解最怕"背结论",不怕"看代码"。下次再看到
.xml里的 bean,你可以在心里问自己三个问题:它 new 的是谁?它把什么塞给了谁?这套动作背后有没有代理在偷偷干活? 想通了这三个问题,SSM 的配置文件在你眼里就不再是"咒语",而是一张看得懂的装配图。评论区聊聊:你项目里用的是老风格(SqlSessionTemplate)还是新风格(MapperScannerConfigurer)?遇到过哪个坑让你 debug 到崩溃?点赞收藏,下一篇我们拆 MyBatis 一级缓存和 MVCC,保证也这么通俗。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)