news 2026/9/26 7:36:01

Spring容器核心原理:从Bean生命周期到循环依赖与三级缓存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring容器核心原理:从Bean生命周期到循环依赖与三级缓存

1. 先回答那个最基础的问题:为什么我们不再到处 new 对象

我见过太多人学 Spring,上来就背"控制反转""依赖注入"这些概念,背得滚瓜烂熟,但问一句"容器到底帮你做了什么",立刻就哑火了。今天我想换个角度聊 Spring 容器,不堆概念,直接从代码的演变讲起。

1.1 一个最朴素的例子:订单服务的一天

假设你正在写一个订单服务,没有 Spring,最原始的方式大概长这样:

public class OrderService { // 依赖:商品库存、用户账户、消息通知 private ProductStockService stockService = new ProductStockService(); private UserAccountService accountService = new UserAccountService(); private MessageNotifyService notifyService = new MessageNotifyService(); public void createOrder(Order order) { stockService.deductStock(order.getProductId()); accountService.deductBalance(order.getUserId(), order.getAmount()); notifyService.sendOrderCreatedMessage(order); } }

看起来没什么问题对不对?但只要你在这个项目里待上一两年,就会踩到这几个坑:

  • 每次new ProductStockService()的时候,它的构造函数里可能又要new好几个别的服务,层层嵌套,牵一发动全身。
  • 如果某个服务需要被多个地方共享(比如数据库连接池),每个调用方各 new 一份,资源瞬间就爆了。
  • 你想给某个服务加个缓存、加个日志代理,得去改所有创建它的地方。
  • 单元测试的时候,new出来的硬编码依赖根本没法替换成 Mock 对象。

这几个问题归结起来就一句话:对象的创建和组装逻辑,不该由每个业务调用方自己负责。谁都不该关心"我调用的那个服务到底怎么来的",业务方只关心"给我一个能用的服务"。

1.2 容器到底接管了什么

Spring 容器做的事情,本质上就是把你从"自己造对象"变成"向容器要对象"。当时,所有的类都由容器统一创建、统一管理、统一装配,业务代码只负责声明依赖关系,剩下的交给容器。

@Service public class OrderService { private final ProductStockService stockService; private final UserAccountService accountService; private final MessageNotifyService notifyService; public OrderService(ProductStockService stockService, UserAccountService accountService, MessageNotifyService notifyService) { this.stockService = stockService; this.accountService = accountService; this.notifyService = notifyService; } // 业务逻辑不变 }

对比一下就能看出来,依赖还是那些依赖,但顺序变了:不再是 OrderService 自己去new别人,而是被别人(容器)把依赖"注入"进来。这就叫控制反转——对象创建的控制权从代码手里移交给了容器。

控制反转之后,前面说的几个痛点就都解决了:

  • 依赖的创建过程集中在容器里,替换实现只改配置或注解,不动业务代码。
  • 默认单例管理,同样的服务全项目只有一个实例,资源不浪费。
  • 想给服务加横切逻辑(事务、日志、鉴权),容器可以在不改变业务类的前提下包装代理对象。
  • 测试时容器可以被 Mock 上下文替代,注入什么依赖完全由测试代码说了算。

理解了这个"为什么",后面所有关于容器的细节——Bean 生命周期、三级缓存、后置处理器——都会变得顺理成章,因为你始终知道容器做这一切的目的:把对象的创建、管理、装配从业务代码中彻底剥离出来。

2. BeanFactory 与 ApplicationContext:容器家族的两个核心成员

Spring 不是一个容器,而是一族容器。最低层的是一个叫BeanFactory的接口,往上派生出了ApplicationContext,再到你日常用的AnnotationConfigApplicationContext、ClassPathXmlApplicationContext。很多人傻傻分不清这几个东西,其实只需要抓住一条主线:BeanFactory 是"骨架",ApplicationContext 是"骨架 + 增强"。

2.1 BeanFactory:最底层的那个"货架"

BeanFactory按字面理解就是"Bean 工厂",它定义了最核心的能力:根据名字或类型获取 Bean,判断某个 Bean 是否存在,获取 Bean 的类型信息,等等。核心方法就那么几个,你能记住的其实就一个getBean()。

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml")); OrderService orderService = factory.getBean(OrderService.class);

这是最原始的容器形态,XmlBeanFactory在多数的 Spring 版本里已经被废弃了,但它代表的思想没有变:BeanFactory 只负责两件事——注册 Bean 的定义(BeanDefinition),以及按定义生产 Bean 实例并缓存起来。

它更像一个"极简货架":架子有了、货能放上去、也能取下来,仅此而已。什么国际化、事件广播、资源加载、AOP 织入,对不起,BeanFactory 一概不管。所以在实际项目中你几乎不会直接操作 BeanFactory,你接触到的都是它的子接口。

2.2 ApplicationContext:加满了增值服务的全功能容器

ApplicationContext继承自BeanFactory,在货架的基础上扩展了大量企业级功能。我随手列几个最常用的:

