Spring中@Autowire在属性和构造函数上的区别

323

自从我使用Spring以来,如果我要编写一个具有依赖性的服务,我会按照以下步骤进行:

@Component
public class SomeService {
     @Autowired private SomeOtherService someOtherService;
}

我现在遇到了使用另一种约定来实现相同目标的代码

@Component
public class SomeService {
    private final SomeOtherService someOtherService;

    @Autowired
    public SomeService(SomeOtherService someOtherService){
        this.someOtherService = someOtherService;
    }
}
这两种方法都可以工作,我明白。但是使用B选项有什么优势吗?对我来说,在类和单元测试中它会创建更多的代码。(需要编写构造函数且无法使用@InjectMocks)
我是否遗漏了什么?自动装配构造函数除了在单元测试中添加代码之外,还有其他任何作用吗?这是进行依赖注入的更可取方式吗?

相关链接:https://softwareengineering.stackexchange.com/q/300706/172464 - Dherik
10个回答

425

是的,选项B(也称构造函数注入)实际上比字段注入更受推荐,并且具有以下几点优势:

  • 依赖关系清晰明确。在测试或者在任何其他情况下实例化对象时(例如在配置类中显式地创建Bean实例),不可能忘记其中一个依赖关系。
  • 依赖关系可以为final,这有助于提高鲁棒性和线程安全性。
  • 您不需要使用反射设置依赖关系。InjectMocks仍然可用,但并非必需。您可以自己创建模拟对象,并通过调用构造函数将它们注入。

请参阅此博客文章以获取更详细的信息,作者是Spring贡献者之一的Olivier Gierke


11
让我们深入探讨一下,假设您有其他属性,比如:@Value("some.prop") private String property;您是否也会将其放入构造函数中?这似乎会导致构造函数变得非常长和复杂。虽然这并不是坏事,但需要编写更多的代码。 我绝对欣赏您的评论! - GSUgambit
26
是的,你也需要将它放在构造函数中。正如链接文章所述,当构造函数开始有太多参数时,往往是应该将类分成更小的,责任更少、依赖性更少的类的迹象。 - JB Nizet
1
不知道怎么回事,除了博客文章行我都看到了。谢谢! - GSUgambit
9
你可以拥有任意数量的构造函数,但只能有一个构造函数被注解为Autowired,Spring 只会调用这个构造函数。 - JB Nizet
3
@NisargPatil 在字段上使用@Autowired注解会直接设置该字段,使用反射机制,而不通过任何setter方法。在setter方法上使用@Autowired注解则会调用该setter方法。 - JB Nizet
显示剩余4条评论

67

我会用简单易懂的话来解释:

在选项(A)中,你允许任何人(在Spring容器内外不同类中)使用默认构造函数创建一个实例(如new SomeService()),这是不好的,因为你需要SomeOtherService对象作为依赖项来使用你的SomeService

自动装配的构造函数除了添加代码到单元测试中之外,还有其他作用吗?它是进行依赖注入的更优选方式吗?

选项(B)是首选方法,因为它不允许在没有实际解决SomeOtherService依赖关系的情况下创建SomeService对象。


2
有了选项B,这不是一个“泄漏的抽象”吗?如果我事先不知道某个服务的实际实现方式,而Impl1具有依赖关系A/B/C和Impl2具有A/B/F/G,则构造函数方法将要求我事先知道这些依赖关系。但是,使用@autowire时,当我尝试使用服务时,如果容器中已经注册了所有必要的依赖项,那么我就可以了解到服务的依赖项,而我不需要知道任何与服务相关的依赖项信息。 - Chris Knoll
4
@ChrisKnoll的观点是要知道依赖项对于您的服务的工作是必需的(尤其是用于测试!)。知道您的汽车需要钥匙才能启动不是一个'漏洞的抽象层'。您不需要知道钥匙的具体实现,但您确实需要知道您需要一把钥匙。此外,只要您将所涉及的服务(SomeService)注释为say(Service),则无需知道它的依赖项。在使用该服务的类中,对其进行相同的自动装配注释,然后您可以调用SomeService.perform()而不需要使用'new'关键字。 - javaBean007

52

请注意,自从Spring 4.3版本以来,您甚至不需要在构造函数上使用@Autowired注解,因此您可以按照Java风格编写代码,而不是依赖于Spring的注释。

@Component
public class SomeService {
    private final SomeOtherService someOtherService;

    public SomeService(SomeOtherService someOtherService){
        this.someOtherService = someOtherService;
    }
}

