1. 先厘清:Spring 里的“实例化”到底指什么
1.1 容器为什么需要掌控创建过程
Spring 的 XML 配置在今天看来确实有点老派,但只要你维护过任何一个五年以上的 Java 服务,几乎都见过类似<bean id="xxx" class="com.xxx.Xxx"/>的片段。很多人第一反应是这无非是帮我们省掉一个 new,把配置文件当成一种花哨的写法。这种理解不能说错,却错过了更关键的东西。
当我们用 new 创建对象时,创建顺序和对象组装逻辑是写死在代码里的。对象 A 依赖对象 B,B 依赖 C,A 的构造函数里就要先拿到 B,B 的构造函数里又要先拿到 C,拆起来非常繁琐。Spring 的思路完全不同:它把对象以及对象之间的依赖关系收集到容器里,通过配置描述声明式地告诉容器需要一个什么样的对象。容器启动阶段解析这些配置,判断依赖顺序,然后逐个实例化、组装。
实例化只是第一步,后面还有属性填充、初始化回调、AOP 代理包装等一连串动作。可排查问题时,我们最关注的恰恰是实例化这一步,因为大量报错都堆在这里。基于 XML 配置至少有四类实例化路径:默认构造器、静态工厂方法、实例工厂方法、FactoryBean 接口。它们各有适用场景,启动报错的分析方式也完全不同,把这四条路径理清楚,容器的启动逻辑就有了一个很具体的抓手。
1.2 XML 配置在这条链路里的定位
要真正看懂实例化方式,得先明白 XML 配置到底扮演什么角色。Spring 会把每个<bean>标签解析成 BeanDefinition,BeanDefinition 里记录了 bean 的类名、作用域、构造参数、属性和各种回调方法。ApplicationContext 启动时,先注册这些 bean 定义,再在后续阶段根据定义去创建对象。
XML 并不是唯一提供 BeanDefinition 的方式,注解扫描、JavaConfig 最终也会转化成 BeanDefinition。不过 XML 是最直白的一种形式:标签和属性差不多就是 BeanDefinition 的镜像,你几乎能一眼看到 Spring 接下来要执行的操作。这也是为什么我建议刚学 Spring 的人先从 XML 入手,哪怕现在主流已经转向注解,XML 里的这些概念在注解背后依然存在,只是被藏起来了。
下面四种方式我按从简单到灵活的顺序来讲,每种都会配一个可以直接跑起来的例子,并说清楚为什么在那个场景下要选它。
2. 方式一:默认构造器实例化——绕不开的基础操作
2.1 最简单的 bean 配置
第一种方式就是默认构造器实例化,使用频率最高、写起来也最简单。只要目标类存在一个可访问的无参数构造器,Spring 就能通过反射调用它来创建对象。配置里只需要给出 class,不需要额外指定什么。
public class UserDao { public UserDao() { System.out.println("UserDao 实例化完成"); } public void findById(Long id) { System.out.println("查询用户: " + id); } }对应的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userDao" class="com.example.dao.UserDao"/> </beans>启动容器时,用下面的代码验证对象是否被创建:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = (UserDao) context.getBean("userDao");这里有一个关键点:ApplicationContext 在启动时默认会实例化所有单例 bean,并不是等到 getBean 才创建。所以容器一启动,构造器里的打印就会立即出现。理解了这一点,再看 Spring 应用启动慢的问题,你会明白很大一部分原因就是容器启动时成批创建了对象,而不是某个业务请求进来才慢。
2.2 带构造参数时的写法
实际项目中的类不可能永远只有一个空构造器。比如数据源配置需要 url、username、maxActive 这些参数,设计上就要求用带参构造器创建对象。此时在 XML 里用 constructor-arg 给构造器传参。
public class DataSource { private String url; private String username; private int maxActive; public DataSource(String url, String username, int maxActive) { this.url = url; this.username = username; this.maxActive = maxActive; } }XML 里常见的写法有三种:
<bean id="dataSource" class="com.example.config.DataSource"> <constructor-arg index="0" value="jdbc:mysql://localhost:3306/demo"/> <constructor-arg index="1" value="root"/> <constructor-arg index="2" value="10"/> </bean>index 按参数下标匹配,简单直接;name 按参数名匹配,可读性好;type 显式指定参数类型,主要用在构造器重载或类型混淆的场景。我自己的习惯是优先用 name,因为 index 写多了以后,根本看不出 0、1、2 分别代表什么。
不过 name 依赖编译时保留参数名,如果你在 Maven 的 pom.xml 里没有配置-parameters编译参数,启动时可能提示找不到参数名,这时候改用 index 反而更稳。还有一个更常见的传对象引用场景:构造器参数如果依赖另一个 bean,就不能用 value,应该用 ref。
<bean id="userService" class="com.example.service.UserService"> <constructor-arg index="0" ref="userDao"/> </bean>Spring 拿到 ref 后,会先确保 userDao 已被实例化,再把它的引用传给 userService 的构造器。这正是控制反转在实例化阶段的体现:依赖关系由容器来编排,而不是靠代码里手动传递。
2.3 这类实例化的常见坑
默认构造器反射调用看似没有技术含量,但下面几个坑我见得太多了。
第一个坑是类里定义了带参构造器却忘记留无参构造器。JVM 只要看到自定义构造器就不会生成默认无参构造器,Spring 反射时找不到无参构造器,就会抛出 BeanCreationException 或 NoSuchMethodError。有些老项目在旧版本 Spring 下能正常启动,升级版本后突然报错,很多就是这类问题。
第二个坑是构造器里做太多事。有人喜欢在构造器里连数据库、读文件、调用远程接口,结果容器一启动就卡住或者直接报错。构造器的职责是完成必要的参数赋值,而不是初始化外部资源。复杂逻辑应放到 init-method、InitializingBean 或者 @PostConstruct 里,这样至少能把实例化和初始化分开排查。
第三个坑和实例化时机有关。单例 bean 默认在启动时创建,如果重量级对象被配成了单例,启动时间就会变长。有人会用 lazy-init="true" 延迟到第一次 getBean 再创建,但这只是把问题后移,创建开销并没有减少。我更倾向于从设计上拆分对象,把真正重量级的初始化放进回调方法里按需执行。
3. 方式二:静态工厂方法实例化——处理复杂创建逻辑
3.1 静态工厂方法的基本配置
第二种方式面向的是不能直接用构造器创建的类。典型情况是目标类的构造器是私有的,或者创建对象前需要经过一套校验逻辑。这种场景在 JDK 里随处可见,比如 Integer.valueOf()、LocalDateTime.now() 都是静态工厂方法的例子。
在 XML 配置中,可以通过 factory-method 指向静态方法。看这段代码:
public class MyConnection { private String ip; private int port; private MyConnection(String ip, int port) { this.ip = ip; this.port = port; } public static MyConnection of(String ip, int port) { if (ip == null || ip.isEmpty()) { throw new IllegalArgumentException("ip 不能为空"); } return new MyConnection(ip, port); } }MyConnection 的构造器是私有的,外部没有办法直接 new,只能通过静态方法 of 创建。XML 里这样声明:
<bean id="myConnection" class="com.example.util.MyConnection" factory-method="of"/>与构造器实例化最大的区别是:这里的 class 不再是目标对象的类,而是工厂类,也就是静态方法所在的类。Spring 不会去调用它的构造器,而是直接调用静态方法 of,再把返回值注册为容器中的 myConnection。
这么设计的好处是目标对象被封闭起来,不强制暴露构造器,创建规则被集中到工厂方法里。比如上面 ip 的校验,就不需要依赖调用方自觉,而是由工厂方法统一保证。
3.2 给静态工厂方法传入参数
静态工厂方法通常也不是无参的,像 MyConnection.of 就需要 ip 和 port。传入方式仍然使用 constructor-arg,只是这里的 constructor-arg 不再给构造函数传参,而是给静态工厂方法传参。
<bean id="myConnection" class="com.example.util.MyConnection" factory-method="of"> <constructor-arg name="ip" value="192.168.1.10"/> <constructor-arg name="port" value="8080"/> </bean>容易混淆的点:在 factory-method 场景下,constructor-arg 传的是工厂方法参数,不是构造器参数。Spring 不会关心这个类有没有可访问的构造器,反而会根据参数列表去匹配名为 of 的静态方法。
匹配规则和构造器一样,支持按 index、name、type 混合使用。我实际使用中更建议按 name 传参,毕竟静态工厂方法的名字通常很短,参数名才是真正的语义所在。
3.3 选择静态工厂的思考
静态工厂方法的最大优势是简单,不需要额外定义工厂对象,尤其适合处理“类已经提供了静态创建方法”的场景。但它也有明显的限制:工厂方法无法访问 Spring 管理的依赖,也无法持有状态,因为它是静态的,不属于任何对象实例。
所以在设计层面,我不建议把静态工厂当成唯一选择。如果一个对象创建逻辑很复杂,又需要依赖配置数据,那你更值得看接下来两种方式。静态工厂适合解决单点问题,复杂的创建策略建议交给实例工厂或 FactoryBean。
4. 方式三:实例工厂方法实例化——由容器管理的工厂
4.1 实例工厂方法的基本配置
第三种方式叫实例工厂方法,思路和静态工厂很像,但方法不写 static,工厂本身也要交给 Spring 管理。因为工厂变成了一个 bean,所以它可以被注入依赖、持有状态,创建子对象时还能把这份状态传递下去。
public class ServiceFactory { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public UserService createUserService() { UserService userService = new UserService(); userService.setDataSource(dataSource); return userService; } }XML 配置分成两步:先定义工厂 bean,再通过 factory-bean 引用它。
<bean id="dataSource" class="com.example.config.DataSource"> <constructor-arg name="url" value="jdbc:mysql://localhost:3306/demo"/> <constructor-arg name="username" value="root"/> <constructor-arg name="maxActive" value="10"/> </bean> <bean id="serviceFactory" class="com.example.factory.ServiceFactory"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="userService" factory-bean="serviceFactory" factory-method="createUserService"/>注意第三个 bean 没有 class 属性,因为它的类型取决于工厂方法的返回值,Spring 无法静态推断出具体类。如果强行加上 class 属性,某些版本下也不是致命错误,但语义上并不准确,容易引发不可预期的警告。
这里能看到实例工厂模式相对依赖注入的区别:dataSource 只被设置到工厂上,工厂在创建 userService 时再把数据源组装进去。工厂还可以做更多事情,比如统计创建次数、打印日志、根据配置返回不同实现,这些在静态工厂里都不方便做。
4.2 一个工厂方法匹配多个场景
实例工厂一个很大的应用场景,是通过一个工厂批量创建一系列对象,把每组创建逻辑集中到对应方法中。
public class ServiceFactory { public UserService createUserService() { return new UserService(); } public OrderService createOrderService() { return new OrderService(); } public ProductService createProductService() { return new ProductService(); } }XML 里不需要改 Java 代码,只要定义一次工厂,再为每个目标对象配一个 bean 即可:
<bean id="serviceFactory" class="com.example.factory.ServiceFactory"/> <bean id="userService" factory-bean="serviceFactory" factory-method="createUserService"/> <bean id="orderService" factory-bean="serviceFactory" factory-method="createOrderService"/> <bean id="productService" factory-bean="serviceFactory" factory-method="createProductService"/>这种写法在遗留代码里很常见,公司内部往往已经有一套工厂类,只要在 XML 里按这个模式接入容器,不用把原有工厂类推倒重来。它的可维护性比在业务代码里到处 new 要好得多,所有创建关系在 XML 里一眼就能看清。
4.3 实例工厂与静态工厂怎么选择
两者都能用的时候,我会从“工厂本身是否需要依赖”这个维度来判断。工厂不需要依赖、没有状态,用静态工厂更省事,少一个 bean 定义。工厂需要注入数据源、配置参数或者持有缓存,就选实例工厂。
有一个辅助记忆点:实例工厂的本质就是把“工厂对象”也当成一个 bean,它享受和其他 bean 一样的生命周期,包括依赖注入、初始化、销毁回调,这是静态工厂无法获得的能力。所以当你觉得工厂方法内部要读写状态时,实例工厂基本是唯一合理的选择。
5. 方式四:通过 FactoryBean 接口定制实例化
5.1 FactoryBean 到底做了什么
很多人刚看到 FactoryBean 这个名字时会把它跟 BeanFactory 混在一起,其实它们的角色完全不同。BeanFactory 是 Spring 容器的基础接口,让我们去 getBean;FactoryBean 则是 Spring 框架留给开发者的一个定制接口,用来干预某个 bean 的创建过程。
它的工作方式很特别:我们在 XML 里配置的 class 指向 FactoryBean 的实现类,但当容器初始化这个 bean 时,它不会把 FactoryBean 实例本身注册成正式 bean,而是调用 getObject() 方法,把返回值注册到容器。换句话说,你声明的是一个工厂,拿到的却是工厂生产的产品。
这种间接层给了开发者极大的定制空间。getObject() 里可以做缓存、做代理、根据条件返回不同实现,甚至返回对象的类型都可以不在配置里提前写死。Spring 内部大量使用这种机制:AOP 代理对象、MyBatis 的 MapperFactoryBean、Ribbon 的负载均衡客户端等,都是通过 FactoryBean 方式向容器提供对象的。
5.2 手写一个 FactoryBean
为了把这个机制看得清楚,建议亲手写一个最简单的 FactoryBean,比单纯看文档要直观得多。
public class UserServiceFactoryBean implements FactoryBean<UserService> { @Override public UserService getObject() { UserService userService = new UserService(); userService.setName("created by FactoryBean"); return userService; } @Override public Class<?> getObjectType() { return UserService.class; } @Override public boolean isSingleton() { return true; } }XML 配置反而很简单,class 指向这个实现类即可:
<bean id="userService" class="com.example.factory.UserServiceFactoryBean"/>使用时注意,getBean("userService") 拿到的是 UserService,而不是 UserServiceFactoryBean:
UserService userService = context.getBean("userService", UserService.class); System.out.println(userService); // 真的是 UserService 实例如果偏要拿 FactoryBean 本身,可以在 bean 名称前加前缀 &:
UserServiceFactoryBean factoryBean = context.getBean("&userService", UserServiceFactoryBean.class);这句代码足够说明 FactoryBean 与普通 bean 之间的特殊绑定关系。常规业务代码里 & 前缀很少真的出现,但它是排查容器内部问题时的重要工具。
5.3 我为什么要区分 isSingleton 和 getObject()
FactoryBean 接口需要实现三个核心方法:getObject() 返回对象,getObjectType() 告诉容器返回类型,isSingleton() 标明返回对象是否单例。这里的单例语义需要单独理解:FactoryBean 本身的单例由 bean 定义控制,而 isSingleton() 控制的是 getObject() 返回对象的单例状态。
比如 MyBatis 的 MapperFactoryBean,它为每个 Mapper 接口生成一个代理对象。如果 getObject() 每次调用都重新创建,代理对象的缓存价值就完全被掏空了,所以 isSingleton() 返回 true,让同一接口类型只有一个代理对象被缓存。反过来,如果每个请求都需要新对象,就必须在 getObject() 里 new 一个新实例,同时 isSingleton() 返回 false。
这个设计是 FactoryBean 跟实例工厂方法最大的不同:工厂方法是 Java 层面的工厂模式,FactoryBean 是框架层面的扩展接口。前者更强调方法的复用,后者更强调对容器创建过程的可定制性。
6. 四种方式如何结合场景进行选择
6.1 决策型选择建议
四种方式并不是互斥的,真正重要的是根据场景选对配置。
从使用频率来看,默认构造器实例化占到了绝大多数。普通业务类、Service、Dao、Controller 都适用这种方式,构造器要么无参,要么只依赖其他 bean,constructor-arg 加 ref 完全可以解决。只有当构造器是私有的,或者创建逻辑已经被抽到工厂中时,才会考虑静态工厂方法。实例工厂适合已有工厂类需要接入 Spring、且工厂本身需要依赖注入的情况,配置上会多一个工厂 bean。FactoryBean 则是四者中扩展性最强的一种,专门用于第三方框架集成或者复杂对象的定制,MyBatis、Feign 这类框架都靠它来注入对象。
我整理了一个对照表,方便决策时参考:
| 方式 | XML 常用属性 | 工厂对象 | 能注入依赖 | 适用场景 |
|---|---|---|---|---|
| 默认构造器 | class + constructor-arg | 无 | 能,通过 ref | 绝大多数普通对象 |
| 静态工厂方法 | class + factory-method | 无 | 不能 | 第三方类、私有构造器 |
| 实例工厂方法 | factory-bean + factory-method | 有 | 能 | 已有工厂类、有状态工厂 |
| FactoryBean 接口 | class 指向 FactoryBean | 有 | 能 | 框架集成、复杂对象定制 |
6.2 混合使用的真实案例
一个典型的 SSM 项目里,这几种方式基本是同时出现的。数据源通常走构造器或者第三方连接池的静态工厂;Dao 实现类直接无参构造器;MyBatis 的 Mapper 接口靠 MapperFactoryBean 注入;业务层则大量通过 ref 实现依赖注入。很难找到一个项目只用了其中一种。
无论哪种方式,最终都会转化为 BeanDefinition,再被 BeanFactory 实例化。翻到 Spring 源码,你会发现这几个实例化路径最终汇聚到 AbstractAutowireCapableBeanFactory 的 createBeanInstance() 方法:如果配置了 factory-method 就走工厂路径,否则走构造器反射路径;如果实现的是 FactoryBean,则在 getObject() 这一层旁路处理。理解了这个汇聚点,排查实例化问题时,你就知道该往源码的哪个方向看。
7. 日常排查与避坑清单
7.1 报错一:找不到默认构造器
最常见异常是 No default constructor found 或者 BeanInstantiationException。原因通常就是类里定义了带参构造器却没有补无参构造器。修复方式是补一个无参构造器,或者改用 constructor-arg 把参数补全。
还有一种隐蔽的情况:类的构造器存在,但访问权限是 private,Spring 在默认访问策略下无法调用,同样会报错。这种情况要么把构造器改成 protected 或 public,要么按静态工厂方式接入容器。我个人更建议后者,因为私有构造器往往是类设计上有意为之,不要为了适配 Spring 而破坏封装。
7.2 报错二:factory-method 相关异常
使用静态工厂时,最常见问题是方法名写错、方法不是 static,或者方法签名与 constructor-arg 参数不匹配。Spring 会提示 Factory method 'xxx' threw exception,或者 No such static method。定位思路是回到源码确认方法签名,尤其注意方法是否 static。
使用实例工厂时,容易出现 factory-bean 声明的工厂对象没有被定义,或者 factory-bean 引用了尚未初始化的 bean。前者会直接报 No bean named,后者则可能抛循环依赖。我用实例工厂踩过最频繁的坑,是忘了给工厂 bean 配置它在创建子对象时依赖的数据源属性,结果创建出来的对象配置全是空的。这种问题通常不会报错,只能通过日志一步步看,所以建议一开始就把工厂 bean 和它依赖的对象配置检查清楚。
7.3 报错三:getBean 拿到的对象不对
如果你配了 FactoryBean,用 context.getBean("name") 却得到 FactoryBean 本身,最常见的原因是容器里存在同名 bean 覆盖了预期定义,或者 XML 里把 class 指向了错误的类。
判断方法很简单:加 & 前缀再去取一次,如果能拿到 FactoryBean,说明容器里确实注册了它,问题出在名称或类型匹配上。另外要注意 isSingleton() 返回 false 的 FactoryBean,getBean 每次都会新创建对象,如果对象内部维护了状态,行为可能和你预期的单例完全不同。排查时先看装配方式,再看 scope,这两个维度对了,很多诡异问题都能解释得通。
8. 一点个人经验
用 XML 配置实例化对象这件事,放到今天看不是多前沿的技术,但它确实是理解 Spring 容器如何工作的最好入口。我带过不少从注解开发起步的团队,大家第一次看到 XML 里的 constructor-arg 都会问:这跟我直接在构造器里 new 一个依赖有什么区别?区别就在于依赖关系变成了一份可以被读取、替换、测试的描述,而不是写死在类深处的不可见代码。
我个人在实际排查时还有一个习惯:遇到实例化相关的异常,先确认对象的创建方式在脑子里过一遍,走的到底是构造器还是工厂方法。只要路径确认对了,大多数启动期问题都能在几分钟内定位。理解这四种方式看起来是在啃老技术,实际上是在给 Spring 源码阅读铺路。后续想搞懂三级缓存、循环依赖、AOP 代理生成这些话题,实例化路径都是绕不开的前置知识。