如果你已经使用 Spring 框架开发了多个项目,却依然对@Autowired注解背后发生了什么感到困惑,或者在面试中被问到“Spring 如何解决循环依赖”时只能回答“三级缓存”,却说不清其具体流程和设计意图,那么这篇文章就是为你准备的。
很多开发者对 Spring 的理解停留在“会用”的层面:知道如何配置 Bean、使用注解、集成 MyBatis。但当线上出现诡异的 Bean 创建失败、事务不生效、或者 MyBatis 动态 SQL 拼接出错时,往往只能靠搜索引擎和试错来解决问题,缺乏从源码层面进行系统性排查的能力。这种“黑盒”状态,不仅影响问题解决效率,更限制了技术深度的提升和架构设计能力。
本文将带你穿透表面,直击 Spring 生态中几个最核心、也最常被问及的源码实现:IOC 容器的启动与 Bean 生命周期、Spring MVC 的请求处理链路、MyBatis 的 SQL 执行引擎,以及Spring Boot 的自动化配置魔法。我们的目标不是罗列每一行代码,而是提炼出那些真正影响你日常开发、面试和系统设计的关键流程与设计思想。读完本文,你将能清晰地回答:一个 HTTP 请求是如何被 Spring MVC 一步步处理的?MyBatis 是如何把一句#{name}转换成?并设置参数的?Spring Boot 的spring-boot-autoconfigure到底做了什么?
1. 这篇文章真正要解决的问题:从“会用”到“懂为什么”
在开始深入源码之前,我们必须明确目标:阅读源码不是为了炫技,而是为了解决实际问题。对于 Spring 生态,最常见的痛点集中在以下几个方面:
- 配置生效之谜:为什么我的
@Transactional注解有时不生效?@Bean和@Component在容器初始化顺序上有什么区别?自定义的BeanPostProcessor该如何编写才能影响特定 Bean? - 运行时诡异问题:循环依赖在什么情况下 Spring 也无法解决?为什么
MyBatis的#{}和${}在动态表名场景下会有不同的表现?Spring MVC的HandlerInterceptor和Filter谁先执行? - 面试深度瓶颈:当被问到“Spring 如何管理 Bean 的生命周期”、“MyBatis 的一级缓存和二级缓存有何区别”、“Spring Boot Starter 原理”时,能否给出有层次、有细节的回答,而不是几个干巴巴的名词?
本文将围绕这些真实、高频的痛点,通过剖析核心源码流程,为你建立一幅清晰的“Spring 生态核心原理地图”。你会发现,很多复杂的现象,其根源都藏在那些精妙的设计模式(如模板方法、工厂模式、代理模式)和核心类(如BeanFactory、ApplicationContext、SqlSession、DispatcherServlet)的交互之中。
2. 核心基石:IOC 容器的启动与 Bean 生命周期全景
IOC(控制反转)是 Spring 的基石。理解它,就理解了 Spring 如何管理你的对象。
2.1 容器启动的入口:不仅仅是 new ClassPathXmlApplicationContext()
对于传统的 XML 配置方式,入口是ClassPathXmlApplicationContext。但更重要的是它的父类AbstractApplicationContext中的refresh()方法。这个方法是一个模板方法,定义了容器刷新的完整流程。
// 简化版 refresh() 方法流程 (位于 AbstractApplicationContext) public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 1. 准备刷新:设置启动时间、激活状态、初始化属性源等 prepareRefresh(); // 2. 获取刷新后的 BeanFactory:这一步会解析 XML/注解,生成 BeanDefinition,但尚未创建 Bean 实例。 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3. 准备 BeanFactory:配置 BeanFactory 的标准上下文特征,如 ClassLoader、后置处理器等。 prepareBeanFactory(beanFactory); try { // 4. 后置处理 BeanFactory(空方法,子类可扩展) postProcessBeanFactory(beanFactory); // 5. 调用 BeanFactory 后置处理器:这是关键!@Configuration, @Component 等注解的解析在此发生。 invokeBeanFactoryPostProcessors(beanFactory); // 6. 注册 Bean 后置处理器:为后续 Bean 实例化、初始化做准备。 registerBeanPostProcessors(beanFactory); // 7. 初始化消息源(国际化) initMessageSource(); // 8. 初始化事件广播器 initApplicationEventMulticaster(); // 9. 空方法,子类可扩展(如 Spring Boot 的 Web 环境启动) onRefresh(); // 10. 注册监听器 registerListeners(); // 11. 实例化所有非懒加载的单例 Bean:核心中的核心! finishBeanFactoryInitialization(beanFactory); // 12. 完成刷新:发布容器刷新完成事件 finishRefresh(); } catch (BeansException ex) { // ... 异常处理,销毁已创建的 Bean destroyBeans(); cancelRefresh(ex); throw ex; } finally { // 重置缓存 resetCommonCaches(); } } }关键洞察:refresh()方法清晰地划分了阶段。Bean 的定义(BeanDefinition)加载和Bean 的实例化是分开的。第 2 步就完成了所有 Bean 定义的注册,而真正的对象创建在第 11 步。这解释了为什么在BeanPostProcessor中不能依赖还未完全初始化的 Bean。
2.2 Bean 生命周期的详细拆解:从定义到销毁
第 11 步finishBeanFactoryInitialization()最终会调用DefaultListableBeanFactory的preInstantiateSingletons()方法,开始实例化 Bean。单个 Bean 的创建流程如下:
- 实例化(Instantiation):调用构造函数创建原始对象。如果配置了
lookup-method或@Lookup,则会使用 CGLIB 生成子类。 - 属性填充(Population):为对象的属性赋值。这是
@Autowired、@Value、@Resource等注解生效的地方。AutowiredAnnotationBeanPostProcessor在此阶段工作。 - Aware 接口回调:如果 Bean 实现了
BeanNameAware、BeanFactoryAware等接口,会在此刻注入相关信息。 - BeanPostProcessor.postProcessBeforeInitialization:所有
BeanPostProcessor的before方法被调用。例如,@PostConstruct注解的处理器就在此阶段执行标注的方法。 - 初始化(Initialization):如果 Bean 实现了
InitializingBean接口,调用afterPropertiesSet()方法。如果配置了init-method,也在此处调用。 - BeanPostProcessor.postProcessAfterInitialization:所有
BeanPostProcessor的after方法被调用。这是AOP 代理对象生成的关键时机。如果 Bean 需要被代理(如事务、异步方法),AbstractAutoProxyCreator会在这里返回一个代理对象,而不是原始对象。 - 销毁(Destruction):容器关闭时,如果 Bean 实现了
DisposableBean接口,调用destroy()方法,或执行配置的destroy-method。
一个常见的误区:很多人认为 AOP 代理是在 Bean 初始化“之后”才创建的。实际上,代理是在postProcessAfterInitialization中创建的,它包装了已经完成属性填充和初始化的原始对象。这意味着,如果你在@PostConstruct方法里调用自己的另一个@Transactional方法,事务可能不生效,因为此时this引用的是原始对象,而不是代理对象。
2.3 循环依赖与三级缓存:为什么是三级,不是两级?
这是面试必考题。Spring 只能解决单例模式、Setter 注入(或字段注入)的循环依赖。构造函数注入的循环依赖无法解决。
三级缓存的定义(在DefaultSingletonBeanRegistry中):
/** 一级缓存:存放完整的单例 Bean */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** 二级缓存:存放早期的 Bean(已实例化,但未完成属性填充和初始化),用于解决循环依赖 */ private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); /** 三级缓存:存放 ObjectFactory,用于生成早期引用(可能被 AOP 代理) */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);解决循环依赖的流程(以 A 依赖 B,B 依赖 A 为例):
- 开始创建 A,实例化 A(调用构造函数),得到一个原始对象。此时,将 A 的
ObjectFactory放入三级缓存。 - 为 A 进行属性填充,发现需要 B。于是去获取 B。
- 开始创建 B,实例化 B,将 B 的
ObjectFactory放入三级缓存。 - 为 B 进行属性填充,发现需要 A。于是去获取 A。
- 关键步骤:获取 A。首先从一级缓存找,没有。从二级缓存找,也没有。从三级缓存找,找到了 A 的 ObjectFactory。调用
getObject()方法。这个方法会执行SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()。如果 A 需要被 AOP 代理,这里就会提前生成代理对象;如果不需要,则返回原始对象。将这个早期引用放入二级缓存,并从三级缓存删除 A 的工厂。将获取到的 A(可能是代理)注入给 B。 - B 完成属性填充、初始化,成为一个完整的 Bean,放入一级缓存。同时清理二、三级缓存中关于 B 的条目。
- B 创建完毕,返回给正在创建 A 的流程。
- A 拿到了 B,完成自己的属性填充、初始化。最后,A 也要放入一级缓存。但在放入之前,它会检查二级缓存中是否已经有自己的早期引用(即第5步放入的那个)。如果有,说明发生了循环依赖,并且可能已经生成了代理。此时,Spring 会确保最终放入一级缓存的对象,与之前暴露给其他 Bean 的早期引用是同一个对象(即那个可能被代理的对象)。
为什么需要三级缓存?
- 一级缓存:存放最终可用的 Bean。
- 二级缓存:存放“不完整但已暴露”的 Bean,避免重复创建早期引用。
- 三级缓存(
ObjectFactory):核心在于延迟决策。它封装了生成早期引用的逻辑。如果 A 需要代理,在 B 依赖 A 时(第5步),通过ObjectFactory可以生成代理对象;如果 A 不需要代理,则返回原始对象。如果将生成早期引用的逻辑直接放在二级缓存,那么所有 Bean 在实例化后都必须立即判断是否需要代理并生成,这不符合 Spring 的设计(代理通常在postProcessAfterInitialization中生成)。三级缓存将“是否提前生成代理”的决策推迟到真正发生循环依赖、需要暴露引用的时候。
3. Spring MVC:一个 HTTP 请求的完整旅程
理解了 IOC,我们再看 Web 层。DispatcherServlet是 Spring MVC 的心脏,它继承了HttpServlet。
3.1 DispatcherServlet 的核心处理流程
其核心方法是doDispatch()。一个请求的大致流程如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { // ... 变量定义 try { // 1. 确定处理当前请求的 Handler(Controller 中的方法) mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { // 没有找到 Handler,返回 404 noHandlerFound(processedRequest, response); return; } // 2. 确定处理该 Handler 的 HandlerAdapter(适配器,如 RequestMappingHandlerAdapter) HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 3. 执行拦截器的 preHandle 方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; // 如果 preHandle 返回 false,直接返回 } // 4. 真正的处理器执行:调用 Controller 方法,并返回 ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 5. 应用默认视图名(如果返回的是 String 视图名) applyDefaultViewName(processedRequest, mv); // 6. 执行拦截器的 postHandle 方法 mappedHandler.applyPostHandle(processedRequest, response, mv); // 7. 处理结果:渲染视图,或处理 @ResponseBody processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { // ... 异常处理 } finally { // 8. 执行拦截器的 afterCompletion 方法(无论成功失败都会执行) if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(processedRequest, response, dispatchException); } // ... 其他清理 } }3.2 关键组件深度解析
- HandlerMapping:负责将请求 URL 映射到具体的 Handler(通常是
HandlerMethod对象,封装了 Controller 类和方法信息)。RequestMappingHandlerMapping是处理@RequestMapping及其衍生注解(@GetMapping等)的默认实现。 - HandlerAdapter:负责实际调用 Handler。因为 Handler 可能是多种类型(如基于
@Controller的类、HttpRequestHandler等),适配器模式统一了调用接口。RequestMappingHandlerAdapter是最常用的,它负责:- 利用
HandlerMethodArgumentResolver解析方法参数(将HttpServletRequest、@RequestBody、@PathVariable等转换成 Java 对象)。 - 调用目标方法。
- 利用
HandlerMethodReturnValueHandler处理返回值(将返回的 Object 转换为ModelAndView或直接写入HttpServletResponse)。
- 利用
- ViewResolver:将逻辑视图名(如
"home")解析为具体的View对象(如JstlView、ThymeleafView)。 - HandlerExceptionResolver:处理 Controller 层抛出的异常,将其转换为特定的错误响应(如 ModelAndView 或 JSON)。
一个常见问题的根源:当你使用@ResponseBody或@RestController时,返回值是如何变成 JSON 的?关键在于RequestMappingHandlerAdapter会在初始化时注册一系列HandlerMethodReturnValueHandler,其中RequestResponseBodyMethodProcessor负责处理@ResponseBody。它内部会使用HttpMessageConverter(如MappingJackson2HttpMessageConverter)将对象序列化为 JSON 并写入 Response。
3.3 拦截器(Interceptor) vs 过滤器(Filter)
- 执行位置:
Filter是 Servlet 规范,在DispatcherServlet之前和之后执行。Interceptor是 Spring MVC 的机制,在DispatcherServlet内部,Handler执行前后执行(见上面doDispatch流程的第3、6、8步)。 - 依赖关系:
Interceptor是 Spring Bean,可以享受 Spring 的依赖注入和 AOP 等功能。Filter不是 Spring Bean(除非使用DelegatingFilterProxy),通常无法直接注入 Spring 管理的服务。 - 使用场景:权限校验、日志记录既可以用
Filter也可以用Interceptor。但如果需要访问 Spring 的HandlerMethod信息(如判断是否有@RequiresPermissions注解),则必须用Interceptor。
4. MyBatis 源码核心:SQL 是如何被执行
MyBatis 的核心是SqlSession。但更底层的是Executor、StatementHandler、ParameterHandler、ResultSetHandler这四个组件。
4.1 一次查询的完整链路
// 简化版查询流程 (以 DefaultSqlSession.selectOne 为例) public <T> T selectOne(String statement, Object parameter) { // 1. 根据 statement id (如 com.xxx.UserMapper.selectById) 获取 MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 2. 执行查询,底层委托给 Executor return executor.query(ms, wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER); }Executor是执行器,负责整个 SQL 执行过程的调度。以SimpleExecutor为例:
- 创建 StatementHandler:根据
MappedStatement中的statementType(STATEMENT, PREPARED, CALLABLE)创建对应的StatementHandler(如PreparedStatementHandler)。 - 创建 ParameterHandler:用于对 PreparedStatement 设置参数。
- 创建 ResultSetHandler:用于将 ResultSet 转换为 Java 对象。
- 执行:调用
StatementHandler.prepare()创建Statement,ParameterHandler.setParameters()设置参数,StatementHandler.execute()执行 SQL,最后ResultSetHandler.handleResultSets()处理结果集。
4.2 #{} 和 ${} 的本质区别
这是 MyBatis 中最容易混淆的点,也直接关系到 SQL 注入安全。
#{}:会被解析为 JDBC 的PreparedStatement的占位符?。参数值会经过ParameterHandler进行类型处理和安全地设置。能有效防止 SQL 注入。<!-- XML 中 --> SELECT * FROM user WHERE name = #{name} <!-- 最终执行的 SQL: SELECT * FROM user WHERE name = ? --> <!-- 参数 'Alice' 会被安全地设置进去 -->${}:是纯粹的字符串替换。MyBatis 会将${}中的内容直接替换到 SQL 语句中,不会进行转义或参数化。<!-- XML 中 --> SELECT * FROM ${tableName} WHERE id = #{id} <!-- 如果 tableName 的值为 `user`,则最终 SQL 为: SELECT * FROM user WHERE id = ? --> <!-- 这里 `${tableName}` 是直接替换,`#{id}` 是参数化。 -->
关键源码位置:在SqlSourceBuilder的parse()方法中,会对 SQL 进行解析。#{}被识别为ParameterMappingTokenHandler处理,生成?和参数映射。${}被识别为TextTokenHandler处理,直接替换文本。
安全建议:永远不要用${}来拼接用户输入的、用于 WHERE 条件或 VALUES 的值。它只应用于动态指定表名、列名等数据库元数据,且这些值必须是可信的(如来自枚举或配置)。
4.3 一级缓存与二级缓存
一级缓存(本地缓存):
- 范围:
SqlSession级别。同一个SqlSession执行相同的查询,第二次会直接从缓存返回。 - 生命周期:与
SqlSession相同。SqlSession关闭,缓存清空。执行insert、update、delete或调用sqlSession.clearCache()也会清空该SqlSession的一级缓存。 - 实现:在
BaseExecutor中有一个localCache字段(PerpetualCache)。
- 范围:
二级缓存:
- 范围:
Mapper命名空间级别(可跨SqlSession)。多个SqlSession操作同一个 Mapper,可以共享缓存。 - 开启:需要在 MyBatis 全局配置中开启
<setting name="cacheEnabled" value="true"/>,并在具体的 Mapper XML 中添加<cache/>标签。 - 生命周期:整个应用生命周期。缓存数据会序列化/反序列化存储。执行对应 Mapper 的
insert、update、delete操作会清空该命名空间的二级缓存。 - 实现:
CachingExecutor装饰了基本的Executor。查询时先查二级缓存,再查一级缓存,最后查数据库。
- 范围:
一个常见的坑:在分布式或微服务环境下,默认的二级缓存(PerpetualCache)是单机内存缓存,会导致数据不一致。生产环境通常需要集成 Redis 等分布式缓存来实现二级缓存。
5. Spring Boot 的魔法:自动配置与 Starter 原理
Spring Boot 的核心目标是“约定大于配置”。它的魔法主要来自@SpringBootApplication注解和spring-boot-autoconfigure模块。
5.1 @SpringBootApplication 的三合一
这个注解是三个注解的合成:
@SpringBootConfiguration:本质是@Configuration,标记该类为配置类。@EnableAutoConfiguration:开启自动配置的核心。@ComponentScan:扫描当前包及其子包下的组件。
5.2 自动配置的触发机制
@EnableAutoConfiguration的关键是@Import(AutoConfigurationImportSelector.class)。
AutoConfigurationImportSelector会调用SpringFactoriesLoader.loadFactoryNames()方法。- 这个方法会从所有依赖 jar 包的
META-INF/spring.factories文件中,读取org.springframework.boot.autoconfigure.EnableAutoConfiguration键对应的配置类全限定名列表。 - Spring Boot 内置的
spring-boot-autoconfigure-x.x.x.jar中就有一个spring.factories文件,里面定义了上百个自动配置类,如DataSourceAutoConfiguration、JacksonAutoConfiguration、WebMvcAutoConfiguration等。
5.3 条件化配置:@Conditional 家族
自动配置类不会无条件生效。它们大量使用了@ConditionalOnXxx注解。
@ConditionalOnClass:当类路径下存在指定的类时才生效。例如,DataSourceAutoConfiguration上有@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }),意味着只有你的项目中引入了数据库相关的 jar(包含了这些类),这个自动配置才会被加载。@ConditionalOnMissingBean:当 Spring 容器中不存在指定类型的 Bean 时才生效。这是自动配置的“退让”机制。它允许你通过自定义@Bean来覆盖自动配置提供的默认 Bean。@ConditionalOnProperty:当指定的配置属性满足条件时才生效。@ConditionalOnWebApplication/@ConditionalOnNotWebApplication:根据应用类型决定。
这就是为什么你引入spring-boot-starter-web后,Tomcat 会自动启动,DispatcherServlet会自动配置的原因。相应的自动配置类(如ServletWebServerFactoryAutoConfiguration、DispatcherServletAutoConfiguration)满足了条件。
5.4 理解 Starter:一个“懒人包”
Starter 本身通常不包含代码,它只是一个pom.xml文件,定义了该功能所需的一组依赖。例如spring-boot-starter-web引入了 Spring MVC、Tomcat、Jackson 等。
最佳实践:当你需要为团队封装一个通用功能(如短信服务、分布式锁)时,可以创建一个自定义 Starter。它的核心是:
- 一个
autoconfigure模块:包含你的自动配置类(使用@Configuration和@Conditional)和核心业务代码。 - 一个
starter模块:只包含一个pom.xml,依赖autoconfigure模块和其他必要库。然后在autoconfigure模块的META-INF/spring.factories中声明你的自动配置类。
6. 实战:从源码角度解决一个典型问题——@Transactional 失效
理论结合实践。我们分析一个高频问题:在同一个类中,一个非事务方法 A 调用事务方法 B,事务不生效。
@Service public class UserService { public void updateUser() { // 一些非数据库操作... this.insertLog(); // 调用本类的事务方法 } @Transactional public void insertLog() { // 插入日志到数据库 logMapper.insert(...); } }原因分析: Spring 的声明式事务是基于 AOP 代理实现的。当调用updateUser()时,实际上是在调用代理对象的方法。代理对象的方法内部会先开启事务,再调用目标对象(原始UserService对象)的方法。但是,在updateUser()方法内部,this.insertLog()中的this指的是目标对象本身,而不是代理对象。因此,这个调用绕过了代理,事务切面无法介入,@Transactional注解自然失效。
源码佐证:事务的 AOP 代理是在BeanPostProcessor.postProcessAfterInitialization阶段由AbstractAutoProxyCreator创建的。代理对象只拦截从外部进入的调用。内部调用发生在目标对象内部,代理机制无法感知。
解决方案:
- (推荐)将事务方法抽取到另一个 Service:这是最清晰的方式,符合单一职责。
- 在类中注入自身的代理:通过
@Autowired或ApplicationContext.getBean()获取代理对象,然后调用。@Service public class UserService { @Autowired private UserService selfProxy; // 注入代理 public void updateUser() { selfProxy.insertLog(); // 通过代理调用,事务生效 } @Transactional public void insertLog() { ... } } - 使用 AspectJ 的编译时/加载时织入(LTW):这种方式可以直接修改字节码,使得内部调用也能被拦截,但配置复杂,一般不推荐。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
Bean 创建失败,提示NoSuchBeanDefinitionException | 1. Bean 未被扫描到(包路径不对) 2. Bean 的依赖 Bean 不存在 3. @Conditional条件不满足 | 1. 检查@ComponentScan或 Spring Boot 主类位置2. 检查依赖 Bean 的配置和条件 3. 开启 debug=true查看自动配置报告 | 调整包路径,检查依赖,查看条件注解 |
@Autowired注入失败,提示No qualifying bean | 1. 存在多个同类型 Bean,未指定@Qualifier2. Bean 的作用域不是 Singleton,且未在正确的上下文中获取 | 1. 检查是否有多个实现类 2. 检查 Bean 的作用域(如 @Scope("prototype")) | 使用@Qualifier或@Primary,检查作用域 |
| MyBatis 查询结果映射失败,属性为 null | 1. 数据库列名与 Java 属性名不匹配(下划线转驼峰) 2. resultMap配置错误3. 类型处理器(TypeHandler)缺失 | 1. 检查 SQL 查询返回的列名 2. 开启 MyBatis 的日志,查看实际执行的 SQL 和返回结果 3. 检查 mybatis.configuration.map-underscore-to-camel-case配置 | 配置列别名,使用resultMap,配置全局驼峰映射或自定义 TypeHandler |
| Spring MVC 接口返回 404 | 1.@RequestMapping路径错误2. Controller 未被扫描到(不是 @RestController/@Controller)3. 静态资源路径冲突 | 1. 检查控制台启动日志,看 HandlerMapping 的注册信息 2. 检查是否在 @ComponentScan范围内 | 检查注解和路径,查看DispatcherServlet映射路径(默认/) |
| 事务不生效 | 1. 方法非public2. 异常未被正确抛出(被 catch 吞没) 3. 数据库引擎不支持事务(如 MyISAM) 4. 同 Bean 内方法调用(见第6节) | 1. 检查方法修饰符 2. 检查异常类型(默认只回滚 RuntimeException和Error)3. 检查数据库引擎和连接池配置 | 确保方法为public,抛出RuntimeException,检查@Transactional(rollbackFor=Exception.class),避免自调用 |
8. 最佳实践与工程建议
- 理解而非记忆:不要死记硬背源码的类名和方法名。理解设计模式(工厂、模板方法、代理、装饰器)在 Spring 中的应用,理解核心流程(容器启动、Bean 生命周期、请求处理、SQL 执行),这比记住所有细节更重要。
- 善用调试工具:在遇到复杂问题时,在关键类(如
AbstractAutowireCapableBeanFactory.doCreateBean、DispatcherServlet.doDispatch、SimpleExecutor.doQuery)上打上断点,跟着调试一遍,是理解源码最直接的方式。 - 关注官方文档和版本更新:Spring 和 MyBatis 的官方文档质量很高。每个大版本(如 Spring 5.x 到 6.x)都可能引入重要变化(如 Spring 6 的 HTTP 接口客户端、MyBatis 3.5 的新特性),关注更新日志可以提前规避兼容性问题。
- 生产环境谨慎使用高级特性:对于循环依赖,尽管 Spring 能解决一部分,但在设计上应尽量避免。对于 MyBatis 的二级缓存,在分布式环境下要使用集中式缓存实现。对于 Spring 的
@Async、@Transactional等基于代理的 AOP 功能,要清楚其局限性(如自调用问题)。 - 自定义扩展点:在理解源码的基础上,可以合理使用 Spring 提供的扩展点,如
BeanPostProcessor、BeanFactoryPostProcessor、HandlerMethodArgumentResolver、HandlerMethodReturnValueHandler、MyBatis 的Interceptor(插件)等,来实现定制化需求,而不是粗暴地修改框架代码。
9. 总结与后续方向
通过本文的梳理,我们不再将 Spring、MyBatis、Spring Boot 视为黑盒。我们从 IOC 容器的refresh()方法出发,看到了 Bean 从定义到销毁的完整旅程;我们跟随一个 HTTP 请求穿越了DispatcherServlet的层层关卡;我们拆解了 MyBatis 将#{}转换为?的精密过程;我们也揭开了 Spring Boot Starter 自动配置的神秘面纱。
源码阅读的价值,在于当系统出现“反直觉”的行为时,你能拥有直指问题根源的洞察力,而不是盲目地尝试和搜索。它让你在技术选型、架构设计和代码评审时,能做出更合理的决策。
如果你希望继续深入:
- Spring 方面:可以研究
Spring AOP与AspectJ的集成、Spring Transaction的传播机制源码、Spring Security的过滤器链和投票器机制。 - MyBatis 方面:可以深入研究插件(Interceptor)的开发,如何实现分页、慢SQL日志、数据加解密等通用功能。
- Spring Boot 方面:可以学习
Actuator端点原理、Spring Boot Admin的集成、以及如何编写一个高质量的自定义 Starter。
技术的深度,决定了你解决问题的能力边界。从今天起,尝试带着问题去阅读源码,你收获的将不仅仅是面试题的答案,更是成为一名优秀架构师的坚实基础。建议将本文作为一份原理地图收藏,在未来的开发实践中反复对照和印证。