☰
Spring Bean实例化方式详解:从构造器到工厂方法一次打通
2026/10/3 23:25:43 网站建设 项目流程

Spring 里的 bean 实例化方式,说白了就是搞清楚一个问题:容器到底是怎么把一个类变成对象的。很多人写了很久的 @Component、@Service,结果一旦要接管第三方 jar 包里的类,就愣住了——类的源码不在你手上,构造器能不能用、有没有工厂方法,都不是你说了算。这篇博文专门用“非自定义 bean”来做演示,也就是拿 JDK 自带的类(SimpleDateFormat、Calendar、DateFormat 这类)把 Spring 的构造器实例化、静态工厂实例化、实例工厂实例化全部跑一遍,再补上 FactoryBean 和 @Bean 两种写法,把 bean 实例化方式这个点一次打通。

1. 为什么用“非自定义 bean”来演示实例化方式

1.1 先纠一个学习误区

很多初学者会默认认为:bean 就是“类上面加个注解,或者 XML 里写个 class 属性,容器帮我 new 一下”。这个理解在你自己写的类上是成立的,因为你的类往往有公开的无参构造器,怎么折腾都行。但真实的项目里,大量 bean 来自第三方依赖,比如数据库连接池、Redis 客户端、消息队列的 producer,这些类的设计者不会为 Spring 专门留一个无参构造器。

更麻烦的情况是:有的类构造器是 private 的(典型的单例),有的类本身是抽象类,根本不能 new,还有的类创建对象必须传入一堆必填参数。这些场景下,容器不能靠默认逻辑“瞎 new”,必须由配置者明确告诉它:你该走哪条路。这个“告诉容器怎么创建对象”的过程,就是 bean 的实例化方式。

1.2 用 JDK 自带类做实验的好处

标题里说的“非自定义 bean”,意思就是这些类不是我们写的,我们无法通过修改源码去迁就容器。用它来演示实例化方式,有一个特别好的实验效果:把变量隔离了。

如果我们自己定义一个 Student 类,再配一个<bean class="com.example.Student">,你很难分清楚“实例化成功”是因为 Spring 本来就会这样做,还是因为你的类刚好写了无参构造器。而换成 JDK 自带的类,比如java.util.Calendar,它是抽象类,你第一反应就是“这玩意儿怎么 new 出来”?这时候再去研究 factory-method,印象会深得多。

这些 JDK 类还有一个好处:不需要引入任何第三方依赖,Maven 里只要一个 spring-context,JDK 自带的类就能撑起全部演示场景。做实验的成本低,踩坑的成本也低。

2. 构造器实例化:最直观的<bean>方式

2.1 无参构造器:容器最省事的“new”

构造器实例化是最基础的方式。配置里只要给一个 class 全限定名,容器启动时就会通过反射找到这个类,调用它的无参构造器创建对象。

<bean id="stringList" class="java.util.ArrayList"/>

这样一个 bean 定义,等价于new ArrayList()。注意 class 属性必须是全限定名,因为 Spring 底层的Class.forName()需要完整包名才能加载类。如果只写ArrayList,容器会直接抛ClassNotFoundException。

测试代码很简单:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); ArrayList<?> list = context.getBean("stringList", ArrayList.class); System.out.println(list.size()); // 0

这里有个很容易忽略的细节:getBean的时候我传了ArrayList.class作为第二个参数,Spring 会帮你做类型转换和校验。如果类型不匹配,会得到BeanNotOfRequiredTypeException。

2.2 带参构造器与 constructor-arg 的匹配

现实里很多第三方的类没有无参构造器,必须传参数才能创建。比如java.text.SimpleDateFormat,它最常用的构造器是SimpleDateFormat(String pattern),需要一个日期格式字符串。这个时候配置要变成:

<bean id="dateFormatter" class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd"/> </bean>

