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


    我们知道,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 文件,添加依赖:

            ......
            
            <dependency>
                <groupId>junitgroupId>
                <artifactId>junitartifactId>
                <version>4.13.2version>
                <scope>testscope>
            dependency>
    
            
            <dependency>
                <groupId>org.springframeworkgroupId>
                <artifactId>spring-webmvcartifactId>
                <version>5.3.21version>
            dependency>
            ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    创建如下POJO:

    • Axe :Axe接口;
    • StoneAxe :Axe实现类;
    • SteelAxe :Axe实现类;
    • Person :Person持有Axe;
    package pojo;
    
    public interface Axe {
        public void chop();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    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!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    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");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行测试,结果如下:

    Person constructor
    SteelAxe constructor
    StoneAxe constructor
    before getBean
    
    • 1
    • 2
    • 3
    • 4

    可见,这几个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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    I am Tom
    Stone axe!
    
    • 1
    • 2

    构造器注入

    修改后的 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    I am Tom
    Stone axe!
    
    • 1
    • 2

    不可变对象

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

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

        ......
        private final String name;
        private final Axe axe;
        ......
    
    • 1
    • 2
    • 3
    • 4

    setter方法注入

    编译报错如下:

    java: cannot assign a value to final variable name
    
    • 1

    构造器

    运行测试,没有问题,一切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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    运行测试,结果如下:

    Person constructor
    StoneAxe constructor
    SteelAxe constructor
    before getBean
    I am null
    Stone axe!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可见,没有注入 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    运行测试,报错: 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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

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

    构造器注入

    修改后的 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    虽然运行测试能成功,但是我们一眼就能看出,构造器的参数非常多,这就是代码异味,很容易引起警觉。正如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
  • 相关阅读:
    hive 自定义函数、GenericUDF、GenericUDTF(自定义函数计算给定字符串的长度、将一个任意分割符的字符串切割成独立的单词)
    3.2python基础02
    PyCLIPS的安装
    ArgoCD技术总结待续
    Chapter13 : Ultrahigh Throughput Protein-Ligand Docking with Deep Learning
    虚拟机VirtualBox和Vagrant安装
    ARMAAAAA
    流量卡可以自己选地区吗?看完你就明白了!
    java基础讲义03
    【0137】【libpq】向postmaster发送 startup packet 数据包(7)
  • 原文地址:https://blog.csdn.net/duke_ding2/article/details/126589946