前言:为什么我们要死磕源码?
在2026年的今天,Java生态早已不是当年那个“配置即地狱”的时代。Spring 6.x全面拥抱Jakarta EE,最低支持Java 17,虚拟线程(Virtual Threads)和AOT编译让启动速度飙升;MyBatis也在持续进化,插件机制和动态SQL成为了无数大厂中间件的基石。
但很多开发者——包括工作三五年的老手——依然停留在“会用注解”、“能跑通CRUD”的层面。一旦线上出现循环依赖、事务失效、SQL执行慢如蜗牛,或者需要自定义扩展功能时,就束手无策,只能盲目搜索、复制粘贴,甚至重启服务碰运气。
真正的核心竞争力,从来不是你会用多少个框架,而是你是否理解它们背后的设计思想与实现细节。
本文不谈虚的,直接从Spring和MyBatis的源码入手,结合真实生产环境中的踩坑案例,带你深入核心机制,掌握从原理到实战的完整链路。所有代码示例均基于最新稳定版本(Spring 6.1 + MyBatis 3.5.15),确保你学到的东西明天就能用上。
一、Spring核心:IoC容器是如何“无中生有”的?
1.1 Bean的生命周期:不只是“创建”那么简单
很多人以为
@Autowired就是魔法,其实背后是一整套精密的Bean生命周期管理。我们以一个典型的Service为例:java
编辑
1@Service
2public class UserService {
3 @Autowired
4 private OrderService orderService;
5
6 public void createUser() {
7 // 业务逻辑
8 }
9}
当Spring容器启动时,它到底做了什么?
源码追踪:从refresh()开始
一切始于
AbstractApplicationContext.refresh()方法。这个方法只有短短十几行,却 orchestrates 了整个容器的初始化过程:java
编辑
1// AbstractApplicationContext.java
2public void refresh() throws BeansException, IllegalStateException {
3 synchronized (this.startupShutdownMonitor) {
4 // 1. 准备刷新上下文
5 prepareRefresh();
6
7 // 2. 获取新的BeanFactory
8 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
9
10 // 3. 填充BeanFactory(关键步骤!)
11 prepareBeanFactory(beanFactory);
12
13 // 4. 后置处理器注册
14 postProcessBeanFactory(beanFactory);
15
16 // 5. 注册BeanPostProcessor
17 invokeBeanFactoryPostProcessors(beanFactory);
18
19 // 6. 注册拦截器
20 registerBeanPostProcessors(beanFactory);
21
22 // 7. 初始化消息源
23 initMessageSource();
24
25 // 8. 初始化事件广播器
26 initApplicationEventMulticaster();
27
28 // 9. 留给子类扩展
29 onRefresh();
30
31 // 10. 注册监听器
32 registerListeners();
33
34 // 11. 实例化所有非懒加载的单例Bean(重点!)
35 finishBeanFactoryInitialization(beanFactory);
36
37 // 12. 完成刷新
38 finishRefresh();
39 }
40}
其中第11步
finishBeanFactoryInitialization是重中之重。它会调用beanFactory.preInstantiateSingletons(),触发所有非懒加载单例Bean的创建。三级缓存解决循环依赖的秘密
上面例子中,如果
OrderService也依赖UserService,就会形成循环依赖。Spring如何解决?答案是三级缓存。- 一级缓存(
singletonObjects):存放完全初始化好的Bean - 二级缓存(
earlySingletonObjects):存放早期暴露的Bean(尚未填充属性) - 三级缓存(
singletonFactories):存放Bean工厂,用于生成代理对象
当A依赖B,B又依赖A时:
- A开始创建,放入三级缓存
- A需要B,去创建B
- B需要A,从三级缓存拿到A的工厂,生成早期引用(可能是代理),放入二级缓存
- B完成创建,放入一级缓存
- A拿到B的引用,完成自身创建
这个过程在
DefaultSingletonBeanRegistry.getSingleton()方法中实现,代码虽复杂,但逻辑严密。实战提示:构造器注入无法解决循环依赖!务必使用Setter注入或@Lazy注解。我们在某金融项目中就曾因误用构造器注入导致启动失败,排查整整一天。
二、MyBatis深度剖析:SQL执行的完整链路
2.1 从Mapper接口到JDBC Statement
很多人认为MyBatis只是“XML映射工具”,其实它是一个完整的SQL执行引擎。让我们跟踪一次查询的全过程:
java
编辑
1User user = userMapper.selectById(1L);
这行简单代码背后,经历了以下阶段:
阶段一:SqlSession的获取
通过
SqlSessionFactory.openSession()获得SqlSession,它是MyBatis的核心接口,相当于数据库操作的门面。阶段二:Executor的执行策略
SqlSession将请求委托给Executor。MyBatis提供三种执行器:SimpleExecutor:每次执行都创建新StatementReuseExecutor:复用Statement,适合批量操作BatchExecutor:批量执行,用于INSERT/UPDATE/DELETE
默认使用
SimpleExecutor,但在高并发场景下,ReuseExecutor能显著减少资源消耗。阶段三:StatementHandler的封装
StatementHandler负责创建和操作JDBC的Statement对象。它内部又包含:ParameterHandler:处理参数映射ResultSetHandler:处理结果集映射
阶段四:动态SQL的解析
如果你的XML中包含
<if>、<choose>等标签,MyBatis会使用SqlNode树进行动态拼接。这个过程在DynamicSqlSource.getBoundSql()中完成。xml
编辑
1<select id="selectUser" resultType="User">
2 SELECT * FROM users
3 <where>
4 <if test="name != null">
5 AND name = #{name}
6 </if>
7 <if test="age != null">
8 AND age > #{age}
9 </if>
10 </where>
11</select>
上述XML会被解析成一系列
SqlNode,最终生成可执行的SQL语句。三、插件机制:如何在不改源码的情况下扩展功能?
MyBatis最强大的特性之一就是插件机制。通过实现
Interceptor接口,你可以在SQL执行的任意环节插入自定义逻辑。3.1 插件的工作原理
MyBatis使用责任链模式 + JDK动态代理实现插件。四大可拦截对象:
ExecutorStatementHandlerParameterHandlerResultSetHandler
示例:一个简单的SQL日志插件
java
编辑
1@Intercepts({
2 @Signature(
3 type = Executor.class,
4 method = "query",
5 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
6 )
7})
8public class SqlLoggerInterceptor implements Interceptor {
9
10 @Override
11 public Object intercept(Invocation invocation) throws Throwable {
12 long start = System.currentTimeMillis();
13 MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
14 Object parameter = invocation.getArgs()[1];
15
16 System.out.println("执行SQL: " + ms.getId());
17 System.out.println("参数: " + parameter);
18
19 Object result = invocation.proceed();
20
21 long cost = System.currentTimeMillis() - start;
22 System.out.println("耗时: " + cost + "ms");
23
24 return result;
25 }
26
27 @Override
28 public Object plugin(Object target) {
29 return Plugin.wrap(target, this);
30 }
31
32 @Override
33 public void setProperties(Properties properties) {
34 // 可配置参数
35 }
36}
配置方式(mybatis-config.xml):
xml
编辑
1<configuration>
2 <plugins>
3 <plugin interceptor="com.example.SqlLoggerInterceptor"/>
4 </plugins>
5</configuration>
3.2 生产环境实战:分页插件的实现思路
著名的PageHelper就是基于此机制。其核心思路是:
- 拦截
Executor.query()方法 - 解析原始SQL,自动添加LIMIT子句
- 同时执行COUNT查询获取总数
- 返回分页结果
这种设计完全无侵入,用户只需在调用前设置
PageHelper.startPage(pageNum, pageSize)即可。避坑指南:插件顺序很重要!多个插件会形成责任链,顺序由配置决定。曾有个项目因分页插件放在事务插件之后,导致计数SQL未纳入事务,数据不一致。
四、高频踩坑实录:那些年我们交过的学费
坑点一:事务失效的隐形杀手
场景:在Service层调用另一个Service方法,期望共享事务,结果第二个方法独立提交了。
原因:Spring事务基于AOP代理。如果同类内部方法调用(this.method()),不会经过代理,事务注解失效。
解决方案:
- 注入自身:
@Autowired private SelfService self; - 使用
AopContext.currentProxy()获取代理对象 - 重构代码,避免内部调用
坑点二:MyBatis批量插入性能暴跌
错误写法:
java
编辑
1for (User user : userList) {
2 userMapper.insert(user); // 每次提交事务
3}
正确姿势:
java
编辑
1SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
2try {
3 UserMapper mapper = sqlSession.getMapper(UserMapper.class);
4 for (User user : userList) {
5 mapper.insert(user);
6 }
7 sqlSession.commit(); // 一次性提交
8} finally {
9 sqlSession.close();
10}
实测数据:插入1万条记录,前者耗时12秒,后者仅0.8秒,提升15倍!
坑点三:循环依赖导致的启动失败
现象:
BeanCurrentlyInCreationException根因:构造器注入形成环路,且未使用
@Lazy修复方案:
java
编辑
1@Service
2public class AService {
3 private final BService bService;
4
5 // 改为Setter注入或使用@Lazy
6 @Autowired(required = false)
7 public void setBService(BService bService) {
8 this.bService = bService;
9 }
10}
五、性能优化:从源码角度提升系统响应速度
5.1 Spring Bean的懒加载策略
对于大型应用,数百个Bean的初始化可能拖慢启动时间。启用懒加载:
yaml
编辑
1spring:
2 main:
3 lazy-initialization: true
或在配置类上标注
@Lazy。注意:某些Bean(如定时任务、消息监听器)不能懒加载。5.2 MyBatis缓存调优
- 一级缓存(SqlSession级别):默认开启,同一会话内重复查询直接返回
- 二级缓存(Namespace级别):需手动配置,注意分布式环境下的脏读问题
建议:在高并发场景下关闭二级缓存,改用Redis等外部缓存,避免内存溢出和数据不一致。
5.3 SQL执行计划分析
在MySQL中开启慢查询日志,配合MyBatis插件记录每条SQL的执行时间。对超过阈值的SQL,使用
EXPLAIN分析执行计划,优化索引。六、结语:源码学习的正确姿势
阅读源码不是为了背诵代码,而是为了:
- 理解设计思想:如Spring的模板方法模式、MyBatis的责任链模式
- 掌握调试技巧:学会打断点、看调用栈、分析日志
- 提升问题解决能力:遇到Bug不再慌,能快速定位根源
- 培养架构思维:从宏观视角看待系统设计
最后送大家一句话:框架是死的,人是活的。只有深入源码,才能真正驾驭它们,而不是被它们束缚。