首页 / 资讯中心 / 文章详情

Spring Bean实例化方式详解:从构造器到工厂方法一次打通

Spring Bean实例化方式详解:从构造器到工厂方法一次打通 ★ FEATURED ARTICLE
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 classcom.example.Student你很难分清楚“实例化成功”是因为 Spring 本来就会这样做还是因为你的类刚好写了无参构造器。而换成 JDK 自带的类比如java.util.Calendar它是抽象类你第一反应就是“这玩意儿怎么 new 出来”这时候再去研究 factory-method印象会深得多。这些 JDK 类还有一个好处不需要引入任何第三方依赖Maven 里只要一个 spring-contextJDK 自带的类就能撑起全部演示场景。做实验的成本低踩坑的成本也低。2. 构造器实例化最直观的bean方式2.1 无参构造器容器最省事的“new”构造器实例化是最基础的方式。配置里只要给一个 class 全限定名容器启动时就会通过反射找到这个类调用它的无参构造器创建对象。bean idstringList classjava.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 iddateFormatter classjava.text.SimpleDateFormat constructor-arg valueyyyy-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 idcalendar classjava.util.Calendar factory-methodgetInstance/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 idshortDateFormat classjava.text.DateFormat factory-methodgetDateInstance constructor-arg value2/ /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 构造器的选择思路选构造器还是静态工厂不是凭喜好而是看类的设计。日常判断就三步看看类有没有可访问的构造器有就直接用构造器方式。构造器是 private 的、类本身是抽象类、或者类注释里明确写了“请用 getInstance 获取”必须走静态工厂。如果创建对象还需要读配置文件、准备环境、初始化缓存通常静态工厂也不够用了得往下看实例工厂。注意静态工厂方法本身必须是 public static 的。如果类里有个同名的实例方法Spring 是区分得清的因为factory-method属性专门走静态方法这条逻辑不会乱调用。4. 实例工厂实例化先有工厂 bean再有产品 bean4.1 什么时候必须用实例工厂静态工厂有个先天限制方法必须写在类自身上而且不能使用实例状态。现实场景里工厂往往是有状态的——它内部维护了一个配置项、一个缓存、一个连接池这些状态必须在创建产品之前先初始化好。这时候就要用实例工厂。实例工厂在 XML 里的关键属性是factory-bean和factory-method。factory-bean指向一个已经定义好的工厂 beanfactory-method是工厂 bean 上的实例方法名。我写个例子工厂自己定义但生产出来的产品是 JDK 自带的 SimpleDateFormatpackage 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-methodbean idformatterFactory classcom.example.factory.MyFormatterFactory property namepattern valueyyyy/MM/dd/ /bean bean idformatter factory-beanformatterFactory factory-methodcreateFormatter/容器处理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容器只在启动时调用一次工厂方法后面拿到的都是同一个实例如果产品配了scopeprototype每次 getBean 都会重新调用一次工厂方法。这个细节一旦理解就能解释为什么有些“每次调用都该是新对象”的工厂产品会出现“看起来像是同一个对象”的诡异问题。4.3 实例工厂和静态工厂的本质区别一句话总结静态工厂是“类直接生”实例工厂是“个体生”。前者不需要先创建任何对象后者必须有一个存活在容器里的工厂对象。前者适合无状态的工具类后者适合有初始化逻辑、有配置状态的生产线。从这个角度回头看你会发现 Spring 把这两种方式都封装在 XML 的“两个关键词”里了但背后一个是Method.invoke(静态方法)一个是先getBean(工厂)再Method.invoke(实例方法)。把这两条路径在脑子里串起来后面看任何框架源码里对 bean 工厂的抽象就不会晕了。5. FactoryBean 与 Bean容器视角的另类实例化5.1 FactoryBean把自己伪装成产品的 beanSpring 里还有一种特殊的接口叫FactoryBeanT。凡是实现它的 bean容器会先创建这个“工厂 bean”本身然后调用它的getObject()方法把返回值当作真正暴露给外界的 bean。我用它包装一个LocalDate的生产过程package com.example.factory; import org.springframework.beans.factory.FactoryBean; import java.time.LocalDate; public class LocalDateFactoryBean implements FactoryBeanLocalDate { Override public LocalDate getObject() { return LocalDate.now(); } Override public Class? getObjectType() { return LocalDate.class; } Override public boolean isSingleton() { return true; } }XML 里的配置非常简单bean idtoday classcom.example.factory.LocalDateFactoryBean/注意这里 class 写的是LocalDateFactoryBean但你在外面getBean(today)拿到的不是 LocalDateFactoryBean而是LocalDateLocalDate today context.getBean(today, LocalDate.class); System.out.println(today);这就是 FactoryBean 的“伪装”能力定义的是工厂暴露的是产品。老项目里做 AOP 时常见的ProxyFactoryBean就是这个机制的经典应用——你配一个 ProxyFactoryBeangetBean 拿到的却是代理对象。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 方法的本质就是实例工厂方法配置类本身是工厂 beanBean 方法是工厂方法返回值就是产品 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.ArrayListNo default constructor found或NoSuchMethodException类没有无参构造器构造器实例化缺参数补constructor-arg或换静态工厂/实例工厂方式NoSuchBeanDefinitionException: No bean named xxxbean id 写错或 factory-bean 引用不存在检查 id 拼写确认工厂 bean 已定义BeanCreationException附带Factory method ... threw exception工厂方法内部抛异常先单独调用工厂方法确认业务逻辑是否正常拿到的 bean 类型和预期不一致工厂方法返回类型与声明类型有偏差检查工厂方法返回值类型必要时强转或用具体类型 getBean多次 getBean 却拿到同一个对象产品是 singleton 且工厂方法不返回新实例确认是否需要scopeprototype并检查 isSingleton 返回值6.2 本次实验踩过的三个经典坑第一个坑是静态工厂方法的参数类型匹配。我一开始给DateFormat.getDateInstance传的是valueMEDIUMSpring 没法把 “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 实例化这件事就算真正长在脑子里了。
阅读完成 · 觉得有帮助?
咨询建站