本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
- 🚀 魔都架构师 | 全网30W技术追随者
- 🔧 大厂分布式系统/数据中台实战专家
- 🏆 主导交易系统百万级流量调优 & 车联网平台架构
- 🧠 AIGC应用开发先行者 | 区块链落地实践者
- 🌍 以技术驱动创新,我们的征途是改变世界!
- 👉 实战干货:编程严选网
0 前言
实际的系统几乎不可能仅有单一的bean,都是很多个bean协作提供服务。本文目标也就是讨论如何冲破单一 bean 定义而让多 bean 协作实现系统。
1 什么是依赖注入(Dependency Injection)?
之前文章说过, DI其实是一个过程。该过程中,bean可通过如下方式定义它们之间的依赖关系:
- 构造器参数
- 工厂方法参数
- 从工厂方法构造或返回的对象实例上设置的属性
接着,容器在创建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>