最全的Spring依赖注入方式
# 控制反转 IOC
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。
为什么叫控制反转?
- 控制:指的是对象创建(实例化、管理)的权力
- 反转:控制权交给外部环境(Spring 框架、IoC 容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样做可以解耦。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
# 三种常规注入方式
接下来就让我们分别介绍一下三种常规的注入方式。
# 属性注入
通过属性注入的方式非常常用,这个应该是大家比较熟悉的一种方式:
@Service
public class UserService {
@Autowired
private My1Bean my1Bean;//通过属性注入
}
2
3
4
5
# 构造器注入
当两个类属于强关联时,我们也可以通过构造器的方式来实现注入:
@Service
public class UserService {
private My2Bean my2Bean;
@Autowired //通过构造器注入
public UserService(My2Bean my2Bean) {
this.my2Bean = my2Bean;
}
}
2
3
4
5
6
7
8
9
# setter 方法注入
除了通过属性注入,通过 setter
方法也可以实现注入:
@Service
public class UserService {
private My3Bean my3Bean;
@Autowired //通过setter方法实现注入
public void setmy3Bean(my3Bean my3Bean) {
this.my3Bean = my3Bean;
}
}
2
3
4
5
6
7
8
9
# 接口注入
在上面的三种常规注入方式中,假如我们想要注入一个接口,而当前接口又有多个实现类,那么这时候就会报错,因为 Spring
无法知道到底应该注入哪一个实现类。比如我们上面的三个类全部实现同一个接口 IMy
,那么这时候直接使用常规的,不带任何注解元数据的注入方式来注入接口 IMy
。
@Autowired
private IMy imy;
2
此时启动服务就会报错,这个就是说本来应该注入一个类,但是 Spring
找到了三个,所以没法确认到底应该用哪一个。
解决思路主要有以下方式:
# @Qualifier 注解
通过使用 @Qualifier
注解,我们可以消除需要注入哪个 bean 的问题。
@Component
public class FooService {
@Autowired
@Qualifier("my3Bean")
private Imy my;
}
2
3
4
5
6
7
# 通过配置文件和 @ConditionalOnProperty 注解实现
通过 @ConditionalOnProperty
注解可以结合配置文件来实现唯一注入。下面示例就是说如果配置文件中配置了 lonely.my=test1
,那么就会将 My1Bean
初始化到容器,此时因为其他实现类不满足条件,所以不会被初始化到 IOC
容器,所以就可以正常注入接口:
@Component
@ConditionalOnProperty(name = "lonely.my",havingValue = "test1")
public class my1Bean implements Imy{
}
2
3
4
当然,这种配置方式,编译器可能还是会提示有多个 Bean
,但是只要我们确保每个实现类的条件不一致,就可以正常使用。
# 通过其他 @Condition 条件注解
除了上面的配置文件条件,还可以通过其他类似的条件注解,如:
- @ConditionalOnBean:当存在某一个
Bean
时,初始化此类到容器。 - @ConditionalOnClass:当存在某一个类时,初始化此类的容器。
- @ConditionalOnMissingBean:当不存在某一个
Bean
时,初始化此类到容器。 - @ConditionalOnMissingClass:当不存在某一个类时,初始化此类到容器。
- ...
类似这种实现方式也可以非常灵活的实现动态化配置。
不过上面介绍的这些方法似乎每次都只能固定注入一个实现类,那么如果我们就是想多个类同时注入,不同的场景可以动态切换而又不需要重启或者修改配置文件,又该如何实现呢?
# 通过 @Resource 注解动态获取
如果不想手动获取,我们也可以通过 @Resource
注解的形式动态指定 BeanName
来获取:
@Component
public class InterfaceInject {
@Resource(name = "my1Bean")
private Imy imy;
}
2
3
4
5
如上所示则只会注入 BeanName
为 my1Bean
的实现类。
# 通过集合注入
除了指定 Bean
的方式注入,我们也可以通过集合的方式一次性注入接口的所有实现类:
@Component
public class InterfaceInject {
@Autowired
List<IMy> list;
@Autowired
private Map<String,IMy> map;
}
2
3
4
5
6
7
8
上面的两种形式都会将 IMy
中所有的实现类注入集合中。如果使用的是 List
集合,那么我们可以取出来再通过 instanceof
关键字来判定类型;而通过 Map
集合注入的话,Spring
会将 Bean
的名称(默认类名首字母小写)作为 key
来存储,这样我们就可以在需要的时候动态获取自己想要的实现类。
# @Primary 注解实现默认注入
除了上面的几种方式,我们还可以在其中某一个实现类上加上 @Primary
注解来表示当有多个 Bean
满足条件时,优先注入当前带有 @Primary
注解的 Bean
:
@Component
@Primary
public classMy1Bean implements IMy{
}
2
3
4
通过这种方式,Spring
就会默认注入 my1Bean
,而同时我们仍然可以通过上下文手动获取其他实现类,因为其他实现类也存在容器中。
# 手动获取 Bean 的几种方式
在 Spring
项目中,手动获取 Bean
需要通过 ApplicationContext
对象,这时候可以通过以下 5
种方式进行获取:
# 直接注入
通过直接注入的方式获取 ApplicationContext
对象,然后就可以通过 ApplicationContext
对象获取 Bean
:
@Component
public class InterfaceInject {
@Autowired
private ApplicationContext applicationContext;//注入
public Object getBean(){
return applicationContext.getBean("my1Bean");//获取bean
}
}
2
3
4
5
6
7
8
9
# 通过 ApplicationContextAware 接口获取
通过实现 ApplicationContextAware
接口来获取 ApplicationContext
对象,从而获取 Bean
。需要注意的是,实现 ApplicationContextAware
接口的类也需要加上注解,以便交给 Spring
统一管理(这种方式也是项目中使用比较多的一种方式):
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 通过名称获取bean
*/
public static <T>T getBeanByName(String beanName){
return (T) applicationContext.getBean(beanName);
}
/**
* 通过类型获取bean
*/
public static <T>T getBeanByType(Class<T> clazz){
return (T) applicationContext.getBean(clazz);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
封装之后,我们就可以直接调用对应的方法获取 Bean
了:
my2Bean my2Bean = SpringContextUtil.getBeanByName("my2Bean");
my3Bean my3Bean = SpringContextUtil.getBeanByType(my3Bean.class);
2
# 通过 ApplicationObjectSupport 和 WebApplicationObjectSupport 获取
这两个对象中,WebApplicationObjectSupport
继承了 ApplicationObjectSupport
,所以并无实质的区别。
同样的,下面这个工具类也需要增加注解,以便交由 Spring
进行统一管理:
@Component
public class SpringUtil extends /*WebApplicationObjectSupport*/ ApplicationObjectSupport {
private static ApplicationContext applicationContext = null;
public static <T>T getBean(String beanName){
return (T) applicationContext.getBean(beanName);
}
@PostConstruct
public void init(){
applicationContext = super.getApplicationContext();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
有了工具类,在方法中就可以直接调用了:
@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
@GetMapping("/bean3")
public Object getBean3(){
my1Bean my1Bean = SpringUtil.getBean("my1Bean");
return my1Bean.toString();
}
}
2
3
4
5
6
7
8
9
10
# 通过 HttpServletRequest 获取
通过 HttpServletRequest
对象,再结合 Spring
自身提供的工具类 WebApplicationContextUtils
也可以获取到 ApplicationContext
对象,而 HttpServletRequest
对象可以主动获取(如下 getBean2
方法),也可以被动获取(如下 getBean1
方法):
@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
@GetMapping("/bean1")
public Object getBean1(HttpServletRequest request){
//直接通过方法中的HttpServletRequest对象
ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
my1Bean my1Bean = (my1Bean)applicationContext.getBean("my1Bean");
return my1Bean.toString();
}
@GetMapping("/bean2")
public Object getBean2(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//手动获取request对象
ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
my2Bean my2Bean = (my2Bean)applicationContext.getBean("my2Bean");
return my2Bean.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 谈谈 @Autowrite 和 @Resource 以及 @Qualifier 注解的区别
上面我们看到了,注入一个 Bean
可以通过 @Autowrite
,也可以通过 @Resource
注解来注入,这两个注解有什么区别呢?
@Autowrite
:通过类型去注入,可以用于构造器和参数注入。当我们注入接口时,其所有的实现类都属于同一个类型,所以就没办法知道选择哪一个实现类来注入。@Resource
:默认通过名字注入,不能用于构造器和参数注入。如果通过名字找不到唯一的Bean
,则会通过类型去查找。如下可以通过指定name
或者type
来确定唯一的实现:
@Resource(name = "my2Bean",type = my2Bean.class)
private Imy imy;
2
而 @Qualifier
注解是用来标识合格者,当 @Autowrite
和 @Qualifier
一起使用时,就相当于是通过名字来确定唯一:
@Qualifier("my1Bean")
@Autowired
private Imy imy;
2
3
# 总结
本文主要讲述了如何在 Spring
中使用灵活的方式来实现各种场景的注入方式,并且着重介绍了当一个接口有多个实现类时应该如何注入的问题,最后也介绍了常用几个注入注解的区别,通过本文,相信大家对如何使用 Spring
中的依赖注入会更加的熟悉。