1. 先厘清Spring 里的“实例化”到底指什么1.1 容器为什么需要掌控创建过程Spring 的 XML 配置在今天看来确实有点老派但只要你维护过任何一个五年以上的 Java 服务几乎都见过类似bean idxxx classcom.xxx.Xxx/的片段。很多人第一反应是这无非是帮我们省掉一个 new把配置文件当成一种花哨的写法。这种理解不能说错却错过了更关键的东西。当我们用 new 创建对象时创建顺序和对象组装逻辑是写死在代码里的。对象 A 依赖对象 BB 依赖 CA 的构造函数里就要先拿到 BB 的构造函数里又要先拿到 C拆起来非常繁琐。Spring 的思路完全不同它把对象以及对象之间的依赖关系收集到容器里通过配置描述声明式地告诉容器需要一个什么样的对象。容器启动阶段解析这些配置判断依赖顺序然后逐个实例化、组装。实例化只是第一步后面还有属性填充、初始化回调、AOP 代理包装等一连串动作。可排查问题时我们最关注的恰恰是实例化这一步因为大量报错都堆在这里。基于 XML 配置至少有四类实例化路径默认构造器、静态工厂方法、实例工厂方法、FactoryBean 接口。它们各有适用场景启动报错的分析方式也完全不同把这四条路径理清楚容器的启动逻辑就有了一个很具体的抓手。1.2 XML 配置在这条链路里的定位要真正看懂实例化方式得先明白 XML 配置到底扮演什么角色。Spring 会把每个bean标签解析成 BeanDefinitionBeanDefinition 里记录了 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 xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean iduserDao classcom.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 iddataSource classcom.example.config.DataSource constructor-arg index0 valuejdbc:mysql://localhost:3306/demo/ constructor-arg index1 valueroot/ constructor-arg index2 value10/ /beanindex 按参数下标匹配简单直接name 按参数名匹配可读性好type 显式指定参数类型主要用在构造器重载或类型混淆的场景。我自己的习惯是优先用 name因为 index 写多了以后根本看不出 0、1、2 分别代表什么。不过 name 依赖编译时保留参数名如果你在 Maven 的 pom.xml 里没有配置-parameters编译参数启动时可能提示找不到参数名这时候改用 index 反而更稳。还有一个更常见的传对象引用场景构造器参数如果依赖另一个 bean就不能用 value应该用 ref。bean iduserService classcom.example.service.UserService constructor-arg index0 refuserDao/ /beanSpring 拿到 ref 后会先确保 userDao 已被实例化再把它的引用传给 userService 的构造器。这正是控制反转在实例化阶段的体现依赖关系由容器来编排而不是靠代码里手动传递。2.3 这类实例化的常见坑默认构造器反射调用看似没有技术含量但下面几个坑我见得太多了。第一个坑是类里定义了带参构造器却忘记留无参构造器。JVM 只要看到自定义构造器就不会生成默认无参构造器Spring 反射时找不到无参构造器就会抛出 BeanCreationException 或 NoSuchMethodError。有些老项目在旧版本 Spring 下能正常启动升级版本后突然报错很多就是这类问题。第二个坑是构造器里做太多事。有人喜欢在构造器里连数据库、读文件、调用远程接口结果容器一启动就卡住或者直接报错。构造器的职责是完成必要的参数赋值而不是初始化外部资源。复杂逻辑应放到 init-method、InitializingBean 或者 PostConstruct 里这样至少能把实例化和初始化分开排查。第三个坑和实例化时机有关。单例 bean 默认在启动时创建如果重量级对象被配成了单例启动时间就会变长。有人会用 lazy-inittrue 延迟到第一次 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 idmyConnection classcom.example.util.MyConnection factory-methodof/与构造器实例化最大的区别是这里的 class 不再是目标对象的类而是工厂类也就是静态方法所在的类。Spring 不会去调用它的构造器而是直接调用静态方法 of再把返回值注册为容器中的 myConnection。这么设计的好处是目标对象被封闭起来不强制暴露构造器创建规则被集中到工厂方法里。比如上面 ip 的校验就不需要依赖调用方自觉而是由工厂方法统一保证。3.2 给静态工厂方法传入参数静态工厂方法通常也不是无参的像 MyConnection.of 就需要 ip 和 port。传入方式仍然使用 constructor-arg只是这里的 constructor-arg 不再给构造函数传参而是给静态工厂方法传参。bean idmyConnection classcom.example.util.MyConnection factory-methodof constructor-arg nameip value192.168.1.10/ constructor-arg nameport value8080/ /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 iddataSource classcom.example.config.DataSource constructor-arg nameurl valuejdbc:mysql://localhost:3306/demo/ constructor-arg nameusername valueroot/ constructor-arg namemaxActive value10/ /bean bean idserviceFactory classcom.example.factory.ServiceFactory property namedataSource refdataSource/ /bean bean iduserService factory-beanserviceFactory factory-methodcreateUserService/注意第三个 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 idserviceFactory classcom.example.factory.ServiceFactory/ bean iduserService factory-beanserviceFactory factory-methodcreateUserService/ bean idorderService factory-beanserviceFactory factory-methodcreateOrderService/ bean idproductService factory-beanserviceFactory factory-methodcreateProductService/这种写法在遗留代码里很常见公司内部往往已经有一套工厂类只要在 XML 里按这个模式接入容器不用把原有工厂类推倒重来。它的可维护性比在业务代码里到处 new 要好得多所有创建关系在 XML 里一眼就能看清。4.3 实例工厂与静态工厂怎么选择两者都能用的时候我会从“工厂本身是否需要依赖”这个维度来判断。工厂不需要依赖、没有状态用静态工厂更省事少一个 bean 定义。工厂需要注入数据源、配置参数或者持有缓存就选实例工厂。有一个辅助记忆点实例工厂的本质就是把“工厂对象”也当成一个 bean它享受和其他 bean 一样的生命周期包括依赖注入、初始化、销毁回调这是静态工厂无法获得的能力。所以当你觉得工厂方法内部要读写状态时实例工厂基本是唯一合理的选择。5. 方式四通过 FactoryBean 接口定制实例化5.1 FactoryBean 到底做了什么很多人刚看到 FactoryBean 这个名字时会把它跟 BeanFactory 混在一起其实它们的角色完全不同。BeanFactory 是 Spring 容器的基础接口让我们去 getBeanFactoryBean 则是 Spring 框架留给开发者的一个定制接口用来干预某个 bean 的创建过程。它的工作方式很特别我们在 XML 里配置的 class 指向 FactoryBean 的实现类但当容器初始化这个 bean 时它不会把 FactoryBean 实例本身注册成正式 bean而是调用 getObject() 方法把返回值注册到容器。换句话说你声明的是一个工厂拿到的却是工厂生产的产品。这种间接层给了开发者极大的定制空间。getObject() 里可以做缓存、做代理、根据条件返回不同实现甚至返回对象的类型都可以不在配置里提前写死。Spring 内部大量使用这种机制AOP 代理对象、MyBatis 的 MapperFactoryBean、Ribbon 的负载均衡客户端等都是通过 FactoryBean 方式向容器提供对象的。5.2 手写一个 FactoryBean为了把这个机制看得清楚建议亲手写一个最简单的 FactoryBean比单纯看文档要直观得多。public class UserServiceFactoryBean implements FactoryBeanUserService { 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 iduserService classcom.example.factory.UserServiceFactoryBean/使用时注意getBean(userService) 拿到的是 UserService而不是 UserServiceFactoryBeanUserService 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 都适用这种方式构造器要么无参要么只依赖其他 beanconstructor-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 把参数补全。还有一种隐蔽的情况类的构造器存在但访问权限是 privateSpring 在默认访问策略下无法调用同样会报错。这种情况要么把构造器改成 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 的 FactoryBeangetBean 每次都会新创建对象如果对象内部维护了状态行为可能和你预期的单例完全不同。排查时先看装配方式再看 scope这两个维度对了很多诡异问题都能解释得通。8. 一点个人经验用 XML 配置实例化对象这件事放到今天看不是多前沿的技术但它确实是理解 Spring 容器如何工作的最好入口。我带过不少从注解开发起步的团队大家第一次看到 XML 里的 constructor-arg 都会问这跟我直接在构造器里 new 一个依赖有什么区别区别就在于依赖关系变成了一份可以被读取、替换、测试的描述而不是写死在类深处的不可见代码。我个人在实际排查时还有一个习惯遇到实例化相关的异常先确认对象的创建方式在脑子里过一遍走的到底是构造器还是工厂方法。只要路径确认对了大多数启动期问题都能在几分钟内定位。理解这四种方式看起来是在啃老技术实际上是在给 Spring 源码阅读铺路。后续想搞懂三级缓存、循环依赖、AOP 代理生成这些话题实例化路径都是绕不开的前置知识。
阅读完成 · 觉得有帮助?