这里constructor-arg就是给构造器传参。Spring 拿到“yyyy-MM-dd”这个字符串,会先尝试做类型转换,匹配到SimpleDateFormat(String)这个构造器,然后创建对象。最终ctx.getBean("dateFormatter")拿到的就是一个能直接用的、pattern 为yyyy-MM-dd的 SimpleDateFormat。

如果构造器有多个参数,就写多个constructor-arg,顺序必须和构造器参数顺序一致。Spring 还支持用index、name属性显式指定参数位置或参数名,这在构造器重载较多时特别有用,能避免参数匹配歧义。

2.3 构造器实例化的两个坑

第一个坑:类没有无参构造器,而你又没给 constructor-arg。Spring 启动时不会等你自己想起来,它会直接抛BeanCreationException,错误信息里会写明No default constructor found。所以看到这个异常,第一反应不是去改代码,而是去检查这个 bean 定义有没有把构造参数补全。

第二个坑:构造器参数类型不匹配。比如构造器是SimpleDateFormat(int)你给了一个“abc”,Spring 在类型转换阶段就会失败。Spring 对 String 转 int、转 boolean 这些基础类型转得很顺畅,但转成自定义对象就不行了,必须用ref引用另一个 bean。

提示:constructor-arg的 value 是给“基本类型或字符串”参数用的;ref是给“另一个 bean”参数用的。记不清的时候宁愿多写一点,也别让容器替你猜。

3. 静态工厂实例化:处理“不能直接 new”的类

3.1 factory-method 的工作原理

有些类你就是没法 new,比如抽象类java.util.Calendar、构造器私有的单例类,又或者类设计者约定“只能用静态方法获取实例”。这时候就要用静态工厂方式。

XML 里写法是:class指定工厂类,factory-method指定静态方法名。

<bean id="calendar" class="java.util.Calendar" factory-method="getInstance"/>

Spring 看到factory-method,不会再尝试调用构造器,而是执行Calendar.getInstance()这个静态方法,把方法返回值作为 bean 放进容器。本质上就是反射调用:Calendar.class.getMethod("getInstance").invoke(null)。

这样做有什么意义?最直接的意义是:你写配置的人不需要关心那个类能不能 new,你只关心怎么拿到它的合法实例。Calendar是抽象类,你不可能new Calendar(),但getInstance()能给你一个基于默认时区的GregorianCalendar实例,这就够了。

测试一下:

Calendar calendar = context.getBean("calendar", Calendar.class); System.out.println(calendar.getTime());

容器里这个 bean 的类型是Calendar,实际运行时才确定真实子类。Spring 对这类“声明类型和运行时类型不一致”的情况是接受的,因为容器只管往你手里塞东西,具体是什么类型要看工厂方法的返回值。

3.2 带参静态工厂方法与参数的坑

静态工厂方法也可以是带参数的。比如java.text.DateFormat有个静态方法getDateInstance(int style),它根据 style 返回不同格式的 DateFormat。配置写法是这样的:

<bean id="shortDateFormat" class="java.text.DateFormat" factory-method="getDateInstance"> <constructor-arg value="2"/> </bean>

注意,这里constructor-arg不是给构造器传参,而是给静态工厂方法传参。Spring 看到 factory-method 后面跟了 constructor-arg,会把它解析为工厂方法的实参列表。value 是 2,对应DateFormat.MEDIUM,最终得到的是一个中等精度的日期格式化器。

这个环节我踩过一个很隐蔽的坑:如果 factory-method 的方法名在类里存在多个重载版本,Spring 必须靠参数个数和类型去选择调用哪一个。一旦你传的参数类型和所有重载版本都对不上,容器会抛NoSuchMethodException。比如getDateInstance(int)需要 int,你却在 value 里传了一个无法转为 int 的字符串,报错时只提示方法找不到,不会告诉你参数类型不匹配。排查的时候一定要把工厂方法的签名和 constructor-arg 逐个对上。

3.3 静态工厂 vs 构造器的选择思路

