Spring系列4:依赖注入的2种方式

本文内容

  1. 基于构造器的依赖注入
  2. 基于setter的依赖注入

基于构造器的依赖注入

案例

定义2个简单的bean类,BeanOne 和 BeanTwo,前者依赖后者。

package com.crab.spring.ioc.demo02;

public class BeanTwo {
}
package com.crab.spring.ioc.demo02;

/**
 * @author zfd
 * @version v1.0
 * @date 2022/1/12 16:59
 */
public class BeanOne {
    private int age;
    private String name;
    private BeanTwo beanTwo;
   
    /**
     * 构造函数,用于依赖注入,定义3个依赖
     * @param age
     * @param name
     * @param beanTwo
     */
    public BeanOne(int age, String name, BeanTwo beanTwo) {
        this.age = age;
        this.name = name;
        this.beanTwo = beanTwo;
    }

    @Override
    public String toString() {
        return "BeanOne{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", beanTwo=" + beanTwo +
                '}';
    }
}

通过xml配置文件实现bean定义和依赖注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="//www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bean2" class="com.crab.spring.ioc.demo02.BeanTwo"/>

    <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
        <constructor-arg name="age" index="0" type="int" value="20"/>
        <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
        <constructor-arg name="beanTwo" index="2" type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
    </bean>
</beans>

来个测试类验证下注入

package com.crab.spring.ioc.demo02;
/**
 * @author zfd
 * @version v1.0
 * @date 2022/1/12 17:09
 */
public class demo02Test {

    @Test
    public void test_construct() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo02/spring1.xml");
        BeanOne bean1 = context.getBean("bean1", BeanOne.class);
        System.out.println(bean1);
        context.close();
    }

输出如下

BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@5204062d}

对照配置文件BeanOne的3个依赖都通过构造器的方式进行注入了,符合预期,很简单。

constructor-arg详解

标签constructor-arg支持的元素列表如下。

元素名 作用
name 参数名
index 参数索引,0开始
type 参数类型
value
ref bean引用

例如案例中的配置

<bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
    <constructor-arg name="age" index="0" type="int" value="20"/>
    <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
    <constructor-arg name="beanTwo" index="2" 		            		type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
</bean>

注意: 在没有引起歧义的情况下,上面的部分元素并不是都必须配置的。如指定了index时可以定位参数位置,那么name是可以不配置的,又如通过ref引用依赖bean,type可以省略

<bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
    <constructor-arg index="0" type="int" value="20"/>
    <constructor-arg index="1" type="java.lang.String" value="xxx"/>
    <constructor-arg  index="2" ref="bean2"/>
</bean>

注意事项

当在<constructor />使用 name元素指定构造函数中的参数名时,尤其要方法参数名编译后是否保留的情况。举个例子

// 编译前的方法参数
public BeanOne(int age, String name, BeanTwo beanTwo)
// 编译后的方法参数
public BeanOne(int var1, String var2, BeanTwo var3)

编译后方法参数被编译成var1这种形式会导致仅指定name的构造注入失效。

如何解决?提供2种方法

  1. Maven编译插件添加编译参数,保留参数名,pom文件下增加如下插件配置

        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <encoding>UTF8</encoding>
                        <compilerArgs>
                            <arg>-parameters</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
    
            </plugins>
        </build>
    
  2. 使用 @ConstructorProperties JDK 注释显式命名构造函数参数

        /**
         * 构造函数,用于依赖注入,定义3个依赖
         * @param age
         * @param name
         * @param beanTwo
         */
        @ConstructorProperties({"age", "name", "beanTwo"}) // 显式声明构造参数名称
        public BeanOne(int age, String name, BeanTwo beanTwo) {
            this.age = age;
            this.name = name;
            this.beanTwo = beanTwo;
        }
    

基于setter的依赖注入

基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。

来一个简单类BeanThree依赖BeanOneBeanTwo并提供了Setter方法来设置。

package com.crab.spring.ioc.demo02;

/**
 * @author zfd
 * @version v1.0
 * @date 2022/1/13 8:18
 */
public class BeanThree {
    private BeanTwo beanTwo;
    private BeanOne beanOne;

