本篇主要介绍Spring两大核心思想之一的IOC以及与之密切相关的DI。
目录
一、IOC与DI是什么?
IOC,可以简单概况为四个字,“控制反转”,具体来说就是将一个对象创建、销毁、使用的控制权统一反转给一个固定的第三方来进行管理,而这个统一的第三分被称为IOC容器,Spring就是这样一种IOC容器。
下面我们通过一个案例来体会一下
一辆车一般有以下依赖关系:
转换成代码表示:
这样的依赖关系虽然不影响一辆车的制造,但当我们需要更改轮胎的尺寸也就是size变量时,会直接对这四个组件(四个类)都造成影响, 这样耦合程度就显得太高了,下面我们基于IOC思想优化一下,
在优化后的代码中我们可以发现在main方法中已经事先创建好了当前组件所需要的依赖,而不是像先前那样自己创建依赖,因此当我们需要修改size时,只需要修改Tire的实参即可,其他组件是无需做出任何反应的,因为其他组件只需要去拿自己需要的依赖即可,至于依赖的内部如何实现,发生了怎样的变化,是不需要去关心的,这样一来也就实现了各个组件之间的解耦 。
从汽车的案例中我们可以总结出IOC具有以下两大优点:
- 依赖交由第三方(容器)来进行创建,我们要使用时直接去容器中拿即可,因此对于依赖的使用相比传统开发更加方便。
- 不再需要关心依赖的具体实现细节,只需要关心自己的业务逻辑即可,降低了各依赖之间的耦合程度
DI,直接的意思是依赖注入,具体来说就是将存入IOC容器的依赖对象取出来,并注入到需要使用该依赖的地方。DI也可以理解成从取的角度对IOC的一种实现。
二、Spring中IOC&DI的使用
前面我们说过Spring是一个IOC容器,因此IOC&DI在spring中均有体现,下面我们来看一下IOC&DI在spring中的具体代码实现。
IOC
Bean
Bean可以简单理解成保存在spring容器中的对象,针对Bean的操作具体有两种:
- 存入
- 取出
下面我们来具体来了解一下。
存入Bean
存入bean一般有两种方式,一种是使用五大类注解,另一种是使用方法注解@Bean。
五大类注解
Spring中的五大注解具体如下:
- @Controller
- @Service
- @Repository
- @Component
- @Configuration
如果要将当前类的对象存入到Sping中,直接在类上添加五大类注解中的其中一个,Spring就会自动为我们创建一个对象并存入,例如我们在Spring Boot项目中创建一个Controller类,并将其通过@Controller存到spring中,代码如下:
接下来我们观察一下这五个注解的源码
通过源码可以发现,@Component在其他另外四个类注解中都有使用,这也就意味着这四个类注解 都是由@Component衍生而来。并且仔细观察可以发现这些注解除了名字不一样外,其他部分基本上可以说是一模一样的,也就是说这五个注解的功能基本上是大差不差的,那为什么Spring还要实现五个呢?直接用一个不就行了吗?事实上,这五个注解都有自己特定的使用场景,具体如下:
- @Controller:控制层,接收请求,对请求进行处理,并进行响应
- @Service:业务逻辑层,处理具体的业务逻辑
- @Repository:数据访问层,负责数据相关的操作
- @Configuration:配置层,处理项目中配置相关的信息
- @Component:上述四个注解未涉及的均使用@Component
通过在特定功能的类上加上对应的注解,可以让开发人员能够一眼就知道这个类大概是什么功能,从而加快开发效率。并且通常情况下我们会将使用相同类注解的类放到一个包下,从而实现前面在介绍Spring MVC中所说的“三层架构”。
因此我们在使用类注解时,应当结合当前类的特点来使用,并将使用相同类注解的类放到同一个包下进行管理。
最后,在使用五大类注解时需要注意确保类中具有一个无参的构造方法,因为在Spring创建对象时会调用这个无参构造方法来创建 ,如果没有bean是无法存入Spring的。
方法注解@Bean
另一种存Bean的方式是使用方法注解@Bean,他与五大类注解不同的是@Bean是加在方法上的,并且会将方法的返回值作为Bean存入到Spring中,例如我们先创建一个User类,然后将其通过方法注解的方式存入Bean中,具体代码如下
需要注意的是只有在添加了五大类注解的类中的方法才能使用@Bean来存Bean,在其他类中使用@Bean是存不了的。
Bean的命名
Bean是有名称的,如果我们使用五大类注解存入的Bean,bean的名称默认为类名的小驼峰,另外还有一个特殊情况,如果类名前为大写,bean名默认为类名,如果使用的是@Bean,bean名默认为方法名称。我们还可以通过注解中的value参数,来指定Bean的名称,指定Bean名称后,Bean的名称将不会再是先前默认的名称,而是我们指定的名称,具体代码如下:
另外,使用@Bean时我们还可以为一个bean指定多个名称,具体为使用name参数代码如下:
扫描路径
在spring中并不是项目中所有的使用了五大类注解和@Bean的类都会被存到Spring中的,只有处于扫描路径下,才会被Spring扫描并存入Spring中。在Spring Boot项目中默认的扫描路径为加了@SpringBootApplication注解的类(启动类)所在路径及其子路径,如果类或方法不在这个路径下,就无法被Spring扫描到,也就无法存入到Spring容器中。我们可以通过@ComponentScan来配置扫描路径,例如,我们要把扫描路径配置为只扫描我们前面的controller包,代码如下:
在大括号内我们还可以添加多给路径作为扫描路径。
之所以默认的路径为启动类所在路径,也是因为在@SpringBootApplication中通过@ComponentScan对扫描路径进行了配置
由于在@SpringBootApplication中也使用了@ComponentScan,因此,并不推荐自己使用@ComponentScan来配置扫描路径,如果一定要修改扫描路径,直接移动启动类即可。
取出Bean
从Spring容器中取出Bean可以通过使用上下文的方式,上下文可以简单的理解为就是用来储存Bean的Spring容器,上下文具体为ApplicationContext接口,其创建方法如下:
ApplicationContext context = SpringApplication.run(启动类类型);
我们可以通过上下文为我们提供的方法来获取Bean,具体如下:
方法 | 作用 |
T getBean(Class<T> aClass) | 通过Bean的类型获取 |
Object getBean(String s) | 通过Bean的名称获取 |
TgetBean(String s , Class<T> aClass) | 通过Bean的名称和类型获取 |
在使用Bean的名称来获取Bean时由于返回类型是Object,所以在使用时需要强转成Bean的类型再使用。下面我们来通过上下文来获取一下前面存的Bean
通过类型获取:
存入Bean
控制台的输出:
需要注意的是如果在Spring中存在多个相同类型的Bean时,使用类型来获取Bean会报错,具体报错信息如下:
因此,当存在多个类型相同的Bean时,更建议通过Bean的名称来获取。具体代码如下:
存入Bean
通过名称取Bean
控制台:
当Bean被设置了多个名称时,使用任意一个都能获取,具体代码如下
存Bean:
取Bean:
控制台:
和使用“u1”结果一样。
如果Spring中有两个相同名称(通过五大类注解创建相同名称的Bean时会报错,使用@Bean则不会),则会获取先存入Spring的那个,示例代码如下
存Bean
取Bean:
控制台:
最后我们再来看一下根据类名和名称来获取Bean,代码示例:
存Bean:
取Bean:
控制台:
如果spring中存在多个相同Bean,结果和使用名称获取相似。
在Spring中还可以通过BeanFactory来获取Bean,但上下文已经对BeanFactory进行了封装,因此上下文是具备BeanFactory的所有功能,并且上下文还在BeanFactory的基础上进行了很多拓展,功能更丰富。BeanFactory在处理Bean时采用的是懒汉模式,只有当需要使用Bean才会真正去创建Bean,而上下文则是采用的饿汉模式,在使用前就把所有Bean创建好了,因此获取Bean时,上下文的速度会更快。所以,一般情况下,更推荐使用上下文的方式来获取Bean。
DI
前面我们介绍过,通过上下文的方式可以取出Bean,但在日常开发中,更多的还是通过依赖注入,也就是DI来获取Bean。DI,一共有三种注入方式,属性注入、构造方法注入和Setter注入。下面我们来具体了解一下这三种注入方式。
属性注入
属性注入的方式非常简单,只需要在需要注入的属性上加上@Autowired(一般在加了五大类注解的类中使用),然后Spring就会自动将容器中与属性类型相同的Bean注入到属性中。接下来我们创建一个类Controllertest2,然后创建属性ControllerTest,然后再将先前存入的ControllerTest类型的Bean注入到该属性中,并通过web的方式检验是否真正注入,具体代码如下:
访问接口后控制台的显示:
可以发现属性注入成功了。
构造方法注入
构造方法注入与属性注入类似,也是使用@Autowired,不过构造方法注入是加在构造方法上,具体代码如下:
控制台:
这里在把Controllertest2存到Spring中时将不会再调用默认的无参构造方法来创建对象,而是调用这个加了@Autowired的构造方法来创建对象,因此这里可以省略无参构造方法,但通常情况下还是建议加一个无参构造方法,因为其他的一些Api可能会调用到无参构造方法,例如通过对象来接收请求中的JSON字符串时,需要调用无参构造方法来构建对象。
Setter注入
最后一种注入方法是Setter注入,这种注入方式也是通过@AutoWired来实现的,具体代码如下:
控制台:
由于set方法在其他地方也可以调用,因此通过这种方式注入的属性值是很容易修改的。
三种注入方式的优缺点
下面我们来总结一下这三种注入方式的优缺点
注入方式 | |
属性注入 | 优点:简单使用方便 缺点:1.不能注入final修饰的属性 2.只能再IOC容器中使用,其他环境下使用不了 |
构造方法注入(Spring4.x推荐使用) | 优点:1.能够注入final修饰的属性(因为在构造方法中对属性进行初始化) 2.注入的属性值不能修改 3.由于依赖是注入到构造方法中,因此依赖在使用前,一定是已经初始化完毕的,因为构造方法再类加载阶段就已经完成了 4.通用性好,构造方法是JDK支持的,在任何框架下都能使用 缺点:注入多个对象时会很繁琐 |
Setter注入(Spring3.x推荐使用) | 优点:1.在创建类实例后,还能对注入的对象进行修改和重新配置 缺点:1.不能注入一个Final修饰的属性。 2.Set能够在很多地方使用,有被修改的风险 |
使用@Autowired存在的问题
@Autowired在进行依赖注入时会优先根据属性类型去Spring中找Bean,如果找到的Bean有多个,就会根据属性名称去找,如果没有找到与属性名称同名的Bean,就会报异常
当我们通过指定属性名称去Spring中取Bean时,会出现这样一个问题。通常情况下,大家都会认为属性名称的修改对项目并不会影响。因此,在开发时很有可能在无意中将注入的属性名称给改了,如果此时该属性类型的Bean有多个,就会导致注入失败。这种问题的解决方案通常有三种,一种是加@Primary注解,通过@Primary注解就会将默认注入的Bean设置为由加了@Primary的方法存入spring的Bean。具体代码示例如下:
此时通过@Autowried注入的User类型的对象就会默认为使用上述加了@Primary的user1了。
另一种方法是通过@Qualifier,这个注解有一个value参数,它可以指定@Autowired注入的bean的名称,具体代码示例如下:
此时@Autowired会根据@Qualifier设置的bean名称去Spring寻找需要注入的Bean。这样无论如何改变属性名称都只会去获取@Qualifiler指定名称的bean了。
最后还有一种方法是使用由JDK提供@Resource来代替@Autowired来进行依赖注入,在@Resource中自带一个name属性用来设置需要注入的Bean的名称,其效果和前面使用@Qualifiler是一样的,代码示例如下:
最后我们再来看一个常见面试题:
@Autowired和@Resource的区别?
- 提供方不同,@Autowired是由Spring提供的,@Resources是由JDK提供的
- 默认方式不同,@Autowire的默认根据类型注入,当同类型Bean存在多个就会根据属性名注入,而@Resource则是根据指定的Bean名称进行注入
- 支持的参数不同,@Resource支持更多的参数,如name等。