选构造器还是静态工厂,不是凭喜好,而是看类的设计。日常判断就三步:

  1. 看看类有没有可访问的构造器,有就直接用构造器方式。
  2. 构造器是 private 的、类本身是抽象类、或者类注释里明确写了“请用 getInstance 获取”,必须走静态工厂。
  3. 如果创建对象还需要读配置文件、准备环境、初始化缓存,通常静态工厂也不够用了,得往下看实例工厂。

注意:静态工厂方法本身必须是 public static 的。如果类里有个同名的实例方法,Spring 是区分得清的,因为factory-method属性专门走静态方法这条逻辑,不会乱调用。

4. 实例工厂实例化:先有工厂 bean,再有产品 bean

4.1 什么时候必须用实例工厂

静态工厂有个先天限制:方法必须写在类自身上,而且不能使用实例状态。现实场景里,工厂往往是有状态的——它内部维护了一个配置项、一个缓存、一个连接池,这些状态必须在创建产品之前先初始化好。这时候就要用实例工厂。

实例工厂在 XML 里的关键属性是factory-bean和factory-method。factory-bean指向一个已经定义好的工厂 bean,factory-method是工厂 bean 上的实例方法名。我写个例子,工厂自己定义,但生产出来的产品是 JDK 自带的 SimpleDateFormat:

package com.example.factory; import java.text.SimpleDateFormat; public class MyFormatterFactory { private String pattern = "yyyy-MM-dd HH:mm:ss"; public void setPattern(String pattern) { this.pattern = pattern; } public SimpleDateFormat createFormatter() { return new SimpleDateFormat(pattern); } }

然后在 beans.xml 里配置两个 bean。第一个是工厂 bean 本身,它通过构造器实例化 + 属性注入完成初始化;第二个是产品 bean,它不写 class,只写 factory-bean 和 factory-method:

<bean id="formatterFactory" class="com.example.factory.MyFormatterFactory"> <property name="pattern" value="yyyy/MM/dd"/> </bean> <bean id="formatter" factory-bean="formatterFactory" factory-method="createFormatter"/>

容器处理formatter这个 bean 时,会先确保formatterFactory已经被创建好,再调用它的createFormatter()实例方法,把返回的 SimpleDateFormat 当作 product bean。测试:

SimpleDateFormat formatter = context.getBean("formatter", SimpleDateFormat.class); System.out.println(formatter.format(new Date()));

输出应该是“2026/05/12”这种斜杠格式,说明工厂里的pattern属性值通过 setter 注入生效了。这个例子把“实例工厂”和“属性注入”串在一起,非常直观。

4.2 factory-bean 的注意事项

实例工厂看着简单,实际使用里有几个坑要记牢。

第一,factory-bean 引用的必须是一个已经存在于容器中的 bean id。写错 id 会得到NoSuchBeanDefinitionException,而且报错信息只提示找不到formatterFactory,不会提示你写错了。这种错排查起来很费时间,所以命名规范一定要统一,后缀统一用Factory。

第二,product bean 的定义里不要写 class 属性,写了反而容易误导自己。Spring 更关心的是工厂方法返回什么,而不是你写了什么 class。

第三,工厂方法的调用时机取决于产品 bean 的 scope。如果产品是 singleton,容器只在启动时调用一次工厂方法,后面拿到的都是同一个实例;如果产品配了scope="prototype",每次 getBean 都会重新调用一次工厂方法。这个细节一旦理解,就能解释为什么有些“每次调用都该是新对象”的工厂产品会出现“看起来像是同一个对象”的诡异问题。

4.3 实例工厂和静态工厂的本质区别

一句话总结:静态工厂是“类直接生”,实例工厂是“个体生”。前者不需要先创建任何对象,后者必须有一个存活在容器里的工厂对象。前者适合无状态的工具类,后者适合有初始化逻辑、有配置状态的生产线。

