尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring核心原理:IoC、Bean生命周期与三级缓存解析

Spring核心原理:IoC、Bean生命周期与三级缓存解析 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 InjectionDI。而控制反转这个词描述的是同一个事实的另一个角度对象创建和装配的控制权从对象自己手里反转到容器手里。可以打个比方。传统方式就像你住酒店想喝水得自己烧水、自己带杯子、自己清理一切自己动手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-webMaven就会把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 MapString, Object singletonObjects new ConcurrentHashMap(); private final MapString, 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(ClassT 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容器真实SpringBean定义仅存Class对象BeanDefinition含作用域、懒加载、初始化方法等单例缓存一个Map三级缓存成品、半成品、ObjectFactory实例化方式仅无参构造构造器推断、工厂方法、CGLIB代理依赖注入仅字段注入字段注入、Setter注入、构造器注入、Qualifier生命周期回调无Aware接口、BeanPostProcessor、PostConstructAOP支持无通过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源码深度解析内容却是贴了一堆源码片段然后加几句注释对读者帮助有限。判断一个教程质量的方法是看它能不能讲清楚为什么是这样的设计。如果读者读完既不知道这个机制解决的具体问题也不知道哪些场景下会踩坑那再长的博客也只是噪声。希望这篇文章给你的感觉恰好相反。
返回列表