Spring的setter方法注入和构造器注入的对比

12 篇文章 0 订阅

我们知道,Spring的依赖注入,有setter方法注入,实例变量注入,构造器注入等。

Spring官方文档里,提到:

依赖注入存在两种主要形式:

  • 构造器注入
  • setter方法注入

注:其实对于Spring注解,目前最常见的注入方式是实例变量注入,无需setter方法,直接在实例变量上添加 @Autowired 注解即可,详见我另一篇文档。

但是实例变量注入是Spring不推荐的方法,在IntelliJ IDEA里,如果使用该方法,会得到一个警告,如下图所示:

在这里插入图片描述
事实上,Spring推荐使用的是构造器注入。官方文档的原文如下:

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

翻译过来大致就是:Spring团队一般提倡使用构造器注入,因为它使得你实现应用组件为不可变对象,并确保所需的依赖非空。更进一步,使用构造器注入的组件总是会返回为一个完全初始化的状态。一个副作用是,一个带有很多参数的构造器,会带来代码异味,因为这暗示了该类很有可能有太多职责,需要重构并拆分,来更好的解决问题。

下面我们通过实际代码来理解一下这段话(当然只是我个人的理解)。

环境

  • Ubuntu 22.04
  • IntelliJ IDEA 2022.1.3
  • JDK 17.0.3
  • Spring 5.3.21

准备

创建Maven项目 test0829

修改 pom.xml 文件,添加依赖:

        ......
        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.21</version>
        </dependency>
        ......

src/main/resources 目录下创建 applicationContext.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
       
    <context:component-scan base-package="pojo"/>
    
</beans>

创建如下POJO:

  • Axe :Axe接口;
  • StoneAxe :Axe实现类;
  • SteelAxe :Axe实现类;
  • Person :Person持有Axe;
package pojo;

public interface Axe {
    public void chop();
}
package pojo;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class StoneAxe implements Axe{
    public StoneAxe() {
        System.out.println("StoneAxe constructor");
    }

    @Override
    public void chop() {
        System.out.println("Stone axe!");
    }
}
package pojo;

import org.springframework.stereotype.Component;

@Component
public class SteelAxe implements Axe{
    public SteelAxe() {
        System.out.println("SteelAxe constructor");
    }

    @Override
    public void chop() {
        System.out.println("Steel axe!");
    }
}
package pojo;

import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;

    public void setName(String name) {
        this.name = name;
    }

    public void setAxe(Axe axe) {
        this.axe = axe;
    }

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    public Person() {
        System.out.println("Person constructor");
    }

在 src/test/java 目录下创建测试:

import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.Person;

public class MyTest {
    @Test
    public void test1() {
        var ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        System.out.println("before getBean");

        var person = ctx.getBean("person", Person.class);

//        person.useAxe();
    }
}

运行测试,结果如下:

Person constructor
SteelAxe constructor
StoneAxe constructor
before getBean

可见,这几个POJO已经被Spring容器管理了。

当然,现在如果调用 person.useAxe() 方法会报NPE错,因为我们还没有把Axe注入到Person里。

Setter方法注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;

    @Value("Tom")
    public void setName(String name) {
        this.name = name;
    }

    @Autowired
    public void setAxe(Axe axe) {
        this.axe = axe;
    }

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    public Person() {
        System.out.println("Person constructor");
    }
}

现在,就可以调用 person.useAxe() 方法了:

I am Tom
Stone axe!

构造器注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    @Autowired
    public Person(@Value("Tom") String name, Axe axe) {
        this.name = name;
        this.axe = axe;
    }
}

现在,就可以调用 person.useAxe() 方法了:

I am Tom
Stone axe!

不可变对象

如果使用构造器注入,可以把被注入的对象声明为final,保证其不可变。而如果使用setter方法注入,显然不能把被注入的对象声明为final。

