写这篇文章的起因很实在。有一年做项目要接一个第三方短信服务的SDK对方给了一个SmsClient类构造器要传apiKey和secret另外还附带了一个SmsClientFactory里面既有静态创建方法也有实例方法。代码是jar包里的不可能去给它们加Component想在Spring容器里用就只能靠配置。当时我把XML、Java Config、FactoryBean挨个试了一遍也是从那次起bean的实例化方式在我脑子里才算彻底清晰起来。如果你也遇到过类似状况——依赖了某个第三方库类上不可能有注解但你又确实想让Spring来管理它的生命周期——那这篇文章就是为你准备的。我会以配置非自定义bean为切入点把构造器实例化、静态工厂实例化、实例工厂实例化三种方式完整演示一遍顺便讲讲FactoryBean怎么用以及实操中非常容易踩的几个坑。不管你是刚开始学Spring还是已经写过不少Bean方法都建议把这篇看完里面的细节在常规文档里很难找全。1. 为什么拿“非自定义bean”来拆实例化1.1 什么是非自定义bean一句话解释清楚先统一一下概念。我们平时在项目里写的Service、Repository、Component这些属于“自定义bean”它们由Spring通过组件扫描自动注册交给容器的过程基本是黑盒。而“非自定义bean”指的是那些不是由你的代码标注注解、而是通过外部配置显式声明后交给容器的对象。最常见的两类第一类是第三方jar包里的类比如上面提到的SmsClient、数据库驱动、JedisPool、各类中间件客户端第二类是你在配置阶段手动new出来的基础设施对象比如一个RestTemplate、一个ObjectMapper、一个自定义的线程池。原始class文件里不会出现任何Spring注解但你确实把它们变成了容器中的bean。这也正是配置发挥作用的场景。你是通过Bean方法、XML的bean标签或者FactoryBean去描述“这个对象该怎么被创建”而不是依赖Spring的结构化扫描。因为每一步都是显式写出来的容器到底做了什么、发生在什么时候都清晰可查。1.2 为什么这个切入点最能看到实例化真相如果你是新手直接去看自定义bean的实例化过程会很痛苦。因为注解驱动意味着Spring替你做了太多决策构造器自动推断、字段自动注入、AOP自动代理、循环依赖自动处理。这些行为全是在后置处理器里隐式完成的你看到的只是一个“突然就成功了”的结果。换成非自定义bean就完全不一样。你用bean class...或者Bean方法去声明那class是什么、工厂方法叫什么、构造器参数怎么传、初始化方法调哪一个全部由配置决定每一步都是可观察、可验证的。理论上说配置就是容器创建这个bean的“操作说明书”你只需要盯着这份说明书就能把实例化机制看穿。再加上一个现实因素很多老系统、内部框架、遗留项目里大量第三方组件正是以这种方式接进容器的。你如果只会用注解而不理解这种配置驱动的接入方式遇到历史代码时会非常被动。所以从非自定义bean入手既是理解实例化原理的最佳路径也是应对真实项目场景的刚需。1.3 三条必经之路构造器、静态工厂、实例工厂Spring实例化bean绕来绕去逃不出三种底层方式。构造器方式直接通过反射调用类的构造函数。这是最朴素的也是大多数人最容易理解的一种。静态工厂方式不直接new而是调用某个类的静态方法由那个静态方法返回实例。典型代表是Calendar.getInstance()你把这种创建逻辑放进配置文件容器就去调那个静态方法。实例工厂方式先准备好一个工厂实例再调用这个工厂实例的某个方法来创建目标对象。老版的Hibernate里就有这种味道先有Configuration再调它的buildSessionFactory()得到目标对象。在具体配置时XML和Java Config各有对应的写法我会在下一部分逐个展示。理解这三种方式等于拿到了剖析一切Spring bean创建过程的钥匙。2. 实例化背后从配置到BeanDefinition再到容器2.1 配置是怎么变成BeanDefinition的不管是写XML还是写Configuration类最终都要被解析成Spring容器真正认识的数据结构——BeanDefinition。你可以把它理解成bean的“档案袋”袋子里装着beanClassName、scope、lazyInit、initMethodName、destroyMethodName、构造参数、属性值、工厂方法名等一整套创建对象所需的信息。如果是XML配置XmlBeanDefinitionReader会逐个解析bean标签把class属性、constructor-arg子标签、property子标签填充到BeanDefinition里。如果是Java Config处理者是ConfigurationClassPostProcessor它会扫描Configuration类中的Bean方法把方法本身当成“工厂方法定义”方法名作为beanName方法返回值类型作为bean的类型方法上的Bean(initMethod ...)等属性被提取到对应字段。这两条解析路径不同但最终产物统一为BeanDefinition。之后的实例化流程对它们一视同仁没有任何区别待遇。2.2 实例化前BeanFactoryPostProcessor先修改定义很多人不知道在实例化真正开始之前还有一个特殊的扩展阶段专门用来“改档案”。实现了BeanFactoryPostProcessor接口的类会在所有BeanDefinition注册完成后、bean开始创建前执行典型代表就是PropertySourcesPlaceholderConfigurer。我们经常在配置里看到${sms.apiKey}这种占位符容器之所以能把它们替换成真实配置值就是因为这个后置处理器在实例化之前扫描了所有BeanDefinition把字符串里的占位符替换成属性源中的值。如果你把属性文件漏加载了或者占位符拼写有误报的却是“Could not resolve placeholder”问题根源其实就是这个阶段失败了然后带着被污染的构造参数进入实例化。这个扩展点的价值在于实例化之前你还有机会修改bean的定义信息。这和后面要讲的BeanPostProcessor正好对应一个管“创建前改配置”一个管“创建后改实例”。2.3 实例化后BeanPostProcessor再完成增强创建完后容器一定会回调所有已注册的BeanPostProcessor给你两个干预时机postProcessBeforeInitialization和postProcessAfterInitialization。直观理解就是对象在完成初始化之前和之后各给一次“动手脚”的机会。Spring自身的很多核心能力就挂在这里。比如AutowiredAnnotationBeanPostProcessor负责处理Autowired、Value的注入逻辑而AOP代理对象的生成发生在AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization中。也就是说你看到的那个“被注入了依赖”的bean、那个“被切面代理了”的bean都是构造完成之后被后置处理器加工过的产物。这也就是为什么前面我说自定义bean的很多行为是个黑盒这些后置处理器对它们照单全收自动完成了大量隐式操作。而通过配置注册的非自定义bean如果没被这些后置处理器特殊关照反而能更清晰地暴露出实例化本身的原始面貌。2.4 实例化≠初始化一段完整生命周期再强调一个高频误区实例化和初始化是两码事。很多人看到构造器里抛异常就说是“初始化失败”其实那叫“实例化失败”。完整的bean生命周期是构造器执行 - 属性填充 - Aware回调如果实现了BeanNameAware、ApplicationContextAware等接口 -BeanPostProcessor.postProcessBeforeInitialization- 初始化方法PostConstruct、InitializingBean、init-method任意一种 -BeanPostProcessor.postProcessAfterInitialization- 进入容器可用状态。如果是XML里配置的init-methodinit那就是在构造和属性填充完成后由容器反射调用那个方法。很多非自定义bean都依赖这一机制来做资源预检、连接预热比如一个缓存客户端的init()方法里可能会提前建立连接。实操中如果初始化阶段失败异常栈里通常是BeanInitializationException如果是构造或工厂方法出问题则是BeanInstantiationException。看到异常前缀你就能快速判断卡在哪一环。3. 三种配置方式实战把非自定义bean请进容器3.1 演示目标模拟接入第三方短信SDK为了一口吃透我们设定一个足够真实的场景第三方jar包里有两个类源码不可修改。public class SmsClient { private String apiKey; private String secret; public SmsClient(String apiKey, String secret) { this.apiKey apiKey; this.secret secret; } public void send(String mobile, String content) { System.out.println(mobile 收到短信: content); } }public class SmsClientFactory { public static SmsClient create() { return new SmsClient(defaultKey, defaultSecret); } public SmsClient createWithRetry(int times) { return new SmsClient(retryKey, retrySecret); } }我要做的事很简单让SmsClient变成一个由Spring容器管理的bean这样后续就能用Autowired或getBean()拿到它。我们先用XML配一遍再用Java Config配一遍最后演示FactoryBean。三种方式对照着看印象会非常深刻。3.2 XML配置最传统的照妖镜XML配置虽然现在用得少了但它是理解实例化机制最容易照见本质的方式。因为class、factory-method、factory-bean这些属性都是明明白白写在标签上的容器的执行逻辑一眼可读。构造器方式的配置长这样context:property-placeholder locationclasspath:application.properties/ bean idsmsClient classcom.example.sms.SmsClient constructor-arg nameapiKey value${sms.apiKey}/ constructor-arg namesecret value${sms.secret}/ /bean这里有两个值得注意的点一是constructor-arg必须和构造器参数顺序、类型对得上Spring底层是通过ConstructorResolver做类型匹配的NAME写错或者类型对不上都会抛异常二是${sms.apiKey}这个占位符正是前面讲的BeanFactoryPostProcessor在实例化之前做了替换找不到属性值的话传入构造器的就是一个原样的${...}字符串影响非常隐蔽。静态工厂方式bean idsmsClientByStaticFactory classcom.example.sms.SmsClientFactory factory-methodcreate/注意class属性此时指向的不是SmsClient而是SmsClientFactory。Spring找到这个类后会反射调用名为create的静态方法把返回值作为bean实例放入容器。如果你把factory-method写错或者方法不是static启动阶段就会直接抛BeanInstantiationException。实例工厂方式bean idsmsClientFactory classcom.example.sms.SmsClientFactory/ bean idsmsClientByInstanceFactory factory-beansmsClientFactory factory-methodcreateWithRetry constructor-arg nametimes value3/ /bean首先要有一个SmsClientFactory的bean然后目标bean不写class而是用factory-bean指向那个工厂bean用factory-method指定要调用的方法。注意即使是非静态工厂方法如果它需要入参依然是写在目标bean的constructor-arg里语法上沿用构造参数的路子但语义是“传给工厂方法”。用XML跑通这三种方式你会形成一种直觉容器创建对象并不一定非得new它只是在忠实地执行你指定的“创建指令”。3.3 Java Config配置更现代化的首选XML已经有了一整套语义Java Config则可以理解为把同样的配置改写为Java代码但底层机制有几个关键差异需要讲透。核心配置类Configuration PropertySource(classpath:application.properties) public class SmsConfig { Bean public SmsClient smsClient(Value(${sms.apiKey}) String apiKey, Value(${sms.secret}) String secret) { return new SmsClient(apiKey, secret); } Bean public SmsClient smsClientByStaticFactory() { return SmsClientFactory.create(); } Bean public SmsClientFactory smsClientFactory() { return new SmsClientFactory(); } Bean public SmsClient smsClientByInstanceFactory() { return smsClientFactory().createWithRetry(3); } }第一眼看过去这不就是用Java方法调用代替了XML标签吗对但有个非常微妙的机制Configuration类默认会被CGLIB代理Spring会拦截被Bean修饰的方法保证同一个配置类中多次调用别的Bean方法时返回的是容器中的同一个单例而不是重新执行方法体。所以上面smsClientByInstanceFactory()方法里调用smsClientFactory()实际上不会重新执行那个方法体去new一个工厂而是直接去容器拿已经注册的单例。这就是配置驱动下的“方法即工厂”语义。但这里有个大坑如果Bean方法是static的CGLIB不会拦截它。在static方法内部调用别的Bean方法时走的是一次普通的Java方法调用不会再经过容器查找结果就是每次都new一个新实例。我在项目里就见过有人把工具方法设成static然后在里面调Bean方法构造依赖启动时不报错运行一段时间后莫名其妙出现多个实例定位半天才发现是这个原因。如果你还需要给非自定义bean指定初始化和销毁回调XML里的init-method在Java Config中对应Bean(initMethod init, destroyMethod close)。这个功能在接入需要预热连接或释放资源的组件时很常用。3.4 FactoryBean自己掌控产生bean的过程FactoryBean是Spring框架里另一个经典的bean创建入口。它和我前面讲的静态工厂、实例工厂最大的区别在于它本身是一个“用于生产bean的bean”由Spring先实例化FactoryBean自身然后反复调用它的getObject()方法得到最终产品。比如我们想给SmsClient的创建过程加入一些定制逻辑写成这样public class SmsClientFactoryBean implements FactoryBeanSmsClient { private String apiKey; private String secret; public SmsClientFactoryBean(String apiKey, String secret) { this.apiKey apiKey; this.secret secret; } Override public SmsClient getObject() { return new SmsClient(apiKey, secret); } Override public Class? getObjectType() { return SmsClient.class; } Override public boolean isSingleton() { return true; } }注册到容器Bean public SmsClientFactoryBean smsClientFactoryBean() { return new SmsClientFactoryBean(keyFromConfig, secretFromConfig); }注意这个Bean方法返回类型是SmsClientFactoryBean但在容器里最终被拿到的bean却并不是SmsClientFactoryBean本身而是getObject()返回的SmsClient。Spring规定当检测到一个bean实现了FactoryBean接口时getBean(smsClientFactoryBean)返回的是产品对象getBean(smsClientFactoryBean)才能拿到FactoryBean本身。那个前缀可以理解为Spring识别FactoryBean的保留符号。在业务场景里FactoryBean非常适合做需要缓存、代理或者复杂初始化的对象创建。比如封装一个从远程拉取配置后动态构建客户端的逻辑把细节收进getObject()里使用方完全无感知。三种配置方式加上FactoryBean的对比我整理成一张表方便你记忆方式配置要点适合场景构造器constructor-arg/Bean方法参数参数固定、直接new的第三方类静态工厂classfactory-method工具类、单例类、静态方法创建实例工厂factory-beanfactory-method需要先管理一个工厂实例的旧组件FactoryBean实现接口自定义getObject()需要定制创建逻辑、缓存、代理时4. 实操过程中最容易踩的坑4.1 Configuration类被重复实例化前面说过CGLIB代理的问题这里再补充一个实操中常发生的场景。有人写配置类时喜欢把公共的构建逻辑抽成一个普通方法比如Configuration public class BadConfig { Bean public SmsClient clientA() { return buildClient(aaa); } Bean public SmsClient clientB() { return buildClient(bbb); } private SmsClient buildClient(String key) { return new SmsClient(key, secret); } }这个写法本身没问题buildClient()不是Bean方法不会被拦截它只是被当成普通Java方法调用。但如果你把buildClient()误加了Bean注解或者在方法内部又调用了其他Bean方法实例化行为就会超出预期。每次调用buildClient()时若其中调用了另一个Bean方法目标bean可能被重复创建或产生代理行为导致数量不对、行为不对。4.2 静态工厂和实例工厂选不对判断标准只有一个工厂方法是不是static。是static就用factory-method不是static必须先注册工厂bean再用factory-bean指向它。常见的错误就是混用。比如在XML里既写了class又写了factory-beanSpring实际执行时会优先按factory-bean走class成了摆设代码看半天摸不着头脑。Java Config里则表现为在Bean方法内部直接new SmsClientFactory().createWithRetry(3)工厂实例没有被Spring管理绕开了容器的单例池。如果工厂自身有状态这样创建的bean就是“游离”的不属于容器生命周期管理范围。4.3 用符号拿FactoryBean本身使用FactoryBean时最容易被绊倒的就是类型不匹配。假设某个类里声明了Autowired private SmsClientFactoryBean smsClientFactoryBean;运行时会直接报NoSuchBeanDefinitionException因为容器眼里smsClientFactoryBean这个beanName对应的对象类型是SmsClient而不是SmsClientFactoryBean。要拿到工厂本身得显式按名字取context.getBean(smsClientFactoryBean)。这个细节如果没读过Spring源码光靠调试很难猜透。我第一次遇到时折腾了快一个小时最后翻到BeanFactory接口的注释才明白是硬编码的转义标记。4.4 prototype下的实例化差异prototype是多例作用域。如果你把SmsClient声明成Scope(prototype)那么每次getBean(smsClient)都会重新走一遍完整实例化流程构造器会再次执行工厂方法会再次调用初始化回调也会再次执行。但销毁回调不会自动触发因为容器认为这个对象该由调用方自己负责释放。这本来不算坑但当你把SmsClient换成连接池、线程池这类有资源状态的第三方对象时误配prototype就是灾难。每次注入拿到的都是新连接用完没人关连接数很快被耗光。从实例化的角度理解这种每次都重新创建的行为本身就是设计意图但放在资源型组件上就是典型配置失误。5. 快速排查速查表最后给一份我自己常用的排查清单。遇到非自定义bean启动或运行异常时先看异常类型再对照这张表基本能定位到问题异常现象可能原因重点排查点Could not resolve placeholder属性源未加载、占位符拼写错误检查PropertySource或XML的property-placeholder配置BeanInstantiationException构造器/工厂方法调用失败检查参数类型、构造器是否存在、工厂方法是否staticBeanInitializationExceptioninit-method执行失败看初始化方法内是否有资源预检逻辑NoSuchBeanDefinitionExceptionBean定义未注册或类型不匹配确认Bean方法是否生效、组件扫描是否覆盖到NoUniqueBeanDefinitionException同一类型存在多个bean检查是否存在重复的Bean定义或扫描重叠BeanCurrentlyInCreationException循环依赖重点检查构造器注入是否形成闭环围绕实例化方式最核心的还是三件事搞清楚类是谁负责创建的、方法参数怎么匹配、创建过程中有哪些后置处理器介入。第三件事比较深但前两件是配置非自定义bean必须掌握的功底。说点个人感受。早年用注解用习惯了总感觉实例化就是反射调一下构造器简单得很。直到有一次排查线上问题一个第三方支付SDK的客户端在容器启动阶段反复初始化失败日志里一会儿是占位符没解析一会儿是工厂方法找不到我才意识到自己对整个创建链路理解得太浅。从那以后凡是接第三方类我都会先在头脑里过一遍它该走哪种实例化方式该用XML还是Java Config还是FactoryBean再动手写配置。这个习惯帮我避掉了不少隐藏极深的问题。如果你也总在非自定义bean上栽跟头建议亲手把本文这个短信SDK的例子从头到尾跑一遍三种配置方式都试一次再故意写错几个参数看看报错长什么样。踩过这几轮坑bean的实例化方式在你这里就不再是抽象概念了。