很多人学Spring都是从IoC容器开始的,但等你打开一个真实的老项目,看到XML里散落着一堆<bean>配置,有时还是会犯迷糊:同样是创建一个对象,为什么有的直接写个class就完事,有的要加factory-method,有的还要配factory-bean,甚至还有专门实现FactoryBean接口的类?这些写法的背后,其实就是Spring实例化对象的几种典型方式——构造器、静态工厂、实例工厂、FactoryBean。这篇文章我不打算讲多高深的源码,就把这四种方式掰开揉碎,用最小可运行的XML配置一个个列出来,告诉你每种方式在什么场景下用、配置里的每个属性是干什么的、踩到坑怎么排查。不管你是刚看完Spring入门教程的新手,还是被循环依赖和代理创建折磨过的中级开发,这份梳理应该都能帮你把那层窗户纸捅破。
1. XML配置实例化Bean的整体图景
1.1 实例化不是全部,创建对象是一条流水线
先说一个很多人容易忽略的概念:<bean>标签里的class属性,并不只是告诉Spring"你要new这个类",而是告诉Spring"这个Bean的BeanDefinition长什么样"。容器启动后,真正发生的事远比一个new关键字复杂。
我自己习惯把Bean的创建过程分成四个阶段:解析配置、实例化、填充属性、初始化。实例化只是中间一小步。你写下的<bean>会被解析成一个BeanDefinition对象,里面记录了class名、作用域、构造参数、属性值、初始化方法名等元数据;然后容器在合适的时机调用构造器或工厂方法,把对象'造'出来,此时对象还是半成品,属性全是默认值;接着Spring根据<property>和<constructor-arg>把依赖塞进去,最后执行init-method或InitializingBean回调,一个能用的Bean才算真正诞生。
理解这条流水线特别重要。因为后面的几种实例化方式,本质上都是在"实例化"这一环做文章,而后面的属性填充和初始化逻辑对四种方式一视同仁。也就是说,不管你是用构造器搭出来的对象,还是从工厂方法里拿到的对象,只要它进了容器,Spring都会继续给它做依赖注入和初始化。很多同学疑惑"为什么从FactoryBean里拿到的对象也能自动注入属性",答案就在这里——注入动作发生在实例化之后,而不是之前。
1.2 BeanDefinition:你写的每行XML都会变成什么
为了让你对后续的配置不产生割裂感,我建议你先建立这个映射关系:
<bean id="userService" class="com.example.UserService"/>表示一个BeanDefinition,class指向最终实例化要用的类;<property name="xxx" value="yyy"/>最终会变成一个PropertyValue,在填充属性阶段被反射写入;<constructor-arg>则对应构造参数列表,供实例化阶段选择匹配的构造器;factory-bean和factory-method组合,相当于把"实例化动作"委托给了另一个Bean的方法;FactoryBean接口则是一种更高级的委托,容器的getBean("xxx")会转发给getObject()。
说白了,XML配置只是BeanDefinition的一种外部表现。你写的每一种方式,最终都会在AbstractAutowireCapableBeanFactory.createBean()里被分派到不同的创建策略。我在看Spring源码时,最喜欢在AbstractAutowireCapableBeanFactory里打断点,观察createBeanInstance()方法怎么走分支:有factoryMethodName走工厂方法,有FactoryBean类型则先创建工厂本身,否则老老实实找构造器。理解了这条主线,下面的各种配置就不会觉得是零散技巧了。
2. 构造器实例化:最朴素也最常用
2.1 无参构造器配置
先看最基础的一种:Spring直接调用类的无参构造器创建对象。配置就一行:
<bean id="userService" class="com.example.UserService"/>如果你的UserService长这样:
public class UserService { private UserDao userDao; public UserService() { System.out.println("UserService 无参构造器被调用"); } public void setUserDao(UserDao userDao) { this.userDao = userDao; } }那么Spring在实例化时,会通过反射找到UserService的无参构造器并执行。这里要提醒一个新手高频坑:如果你的类写了带参构造器,却没写无参构造器,容器启动会直接报No default constructor found。因为反射调clazz.getDeclaredConstructor()找不到默认构造器,Spring就束手无策了。所以用这种最朴素方式创建Bean的类,要么保留无参构造器,要么老老实实走带参构造的配置。
无参构造器之所以是默认选项,是因为它最简单、最不容易出错。Spring不需要猜测该调用哪个构造器,也不需要准备参数,直接BeanUtils.instantiateClass就完事。实际项目里,Spring容器管理的类我几乎都默认设计成无参构造器配合setter注入,这样既方便容器操作,写单元测试时也可以手动new出来然后set依赖,两条路都走得通。
2.2 带参构造器与constructor-arg
有些类就是为"必须传参才能用"设计的,比如一个要求强制传入UserDao和一个初始化次数的服务。这种类直接用无参构造器并不合理,Spring也提供了<constructor-arg>来匹配带参构造器:
<bean id="userService" class="com.example.UserService"> <constructor-arg name="userDao" ref="userDao"/> <constructor-arg name="maxRetry" value="3"/> </bean>对应的Java类:
public class UserService { private UserDao userDao; private int maxRetry; public UserService(UserDao userDao, int maxRetry) { this.userDao = userDao; this.maxRetry = maxRetry; } }这里有个匹配规则要说清楚:Spring会根据<constructor-arg>的类型、顺序去匹配构造器。如果参数是引用类型,用ref指向另一个Bean;如果是基本类型或字符串,用value直接给值。当你用name属性明确指定参数名时,Spring还需要你的代码在编译时开启了-parameters参数,否则参数名拿不到,最好同时用index="0"来兜底,或者保证类型组合足够唯一。
带参构造器的一大好处是"依赖不可变"。对象一旦创建,依赖关系就固定了,不会因为后续有人调了setter而被改掉,这对核心域对象特别友好。代价就是灵活性下降,尤其是构造参数一多,XML配置就变得很啰嗦。我的建议是:超过三四个构造参数的对象,优先考虑建造者模式或拆成多个小对象,别让XML变成火车一样的长串参数。
2.3 一个绕不开的话题:构造函数循环依赖
提到构造器方式,就不得不顺带说一个Spring里最容易挂的场景——循环依赖。假设A的构造器需要B,B的构造器需要A,容器启动时会发生什么?答案是直接抛BeanCurrentlyInCreationException。
原因也好理解:构造器方式意味着"创建对象时必须拿到参数",Spring为了拿到A就得先创建B,为了创建B又得先创建A,两边都在等待,形成一个死结。Spring的三级缓存解决的是setter注入的循环依赖:先提前暴露半成品对象,再填充属性时互相引用;而构造器阶段连半成品都还没造出来,想暴露也没得暴露。
所以遇到构造器循环依赖,不需要研究什么高级配置,直接把其中一边改成setter注入,或者重新设计依赖方向就行。我自己排查这类问题时,第一步就是看报错里的Bean名,把它们画成一张依赖图,然后果断改掉其中一条边。记住一句话:构造器适合表达"必需依赖",但别用它表达"互相依赖"。
3. 静态工厂实例化:适合"不让你new"的类
3.1 原理和适用场景
有些类的构造器不是public的,或者类本身设计成通过静态方法获取实例,比如JDK里的Runtime.getRuntime()、Calendar.getInstance()。如果这样的类要交给Spring管理,直接用<bean class="..."/>反射调构造器肯定会失败,因为构造器压根不公开。这时候就可以用静态工厂方式:配置里告诉Spring"别调构造器了,去调用这个类的某个静态方法,由它把对象给你"。
原理并不复杂:Spring在实例化阶段发现factory-method属性存在,就不再走instantiateClass,而是反射调用factory-method指定的静态方法,拿到返回值作为Bean实例。方法必须是static的,因为它不依赖任何对象实例,本质上是一种"类级别的对象提供器"。
适用场景也比较典型:
- 目标类构造器是
private,只能通过静态方法获取; - 创建逻辑封装在静态方法里,方法内部可能做了缓存、参数校验或者统一配置;
- 你不想让调用方直接
new,而是通过一个名字清晰的工厂方法体现创建意图。
3.2 配置细节
先看最简单的写法。
public class UserServiceFactory { public static UserService getInstance() { return new UserService("created-by-factory"); } }<bean id="userService" class="com.example.UserServiceFactory" factory-method="getInstance"/>注意这个配置里class指向的是工厂类,不是最终要创建的目标类。Spring看到factory-method后,会用class定位工厂类,再找到对应的静态方法,调完之后把方法返回值注册成userService这个Bean。
如果静态方法需要参数,就用<constructor-arg>传入——虽然名字叫constructor-arg,但在工厂方法场景下,它实际上对应的是工厂方法的参数:
public class DateFormatFactory { public static SimpleDateFormat create(String pattern) { return new SimpleDateFormat(pattern); } }<bean id="dateFormat" class="com.example.DateFormatFactory" factory-method="create"> <constructor-arg name="pattern" value="yyyy-MM-dd"/> </bean>这里最容易犯的错就是把class写成目标类,或者忘记加factory-method。如果两者同时出问题,报错会是No default constructor found,因为你把类写成了目标类,Spring又开始傻傻地找构造器了。
3.3 一个随手可试的示例
你可以用一个很简单的例子试明白:假设有个ConnectionManager,构造器是private的,对外只暴露静态工厂方法:
public class ConnectionManager { private final String url; private ConnectionManager(String url) { this.url = url; } public static ConnectionManager createLocal() { return new ConnectionManager("jdbc:local://config"); } }XML配置:
<bean id="connectionManager" class="com.example.ConnectionManager" factory-method="createLocal"/>写个启动类验证:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); ConnectionManager cm = context.getBean("connectionManager", ConnectionManager.class); System.out.println(cm);运行后你会发现,容器成功拿到了ConnectionManager实例,完全绕过了private构造器。静态工厂方式的最大优势就是让那些"不情愿被new"的类也能无缝融入容器,同时把创建细节留在工厂方法内部,XML这边干干净净。
4. 实例工厂实例化:工厂本身也是Bean
4.1 与静态工厂的差别在哪里
如果说静态工厂是"类级别的方法调用",那实例工厂就是把工厂本身变成一个Bean,再通过这个Bean实例的普通方法去创建目标对象。差别非常关键:静态工厂方法不能依赖任何Spring注入的依赖,而实例工厂是容器管理的Bean,可以注入属性、引用其他Bean。
这一下就打开了更多可能性。比如某个工厂方法需要根据环境变量来决定创建什么类型的对象,而环境变量是另一个配置Bean提供的,静态工厂就做不到,因为静态方法没有this,拿不到被Spring注入的状态。实例工厂可以做到:Spring先创建并配置好工厂Bean,再把工厂Bean注入依赖,最后调用它的实例方法产出目标对象。
4.2 配置方法与示例
先看Java端。我设计一个工厂,它自己持有env属性,用来决定返回什么配置的连接管理实例:
public class ServiceFactory { private String env; public void setEnv(String env) { this.env = env; } public UserService createUserService() { return new UserService(env); } }XML配置分两步。第一步先配置工厂Bean本身,第二步配置目标Bean时用factory-bean指向上一步的工厂:
<bean id="serviceFactory" class="com.example.ServiceFactory"> <property name="env" value="dev"/> </bean> <bean id="userService" factory-bean="serviceFactory" factory-method="createUserService"/>注意这里的目标Bean没有class属性,因为类类型由工厂方法的返回值决定。Spring通过反射调用serviceFactory.createUserService(),拿到返回值后注册为userService。factory-bean和factory-method必须成对出现,只写一个就会启动报错。
这种模式的实际价值在于工厂的复用和上下文感知。同一个工厂类,可以在XML里配置成不同状态的多个工厂Bean,比如一个env=dev、一个env=prod,分别产出不同配置的对象;工厂内部还能引用其他Bean,比如引用一个PropertiesLoader来读取外部配置。Spring内部很多复杂组件的装配,都用了类似的思路:先配置一个持有大量依赖的工厂Bean,再让它产出真正需要的客户端对象。
如果你把实例工厂和前面的静态工厂对比一下,会发现实例工厂的配置只多了factory-bean,但灵活性提升了不止一个量级。我自己的习惯是:当创建逻辑需要依赖其他Bean或需要状态时,果断用实例工厂,别硬塞静态方法。
5. FactoryBean:Spring内部最常用的"生产方式"
5.1 接口与约定
FactoryBean是本章节的重头戏,也是很多Spring开发者的困惑点。它听起来和"工厂"有关,但和前面的普通工厂方式有一个本质区别:FactoryBean是Spring容器认可的"一等公民"接口,容器会对实现了FactoryBean接口的Bean做特殊处理。
接口长这样:
public interface FactoryBean<T> { T getObject() throws Exception; Class<?> getObjectType(); boolean isSingleton(); }约定是这样的:你配置了一个class=xxxFactoryBean的Bean,容器在实例化阶段会正常创建这个工厂Bean本身,但当有人调用context.getBean("xxx")时,Spring发现这个Bean实现了FactoryBean接口,就会返回getObject()的产物,而不是工厂Bean本身。换句话说,对外暴露的是getObject()的返回值,工厂Bean本尊被藏了起来。
这个设计精妙在哪里?它把"复杂的创建过程"和"统一的外部获取方式"解耦了。调用方完全不需要知道对象是从哪来的,只要通过Bean名拿就行。Spring内部大量使用这个机制,比如MyBatis的SqlSessionFactoryBean、Ribbon的配置、以及AOP代理对象的创建,都是靠它实现的。
5.2 自定义FactoryBean示例
我写一个尽量贴近实际的例子:假设有个RpcClient,创建时需要初始化连接池、加载协议配置、设置重试次数,过程非常啰嗦,你想把这些细节都封装起来。
先看一下目标类:
public class RpcClient { private final String serverAddress; private final int retryTimes; public RpcClient(String serverAddress, int retryTimes) { this.serverAddress = serverAddress; this.retryTimes = retryTimes; // 模拟建立连接等耗时操作 System.out.println("RpcClient 初始化完成: " + serverAddress); } }然后实现FactoryBean,把复杂的初始化流程放进去:
public class RpcClientFactoryBean implements FactoryBean<RpcClient> { private String serverAddress; private int retryTimes; public void setServerAddress(String serverAddress) { this.serverAddress = serverAddress; } public void setRetryTimes(int retryTimes) { this.retryTimes = retryTimes; } @Override public RpcClient getObject() { // 这里可以写一大段复杂初始化逻辑 return new RpcClient(serverAddress, retryTimes); } @Override public Class<?> getObjectType() { return RpcClient.class; } @Override public boolean isSingleton() { return true; } }XML配置:
<bean id="rpcClient" class="com.example.RpcClientFactoryBean"> <property name="serverAddress" value="127.0.0.1:8080"/> <property name="retryTimes" value="3"/> </bean>这时候Spring会对配置在class里的RpcClientFactoryBean做实例化和属性填充,但调用方取的时候拿的是RpcClient:
RpcClient rpcClient = context.getBean("rpcClient", RpcClient.class);你可能会想:这不就和实例工厂差不多吗?功能上确实有点像,但FactoryBean的优势在于它是Spring的标准接口,容器里的很多基础能力都认识它,比如事务、AOP在自动代理时会对FactoryBean做特殊处理,还会在ApplicationContext里额外持有&前缀的Bean。你不实现这个接口,就享受不到这些容器级待遇。
5.3 &前缀与内部使用场景
FactoryBean还有一个很经典的细节:如果你想获取工厂Bean本身,而不是它的产物,需要加&前缀。
RpcClientFactoryBean factory = context.getBean("&rpcClient", RpcClientFactoryBean.class);没有&拿产物,有&拿工厂本身。这个规则我刚学的时候经常记反,后来我用一个类比才记住:FactoryBean像一个代购,你下单(getBean("xxx"))拿到的是代购买回来的商品,想去问代购本人问题,就得喊"找那个代购"(&前缀)。
Spring内部为什么要大量用FactoryBean呢?最主要的原因是AOP代理。当某个Bean需要被AOP增强时,Spring会创建一个代理对象,这个代理对象的创建逻辑复杂,通常用一个FactoryBean把"创建代理"的细节封装起来。你配置一个普通Bean,结果从容器里拿出来的是增强后的代理,正是因为背后有FactoryBean在干活。理解了这一点,再看那些"XML里配了个beans,但是getBean出来类型不对"的问题,就能猜到八成是FactoryBean在搞鬼了。
如果你感兴趣,还可以看看MyBatis-Spring的SqlSessionFactoryBean源码:它实现了FactoryBean<SqlSessionFactory>,把MyBatis配置解析、环境构建、映射器注册全塞进了getObject()。一个SqlSessionFactory背后涉及那么多步骤,外部调用却只是getBean("sqlSessionFactory")——这种封装力度,才是FactoryBean真正的魅力。
6. 四种方式横向对比与组合实践
6.1 一张表看懂四种方式的配置差异
做了这么多铺垫,我把四种方式的区别整理成一张表,方便你快速对比和回忆。
| 实例化方式 | 关键配置属性 | 创建者是谁 | 典型场景 | 注意事项 |
|---|---|---|---|---|
| 构造器 | class+constructor-arg | Spring容器 | 大多数普通业务Bean | 类需要有对应构造器,参数匹配要稳 |
| 静态工厂 | class+factory-method | 静态方法 | JDK工具类、构造器私有类 | factory-method指向的方法必须是static |
| 实例工厂 | factory-bean+factory-method | 工厂Bean的实例方法 | 创建逻辑依赖其他Bean或状态 | 工厂Bean必须单独配置并先初始化 |
| FactoryBean | class=FactoryBean实现类 | getObject()方法 | 复杂创建、代理对象、框架集成 | 获取工厂本身需要&前缀 |
这张表的核心记忆点就一句话:构造器方式靠Spring自己去new,另外三种方式本质上是"把创建动作委托出去",委托给静态方法、或者委托给另一个Bean的方法、或者委托给FactoryBean的getObject()。
6.2 一个贴近真实的综合示例
光看碎片配置不够,我带你走一个稍微完整点的场景:假设系统里有多种环境的数据源连接管理器,需要根据env配置创建连接管理器。手动new太啰嗦,我们用实例工厂来实现:
public class ConnectionManager { private final String env; private final int maxConnections; public ConnectionManager(String env, int maxConnections) { this.env = env; this.maxConnections = maxConnections; } }工厂类:
public class ConnectionManagerFactory { private AppConfig appConfig; public void setAppConfig(AppConfig appConfig) { this.appConfig = appConfig; } public ConnectionManager createLocal() { return new ConnectionManager(appConfig.getEnv(), appConfig.getLocalMaxConnections()); } public ConnectionManager createRemote() { return new ConnectionManager(appConfig.getEnv(), appConfig.getRemoteMaxConnections()); } }XML配置:
<bean id="appConfig" class="com.example.AppConfig"> <property name="env" value="dev"/> <property name="localMaxConnections" value="10"/> <property name="remoteMaxConnections" value="5"/> </bean> <bean id="connectionFactory" class="com.example.ConnectionManagerFactory"> <property name="appConfig" ref="appConfig"/> </bean> <bean id="localConn" factory-bean="connectionFactory" factory-method="createLocal"/> <bean id="remoteConn" factory-bean="connectionFactory" factory-method="createRemote"/>这样,我们不但在XML里复用了同一个工厂Bean,还让工厂从AppConfig里读取配置。如果用静态工厂,这个AppConfig根本传不进去;如果用构造器硬拼,每个Bean都要重复写一堆参数。实例工厂在这个场景下的优势就很明显了。
如果这里要创建的对象初始化逻辑特别重,比如要读取多个配置文件、要构建子组件,我就直接把工厂类改成实现FactoryBean,把这个"工厂Bean + 工厂方法"两步封装成一步。毕竟FactoryBean对调用方更友好:外部只用getBean("localConn"),连"谁是工厂"都不用知道。
6.3 实例化时机:scope、lazy-init和init-method
聊实例化方式,还绕不开一个实际问题:这些Bean到底什么时候被创建?四个字:取决于scope和lazy-init。
默认情况下scope="singleton"的Bean,容器启动时就会完成实例化和初始化,这被称为"饿汉式"创建。如果你不想启动阶段就创建,可以设置lazy-init="true",容器会推迟到第一次getBean时才实例化。要注意这个懒加载对scope="prototype"其实是没有意义的,因为prototype本来就是每次getBean才新建。scope和实例化时机的对应关系,直接在XML里体验最直观。
<bean id="heavyService" class="com.example.HeavyService" lazy-init="true"/> <bean id="prototypeBean" class="com.example.PrototypeBean" scope="prototype"/>实例化完成之后,Spring还会执行初始化钩子。XML里最直观的就是init-method和destroy-method:
<bean id="rpcClient" class="com.example.RpcClient" init-method="connect" destroy-method="close"/>如果你在这篇文章前面理解了"实例化、填充属性、初始化"三段式的节奏,那init-method出现的位置就很好猜了:它一定在属性填充完成之后执行,因为一个没有依赖注入完的对象去连网、起线程纯属找死。这也是为什么我强调实例化只是第一步,你看到init-method要意识到它已经是容器为你准备的上桌菜了。
7. 常见问题与排查技巧
7.1 高频报错速查
四种方式都讲完了,我把实战里最容易踩的坑整理成一个速查表,遇到报错直接对着看。
| 报错现象 | 常见原因 | 解决建议 |
|---|---|---|
No default constructor found | 类没有无参构造器,但XML没配<constructor-arg> | 补无参构造器,或用index/name明确配置构造参数 |
factory-method ... non-static | 配置的factory-method指向了非静态方法 | 把方法改成static,或用实例工厂方式 |
factory-bean与factory-method缺失其一 | 实例工厂方式缺配置 | 两个属性必须成对出现 |
Bean property 'xxx' is not writable... | <property>的name和setter对不上 | 检查setter命名、检查name拼写 |
ClassCastException: cannot cast ... FactoryBean | 想拿工厂本身却直接用了Bean名 | 用&beanName获取工厂本身 |
BeanCurrentlyInCreationException | 构造器注入循环依赖 | 改成setter注入或重新设计依赖关系 |
7.2 我的几条排查经验
最后再分享几条我在真实项目里的排障心得,这些在文档里都不容易看到。
第一,先确认BeanDefinition长啥样,再猜问题。你可以临时写个ApplicationContext启动类,遍历所有BeanDefinition打印factoryMethodName、beanClassName、scope,一眼就能看出来你写的XML有没有被正确解析。很多"为什么我配置了实例工厂但没生效"的问题,排查来排查去,最后发现是factory-bean的名字写错了。
第二,遇到FactoryBean相关异常,先找&前缀。有时候日志里出现No qualifying bean of type 'xxxFactoryBean',是因为你一直以为getBean("xxx")返回的是工厂本身,结果类型对不上。想清楚你拿的是产物还是工厂,问题就解决了一大半。
第三,XML配置优先用IDEA的Spring面板看依赖图。在IDEA里右键XML文件选择Diagram -> Show dependencies,所有Bean之间的引用关系会以图形展示,循环依赖、指向错误几乎一眼可见。这个功能在我处理老项目时帮了我大忙,比盯着报错日志猜有效多了。
第四,别在XML里堆十来个<constructor-arg>。配置越复杂,出错的概率越高。如果你的一个Bean需要四五个依赖才能创建,请重新审视设计:是不是对象拆得太粗了?如果能拆,拆出来的小对象用无参构造加setter,大对象用工厂组装,整体可维护性会好很多。
我自己在项目里长期遵循一个默认策略:普通业务Bean优先无参构造加setter注入;需要不可变依赖、参数较少时用带参构造;接入框架或创建过程重时用FactoryBean;JDK或第三方库不给new的类才考虑静态工厂。这不是说其他方式不行,而是这个组合踩坑最少、读起来最直观。
如果你正准备改造一段老代码,或者在为团队制定Spring Bean创建规范,我建议你把这篇文章里的四种方式当成一个检查清单,遇到一个新Bean先问一句:它属于哪种方式?配置里对应的属性是什么?该配class还是factory-bean?把基础概念理清楚之后,你会发现很多看似玄乎的Spring问题,其实都能落到这几个简单选择上。