Java企业级框架源码:Spring/MyBatis实战

前言:为什么我们要死磕源码?

在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时:
  1. A开始创建,放入三级缓存
  2. A需要B,去创建B
  3. B需要A,从三级缓存拿到A的工厂,生成早期引用(可能是代理),放入二级缓存
  4. B完成创建,放入一级缓存
  5. 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:每次执行都创建新Statement
  • ReuseExecutor:复用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动态代理实现插件。四大可拦截对象:
  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler
示例:一个简单的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就是基于此机制。其核心思路是:
  1. 拦截Executor.query()方法
  2. 解析原始SQL,自动添加LIMIT子句
  3. 同时执行COUNT查询获取总数
  4. 返回分页结果
这种设计完全无侵入,用户只需在调用前设置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分析执行计划,优化索引。

六、结语:源码学习的正确姿势

阅读源码不是为了背诵代码,而是为了:
  1. 理解设计思想:如Spring的模板方法模式、MyBatis的责任链模式
  2. 掌握调试技巧:学会打断点、看调用栈、分析日志
  3. 提升问题解决能力:遇到Bug不再慌,能快速定位根源
  4. 培养架构思维:从宏观视角看待系统设计
最后送大家一句话:框架是死的,人是活的。只有深入源码,才能真正驾驭它们,而不是被它们束缚。

会员自媒体 源码资讯 Java企业级框架源码:Spring/MyBatis实战 https://yuelu1.cn/25935.html

相关文章

猜你喜欢