从这个角度回头看,你会发现 Spring 把这两种方式都封装在 XML 的“两个关键词”里了,但背后一个是Method.invoke(静态方法),一个是先getBean(工厂)再Method.invoke(实例方法)。把这两条路径在脑子里串起来,后面看任何框架源码里对 bean 工厂的抽象就不会晕了。

5. FactoryBean 与 @Bean:容器视角的另类实例化

5.1 FactoryBean:把自己伪装成产品的 bean

Spring 里还有一种特殊的接口叫FactoryBean<T>。凡是实现它的 bean,容器会先创建这个“工厂 bean”本身,然后调用它的getObject()方法,把返回值当作真正暴露给外界的 bean。

我用它包装一个LocalDate的生产过程:

package com.example.factory; import org.springframework.beans.factory.FactoryBean; import java.time.LocalDate; public class LocalDateFactoryBean implements FactoryBean<LocalDate> { @Override public LocalDate getObject() { return LocalDate.now(); } @Override public Class<?> getObjectType() { return LocalDate.class; } @Override public boolean isSingleton() { return true; } }

XML 里的配置非常简单:

<bean id="today" class="com.example.factory.LocalDateFactoryBean"/>

注意,这里 class 写的是LocalDateFactoryBean,但你在外面getBean("today")拿到的不是 LocalDateFactoryBean,而是LocalDate:

LocalDate today = context.getBean("today", LocalDate.class); System.out.println(today);

这就是 FactoryBean 的“伪装”能力:定义的是工厂,暴露的是产品。老项目里做 AOP 时常见的ProxyFactoryBean就是这个机制的经典应用——你配一个 ProxyFactoryBean,getBean 拿到的却是代理对象。

5.2 用 & 前缀把 FactoryBean 本身拿出来

有时候你就是想拿到 FactoryBean 本体,比如想确认它的配置,或者直接调它的方法。Spring 防住了这一点:在 bean 名字前面加一个&前缀。

LocalDateFactoryBean localDateFactoryBean = context.getBean("&today", LocalDateFactoryBean.class);

这个&是 Spring 的保留约定,不是什么正则语法。很多人第一次看到别人代码里写&xxx会一脸懵,其实就是“我要工厂本体,不要产品”的意思。

提示:FactoryBean和factory-bean一字之差,含义完全不同。前者是一个接口,实现类的实例本身就是“带产品的工厂”;后者是 XML 里的一个属性,用来配置“实例工厂方式”,两者不要混淆。

5.3 @Bean 注解与工厂方法的关系

到了 Spring Boot 时代,大多数人不会再写 XML 了,而是用 @Configuration 类里的 @Bean 方法。很多人没意识到,@Bean 方法的本质就是实例工厂方法:配置类本身是工厂 bean,@Bean 方法是工厂方法,返回值就是产品 bean。

我把上面三种方式用 Java Config 重写一遍:

package com.example.config; import com.example.factory.LocalDateFactoryBean; import com.example.factory.MyFormatterFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.text.SimpleDateFormat; import java.util.Calendar; import java.time.LocalDate; @Configuration public class InstantiationConfig { @Bean public SimpleDateFormat dateFormatter() { return new SimpleDateFormat("yyyy-MM-dd"); } @Bean public Calendar calendar() { return Calendar.getInstance(); } @Bean public MyFormatterFactory formatterFactory() { MyFormatterFactory factory = new MyFormatterFactory(); factory.setPattern("yyyy/MM/dd"); return factory; } @Bean public SimpleDateFormat formatter(MyFormatterFactory factory) { return factory.createFormatter(); } @Bean public LocalDate today() throws Exception { return new LocalDateFactoryBean().getObject(); } }

这里最值得讲的是formatter(MyFormatterFactory factory)这个方法。Spring 看到 @Bean 方法有参数,会自动从容器里按类型找到formatterFactory这个 bean 传进来,效果等同于 XML 里的factory-bean,但写法轻盈得多。这就是为什么很多人说 Java Config 比 XML 更好维护——依赖关系变成方法参数,编译器能帮你检查。

