01 引言
在开发过程中,肯定会使用依赖注入。大多数情况下会通过@Autowired
注解作用在字段上,从而将Bean
注入进来。但是Spring
官网并没有推荐使用这种方式。包括我们常用的开发工具Idea,也会有警告提示:
我们一起来探究一下原因。
02 Spring推荐的依赖注入
官方文档摘自Spring Framework 6.2.7
。
Dependency injection (DI)
就是我们常说的依赖注入。官方推荐了两种主要的表现形式:基于构造的依赖注入和基于Setter
方法的依赖注入。
2.1 基于构造的依赖注入
基于构造函数的依赖注入是通过容器调用一个带有若干参数的构造函数来实现的,每个参数代表一个依赖项。该注入方式不依赖任何容器或者注解。
@RestController
public class FooController {
private final UserService userService;
public FooController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/foo2")
public String foo2() {
userService.test();
return "success";
}
}
如代码所示,UserService
是通过当前类FooController
的构造函数,将UserService
对象注入到当前类中。注入之后,方法foo2()
就可以使用UserService
直接调用方法。
构造函数注入时,被注入的对象必须存在,否则项目启动就会报错。
2.2 基于Setter
方法的依赖注入
基于Setter
的依赖注入是通过容器在调用无参构造函数或无参static
工厂方法实例化 bean
之后,调用 bean
上的 Setter
方法来实现的。
值得注意的是,Setter
方法的注入,必须在实例化调用Setter
方法才能生效。否者注入的Bean
对象就是空。但是项目启动并不会检查是否为空。
容器中正常使用需要借助@Autowired
注解,就会实现自动注入。
@RestController
public class FooController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@RequestMapping("/foo2")
public String foo2() {
userService.test();
return "success";
}
}
被注解修饰后的注入对象必须存在,否则也会报错。
2.3 注入对象为空的处理
上述两种注入方式,如果被注入的Bean
的确实为空,该怎么处理,官方也给出了标准的解决方案。
通过@Nullable
注解可以解决Bean
不存在的问题,。改造代码如下:
private UserService userService;
@Autowired
public void setUserService(@Nullable UserService userService) {
this.userService = userService;
}
public FooController(@Nullable UserService userService) {
this.userService = userService;
}
2.4 循环依赖的处理
循环依赖是Spring
中经典的问题,我们先看看什么是循环依赖。有两个对象A、B,相互注入,实例化A时,需要B对象,实例化B时,需要A对象,导致无法继续实例化的现象。
案例演示:以UserService
和WorkService
为例。
UserService
注入WorkService
:
WorkService
注入UserService
:
启动报错日志:
官方给出的解决方案:
大致意思就是:
Spring
兼容了大部分场景,Spring
会尽可能延迟设置属性和解决依赖关系,直到 bean 实际被创建时才进行。如果有问题,可以覆盖这种默认行为,使单例 bean
延迟初始化。
而@Lazy
就是专门来延迟Bean
的初始化的。
解决方案也就很简单:直接在任意一个Bean
的构造函数上加@Lazy
注解。
上面演示的是构造函数注入,Setter
方法存在同样的问题,解决方案也相同。
03 字段注入
基于字段的注入。通常使用的注解有@Autowired
、@Resource
、@Value
,并将其加在字段上的完成属性的注入。
@Value
是基于外部属性的注入,本次不做讨论。
字段注入的表现形式:
3.1 @Autowired
@Autowired
注解提供了与自动装配协作对象中所述相同的功能,但具有更细粒度的控制和更广泛的适用性。可以作用在构造器、方法、参数、字段以及注解上。
3.2 @Resource
Spring 也支持通过在字段或 bean 属性的设置方法上使用 JSR - 250 的 @Resource
注解(jakarta.annotation.Resource
)来进行依赖注入。@Resource
注解带有一个 name 属性。默认情况下,Spring 将该属性值解释为要注入的 bean 的名称。只能作用在被修饰的注解作用在类、接口和枚举上,方法以及字段上。
3.3 区别
相同点:
在Spring
容器中,两个注解的功能最终的结果是相同的。
@Autowired
private UserService userService;
@Resource
private WorkService workService;
不同点:
-
作用域不同:
@Autowired
可以作用在构造器、方法、参数、字段以及注解上。@Resource
只能作用在方法以及字段上。 -
注入匹配(
byName
,byType
)的顺序不同:@Autowired
在注入的时候,先通过byType
匹配,无法在Spring
容器中找到时,再通过byName
的方式匹配。可以和@Qualifier
配合使用。@Resource
恰好和@Autowired
相反,先byName
,后byType
。 -
支持的来源不同:
@Autowired
是Spring 2.5
以后提供的注解,只用用于Spring
容器。如果不再使用Spring
容器,则该注解失效@Resource
是JDK
官方提供的直接(JSR-250
),不依赖于任何容器。 -
对
Bean
的默认要求不同:@Autowired
默认要求Bean
必须存在,否者会报错。可以通过属性required=false
处理。@Resource
默认支持null
,如果Bean
不存在,就会是null
值。
04 Spring
不推荐字段注入的原因
按照使用结果来讲,没有任何问题。大部分人也确实是这么用的。
4.1单一职责问题
根据设计原则SOLID来讲,一个类的设计应该满足单一职责,就是一个类只做一件事。当我们注入的Bean
越来越多,每一个Bean
的功能都不相同,就会赋予当前类很多功能,从而违背单一职责的原则。
基于构造和Setter
就会解决这个问题么?当然也是不能的,只不过从代码表现形式来看就会显得的很臃肿,这是一种bad smell
,间接的提醒我们要去优化,我们来看下案例:
Setter
方法也是同样的感觉。
4.2 封装性的破坏
Java
的三大特性:封装、继承和多态。其中的封装就是对一个事物、公用组件或流程的进行封装,方面其他人员使用。封装类的私有属性(private
)是无法被直接访问的,只能通过Getter
方法提供public
方法才能使用。
然而字段的注入,就是在私有属性上通过注解的方式,将Bean
对象注入进来。Spring
容器就间接的访问了封装类的私有属性了,这就破坏了封装性。
Spring
作为一个伟大的框架,规范这我们对Java
的使用。
05 小结
规范是规范,使用是使用。就像restfull
一样,是一种架构风格。使用者可以选择遵守和不遵守,都不会影响使用。目前用的最多的可能还是基于字段的注入@Autowired
,用起来更加简洁。
lombok
框架的出现,@RequiredArgsConstructor
代替了我们写构造方法。使得构造的注入和字段注入一样简洁,那种bad smell
的味道直接隐藏起来了。这种情况下字段的注入和构造的注入就变的没有了区别。
你在开发中用的是哪种注入方式呢,评论区讨论!