2
很有趣,我真的很好奇Spring在这种情况下是如何注入依赖关系的? - Mr Matrix
7
Spring 会扫描你的类,查找与该类字段匹配的构造函数。在这里可以找到更多详细信息: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-constructor-injection - stinger
这似乎意味着构造函数注入成为默认的注入机制。但仍然需要显式传递所有依赖项的构造函数。 - Andrey M. Stepanov
3
你可以使用 Lombok 的注解 @RequiredArgsConstructor 来代替显式构造函数。 - Marcus Voltolim
3
@stinger并非完全相关,但我认为许多项目都在同时使用lombok和Spring。在我们公司,有多个项目同时使用它们,这使开发变得轻松。你只需要添加@RequiredArgsConstructor注解,并将任何注入的服务或组件标记为final,而不必编写构造函数(并可能更新它)。Lombok会创建构造函数并由Spring进行注入。 - Stephan Stahlmann
显示剩余3条评论

17

值得注意的是

如果只有一个构造函数调用,就不需要包含@Autowired注释。然后您可以像这样使用:

@RestController
public class NiceController {

    private final DataRepository repository;

    public NiceController(ChapterRepository repository) {
        this.repository = repository;
    }
}

这是Spring Data Repository注入的示例。


8
了解这一点很好,但删除注释并不能显著提高可读性。 - Nitin Gaur
当已经有另一个注释将类标记为DI系统的一部分时,我更喜欢这个而不是一百个冗余的Autowired。 - Novaterata

13

实际上,根据我的经验,第二个选项更好。无需使用 @Autowired。事实上,更明智的做法是创建与框架不太耦合的代码(尽管Spring很好)。您希望代码尽可能采用延迟决策方法。这就是尽可能多地使用POJO的原因,这样可以轻松替换框架。

所以我建议您创建一个单独的配置文件,并在其中定义您的bean,像这样:

SomeService.java 文件中:

public class SomeService {
    private final SomeOtherService someOtherService;

    public SomeService(SomeOtherService someOtherService){
        this.someOtherService = someOtherService;
    }
}

ServiceConfig.java 文件中:

@Config
public class ServiceConfig {
    @Bean
    public SomeService someService(SomeOtherService someOtherService){
        return new SomeService(someOtherService);
    }
}

实际上,如果你想对此进行深入的技术研究,使用字段注入@Autowired)可能会引起线程安全等问题,当然还取决于项目的大小。点击这里了解更多有关自动装配的优点和缺点。实际上,关键人物建议您使用构造函数注入而不是字段注入


10

我希望表达自己的观点不会导致被降级,但对于我来说,选项A更能反映Spring依赖注入的强大之处,而在选项B中,你将类与其依赖耦合在一起,事实上,你无法在不通过构造函数传递其依赖项的情况下实例化对象。 依赖注入是为了避免这种情况而发明的,通过实现控制反转,因此对我来说,选项B毫无意义。


5

Autowired构造函数提供了一个钩子,在将其注册到Spring容器之前添加自定义代码。假设SomeService类扩展了另一个名为SuperSomeService的类,并且它有一个以名称作为其参数的构造函数。在这种情况下,Autowired构造函数可以正常工作。此外,如果您还有其他要初始化的成员,则可以在构造函数中执行,然后将实例返回给Spring容器。

public class SuperSomeService {
     private String name;
     public SuperSomeService(String name) {
         this.name = name;
     }
}

@Component
public class SomeService extends SuperSomeService {
    private final SomeOtherService someOtherService;
    private Map<String, String> props = null;

    @Autowired
    public SomeService(SomeOtherService someOtherService){
        SuperSomeService("SomeService")
        this.someOtherService = someOtherService;
        props = loadMap();
    }
}

3

我更喜欢构造函数注入,只是因为 我可以将我的依赖标记为final,而使用属性注入则不可能。

您的依赖项应该是final的,即不能被程序修改。


1
为什么不能将Bean属性标记为final? - Andrey M. Stepanov

0

有几种情况下,@Autowired是首选的。 其中之一是循环依赖。想象以下场景:

@Service
public class EmployeeService {
    private final DepartmentService departmentService;

    public EmployeeService(DepartmentService departmentService) {
        this.departmentService = departmentService;
    }
}

并且

@Service
public class DepartmentService {
    private final EmployeeService employeeService;

    public DepartmentService(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }
}

然后Spring Bean Factory将抛出循环依赖异常。如果您在两个bean中都使用@Autowired注释,则不会发生这种情况。这是可以理解的:构造函数注入发生在Spring Bean初始化的非常早期阶段,在Bean Factory的createBeanInstance方法中,而基于@Autowired的注入发生得更晚,在后处理阶段完成,并由AutowiredAnnotationBeanPostProcessor执行。

循环依赖在复杂的Spring Context应用程序中非常常见,它不仅需要两个bean相互引用,还可能是几个bean的复杂链。

另一个使用@Autowired非常有帮助的用例是自我注入。

@Service
public class EmployeeService {
    
    @Autowired
    private EmployeeService self;

}

这可能需要从同一个bean中调用一个advised方法。自我注入也在这里这里讨论。


0

有一种方法可以使用Lombok的@RequeiredArgsContructor注解通过构造函数来注入依赖项

@RequiredArgsConstructor
@Service
 class A {
     private final B b // needs to be declared final to be injected
}

这样你就不需要指定构造函数了


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接