Spring干货集|Bean依赖你又觉得行了?

简介: 本文详解Spring依赖注入(DI)核心机制,涵盖构造器注入、setter注入及`depends-on`与`lazy-init`等关键属性应用,助你掌握多Bean协作的系统设计精髓。

本文已收录在Github关注我,紧跟本系列专栏文章,咱们下篇再续!

  • 🚀 魔都架构师 | 全网30W技术追随者
  • 🔧 大厂分布式系统/数据中台实战专家
  • 🏆 主导交易系统百万级流量调优 & 车联网平台架构
  • 🧠 AIGC应用开发先行者 | 区块链落地实践者
  • 🌍 以技术驱动创新,我们的征途是改变世界!
  • 👉 实战干货:编程严选网

0 前言

实际的系统几乎不可能仅有单一的bean,都是很多个bean协作提供服务。本文目标也就是讨论如何冲破单一 bean 定义而让多 bean 协作实现系统。

1 什么是依赖注入(Dependency Injection)?

之前文章说过, DI其实是一个过程。该过程中,bean可通过如下方式定义它们之间的依赖关系:

  1. 构造器参数
  2. 工厂方法参数
  3. 从工厂方法构造或返回的对象实例上设置的属性

接着,容器在创建bean时就会注入这些依赖关系。

该过程实质上就是 bean 本身操作的反转,因此得名 Inversion of Control(IoC,控制反转),而非对象自己直接通过使用其构造器或通过服务定位设计模式来控制其依赖项的实例化或位置。

使用 DI 代码会更整洁,当bean维护其依赖项时,也更解耦。bean不需要查找其依赖项,也无需知晓其依赖项的位置或具体类。如此一来,类也更便于测试,尤其是当依赖项为接口或抽象类时,可方便在UT中使用mock。

知晓了其原理了,那么在开发中又是如何实践的呢?

2 DI的实现形式

2.1 构造器注入

通过Spring容器调用具有多参数的构造器而完成,每个参数代表一个依赖项。调用具有特定参数的静态工厂方法来构造 bean 基本等效。

如下示例中的类仅可使用构造器注入的 DI:

public class MovieLister {
    // MovieLister 有个 MovieFinder 依赖
    private MovieFinder movieFinder;
    // 通过构造器Spring容器可以注入一个MovieFinder
    public MovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    // 省略实际使用注入的MovieFinder的业务逻辑...
}

2.1.1  构造器参数解析

构造器参数解析匹配通过 参数的类型 触发。若在 bean 定义的构造器参数中不存在歧义,则在 bean 定义中定义构造器参数的顺序是当 bean 实例化时这些参数提供给相应的构造器的顺序。说半天估计你也晕了,看如下案例:

package A.B;
public class ThingFirst {
    public ThingFirst(ThingSecond thingSecond, ThingThird thingThird) {
        // ...
    }
}

假设 ThingSencond 和 ThingThird 类无继承关系,那么就没有歧义。因此,下面的配置也能工作良好,而无需在 <constructor-arg/> 标签中显式指定构造器参数的顺序或类型。

<bean id="beanFirst" class="A.B.ThingFirst">
    <constructor-arg ref="beanSecond"/>
    <constructor-arg ref="beanThird"/>
</bean>
<bean id="beanSecond" class="A.B.ThingSecond"/>
<bean id="beanThird" class="A.B.ThingThird"/>

就像刚才案例,当引用另一个bean时,类型已知,所以可触发匹配。但当使用简单类型时,如<value>true</value>, Spring无法确定值的类型,因此在没有帮助时,也就无法通过类型进行匹配。看案例:

package examples;
public class DemoBean {
    // 追寻答案所需的年数
    private int years;
    // 关于生命、宇宙和万物的答案
    private String ultimateAnswer;
    public DemoBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

2.1.2 构造器参数类型匹配

在前面的案例中,若使用 type 属性显式指定构造器参数的类型,则容器可以使用与简单类型相匹配的类型。如下所示:

<bean id="demoBean" class="examples.DemoBean">
    <constructor-arg type="int" value="666"/>
    <constructor-arg type="java.lang.String" value="0"/>
</bean>

2.1.3 构造器参数顺序

可使用 index 属性显式指定构造器参数的顺序,如下所示(注意从零开始计数)

<bean id="demoBean" class="examples.DemoBean">
    <constructor-arg index="0" value="6666666"/>
    <constructor-arg index="1" value="0"/>
</bean>

除了解决多个简单值的不确定性,还解决了构造器具有相同类型的两个参数时的不确定性。

2.1.4 构造器参数名称

也可以使用构造器参数名称消除歧义,如下案例:

<bean id="demoBean" class="examples.DemoBean">
    <constructor-arg name="years" value="666"/>
    <constructor-arg name="ultimateAnswer" value="0"/>
</bean>

请记住,要使这一操作开箱即用,我们的代码必须在启用调试标识的情况下进行编译,以便Spring可以从构造器中查找参数名。若不能或不希望用debug标识编译代码,可用JDK的@ConstructorProperties 注解显式设置该构造函数的参数如何与构造对象的getter方法相对应。

@Documented @Target(CONSTRUCTOR) @Retention(RUNTIME)
public @interface ConstructorProperties {
    /**
     * <p>The getter names.</p>
     * 
     * @return the getter names corresponding to the parameters in the
     * annotated constructor.
     */
    String[] value();
}

看如下案例:

public static class DataClass {
    @NotNull
    public final String param1;
    public final boolean param2;
    public int param3;
    @ConstructorProperties({"param1", "param2", "optionalParam"})
    public DataClass(String param1, boolean p2, Optional<Integer> optionalParam) {
        this.param1 = param1;
        this.param2 = p2;
        Assert.notNull(optionalParam, message: "Optional must not be null");
        optionalParam.ifPresent(integer -> this.param3 = integer);
    }
    public void setParam3(int param3) { this.param3 = param3; }
}

2.2 setter注入

通过调用无参构造器或无参静态工厂方法实例化bean后,通过容器在bean上调用setter方法来完成基于setter注入。

如下案例是一个不依赖于特定于容器的接口,基类或注解,而且只能setter注入方式DI的POJO类。

public class MovieLister {
    // MovieLister 有个 MovieFinder 依赖
    private MovieFinder movieFinder;
    // setter方法, 以便Spring容器注入 MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    // 省略实际使用注入的MovieFinder的业务逻辑...
}

ApplicationContext为其管理的bean的提供了构造器和setter DI的支持。也支持在已通过构造器注入某些依赖后,还支持setter DI。可通过BeanDefinition的形式配置依赖项,将其与PropertyEditor实例结合使用,以将属性从一种格式转为另一种。但大多数开发者并非以编程方式直接使用这些类,而是使用

  • XML形式的 bean定义
  • 带注解的组件,即被@Component@Controller等注解的类
  • 基于Java的@Configuration类中的@Bean方法

然后将这些源在内部转换为BeanDefinition实例,并用于加载整个IoC容器实例。

3 构造器注入 or setter注入

到底哪种 DI 方式好?由于可混用构造器和setter DI,因此将构造器用于强制性依赖项,并搭配将setter方法或配置方法用于可选依赖项是个很好的最佳实践。

可在setter方法用@Required,以使该属性成为必需的依赖;但最好使用带有编程式验证的参数的构造器注入。

Spring团队推荐构造器注入,因其可让开发者将应用的组件实现为不可变对象,并确保所需的依赖项不为null。构造器注入的组件始终以完全初始化的状态返回给客户端(调用)代码。但大量的构造器自变量是种坏代码,因为这意味着该类可能承担太多职责(违反单一职责原则),应对其重构以更好解决关注点的解耦问题。

Setter注入主要应仅用于可在类中分配合理的默认值的可选依赖项。否则,必须在代码使用依赖项的所有地方都执行判空检查。 setter注入的一个好处:setter方法使该类的对象在以后可重新配置或注入。

使用对特定类最有意义的DI方案。有时,在处理没有源代码的第三方类库时,将为你做出选择。例如,若第三方类库未公开任何setter方法,则构造器注入可能就是DI的唯一可用方案咯。

4 deponds-on 属性有何用?

你以为这个东西面试没人问?看图!

若一个bean是另一个的依赖,则通常意味着将一个bean设为另一个的属性。通常可使用XML形式配置元数据中的<ref/>元素完成此操作。但有时bean之间的依赖关系不那么直接。一个示例是何时需要触发类中的静态初始化器,例如用于数据库驱动程序注册。depends-on属性可显式强制初始化一或多个使用该元素的bean之前的bean。

案例

depends-on属性表示对单个bean的依赖关系:

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

要表示对多个 bean 的依赖,请提供 bean 名称列表作为依赖属性的值(逗号、空格和分号都是有效的分隔符):

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

depends-on属性既可以指定一个 初始化期(initialization-time) 依赖项,也可指定一个对应的析构期(destruction-time)依赖项。在销毁给定bean之前,首先销毁定义与给定bean的依赖关系的依赖bean。因此,depends-on还可以用来控制关闭顺序。

5 lazy-init属性有何作用?

在默认的初始化过程中,ApplicationContext会及早地创建并配置所有的单例bean。一般来说,这种预实例化是有好处的,毕竟相比于若干天后的亡羊补牢,这样可立即发现配置或上下文环境的错误。 当然了,如果你的业务决定了不想要这种默认行为,也可将bean定义标记为延迟初始化来防止对单例bean的预实例化。延迟初始化的bean告诉IoC容器在首次请求时而不是在应用启动阶段就创建一个bean实例。

如下案例:

XML形式,通过<bean/>标签内的lazy-init属性控制

<bean id="testBean" class="org.springframework.jmx.export.ExceptionOnInitBean" lazy-init="true">
    <property name="exceptOnInit" value="true"/>
    <property name="name" value="foo"/>
</bean>

注解形式

@Service @Lazy @DependsOn("myNamedComponent")
public abstract class FooServiceImpl implements FooService {

无需多虑,默认值为true就是要延迟初始化。

public @interface Lazy {
    /** Whether lazy initialization should occur. */
    boolean value() default true;
}

当上述的配置被  ApplicationContext 使用时,在 ApplicationContext 启动时不会预实例化惰性bean,未使用该属性的非惰性bean才会被预实例化。

不过需要注意的是,当lazy-init bean是未lazy-init的单例bean的依赖时,ApplicationContext在启动阶段还是会创建lazy-init bean,因为它必须要满足单例的依赖关系,lazy-init bean会被注入到其它未lazy-init 的单例bean中。

另外如果需要,可通过<bean/>标签内的 default-lazy-init 属性控制容器级别的延迟初始化,案例如下:

<beans default-lazy-init="true">
    <bean name="beta" class="org.springframework.beans.factory.FactoryBeanTests$Beta" autowire="byType"...>
    <bean id="alpha" class="org.springframework.beans.factory.FactoryBeanTests$Alpha" autowire="byType"/>
    <bean id="gamma" class="org.springframework.beans.factory.FactoryBeanTests$Gamma"/>
    <bean id="betaFactory" class="org.springframework.beans.factory.FactoryBeanTests$BetaFactoryBean" autowire="constructor"...>
    <bean id="gammaFactory" factory-bean="betaFactory" factory-method="getGamma"/>
    <bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"...>
</beans>
目录
相关文章
|
NoSQL 数据可视化 Redis
Datagrip2020连接redis,可视化插件安装
Datagrip2020连接redis,可视化插件安装
1849 0
|
存储 人工智能 架构师
|
算法 JavaScript 前端开发
84坐标系、02坐标系、百度坐标之间相互转换算法
最近有同学反馈之前的坐标系转换有问题,对之前的工具类进行了修正。 一、地图坐标转换java工具类 包含84坐标系、02坐标系、百度地图、高德地图、腾讯地图坐标之间相互转换的算法 wgs84ToGcj02:将 WGS84 坐标系下的经纬度转换为 GCJ02 坐标系下的经纬度。 gcj02ToWgs84:将 GCJ02 坐标系下的经纬度转换为 WGS84 坐标系下的经纬度。 gcj02ToBd09:将 GCJ02 坐标系下的经纬度转换为 BD09 坐标系下的经纬度。 bd09ToGcj02:将 BD09 坐标系下的经纬度转换为 GCJ02 坐标系下的经纬度。
2461 0
84坐标系、02坐标系、百度坐标之间相互转换算法
|
缓存 NoSQL Redis
Redis缓存雪崩、缓存穿透、缓存击穿解决方案详解
本文详解Redis缓存雪崩、穿透与击穿问题及其解决方案,涵盖差异化过期时间、互斥锁、布隆过滤器等策略,提升系统稳定性与性能。
977 0
Redis缓存雪崩、缓存穿透、缓存击穿解决方案详解
|
人工智能 JSON 数据格式
AI大模型企业应用实战(11)-Document Loader文件加载器机制
本文详解LangChain文档加载器(Loader)机制,涵盖Markdown、CSV、Excel、HTML、JSON、PDF等多格式文件的智能解析与文本提取。依托unstructured等库,实现结构化内容抽取,为RAG系统构建高质量知识库提供核心支持。(239字)
481 0
|
Java BI Sentinel
阿里Sentinel核心源码解析-责任链模式最佳实践
本文深入解析了Sentinel与dashboard的交互机制及秒级QPS统计问题。涵盖sentinel-transport子工程的基础包定义、客户端接入方式(netty-http/simple-http/spring-mvc)以及初始化流程。重点分析HeartbeatSenderInitFunc实现的心跳发送逻辑,确保应用注册与数据同步。同时探讨了BucketLeapArray在秒级统计中的误差问题,并通过实验验证QPS统计的不准确性。
614 91
阿里Sentinel核心源码解析-责任链模式最佳实践
|
SQL 人工智能 SEO
|
数据库 消息中间件
基于RabbitMQ消息队列的分布式事务解决方案 - MQ分布式消息中间件实战
1 极速了解MQ 介绍Rabbitmg用于解决分布式事务必须掌握的5个核心概念 一款分布式消息中间件,基于erlang语言开发, 具备语言级别的高并发处理能力。和Spring框架是同一家公司。支持持久化、高可用 核心5个概念: Queue: 真正存储数据的地方 Exchange: 接收请求,转存数据 Bind: 收到请求后存储到哪里 消息生产者:发送数据的应用 消息消费者: 取出数据处理的应用 2、分布式事务问题 分布式事务是一个业务问题,不能脱离具体的场景。
7357 123
|
Java Spring 容器
深入理解Spring的ImportBeanDefinitionRegistrar接口及其应用
本文深入解析Spring框架中的ImportBeanDefinitionRegistrar接口,探讨其在动态注册Bean定义中的作用、核心方法、应用场景及实例,帮助开发者实现更灵活的Spring配置管理。
985 0
深入理解Spring的ImportBeanDefinitionRegistrar接口及其应用
|
缓存 Linux
Linux系统中的Page cache和Buffer cache
简单说来,page cache用来缓存文件数据,buffer cache用来缓存磁盘数据。在有文件系统的情况下,对文件操作,那么数据会缓存到page cache,如果直接采用dd等工具对磁盘进行读写,那么数据会缓存到buffer cache。 Buffer(Buffer Cache)以块形式缓冲了块设备的操作,定时或手动的同步到硬盘,它是为了缓冲写操作然后一次性将很多改动写入硬盘,避免频繁写硬盘,提高写入效率。 Cache(Page Cache)以页面形式缓存了文件系统的文件,给需要使用的程序读取,它是为了给读操作提供缓冲,避免频繁读硬盘,提高读取效率。
925 0
Linux系统中的Page cache和Buffer cache