如果项目里还保留了老的 XML 配置,可以用@ImportResource("classpath:beans.xml")导入,让 XML 和 Java Config 里的 bean 共存。理解 XML 这些配置方式,读老代码和框架源码时依然有用,不是学了 Spring Boot 就可以完全丢掉的。

6. 常见问题与排查实录

6.1 报错速查表

把这次实验过程中最常踩的错整理成一张表,以后照着查就行:

报错信息典型原因处理办法
ClassNotFoundExceptionclass 写成了简单类名改成全限定名,比如java.util.ArrayList
No default constructor found或NoSuchMethodException类没有无参构造器,构造器实例化缺参数补constructor-arg或换静态工厂/实例工厂方式
NoSuchBeanDefinitionException: No bean named 'xxx'bean id 写错,或 factory-bean 引用不存在检查 id 拼写,确认工厂 bean 已定义
BeanCreationException附带Factory method ... threw exception工厂方法内部抛异常先单独调用工厂方法,确认业务逻辑是否正常
拿到的 bean 类型和预期不一致工厂方法返回类型与声明类型有偏差检查工厂方法返回值类型,必要时强转或用具体类型 getBean
多次 getBean 却拿到同一个对象产品是 singleton 且工厂方法不返回新实例确认是否需要scope="prototype",并检查 isSingleton 返回值

6.2 本次实验踩过的三个经典坑

第一个坑是静态工厂方法的参数类型匹配。我一开始给DateFormat.getDateInstance传的是value="MEDIUM",Spring 没法把 “MEDIUM” 转成 int,一直报NoSuchMethodException。后来才意识到 value 得写数字 2,因为 Spring 对枚举和 int 常量字符串不会自动映射。建议各位传参之前先确认方法签名,别想当然。

第二个坑是instance factory 的工厂 bean 初始化顺序。我有一次在工厂 bean 的构造器里做一堆初始化逻辑,结果那个构造器依赖一个还没注入的属性,导致产品 bean 创建失败。排查下来发现,Spring 创建工厂 bean 时,先调构造器,再注入 property,如果初始化逻辑放在构造器里,就等于在属性注入之前干活,必然出错。正确的做法是把初始化逻辑放到InitializingBean接口的afterPropertiesSet()方法,或者 XML 里的init-method上,保证属性已经注入完成。

第三个坑是FactoryBean 的名字带不带 &。我一开始getBean("today", LocalDateFactoryBean.class),报错说类型不匹配,我愣了好一会儿才想起要写&today。这个知识点文档里其实有,但不出一次错真的记不住。

6.3 一个亲测好用的实验习惯

做这个实验时,强烈建议把 beans.xml 里的 bean 定义逐步添加,每加一个就立刻跑一次测试,而不是一口气写完再调试。我通常写一个简单的 main 方法,用context.getBeanDefinitionNames()把容器里当前所有 bean 名打印出来:

for (String name : context.getBeanDefinitionNames()) { System.out.println(name); }

这样每一步都能看到容器里到底注册了什么、漏了什么。顺序就是:先跑通构造器,再加静态工厂,再加实例工厂,最后上 FactoryBean。每轮只引入一个新知识点,出错了也容易定位。

Maven 依赖方面,我建议用spring-context5.3.x 系列,JDK 8 以上都能跑,实验兼容性最好。如果你用的是 Spring 6 以上,需要 JDK 17,类路径和 API 没有大变化,但报错信息会有细微差异,排查时注意区分版本。

我个人做过很多次这种小实验,最深的体会是:实例化方式不是考试知识点,而是项目里真正天天会遇到的选择题。接手一个老系统,看到 XML 里的 factory-bean 配置不再发懵;自己搭工程,知道想让容器管理一个不听话的第三方类时该走哪条路。建议你把文中的三个 JDK 类例子自己敲一遍,改参数、故意写错、看报错,整套流程走完,Spring bean 实例化这件事就算真正长在脑子里了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询