  • 事件发布与监听:容器内外可以发布自定义事件,业务模块之间解耦通信,比如"订单创建后发布一个事件",统计、通知、风控各自监听,互不干扰。
  • 国际化消息支持:通过MessageSource按 Locale 取文案,不再需要自己写一堆 if-else 判断语言。
  • 资源加载抽象:统一用Resource接口访问 classpath、文件系统、URL 等不同来源的配置。
  • 自动注册后置处理器:像ConfigurationClassPostProcessor这种重要角色,会在容器启动时自动注册并执行,不需要你手动调用。
  • 环境与配置抽象:通过Environment和PropertySource管 profile 和配置项,后面 Boot 的application.yml体系就是建立在这上面的。

实际使用中,AnnotationConfigApplicationContext是最常用的一种 ApplicationContext。基于注解配置的项目里,它就是那台真正运转的机器:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); OrderService orderService = context.getBean(OrderService.class);

启动这行代码之后,Spring 会扫描指定包、解析@Configuration和@ComponentScan、注册 BeanDefinition、实例化所有单例 Bean、填充属性、执行初始化方法……整套流程。你可以把它理解成"容器启动的完整仪式"。

2.3 实际项目中到底选哪个

直接说结论:99% 的场景,用 ApplicationContext 而不是裸的 BeanFactory。哪怕是做一个极其轻量的 Demo,我也建议从AnnotationConfigApplicationContext起步,因为它背后注册了一系列必要的后置处理器,能让你少踩非常多 "为什么我的注解不生效" 的坑。

BeanFactory 的价值不在"用",而在"理解"——当你手写一个迷你 Spring 容器(后面我会专门写一节)的时候,你会发现你实现的其实就是 BeanFactory 那套能力。而 ApplicationContext 那些花哨功能,本质上就是在这个核心能力外围套了一层又一层增强。理解了底层,上层再复杂你也能一眼看穿。

3. 从注册到销毁:Bean 在容器里的完整一生

很多面试题会让你"简述 Spring Bean 的生命周期",但如果你只是背那十几个阶段的名字,没有真正理解每一步在干嘛,遇到稍微变形的题就抓瞎。我建议把 Bean 的生命周期拆成"注册、实例化、初始化、使用、销毁"五个大阶段,然后逐步往里填细节。

3.1 注册阶段:先把"菜谱"登记好

容器不是凭空就知道有哪些 Bean 的。启动时,它先要做的一件事是收集所有 Bean 的元信息,也就是BeanDefinition。BeanDefinition 包含的东西很多:Bean 的类名、作用域(singleton 还是 prototype)、是否懒加载、初始化方法名、销毁方法名、属性值、构造函数参数等——你可以把它理解为一张"菜谱",容器照着菜谱才能做菜。

获取 BeanDefinition 有几种途径:

  • XML 配置时代,XmlBeanDefinitionReader解析<bean>标签,生成 BeanDefinition。
  • 注解时代,ClassPathBeanDefinitionScanner扫描@Component、@Service、@Repository等注解,生成 BeanDefinition。
  • @Configuration类里的@Bean方法,由ConfigurationClassPostProcessor解析,跟扫描组件殊途同归。

这一步最容易被忽略的重点是:这个阶段容器还没有创建任何实体对象,它只是在"登记菜谱"。你现在理解为什么说 Bean 的生命周期从"BeanDefinition 注册"开始了吧?因为一个 Bean 在被实例化之前,必须先被"认识"。

3.2 实例化与初始化阶段:从反射到完整可用的过程

等所有 BeanDefinition 都注册完毕,容器开始逐个实例化。默认情况下,非懒加载的单例 Bean 在容器启动时就会被创建。实例化本身用的是反射:

Object bean = clazz.getDeclaredConstructor().newInstance(); // 或者带参构造器,由容器根据依赖关系解析参数

所以一个不被 Spring 管理的普通类,只要有无参构造器,理论上也能被视频反射创建出来。但真正的"生命周期"重头戏在实例化之后——属性填充和初始化。

我用一个简化但完整的流程说明这个阶段背后发生了什么:

  1. 容器根据 BeanDefinition 找到构造器(或工厂方法),反射创建实例——这时候对象是"裸的",所有属性都是默认值。
  2. 进行属性填充(依赖注入):如果某个属性被@Autowired标注,容器会递归去获取对应的依赖 Bean 并设置进去。
  3. 执行 Aware 系列回调,比如BeanNameAware、BeanFactoryAware,让 Bean 能感知自己在容器中的身份。
  4. 调用BeanPostProcessor的postProcessBeforeInitialization——这是初始化前的一个扩展点,比如@PostConstruct就是在这一步被处理的。
  5. 调用初始化方法:如果你实现了InitializingBean,会执行afterPropertiesSet();如果你配置了init-method或使用@Bean(initMethod = "..."),会执行对应的方法。
  6. 调用BeanPostProcessor的postProcessAfterInitialization——这里最常见的就是 AOP 代理的创建,Spring 会在这时包装出代理对象。

走到这一步,Bean 才算是"完全体",可以交给业务使用了。

3.3 销毁阶段:优雅地挥手告别

容器关闭时,会调用所有单例 Bean 的销毁方法。同样有三条路:实现DisposableBean接口的destroy()方法、配置destroy-method、或者在@Bean上标注destroyMethod属性。

这里有一个容易踩坑的点:prototype 作用域的 Bean,容器是不负责销毁的。因为原型 Bean 每次获取都会创建新实例,容器根本跟踪不过来。这个我后面会再展开说,先记住这句话:单例的善后工作容器全包,原型的善后工作你自己负责。

另外要提醒一句,如果 Bean 实现了AutoCloseable或Closeable,Spring 默认会把它识别为关闭方法。有时候你只是想关闭某个资源,却发现附带的关闭动作被执行了两次,多半就是没搞清楚这个默认行为。

4. 三级缓存:Spring 破解循环依赖的那张底牌

如果说"生命周期"是容器的日常,那么循环依赖就是容器最惊险的一次极限操作。网上讲三级缓存的文章很多,但大多数只告诉你"有三层 Map",却不说清楚每一层到底在缓存什么、为什么非要三级。我先用最平白的语言解释清楚循环依赖是什么。

4.1 场景复现:A 需要 B,B 需要 A

假设有 A 和 B 两个类:

@Service public class A { @Autowired private B b; } @Service public class B { @Autowired private A a; }

A 需要注入 B,B 又需要注入 A,这就是循环依赖。如果 A 必须先创建好才能给 B 注入,而 A 又需要 B 才能创建好,那就成了死循环。Spring 的解决办法是:允许先创建一个"半成品"A,把它放进缓存,然后接着创建 B,当 B 需要 A 时,直接从缓存里拿到那个半成品 A 注入进去。

这个思路本身很简单,复杂在于——到底把"半成品"放在哪、放多久、怎么保证最终拿到的是同一个完整实例。

4.2 每一级缓存都负责什么

先看三个 Map,这是三级缓存的物理载体:

  • 一级缓存singletonObjects:存放完整创建完毕的单例 Bean。这是最终对外暴露的实例。
  • 二级缓存earlySingletonObjects:存放早期暴露的成品对象,也就是已经实例化但还没完成属性填充或初始化的对象。注意,二级缓存里放的是对象本身,不是工厂。
  • 三级缓存singletonFactories:存放ObjectFactory 工厂。每次从工厂getObject()时,可能会产生不同的对象——这给了后置处理器一个"包装代理"的机会。

完整流程我用一个例子走一遍:

