
1. 先从工厂类说起为什么Spring坚持用工厂管理对象1.1 我们自己new对象到底哪里不好我接触Spring也有十个年头了很多人一开始学Spring上来就背“控制反转”“依赖注入”这些概念背得滚瓜烂熟但一问到“工厂类”就懵了。其实Spring最底层的逻辑就是一个超级工厂它代替你new对象代替你管理对象之间谁依赖谁。先看我们最熟悉的传统写法。假设有一个学生服务要调用户服务查数据public class StudentService { private final UserService userService new UserService(); ... }看起来没毛病但仔细想StudentService和UserService被代码写死绑在一起了。今天UserService要加个构造参数StudentService里所有new UserService()的地方全要改明天想把UserService换成远程接口实现StudentService的代码得动后天想给UserService加个代理做权限校验还是得改StudentService。这就叫高耦合。再往深了说new对象的时机、次数、生命周期全部被调用方控制。如果全系统有几十个Service每个Service都new依赖那对象的创建散落在各个角落你想统一管理门都没有。工厂类的本质就是把这套“谁创建、谁管理、什么时候销毁”的逻辑收拢到一个地方。你需要对象不直接new而是向工厂要。工厂里怎么new、要不要单例、要不要代理调用方一概不关心。这个思想“控制反转”四个字就讲完了控制的权力从调用方手里转到了工厂手里。1.2 工厂类的演变从静态工厂到BeanFactoryJava世界里常见的工厂实现分几级。最原始的是静态工厂public class UserServiceFactory { public static UserService create() { return new UserService(new UserDao()); } }它解决了“统一创建”问题但问题也明显工厂自己仍然用new来拼装依赖一旦依赖链变化工厂内部代码要跟着动。而且这个工厂只能生产UserService其他类怎么办每个类写一个工厂那工厂数量比业务类还多。再往上走就是反射工厂。用一个通用工厂读取配置通过反射创建任意类的实例public class SimpleContainer { public static T T getBean(ClassT clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); } }这个思路就和Spring很接近了。Spring的BeanFactory本质上就是一个“万能反射工厂”但它比反射工厂强在三点第一它管理的不只是“对象本身”还有对象的定义信息也就是BeanDefinition。里面存了类名、作用域、是否懒加载、初始化方法等元数据相当于给每个Bean建了档案。第二它管理对象之间的依赖关系。A依赖BB依赖C这些关系由Spring通过依赖注入自动组装不需要业务代码里手动声明。第三它管理对象的各种扩展点。Bean可以参与Spring容器的初始化过程在实例化前后、初始化前后、销毁前均可以挂接自定义逻辑。说句大白话你把对象交给SpringSpring给了你一整套“对象从生到死”的干预通道。实际开发中我们最常用的ApplicationContext就是BeanFactory的升级版。它继承了BeanFactory全部能力额外增加了事件发布、国际化、环境配置等企业级功能。所以你去面试时如果被问到BeanFactory和ApplicationContext的区别就往这个方向上答BeanFactory是最底层的容器标准ApplicationContext是面向实际开发的功能完整版。2. Bean生命周期一个Bean从创建到销毁经历了什么2.1 完整生命周期链条图解我见过太多人把Bean生命周期背成8个阶段、10个阶段却不知道每个阶段到底要处理什么。其实Spring就是一个流水线Bean就是流水线上的零件。我按实际源码执行顺序给你捋清楚一共八站。第一站扫描与解析。容器启动后Spring根据配置扫描包路径把带有Component、Service、Repository等注解的类或者XML里声明的类统统转成BeanDefinition。这一步对象还没生出来但“出生档案”已经建好了。第二站BeanDefinition后置处理。Spring会调用BeanFactoryPostProcessor允许你修改BeanDefinition本身。比如常见的PropertySourcesPlaceholderConfigurer就是在这里把Value里的“${xxx}”占位符替换成真实值。第三站实例化。Spring通过反射构造对象。默认调用无参构造器如果是Autowired构造器注入则调用对应构造器。到这一步Bean对象已经存在了但属性还没赋值还是一个“裸对象”。第四站属性填充。Spring把Bean定义里的属性、Autowired依赖、Value配置值一一填充到对象里。所以说很多人以为构造器执行完Bean就能用了不对只有走到这一步Bean才真正拥有完整的依赖。第五站Aware接口回调。如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAwareSpring会在这里回调。作用是让Bean感知自己在容器中的名字、工厂、容器等环境信息。第六站初始化前置处理。顺序是这样先执行BeanPostProcessor的postProcessBeforeInitialization方法然后执行PostConstruct标注的方法再执行InitializingBean的afterPropertiesSet最后执行XML或Bean指定的init-method。我实测下来很多人分不清PostConstruct和afterPropertiesSet其实一个是标准注解一个是Spring接口方法调用顺序上PostConstruct在前。为什么要设计这么多初始化方式就是给你不同场景的选择不想依赖Spring API就用注解想和生命周期强耦合就实现接口。第七站初始化后置处理。执行BeanPostProcessor的postProcessAfterInitialization。这一步最经典的用途就是AOP。Spring AOP就是在这里发现这个Bean匹配了切点就生成代理对象并把它返回给容器。从此容器里存的是代理对象不是原始对象。第八站使用与销毁。容器关闭时先执行PreDestroy再执行DisposableBean的destroy方法最后执行自定义destroy-method。你发现没有生命周期这些“钩子”并不是给你日常业务用的而是给框架、组件、中间件集成用的。像我以前写过一个缓存组件需要在Bean初始化后自动加载热点数据在容器关闭前落盘缓存就是靠生命周期钩子实现的。2.2 生命周期钩子到底该怎么选很多初学者纠结写了PostConstruct还用不用InitializingBean用disposable-method还是PreDestroy我建议按这个原则选只有Spring环境下使用优先用JSR 250注解即PostConstruct和PreDestroy。好处是代码干净和Spring API解耦换成别的容器也认得。需要拿到Spring特定环境信息比如ApplicationContext选实现*Aware接口。写公共组件、框架代码要保证每个Bean都执行同一套逻辑选BeanPostProcessor它是对容器内所有Bean生效的全局钩子。举一个真实案例。我给一个项目做幂等控制需要把接口请求的唯一标识写入一个本地缓存。我实现了一个HandlerInterceptor但拦截器被Spring实例化时依赖的RedisTemplate还没准备好导致启动报错。后来我把RedisTemplate的初始化逻辑放到一个BeanPostProcessor里这个处理器专门检测某个自定义注解等目标Bean完成属性填充后再把RedisTemplate注入进去。这个问题就是只看“怎么用注解”不懂“生命周期”才踩的坑。2.3 三级缓存与循环依赖只要聊Spring源码循环依赖就跑不掉。你肯定见过这种面试题A依赖BB依赖ASpring怎么解决Spring的解法叫三级缓存。我先说结论默认的单例Bean且不是构造器注入Spring能解决循环依赖。原型Bean和构造器注入不行为什么我们拆开看。三级缓存对应三个Map第一级singletonObjects存的是完整可用的单例Bean。第二级earlySingletonObjects存的是提前暴露的早期对象。这时候对象刚刚实例化完属性还没填充。第三级singletonFactories存的是对象工厂这个工厂可以用来生成早期对象。流程大概这样创建AA实例化完成后发现需要B于是去容器找B容器发现B还没创建就先创建B。B实例化完成后发现需要A于是去容器找A。正常来说A还没初始化完找不到。但A在实例化完成后已经把自己的ObjectFactory放进了三级缓存。B通过这个工厂拿到了A的早期引用然后B完成属性注入和初始化B成功。接着A从容器中拿到B也完成注入和初始化A成功。关键点在于A和B最终在容器里用的是同一个引用吗是的因为早期对象和最终对象没有经过AOP时是同一个如果A被AOP代理了最终会替换成代理对象那一开始B拿到的早期引用就不是最终代理对象Spring会用提前暴露的方式保证最终B里的引用也能被修正。这就是为什么要分三级不是吃饱了撑的是为了兼顾循环依赖和AOP代理。如果你在开发中遇到循环依赖报错先别急着改代码先想想是不是构造器注入导致的。我自己的经验是能用字段注入或setter注入时Spring可以帮你兜底循环依赖但一旦设计到构造器注入就要自己调整设计。务实点讲正经项目里依赖关系应该是清晰的树状或分层结构循环依赖本身就是坏味道能用三级缓存解决是Spring的宽容不能当默认能力依赖。3. 注解配置从XML到注解怎么一步步代替XML3.1 注解驱动到底省了什么早期Spring开发所有Bean的声明都写在applicationContext.xml里。Bean少还行Bean一多那个XML文件能到上千行。改一个类的包名要在XML里翻半天namespace写错一个字母容器启动直接挂。最气的是明明错误很简单查起来却要一条条核对。注解配置的核心是“让类和配置待在一起”。你在一个类上标Component它就天然变成了一个Bean你在字段上标Autowired它就自动注入依赖。配置信息从XML文件里解放出来散落到类本身就近维护。但注解也不是银弹。我见过一些人恨不得把数据库连接池参数都写进类里那就不对了。正确分工是类本身的结构和依赖关系用注解环境相关的变量、开关、数据源用配置文件application.yml/Properties。Spring Boot提倡的这种“约定优于配置”本质上就是你只要按规范写配置代码就能最大程度简化。3.2 Component体系与Bean到底有什么区别很多新手分不清Component和Bean网上解释也绕。我就总结成一句话Component是对类打标Bean是对方法打标。Component及其衍生注解Service、Repository、Controller背后是Spring自动扫描机制。Spring启动时遍历指定包路径凡是标注了这些注解的类都被自动注册成Bean。这种方式适合自己项目里的业务类。Bean用在Configuration配置类里的方法上把这个方法返回值注册成Bean。方式适用于两种情况引入第三方类库你想把别人写的类纳入Spring管理但你又不能去改别人的类源码没法在类上标Component那就在配置类里标Bean。创建Bean的逻辑比较复杂比如连接池、RedisTemplate这种需要多个步骤构建的对象用Bean方法显式组装逻辑更直观。我举一个例子。你在配置类里定义数据源Configuration public class DataSourceConfig { Bean public DruidDataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setUrl(env.getProperty(db.url)); ds.setUsername(env.getProperty(db.username)); ds.setPassword(env.getProperty(db.password)); return ds; } }这段代码如果用Component扫描写在DataSource类上你也得自己搞一个构造器或者setter还没法方便地拿到Environment。而用Bean方法注入环境变量、拼接参数都是顺理成章的事。3.3 Autowired自动装配的几种方式与坑自动装配是注解配置里使用频率最高也最容易出问题的地方。我梳理一下三种常见装配方式。字段注入Autowired private UserService userService;代码最简洁但缺点是类的外部看不到依赖测试时不好替换而且只要Spring容器在这个字段就必须非空你临时想构造一个半成品对象都难。构造器注入public class StudentService { private final UserService userService; public StudentService(UserService userService) { this.userService userService; } }这是官方推荐的方式好处是依赖不可变、能保证注入不会为空、测试时直接new一个传参。Spring Boot 3.x里如果类只有一个构造器甚至可以省略Autowired注解Spring自动按构造器参数找Bean。Setter注入Autowired public void setUserService(UserService userService) { this.userService userService; }用的场景很少一般在可选依赖、需要重新装配的场景下用。实际开发里我看到最多的坑是接口多实现导致注入失败。比如领域里有个OrderHandler接口下面有AHandler、BHandler两个实现。这时候你在业务类里Autowired private OrderHandler handler;Spring就懵了有两个Bean匹配同一个类型给我哪个启动直接报NoUniqueBeanDefinitionException。解决方案有几种Primary标注主实现类让Spring默认选它。Qualifier(aHandler)指定Bean名称。更灵活的做法是注入List 把所有实现集合起来按业务规则遍历调用。这个方案非常适合策略模式。我自己项目里处理支付渠道时就是注入List然后按渠道编码找具体实现实测非常舒服扩展新支付渠道时也不用改老代码。4. 实战手写一个精简注解容器加深核心理解4.1 核心思路与准备光看不如上手。我建议你用最精简的代码实现一个“迷你版Spring”不需要能跑到生产但必须能体现工厂类、生命周期、注解配置这三个核心。先说设计。我们要做这几个能力扫描指定包下的类识别Component注解。实例化单例Bean。识别Autowired注解完成依赖注入。支持PostConstruct初始化回调。提供getBean方法。这已经涵盖了Spring最核心的流程。写下来之后你对Bean生命周期里“实例化、属性填充、初始化回调”这三步会有完全不同于背书的体会。准备工具只需要JDK8、一个Maven项目、依赖就一个javax.annotation-api提供PostConstruct。如果你嫌麻烦可以连依赖都不加自己定义注解也行。但我建议用标准注解方便对照真实Spring。4.2 实现步骤第一步定义注解。直接用标准注解也可以。但在自己容器里扫描的时候要用类名匹配我图省事就自己声明了三个注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface MyComponent { }Target({ElementType.FIELD, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface MyAutowired { }Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface MyPostConstruct { }第二步写容器类MiniApplicationContext。核心是一个ConcurrentHashMap存单例Bean一个类扫描工具。public class MiniApplicationContext { private final MapString, Object singletonObjects new ConcurrentHashMap(); public MiniApplicationContext(Class? configClass) throws Exception { String packageName configClass.getPackage().getName(); scan(packageName); injectDependencies(); invokeInitMethods(); } private void scan(String packageName) throws Exception { // 这里用ClassPathScanningCandidateComponentProvider最方便 // 但是为了展示原理用Files.walk遍历class目录也够。 // 实际代码省略遍历细节只展示核心注册逻辑。 SetClass? classes ScannerUtil.findClasses(packageName); for (Class? clazz : classes) { if (clazz.isAnnotationPresent(MyComponent.class)) { String beanName decapitalize(clazz.getSimpleName()); Object instance clazz.getDeclaredConstructor().newInstance(); singletonObjects.put(beanName, instance); } } } private void injectDependencies() throws Exception { for (Object bean : singletonObjects.values()) { Field[] fields bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MyAutowired.class)) { Object dependency getBean(field.getType()); field.setAccessible(true); field.set(bean, dependency); } } } } private void invokeInitMethods() throws Exception { for (Object bean : singletonObjects.values()) { for (Method method : bean.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(MyPostConstruct.class)) { method.setAccessible(true); method.invoke(bean); } } } } SuppressWarnings(unchecked) public T T getBean(ClassT clazz) { return singletonObjects.values().stream() .filter(clazz::isInstance) .findFirst() .map(bean - (T) bean) .orElseThrow(() - new RuntimeException(找不到对应的Bean)); } }第三步验证。定义两个类MyComponent public class UserService { MyPostConstruct public void init() { System.out.println(UserService初始化 this); } public void hello() { System.out.println(hello from userService); } } MyComponent public class StudentService { MyAutowired private UserService userService; public void test() { userService.hello(); } }启动时public class TestMain { public static void main(String[] args) throws Exception { MiniApplicationContext context new MiniApplicationContext(TestMain.class); StudentService studentService context.getBean(StudentService.class); studentService.test(); } }跑完之后控制台可以看到StudentService里成功注入了UserService并且UserService的MyPostConstruct方法在注入前就已经执行了。这个执行顺序就和Spring一致先实例化再填充属性最后初始化。4.3 实测与局限这个迷你容器的运行结果足够说明问题了。但它离真正的Spring差得远没有解决循环依赖没有BeanPostProcessor没有代理支持没有作用域管理也没有处理接口多实现。不过没关系手写一遍的意义是帮你把黑盒揭开。我认识的很多人就是通过手写迷你容器才真正理解了为什么BeanFactory是工厂、为什么属性注入在构造之后、为什么PostConstruct在注入完成之后执行。所谓“源码看得懂”不如“代码写得通”。如果你要扩展我建议按这个顺序加功能先加BeanPostProcessor调用逻辑随便写一个类模拟Autowired的AutowireAnnotationBeanPostProcessor再加Cglib代理让初始化后的对象能支持方法拦截。做完这两步你对Spring AOP和循环依赖的理解会再上一个台阶。5. 常见问题与排查技巧实录5.1 常见报错速查表我收集了几个日常开发中高频出现的异常附上原因和解决方向方便你排查时直接对照。异常信息根因排查方向NoSuchBeanDefinitionException容器里没有这个类型的Bean确认Component有没有写扫描包路径对不对Bean名称是否正确NoUniqueBeanDefinitionException同一类型有多个BeanSpring不知道选哪个加Qualifier或Primary或改成注入ListBeanCurrentlyInCreationException循环依赖且当前场景无法自动解决调整设计把构造器注入改为setter注入或拆分依赖BeanDefinitionStoreExceptionBean定义解析失败检查XML或注解有没有语法错误、类路径对不对BeanPostProcessor相关异常自定义后置处理器抛错定位到具体后置处理器单独调试注入的Bean为null属性填充没生效或注入方式不对检查是否用Autowired字段是否privateSpring扫描是否覆盖此类5.2 排查调试心得第一遇到Bean相关报错先看堆栈最上层的“找不到”是谁引起的。Spring的异常链很长动不动几屏但根因通常在第一个Caused by里。别一上来就在控制台全文搜索先用IDE的栈折叠功能或分析工具定位根因。第二善用调试断点。在AbstractApplicationContext的refresh方法里打断点能看见整个容器启动过程在doCreateBean方法里打断点能看见Bean是如何一步步创建的。我之前排查一个Bean属性始终为空的问题就是在populateBean方法里看到某个被Autowired的字段其实没被Spring处理原因是这个类不是由Spring管理的而是业务代码里自己new出来的。第三注意组件扫描边界。Spring Boot默认扫描启动类所在包及其子包。如果你把业务类放在启动类平级或上级目录Spring就扫不到。这个问题特别隐蔽因为它不报错只是你的Bean永远起不来最后启动报找不到Bean。我的习惯是启动类放在项目的顶层包下业务包按module划分保险。第四配置文件和注解混合使用时优先级要想清楚。Spring Boot中配置文件的参数会被ConfigurationProperties绑定到Bean属性但如果你同时又在其他地方用Value覆盖容易产生配置冲突。我的原则是一条数据只有一个来源。外部环境相关的用配置文件静态常量用代码动态注入用Value别交叉混用。个人实操体会老实说Spring框架发展到现在很多能力已经被Spring Boot封装得看不出原本的样子了。你可能只需要一个SpringBootApplication项目就能跑起来。但不代表你不需要理解工厂类、Bean生命周期和注解配置。恰恰相反你越往上走越要回到底层去琢磨这三个东西。我这些年处理过的线上故障有一大半最后都归结到对Bean生命周期理解不够一个Bean什么时候初始化依赖注入发生在哪一步代理在哪一步生成。把这些问题想透彻你写的配置就是“有理有据”而不是“照着抄”。最后再分享一个小技巧每接触一个新框架第一时间去翻它有没有自定义BeanPostProcessor这是最快了解框架内部原理的入口。看懂了它你就看懂了框架如何影响你的Bean。