我们知道,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
类的 name
和 axe
成员变量加上 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,如 component1
、 component2
……,等等。为了简单起见,我们用 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