  1. 创建 A,检查各级缓存都没有 A。于是 A 开始实例化,得到一个半成品 A(属性还是空的)。
  2. 创建 A 时发现它依赖 B,于是去获取 B。缓存里没有 B,开始创建 B。
  3. 创建 B 时发现 B 依赖 A,此时 A 已经注册到三级缓存(singletonFactories)。B 从三级缓存里通过 ObjectFactory 得到 A 的引用(可能是个代理,也可能就是原始对象)。
  4. B 拿到 A 的引用后,属性填充完成,继续初始化,最终成为完整 B,放进一级缓存singletonObjects。
  5. 回到 A 的创建流程,把 B 注入 A 中,A 继续初始化,最终成为完整 A,放进一级缓存。

4.3 为什么必须是三级,而不是两级

这是几乎所有面试官都会追问的深水区。两级行不行?理论上,如果只想要"引用共享",两级就够了——实例化后直接把原始对象放进一个缓存,后续所有依赖方都拿这个原始对象。但问题在于:如果 A 在初始化阶段被 AOP 代理了怎么办?

Spring 的 AOP 默认在postProcessAfterInitialization阶段生成代理对象。假设 A 最终要被代理,那么早期暴露给 B 的那个"原始 A",和最终业务里使用的"代理 A",就不是同一个对象了。B 里拿着原始 A,业务里用的是代理 A,两个引用不一致,后面所有依赖 A 的地方都会出问题。

三级缓存里的singletonFactories就是为了解决这个时机错位。它保存的是 ObjectFactory,这个工厂在执行时会让 A 的后置处理器有机会返回一个代理对象。换句话说,没有三级缓存,就无法保证循环依赖中提前注入的引用和最终暴露的引用是同一个。

这也是为什么 Spring 在处理循环依赖时,默认只在单例作用域下有效——单例的生命周期完全受容器掌控,才有"提前暴露 + 最终统一"的余地。

4.4 构造器注入为什么救不了循环依赖

很多人踩过这个坑:把@Autowired从字段移到构造器上,循环依赖直接报错。原因很清晰——构造器注入要求在对象创建的同时就传入依赖,可 A 还没创建完,你连"半成品"都没有,三级缓存里的工厂根本还来不及注册。所以"先暴露半成品再补依赖"这套玩法,对构造器依赖是无效的。

我在项目里碰到循环依赖,第一反应不是去追三级缓存的工作原理,而是先想:这个循环是不是设计上出了问题?大部分情况拆掉一环就解决了——把其中一个依赖改成方法参数传递、用@Lazy延迟注入、或者引入中间层。三级缓存是兜底方案,不是让你肆意写环的许可证。

5. 启动流程中的后置处理器:容器里那些"看不见的搬运工"

Spring 容器启动流程里,有两个名字特别像、作用却完全不同的后置处理器,一个是BeanFactoryPostProcessor,一个是BeanPostProcessor。这两个名字在面试题里出现频率极高,我每次带新人,第一件事就是让他们把这两个词背清楚,因为搞混它们,整个容器原理就崩了。

5.1 BeanFactoryPostProcessor:在"做菜"之前改"菜谱"

BeanFactoryPostProcessor在容器启动的早期执行,执行时机是所有 BeanDefinition 已经注册、但还没有任何 Bean 被实例化的时候。所以它是拿来修改 BeanDefinition 的,也就是我前面说的"菜谱改改还能来得及"。

最有名的实现是ConfigurationClassPostProcessor,它负责解析@Configuration类、扫描@ComponentScan、把@Bean方法转成 BeanDefinition。另外还有PropertySourcesPlaceholderConfigurer,负责把配置文件里的${jdbc.url}这类占位符解析成实际值。

@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { BeanDefinition bd = beanFactory.getBeanDefinition("orderService"); bd.setScope("prototype"); // 在实例化之前把作用域改掉 } }

注意关键点:这时候 beanFactory 里只有 BeanDefinition,没有任何真实对象。你动的是"未来怎么做"的策略,还不涉及"已经做好的菜"。

5.2 BeanPostProcessor:每个 Bean 的必经收费站

BeanPostProcessor则不同,它作用在每个 Bean 的实例化前后,每个 Bean 创建时都得从它这里过一遍。它有两个方法:postProcessBeforeInitialization和postProcessAfterInitialization。

这里有个很多人忽略的细节:BeanPostProcessor本身也是 Bean,但它在容器里是提前被初始化的——容器会优先找出所有 BeanPostProcessor 类型的 Bean,把她们准备好,然后才开始批量创建其他 Bean。

实际开发中最常见的 BeanPostProcessor:

  • AutowiredAnnotationBeanPostProcessor:处理@Autowired和@Value注入。在属性填充阶段,就是它去解析需要注入的依赖并完成赋值。
  • CommonAnnotationBeanPostProcessor:处理@PostConstruct、@PreDestroy等 JSR-250 注解。
  • AnnotationAwareAspectJAutoProxyCreator:处理@Aspect切面,在 Bean 初始化完成后创建 AOP 代理。

如果说 BeanFactoryPostProcessor 是"改菜谱的人",那 BeanPostProcessor 就是"每道菜出锅前都要检查的质检员"。这两者一个是面向容器全局的,一个是面向每个 Bean 的,搞清了主语,就不会再记混。

5.3 它们在启动流程中的执行顺序

把整个启动流程串起来,是这样的一个顺序:

  1. 创建并初始化 ApplicationContext,读取配置,注册 BeanDefinition。
  2. 执行所有BeanFactoryPostProcessor——此时可以修改 BeanDefinition。
  3. 实例化并注册所有BeanPostProcessor——为后续每个 Bean 的创建做准备。
  4. 逐个实例化单例 Bean,走完"实例化 → 属性填充 → 初始化前处理器 → 初始化 → 初始化后处理器"这条链路。
  5. 容器启动完成,所有可用的单例 Bean 已经就绪。

这个顺序解释了为什么你自定义的 BeanFactoryPostProcessor 一定要在属性填充前生效,也解释了为什么 BeanPostProcessor 可以拦截所有 Bean——因为它注册得早,而且容器在创建每个 Bean 的时候都会调用已注册的处理器。

6. 手写一个迷你 Spring 容器:把原理变成肌肉记忆

看书看十遍不如自己写一遍。我当年彻底搞懂 Spring 容器,就是靠照着思路从零手写了一个 200 行的迷你容器。这个迷你容器不搞 AOP、不搞事务,只实现最核心的"扫描、注册、注入、单例缓存",但写完以后,你再看 Spring 源码,会有一种"恍然大悟"的感觉。

6.1 第一步:扫描并注册 BeanDefinition

我用的方式是最简单的包扫描。遍历指定包下的所有 class 文件,筛选出加了@MyComponent注解的类,生成一个简易的 BeanDefinition 注册表:

public class MiniApplicationContext { private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(); private final Map<String, Object> singletonObjects = new HashMap<>(); public MiniApplicationContext(String basePackage) { scan(basePackage); createSingletonBeans(); } private void scan(String basePackage) { // 伪代码示意:遍历 classpath 下 basePackage 对应的目录 for (Class<?> clazz : findAllClasses(basePackage)) { if (clazz.isAnnotationPresent(MyComponent.class)) { String beanName = lowerFirst(clazz.getSimpleName()); beanDefinitionMap.put(beanName, new BeanDefinition(clazz)); } } } }

这里的关键是理解 BeanDefinition 的本质:它就是一个"这个 Bean 怎么造"的元数据对象。真实 Spring 里它有几十个属性,迷你版只需要保留Class<?>就行——因为后面可以反射拿到构造器和字段。

6.2 第二步:实例化与依赖注入

扫描完 BeanDefinition,接下来就是创建单例 Bean。创建的核心逻辑是:反射实例化 → 按注解注入字段 → 放入单例缓存。

private Object createBean(String beanName, BeanDefinition bd) throws Exception { Class<?> clazz = bd.getClazz(); Object instance = clazz.getDeclaredConstructor().newInstance(); // 依赖注入:找出所有 @MyAutowired 字段 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MyAutowired.class)) { String dependentBeanName = lowerFirst(field.getType().getSimpleName()); Object dependentBean = getBean(dependentBeanName); field.setAccessible(true); field.set(instance, dependentBean); } } return instance; } public Object getBean(String beanName) { // 单例缓存优先 if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } BeanDefinition bd = beanDefinitionMap.get(beanName); Object bean = createBean(beanName, bd); singletonObjects.put(beanName, bean); return bean; }

光写到这里,这个容器就已经能支持单例、支持最简单@Autowired注入了。但请注意,因为getBean()是递归进入的,A 依赖 B、B 依赖 A 的循环依赖,在这个朴素版本里会直接栈溢出。这正是为什么接下来要引入单例工厂缓存。

6.3 第三步:加上三级缓存,处理循环依赖

仿照 Spring 的做法,我在类里加一个singletonFactories缓存。当实例化完一个 Bean、还没开始属性填充时,就把一个能生成该 Bean 的 ObjectFactory 放进三级缓存。这样当循环依赖发生时,对方能拿到"半成品"的引用:

public Object getBean(String beanName) { if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } if (earlySingletonObjects.containsKey(beanName)) { return earlySingletonObjects.get(beanName); } ObjectFactory<?> factory = singletonFactories.get(beanName); if (factory != null) { return factory.getObject(); } BeanDefinition bd = beanDefinitionMap.get(beanName); Object instance = instantiate(bd); // 提前暴露半成品,放入三级缓存 singletonFactories.put(beanName, () -> instance); // 属性填充进行依赖注入 populateProperties(instance, bd); singletonObjects.put(beanName, instance); singletonFactories.remove(beanName); return instance; }

写到这里你会发现,理解三级缓存的难点不在于那三行 Map 代码,而在于"哪些对象在什么时候放进哪一层"。我这个迷你版本虽然省去了代理对象的复杂逻辑,但已经足够让你感受 Spring 在循环依赖处理上的核心节奏:先让工厂暴露半成品,再慢慢把饭做熟,最后统一换成成品。

6.4 手写容器的收获

写完这个迷你容器,你会自然地产生几个疑问:为什么真实 Spring 里有那么多扩展点?为什么 BeanPostProcessor 时机那么重要?为什么@Configuration类的处理那么复杂?这时候再回头看 Spring 源码,眼里就不再是"天书",而是一层层可以拆解的"套娃"了。

我把手写容器的过程录成过小课带过几个新人,普遍反馈是:之前背了二十遍生命周期都记不住,自己写完一遍,生命周期再也没忘过。真心建议每个 Java 后端都找个周末试试。

7. 容器日常使用中的几个易踩坑点与我的经验

讲完原理,最后聊点实际工作中天天碰到的情况。这些坑不算深,但每个都真实存在,而且一旦触发,排查起来特别费时间。

7.1 同一个类有多份 Bean 定义,该怎么拿

当接口有多个实现类时,getBean(SomeInterface.class)会直接抛NoUniqueBeanDefinitionException。常见处理方式有三种:

  • 在其中一个实现类上加@Primary,告诉容器"没特别指定就用我"。
  • 用@Qualifier("具体Bean名")精确指定。
  • 直接注入List<SomeInterface>,让容器把全部实现注入到一个集合里。

我个人在项目里最常用第三种——配合策略模式特别顺手。你要实现一个支付场景,支付宝、微信、银行卡各是一个实现,直接注入 List,然后根据类型去匹配调用,比写一串 if-else 干净得多。

7.2 单例与原型:别被"默认单例"坑了

单例是 Spring 默认作用域,这也就意味着:所有线程共享同一个 Bean 实例。如果这个 Bean 有可变状态(比如一个实例字段用来存用户信息),高并发下必然出事。我以前接过一个线上事故,就是因为把用户会话塞进了一个单例 Service 的成员变量里,结果 A 用户的操作被 B 用户的数据覆盖了。

判断标准就一句话:单例 Bean 里只放无状态逻辑和只读配置,有状态数据一律放方法参数、ThreadLocal 或者干脆不存。如果你确实需要每次获取新实例,把作用域改成 prototype,但谨记前端说过的:原型 Bean 不受容器销毁管理,涉及资源清理你得自己兜底。

7.3 启动慢、内存高,先别急着怪容器

很多人会问我,Spring Boot 项目启动为什么越来越慢,或者为什么容器内存占用那么高。这里有个常被忽略的方向:容器启动时默认创建所有非懒加载单例 Bean,你的项目 Bean 数量成百上千,每个都要经历完整的实例化和初始化链路,自然慢。排查思路一般是这样:

  • 用spring.main.lazy-initialization=true开启全局懒加载,启动速度立竿见影,代价是第一次访问某个 Bean 时有加载延迟。
  • 用 Actuator 的beans端点导出所有 Bean 清单,找出哪些 Bean 其实根本没人用。
  • 内存高的问题,先看看是否加载了海量不必要的自动配置类,用debug=true查看自动配置报告,把用不到的排除掉。

容器不是洪水猛兽,但要学会按需裁剪。我现在接手老项目的第一个动作,就是先导出 Bean 清单看看,往往能发现一堆"注册了但从来没人碰"的僵尸 Bean。

7.4 获取 Bean 的位置,决定你的架构风格

ApplicationContext可以在任何地方被注入,但滥用ApplicationContext.getBean()是一种架构腐蚀的信号——它意味着业务代码又在主动找对象,这违背了 IoC 的本意。我在代码评审里看到有人随手在 Service 里注入 ApplicationContext 去 getBean,都会建议他改成构造器注入。让容器把依赖递到你手里,而不是你去容器里淘金,这样才能保证依赖关系清晰可见,测试也更好做。

我自己实际写代码的习惯是:构造器注入为主,@Autowired字段注入只在特殊场景用,能不用getBean()就尽量不用。这个习惯不是洁癖,是长时间被"依赖到底从哪来"这个问题折磨出来的。


关于 Spring 容器的理解,最重要的其实不是记住某个 Map 的名字,而是建立一条完整的链路认知:配置驱动注册,注册驱动实例化,实例化过程里穿插后置处理器的扩展点,扩展点之上再长出 AOP 和事务这些高级特性。有了这条线,无论面试还是排障,你都能顺着它一路摸下去。

如果你正打算面试或者想彻底吃透容器,我建议你按这个顺序做三件事:第一,把 BeanFactory 和 ApplicationContext 的关系画成一张图;第二,自己手动调试一次AnnotationConfigApplicationContext的refresh()方法,跟着断点走一遍启动流程;第三,写一个迷你容器,哪怕只支持注解扫描和字段注入都行。这三件事做完,你对于 Spring 容器的理解会超过绝大多数只刷面试题的人。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 7:35:59

Catch2 贡献指南:测试分层、构建流程与编码规范实战详解

人工智能AI Agent多模态语音AI 应用 【免费下载链接】ten-framework Open-source framework for conversational voice AI agents 项目地址&#xff1a; https://gitcode.com/TEN-framework/ten-framework 点击查看 免费下载 本指南以仓库内置的 Catch2 测试框架&#xff08;位…

作者头像 李华
网站建设 2026/9/26 7:35:01

Python爬虫+MySQL+Flask+Vue:网络小说数据分析系统全链路实战

简介&#xff1a;这是一套面向高校计算机相关专业毕业设计的完整项目资料&#xff0c;主题为基于Python爬虫的网络小说数据分析系统&#xff0c;适合需要完成数据分析类毕设或学习前后端开发的学生参考。系统前台展示作者作品、分类占比、小说名称与分类统计&#xff0c;后台提…

作者头像 李华
网站建设 2026/9/26 7:34:06

AIDL详解:从Binder原理到跨进程通信实战与避坑指南

AIDL&#xff08;Android Interface Definition Language&#xff09;详解提到AIDL&#xff0c;很多Android开发第一反应是“面试必考”或者“跨进程通信”&#xff0c;但真正在项目里动手写过AIDL的人&#xff0c;可能比想象中少。前阵子帮同事排查一个线上偶发崩溃&#xff0…

作者头像 李华
网站建设 2026/9/26 7:33:48

50个AI Skill搭建个人知识管理系统:从采集到应用全流程

1. 从"收藏夹吃灰"说起&#xff1a;为什么知识管理需要一套Skill体系我做了七八年知识管理相关的工具链搭建&#xff0c;见过太多人把Notion、Obsidian、Logseq玩成了"数字垃圾场"——剪藏了几百篇文章&#xff0c;标签打了三层&#xff0c;最后真正需要调…

作者头像 李华
网站建设 2026/9/26 7:33:11

基于SpringBoot的高校学生心理大数据评估与干预平台实现方案

高校心理工作这些年一直在提"预防为主、干预为辅"&#xff0c;但真正落到一线&#xff0c;绝大多数学校还是靠纸质量表加上辅导员的口头摸排。而计算机毕业设计里&#xff0c;SpringBoot加大数据方向的选题年年都有人做&#xff0c;可真正能把"心理健康分析&quo…

作者头像 李华