Person 类的 nameaxe 成员变量加上 final 修饰符:

    ......
    private final String name;
    private final Axe axe;
    ......

setter方法注入

编译报错如下:

java: cannot assign a value to final variable name

构造器

运行测试,没有问题,一切OK。

确保所需的依赖非空

name 为例。

setter方法注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;

//    @Value("Tom")
    public void setName(String name) {
        this.name = name;
    }

    @Autowired
    public void setAxe(Axe axe) {
        this.axe = axe;
    }

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    public Person() {
        System.out.println("Person constructor");
    }
}

运行测试,结果如下:

Person constructor
StoneAxe constructor
SteelAxe constructor
before getBean
I am null
Stone axe!

可见,没有注入 name ,也不会报错, name 为null值。

构造器注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    @Autowired
    public Person(String name, Axe axe) {
        this.name = name;
        this.axe = axe;
    }
}

运行测试,报错: NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate.

可见,如果构造器里的 name 参数没有找到合适的注入对象,会报错。

代码异味

假设 Person 类需要注入其它很多component,如 component1component2 ……,等等。为了简单起见,我们用 String 来模拟。

setter方法注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;
    private String str1;
    private String str2;
    private String str3;
    private String str4;
    private String str5;

    @Value("Tom")
    public void setName(String name) {
        this.name = name;
    }

    @Autowired
    public void setAxe(Axe axe) {
        this.axe = axe;
    }

    @Value("str1")
    public void setStr1(String str1) {
        this.str1 = str1;
    }

    @Value("str2")
    public void setStr2(String str2) {
        this.str2 = str2;
    }

    @Value("str3")
    public void setStr3(String str3) {
        this.str3 = str3;
    }

    @Value("str4")
    public void setStr4(String str4) {
        this.str4 = str4;
    }

    @Value("str5")
    public void setStr5(String str5) {
        this.str5 = str5;
    }

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    public Person() {
        System.out.println("Person constructor");
    }
}

运行测试能成功,从代码表面上,也看不出什么问题,

构造器注入

修改后的 Person 类如下:

package pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Person {
    private String name;
    private Axe axe;
    private String str1;
    private String str2;
    private String str3;
    private String str4;
    private String str5;

    @Value("Tom")
    public void setName(String name) {
        this.name = name;
    }

    @Autowired
    public void setAxe(Axe axe) {
        this.axe = axe;
    }

    public void useAxe() {
        System.out.println("I am " + name);
        axe.chop();
    }

    @Autowired
    public Person(@Value("Tom") String name, Axe axe, @Value("str1") String str1, @Value("str2")String str2,
                  @Value("str3")String str3, @Value("str4")String str4, @Value("str5")String str5) {
        this.name = name;
        this.axe = axe;
        this.str1 = str1;
        this.str2 = str2;
        this.str3 = str3;
        this.str4 = str4;
        this.str5 = str5;
    }
}

虽然运行测试能成功,但是我们一眼就能看出,构造器的参数非常多,这就是代码异味,很容易引起警觉。正如Spring团队所说,该类需要被注入这么多component,这就意味着在设计上很可能出了问题,违反了单一职责原则。这就是构造器注入带来的“副作用”好处。

总结

setter方法注入构造器注入
不可变对象无法设置为不可变可以按需设置为不可变
确保所需的依赖非空无法确保可以确保
代码异味(类被注入了太多对象)不容易识别很容易识别

注意:对于“所需的依赖为空”的情况,也分具体情况,比如通过 @Autowired 给setter方法注入对象,如果找不到满足条件的类(byType),默认情况下会报错,详见我另一篇文档。

所以,通过对比,我们得出结论:构造器注入就是好!

注:Spring文档里还提到了应该在什么情况下使用setter方法注入:

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

大致简而言之:强制的(mandatory)、非空的注入,应该使用构造器注入,而可选的、可动态配置的注入应该使用setter方法注入。

参考

  • https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-setter-injection
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值