1. 为什么还要聊Spring:它真正解决的三个核心痛点
记得刚入行那会儿,带我的前辈让我改一个老项目的订单模块。我打开代码一看,整个Service层里到处都是new UserService()、new OrderMapper(),一个订单类要想干活,得自己把上下游的依赖全部手动造出来。改一个构造函数,全工程十几个调用点跟着报错。那段时间我最怕听到的一句话就是:"你把这个类的构造方法加个参数。"后来项目引入Spring,把这些依赖全部交给容器托管,代码瞬间清爽了,我也终于理解了为什么大家都说"Spring是Java开发的基石"。
很多人对Spring的第一印象是"轻量级、模块化",但这两个词到底意味着什么,没踩过坑的人是体会不到的。所谓轻量级,不是说它的代码量小,而是说它不强迫你使用重量级Java EE容器(比如以前的EJB),一个普通的Servlet容器甚至一个Java SE环境就能跑起来。所谓模块化,则是说Spring把它庞大的能力拆成了很多相对独立的模块,你用到什么就引入什么,不需要把整个框架全家桶都搬进来。
具体到实际开发,Spring解决的核心痛点可以归纳成三个:
第一个痛点是对象之间过度耦合。传统方式里,对象A要使用对象B,就得自己负责创建B,这样A就"被迫"知道了B的所有构造细节。一旦B的构造方式变化,A也要跟着改。Spring引入控制反转(IoC),把对象的创建和装配职责交给容器,A只需要声明"我需要一个B类型的依赖",剩下的由容器来满足。
第二个痛点是横切逻辑无处安放。日志、事务、权限校验这类代码,如果不做任何抽象,会散落在每一个业务方法的每一行里,代码复用率极低,改动一次要动几十个文件。Spring通过面向切面编程(AOP)把这些横切逻辑从业务代码里抽取出来,开发人员只需要关注核心业务。
第三个痛点是配置管理混乱。早期的Java项目,连接数据库、初始化第三方服务这些逻辑往往写死在代码里,换个环境就得重新编译部署。Spring从最早的XML配置,到后来的注解驱动,再到Spring Boot的自动配置,把"环境差异"这件事从代码中剥离了出去。
适合读这篇文章的朋友,我默认你有一定的Java基础,但不需要太深——只要写过JavaWeb项目,被对象管理、配置管理或者循环依赖这类问题折磨过,就知道Spring的价值所在。本文会从IoC/DI的原理出发,讲清楚Bean的生命周期和三级缓存,顺一遍从Spring到Spring Boot、Spring Cloud的演进逻辑,最后通过手写一个微型Spring来验证你对核心机制的理解。这是我认为理解Spring框架最高效的路径。
2. 控制反转到底反转了什么:一个关于"主动权"的故事
2.1 从new对象到容器托管,变化的不只是代码量
先看一段最传统的写法:
public class OrderService { private final UserService userService; private final InventoryService inventoryService; public OrderService() { // 自己创建依赖,并负责它们的完整生命周期 this.userService = new UserService(); this.inventoryService = new InventoryService(); } public void createOrder(Order order) { userService.validateUser(order.getUserId()); inventoryService.deductStock(order.getProductId()); // 核心业务逻辑... } }这段代码表面上没什么大问题,但当你写单元测试的时候就会发现,自己想测OrderService里的业务逻辑,却因为new UserService()的真实实现而被迫同时加载数据库、Redis、第三方接口等一堆外部依赖。你想传一个Mock对象进去替代真正的UserService,但构造函数已经写死了,根本插不进去。
引入Spring之后,同样的类会变成这样:
@Service public class OrderService { private final UserService userService; private final InventoryService inventoryService; public OrderService(UserService userService, InventoryService inventoryService) { this.userService = userService; this.inventoryService = inventoryService; } public void createOrder(Order order) { userService.validateUser(order.getUserId()); inventoryService.deductStock(order.getProductId()); // 核心业务逻辑... } }注意差异:OrderService不再自己去new依赖,而是通过构造器声明自己需要什么。容器在创建OrderService的时候,看到构造器参数,就会先把UserService、InventoryService创建好,再注入进来。这就是依赖注入(Dependency Injection,DI)。而"控制反转"这个词描述的是同一个事实的另一个角度:对象创建和装配的控制权,从对象自己手里反转到容器手里。
可以打个比方。传统方式就像你住酒店,想喝水得自己烧水、自己带杯子、自己清理,一切自己动手;Spring的方式就是入住之后打个电话给前台,前台把水、杯子、甚至你习惯的茶包都送到房间来。你不关心水是从哪个水站送来的,也不关心杯子什么时候被清洗消毒,你只声明"我需要喝水",然后就把精力放在自己要办的事情上。
2.2 三种注入方式怎么选
Spring支持三种依赖注入方式,我见过不少团队为此争论,这里直接给结论。
三种方式分别是:
- 构造器注入:依赖作为构造参数传入,对象创建时依赖就绪,对象始终处于完整状态。
- Setter注入:对象先以无参构造创建,再通过setter方法补齐依赖。
- 字段注入:直接在成员变量上标
@Autowired,由反射机制赋值。
// 字段注入 - 最简洁,但隐患不少 @Autowired private UserService userService; // Setter注入 - 灵活,但依赖可以被中途修改 @Autowired public void setUserService(UserService userService) { this.userService = userService; } // 构造器注入 - Spring官方推荐 public OrderService(UserService userService) { this.userService = userService; }我在实际项目中的建议是:核心依赖用构造器注入,可选依赖用Setter注入,字段注入尽量不用。为什么?因为构造器注入有三个明显优势:第一,依赖不可变,字段可以用final修饰;第二,构造时就能发现依赖缺失,而不是运行时才抛出空指针;第三,测试友好,直接构造对象传入Mock即可。字段注入最大的问题是隐藏了依赖关系,类看起来干干净净,实际上暗藏了一堆运行时才被填充的字段,一旦依赖链断裂,排查起来非常痛苦。
2.3 从BeanFactory到ApplicationContext:容器本身的两代进化
聊到IoC容器,有两个名词必须分清:BeanFactory和ApplicationContext。
BeanFactory是Spring IoC容器的底层接口,它定义了容器最基本的行为——管理Bean的创建、获取、销毁。它采用延迟加载策略,Bean只有在第一次被getBean()获取时才会被实例化。这保证了轻量,但也意味着容器刚启动时无法发现某些配置错误。
ApplicationContext是BeanFactory的子接口,在它的基础上扩展了国际化支持、事件发布、资源加载、自动扫描等功能。它默认采用预加载策略,容器启动时就会把配置的单例Bean全部实例化,因此配置错误能更早暴露。实际开发中我们几乎都是用ApplicationContext,只有极端的资源受限场景(比如嵌入式环境)才会考虑直接用BeanFactory。
从使用者的视角看,容器做的事情可以简化为三步:定义(Configuration)→ 注册(Registration)→ 获取(Retrieval)。你通过@Configuration和@Bean告诉容器"有哪些对象需要管理",容器启动时扫描并注册这些Bean定义,之后你通过@Autowired或者context.getBean()把对象取出来用。你不需要知道对象是什么时候被创建、以什么顺序被创建的,那些都是容器内部需要考虑的事情。
有一个小细节值得注意:通过@Autowired获取Bean时,默认按照类型匹配。如果同一类型有多个Bean,会退化为按名称匹配。这也是为什么我会要求团队给同一个接口的多个实现类起有意义的Bean名称,否则@Qualifier用起来很容易张冠李戴。
3. Bean的一生与三级缓存:Spring面试最高频考点的完整解读
3.1 一个Bean的完整生命周期
面试的时候,如果只能问Spring一个问题,那一定是"你了解Bean的生命周期吗"。这个问题能检验的东西太多了:知不知道实例化和初始化的区别、知不知道BeanPostProcessor的机制、知不知道AOP代理在什么时候介入、知不知道循环依赖为什么能解决。
一个普通单例Bean从生到死,大致要经过这么几步:
- 容器扫描到Bean定义,创建
BeanDefinition对象,记录类的元信息、作用域、初始化和销毁方法等。 - 通过构造器或者工厂方法实例化Bean对象,此时它还是一个"光溜溜"的新对象,依赖属性都还没有填充。
- 对Bean进行属性填充,把
@Autowired、@Value声明的依赖和配置注入进去。 - 执行各类初始化回调:先是
BeanNameAware、BeanFactoryAware等Aware接口回调,然后是BeanPostProcessor的前置处理,接着执行@PostConstruct或InitializingBean的初始化方法,最后是BeanPostProcessor的后置处理。 - Bean进入"可用状态",被容器缓存起来,供上层业务调用。
- 容器关闭时,执行
@PreDestroy或DisposableBean的销毁方法,释放资源。
看到第4步的BeanPostProcessor你就应该明白了:Spring AOP创建代理对象的逻辑就挂在这里。一个Bean属性填充完毕、初始化方法执行完之后,会被AbstractAutoProxyCreator这个后置处理器拦截,如果它匹配到了切点,就生成一个代理对象放回容器。这也意味着,我们拿到的Bean可能根本不是原对象,而是它的代理。理解这一点,对后面理解三级缓存至关重要。
3.2 什么叫循环依赖:最典型的开发事故现场
假如有个场景:UserService需要调用RoleService的某些方法,而RoleService反过来也要用UserService的东西。
@Service public class UserService { @Autowired private RoleService roleService; } @Service public class RoleService { @Autowired private UserService userService; }容器创建UserService时,发现需要RoleService,于是去创建RoleService;创建RoleService时,又发现需要UserService,于是回头去创建UserService……如果不做任何处理,这里会陷入无限递归,直接栈溢出。
Spring解决这个问题靠的是三个缓存,业界喜欢把它们叫做三级缓存:
- 一级缓存
singletonObjects:存放已经完整创建好的单例Bean,也就是"成品"。getBean默认先从这里取。 - 二级缓存
earlySingletonObjects:存放已经实例化但尚未完成属性填充/初始化的"半成品"Bean。 - 三级缓存
singletonFactories:存放用于创建"半成品"的ObjectFactory工厂对象,它能在需要时提前暴露Bean的早期引用(可能还需要处理AOP代理)。
整个流程是这样的:容器创建UserService,实例化完成之后,把它包装成一个ObjectFactory放入三级缓存。然后尝试填充roleService属性,发现RoleService还没创建,就转而创建RoleService。RoleService实例化完成后,同样放入三级缓存,接着填充它的userService属性。这一次,容器去获取UserService时,一级缓存没有,二级缓存没有,但在三级缓存的ObjectFactory里找到了它,于是调用工厂拿到UserService的早期引用(如果当前Bean需要AOP代理,这里会用getEarlyBeanReference提前生成代理),放入二级缓存,并注入给RoleService。等RoleService走完自己的完整生命周期后,回到UserService的填充流程,注入RoleService,随后再走完UserService自身的初始化,最后把成品放入一级缓存。
3.3 为什么必须是三级缓存,两级不行吗
这个问题的答案,直接决定你是"背原理"还是"懂原理"。
先说结论:如果Spring不支持AOP,两级缓存就够用了。一级放成品,二级放半成品,循环依赖发生时从二级缓存拿半成品注入,等对象初始化完成后再替换为成品。逻辑上看是完全闭环的。
但问题是Spring有AOP。一个Bean最终被放回容器的,可能是它初始化之后生成的代理对象。假如只有两级缓存,循环依赖场景下RoleService拿到手的UserService是早期暴露的原始对象,而最终容器里保存的却是UserService的代理对象,两者不是同一个对象,就会出大问题:RoleService里注入的原始对象没有增强逻辑,事务、切面全部失效。
三级缓存的妙处就在于,它缓存的是一个ObjectFactory而不是具体对象,工厂可以在需要提前暴露引用的时候,调用getEarlyBeanReference把代理对象提前生成出来。这样RoleService拿到手的,就是那个带AOP增强的代理对象,与最终放入一级缓存的对象保持了一致。这也是@Autowired在循环依赖和AOP同时存在时依然能拿到正确代理的原因。
另外有两个关键限制需要记住:构造器注入的循环依赖无法解决,因为构造器在实例化阶段就需要依赖,此时Bean还没有进入三级缓存的保护范围;原型作用域(Prototype)的Bean不缓存循环依赖,因为你每次拿到的都是一个全新对象,不存在可以提前暴露的"同一个实例"。遇到这两种情况,常见的绕法是改成Setter/字段注入,或者给其中一个依赖加@Lazy,打破直接加载的依赖链。
4. 从Spring到Spring Boot再到Spring Cloud:框架演变背后的思路
4.1 Spring Boot的出现,不是为了炫技而是为了降低门槛
很多刚接触Spring的读者会困惑:Spring和Spring Boot到底有什么区别?我在这里讲一个非常朴素的演进故事。
早期的Spring以XML配置为主,一个项目里动辄十几个XML文件:配置数据源的要写一个、配置事务的要写一个、配置Bean扫描的要写一个。我记得刚工作那会儿,每次接手老项目都要对着applicationContext.xml翻半天,改一个连接池配置要小心谨慎,生怕把某个Bean的依赖关系弄断。Spring虽然用"约定优于配置"的思想在注解上做了很多改进,但项目初期搭环境这件事,对新人从来不友好。
Spring Boot对这个问题的回答是自动配置(AutoConfiguration)和起步依赖(Starter)。你引入spring-boot-starter-web,Maven就会把Spring MVC、内嵌Tomcat、Jackson等一系列依赖一次性带齐;你main方法上写一个@SpringBootApplication,启动类一跑,Tomcat自动起在8080端口,各类自动配置按条件装配好。所见即所得,几乎没有"搭框架"的痛苦。
我见过不少团队用Spring Boot做项目,但真正用好自动配置的并不多。很多人不知道Spring Boot的自动配置是基于条件注解@ConditionalOnClass、@ConditionalOnMissingBean来实现的,比如你引入spring-boot-starter-data-redis,容器里没有RedisTemplate时它才会自动创建一个。了解这个机制,你就明白为什么在配置类里手动定义一个同类型Bean可以覆盖默认配置——因为自动配置发现你已经定义过了,就选择"让贤"。
搜索词里反复出现"spring boot集成web socket yml配置",这也跟Spring Boot时代配置方式的变化有关。以前配置WebSocket需要在XML里注册Handler和拦截器,现在你只需要实现WebSocketConfigurer,然后在application.yml里写清楚端点前缀、允许的跨域来源等参数:
spring: websocket: endpoint: /ws allowed-origins: "*"Spring Boot把所有配置收敛到统一的application.yml或application.properties中,用千篇一律的键值对描述环境差异。对比XML时代,配置量减少了大概70%,排查问题的路径也清晰了很多。我们团队内部有个不成文的规定:环境相关的配置全部写进application-{profile}.yml,通过spring.profiles.active切换,任何环境都不需要改代码,只需改启动参数。
4.2 微服务时代:Spring Cloud和Spring Cloud Alibaba提供的完整方案
应用规模上去之后,单体应用开始出现发布耦合、局部故障波及全局、数据库连接池被打满等痛点,这时就会考虑拆成微服务。微服务不是一个框架能做出来的事,它需要一整套基础设施:服务注册与发现、配置中心、负载均衡、熔断限流、网关路由、分布式事务跟踪……
Spring Cloud把这套东西做成了标准。以阿里出品的Spring Cloud Alibaba为例,它提供的核心组件包括:
- Nacos:既做服务注册中心,又做配置中心。服务启动时把自己的IP和端口注册上去,其他服务通过服务名调用,不用关心目标实例的地址到底在哪。
- Sentinel:流量控制与熔断降级。某个下游服务响应变慢、甚至挂了,Sentinel可以把流量快速失败,避免线程池耗尽拖垮上游服务。
- OpenFeign:声明式的HTTP客户端。你只需要定义一个接口,加上
@FeignClient("order-service"),Spring会帮你生成一个远程调用代理,看起来就跟调用本地方法一样。 - Gateway:微服务网关,负责统一入口、鉴权、路由转发,把所有服务暴露给前端的入口收敛到一个点。
如果你所在的公司还在做单体应用,我不建议为了"上微服务"而上微服务。微服务的运维复杂度是实打实的,光Nacos集群、SkyWalking链路追踪就能让一个小团队忙得不可开交。我见过一些项目,订单量和并发量根本不需要微服务,硬拆之后反而引入了网络调用延迟、分布式事务一致性等问题。判断标准很简单:团队规模、业务复杂度、流量水位匹配,才值得拆。
4.3 搜索热词里的秘密:Spring AI、Spring Security与若依框架
从热搜词来看,当下大家最为关注的几个方向,恰好反映了Spring生态在不同维度的演进。
Spring Security在很多项目中承担着认证授权职责。很多人觉得它难用,是因为没理解它的核心过滤链机制。Spring Security本质上是一条过滤器链,每个过滤器负责一件独立的事:认证过滤器负责识别你是谁,授权过滤器负责判断你能干什么,异常处理过滤器负责把认证失败结果转成JSON返回给前端。理解了这条链,配置起来就不需要每个方法都去背API,而是顺着链去补充自己的实现。
Spring AI则是一个新动向。OpenAI发布之后,Java社区很受伤——官方SDK是Python和Node的,Java生态里只能自己做HTTP调用拼接JSON。Spring AI试图把这套东西标准化:你通过ChatClient调用大模型接口,就跟调用一个普通的Service一样,模型切换、Prompt模板、输出解析都可以通过配置和注解来处理。如果你所在的公司有Java后端接入大模型的需求,Spring AI是目前最值得持续关注的方向之一。
至于若依框架(RuoYi),在搜索词里出现频率很高,它是基于Spring Boot的一套后台管理脚手架,内置了用户、角色、菜单权限、数据字典、代码生成器、定时任务等开箱即用的模块。对于很多中小团队来说,直接用若依能省掉大量重复的CRUD和权限代码。不过要提醒一下,脚手架类工具最大的风险是"上手容易演进难",用了若依之后,团队的习惯会被它自带的那套代码风格深刻影响,后期要跳出它的框架会比较费劲。把它当起点可以,但不要被它锁死。
从这个角度看,Spring生态的演进逻辑始终如一:明确解决某个阶段的痛点,然后通过模块化设计让你按需取用。单体时代解决对象管理和配置问题,Spring Boot解决应用快速搭建问题,Spring Cloud解决微服务基础设施问题,Spring AI解决AI接入标准化问题。每一层新框架都不是凭空创造概念,而是在前面基础之上继续补短板。
5. 手写一个微型Spring:用200行代码验证IoC与DI的核心机制
5.1 为什么推荐"手写Spring"作为进阶路径
搜索词里 "手写spring"、 "第1关:第一个spring boot程序" 出现的频率很高。学习框架时,很多人会陷入"会用但不懂"的状态:注解加得飞起,但问一句"@Component背后的容器到底做了什么"就答不上来。手写一个简化版的IoC容器,是成本最低的深入理解方式。
我不建议直接去看开源社区里那些"手写Spring"项目的完整源码,它们动辄几千行,已经把很多边界情况都考虑进去了,读起来跟读源码没有太大差别。更好的方式是从零开始,自己实现一个最简版本,然后在这个版本上不断加功能。当你亲手感受到"扫描注解→创建对象→注入依赖"这个过程后,再回去看Spring的源码,就会觉得顺理成章。
5.2 三个核心步骤:扫描、注册、注入
一个最简IoC容器,只需要承担三件事:
- 扫描:找出所有被
@Component标记的类。 - 注册:把这些类的定义记录到容器中,形成Bean定义表。
- 注入:创建对象实例,并把
@Autowired标记的依赖注入进去。
先定义两个注解:
@Component @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Component {} @Autowired @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.CONSTRUCTOR}) public @interface Autowired {}然后是核心容器:
public class SimpleApplicationContext { private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); private final Map<String, Class<?>> beanDefinitions = new ConcurrentHashMap<>(); public SimpleApplicationContext(String basePackage) { // 1. 扫描 scan(basePackage); // 2. 实例化并注入 for (String beanName : beanDefinitions.keySet()) { createBean(beanName); } } private void scan(String basePackage) { String path = basePackage.replace('.', '/'); // 通过类加载器扫描该包下的所有 .class 文件 // 找到标注 @Component 的类,注册到 beanDefinitions } private Object createBean(String beanName) { if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } Class<?> clazz = beanDefinitions.get(beanName); Object instance; try { // 3. 用无参构造实例化 instance = clazz.getDeclaredConstructor().newInstance(); // 4. 遍历字段,完成属性注入 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { Object dependency = getBean(field.getName()); field.setAccessible(true); field.set(instance, dependency); } } } catch (Exception e) { throw new RuntimeException("Failed to create bean: " + beanName, e); } singletonObjects.put(beanName, instance); return instance; } public <T> T getBean(Class<T> clazz) { return clazz.cast(getBean(lowerFirst(clazz.getSimpleName()))); } public <T> T getBean(String beanName) { return (T) createBean(beanName); } }代码省略了扫描的细节,但那只是技术活。核心逻辑就两处:createBean方法里,先实例化对象,然后遍历字段,发现@Autowired就递归调用getBean去拿依赖;getBean方法里,如果目标Bean已经在缓存中就直接返回,否则走创建流程。当你发现这个简版容器在A依赖B、B依赖A的环状依赖下会栈溢出时,你就重新理解了Spring三级缓存的价值。
5.3 这个微型Spring与真实Spring的差距在哪里
对比一下真实Spring,你会发现它的每一步都比我的简版复杂了几个数量级:
| 维度 | 简版IoC容器 | 真实Spring |
|---|---|---|
| Bean定义 | 仅存Class对象 | BeanDefinition,含作用域、懒加载、初始化方法等 |
| 单例缓存 | 一个Map | 三级缓存(成品、半成品、ObjectFactory) |
| 实例化方式 | 仅无参构造 | 构造器推断、工厂方法、CGLIB代理 |
| 依赖注入 | 仅字段注入 | 字段注入、Setter注入、构造器注入、@Qualifier |
| 生命周期回调 | 无 | Aware接口、BeanPostProcessor、@PostConstruct |
| AOP支持 | 无 | 通过BeanPostProcessor机制实现代理 |
看到这张表,你就明白我之前强调"了解Bean生命周期"的意义了。真实Spring所有的高级特性,都是在Bean实例化的各个阶段插入钩子实现的。AOP是挂在BeanPostProcessor上、自动配置是挂在BeanDefinition的加载阶段、循环依赖是靠三级缓存的提前暴露机制。理解了这一层,你对Spring的理解就从"会背诵概念"上升到了"看得懂机制"。
如果你有精力继续往下玩,我建议尝试在简版容器里加入一个简单的BeanPostProcessor接口,然后在初始化Bean时依次调用它。再定义一个TransactionalProxyPostProcessor,用它给目标类生成一个带方法耗时统计的代理对象。做完这一步,你就亲手还原了Spring AOP的最小闭环,那种"原来如此"的体验,比看十篇源码分析文章都要深刻。
6. 学Spring的实际路径与几个常见的认知误区
每次有读者问我"Spring怎么学",我的答案通常都是三个字:看源码。但这个建议往往把人劝退,因为Spring官方源码动辄几十万行,真的从ClassPathXmlApplicationContext开始追,大概率头三天就会放弃。
我的实际推荐路径是这样的:先学Spring框架本身,理解IoC、DI、AOP、Bean生命周期这四大基础概念,然后用最小的项目练手,用Spring Boot快速跑通一个能连数据库的Web应用。接着回头研究Spring Boot的自动配置原理,等你开始思考"spring-boot-starter是怎么把一堆Bean装进容器里的",再去看Spring源码,这时候你已经不是大海捞针,而是带着问题去验证自己的猜测。
在这条路上,有几个常见的认知误区值得专门点出来。
误区一:Spring就是Spring Boot,会用注解就等于会用框架。这是最普遍的问题。很多读者从一开始就直接学Spring Boot,没有经历过XML配置时代,也不理解BeanFactory和ApplicationContext的区别。结果就是注解倒背如流,但一遇到@Transaction不生效、循环依赖报错这类问题就完全没有排查方向。Spring Boot只是帮我们省去了搭建的繁琐,框架底层的原理并没有改变。
误区二:@Autowired就是依赖注入的全部。实际上,@Autowired背后的查找逻辑非常复杂:默认先按类型找,有多个候选就用@Primary或者@Qualifier区分;如果都找不到,还有required = false可配置项决定是否放任空值。这些细节虽然不常被用到,但排查问题的时候却至关重要。我接手过一个线上事故,就是因为两个类实现了同一个接口,其中一个加了@Service没起名,另一个也加了@Service,结果@Autowired注入时直接报NoUniqueBeanDefinitionException,那次的定位过程让我彻底记住了@Primary的用法。
误区三:AOP只能用来做日志。事务管理、权限校验、限流熔断、数据脱敏都是AOP的典型应用场景。理解AOP的关键是理解"代理"和"连接点"模型:代理对象和目标对象的关系是什么,@Before、@AfterReturning、@Around分别在什么时候切入执行。只有理解了代理的创建时机,才能解释为什么自己写的@Transactional偶尔会失效——常见的原因之一就是同类内部方法调用时,被调用的方法没有经过代理对象,事务注解自然不生效。
误区四:Spring Boot的自动配置是"黑魔法"。它不是魔法。自动配置类上那一堆@ConditionalOnClass、@ConditionalOnMissingBean就是Java语言本身的条件判断,只是Spring把它们封装成了注解。你在spring.factories里注册一个自动配置类,再配合@EnableConfigurationProperties把配置属性绑定到application.yml上,整个链路就和设计者最初设想的一模一样。去本地仓库的jar包里翻出spring-boot-autoconfigure的源码看一眼,你会发现每一条规则都清晰得像一份说明书。
学习Spring最大的障碍从来不是难度,而是信息太密。市面上讲Spring的资料多到泛滥,但大多数是API罗列,看过就忘。我自己的经验是:每学一个特性,就问自己两个问题——它解决什么问题?如果不这么做会有什么后果?这两个问题能逼着你从"记忆"走向"理解"。比如学习了三级缓存,你就应该能回答"如果只有一级缓存会发生什么";学习了自动配置,你就能回答"如果不用starter,我需要手动写哪些代码"。带着这种思考方式去学,一年下来你会形成一套非常牢固的知识框架,面试也好、排错也好,都会比别人快很多。
另外,我得提醒一句:市面上很多教学资源动辄标题写着"Spring源码深度解析",内容却是贴了一堆源码片段然后加几句注释,对读者帮助有限。判断一个教程质量的方法,是看它能不能讲清楚"为什么是这样的设计"。如果读者读完,既不知道这个机制解决的具体问题,也不知道哪些场景下会踩坑,那再长的博客也只是噪声。希望这篇文章给你的感觉恰好相反。