    public void setBeanTwo(BeanTwo beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setBeanOne(BeanOne beanOne) {
        this.beanOne = beanOne;
    }
}

对应的配置文件可以通过标签property中的refname来设置属性引用或是属性值

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="//www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bean2" class="com.crab.spring.ioc.demo02.BeanTwo"/>

    <!--构造函数注入-->
    <bean id="bean1" class="com.crab.spring.ioc.demo02.BeanOne">
        <constructor-arg name="age" index="0" type="int" value="20"/>
        <constructor-arg name="name" index="1" type="java.lang.String" value="xxx"/>
        <constructor-arg name="beanTwo" index="2" type="com.crab.spring.ioc.demo02.BeanTwo" ref="bean2"/>
    </bean>

    <!--setter注入-->
    <bean id="bean3" class="com.crab.spring.ioc.demo02.BeanThree">
        <!-- 1 ref元素-->
        <property name="beanOne" ref="bean1"></property>
        <!-- 2 ref标签-->
        <property name="beanTwo">
            <ref bean="bean2"></ref>
        </property>
         <property name="name" value="xxxx"/>
    </bean>
</beans>

运行测试类和结果,可见依赖注入成功

public class demo02Test {

    @Test
    public void test_construct() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo02/spring1.xml");
        BeanOne bean1 = context.getBean("bean1", BeanOne.class);
        System.out.println(bean1);

        System.out.println("演示Setter注入");
        BeanThree beanThree = context.getBean(BeanThree.class);
        System.out.println(beanThree);
        context.close();
    }
}
BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24}
演示Setter注入
BeanThree{name='xxxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24, beanOne=BeanOne{age=20, name='xxx', beanTwo=com.crab.spring.ioc.demo02.BeanTwo@68ceda24}}

依赖解决过程

  • ApplicationContext是用描述所有bean的配置元数据创建和初始化的。配置元数据可以通过XML、Java代码或注释指定。
  • 对于每个bean,它的依赖项都以属性、构造函数参数或静态工厂方法的参数的形式表示(如果使用静态工厂方法而不是普通构造函数的话)。当bean实际创建时,这些依赖项被提供给bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
  • 每个具有值的属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将字符串格式提供的值转换为所有内置类型,如int、long、string、boolean等。

什么是循环依赖,如何处理?

如果主要使用构造函数注入,则可能会创建一个不可解析的循环依赖场景。

例如:类A需要类B的一个实例通过构造函数注入,类B需要类A的一个实例通过构造函数注入。如果将类A和类B的bean配置为相互注入,Spring IoC容器会在运行时检测此循环引用,并抛出BeanCurrentlyInCreationException

一个可能的解决方案是编辑一些由setter而不是构造函数配置的类的源代码。或者,避免构造函数注入,只使用setter注入。换句话说,可以使用setter注入配置循环依赖项。

与典型情况(没有循环依赖关系)不同,bean A 和 bean B 之间的循环依赖关系强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。

萝卜青菜各有所爱

选择用构造器注入还是Setter注入?

Spring官方的推荐,构造函数用于强制依赖项,将 setter 方法或配置方法用于可选依赖项。请注意,在 setter 方法上使用 @Required 注释可用于使属性成为必需的依赖项;然而,带有参数的编程验证的构造函数注入是更可取的。

Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。作为旁注,大量的构造函数参数是一种不好的代码气味,这意味着该类可能有太多的职责,应该重构以更好地解决适当的关注点分离。 Setter 注入应该主要只用于可以在类中分配合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何地方执行非空检查。 setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。

有时,在处理没有源代码的第三方类时,如果第三方类没有公开任何 setter 方法,那么构造函数注入可能是 DI 的唯一可用形式。

总结

本文演示2种依赖注入的方式:构造函数注入和Setter方法注入,并对比如何选择这2种方式。下一篇继续深入依赖注入。

本篇源码地址: //github.com/kongxubihai/pdf-spring-series/tree/main/spring-series-ioc/src/main/java/com/crab/spring/ioc/demo02

知识分享,转载请注明出处。学无先后,达者为先!

Tags: