目录
- 1. IOC和DI
- 2. Spring容器加载Bean/创建对象的时机
- 3. @Autowired注解
- 4. @Configuration配置类
- 5. @Conditional条件注解
- 6. SpringBoot的自动配置/自动装配
- 总结
1. IOC和DI
首先,我们应该明确,IOC是一种思想,并不是Spring特有的,而是软件工程逐步发展的一种产物,是一种优秀的编程思想,之所以我们经常会把IOC理解成是Spring特有的东西,是因为Spring框架可以帮助我们很好的去实现IOC。IOC代表的是控制反转,控制的是什么?反转的是什么?控制的是一个对象的创建、初始化和销毁,没有使用IOC之前,我们使用一个对象,会主动的去创建对象,也就是在需要使用这个对象的地方去执行 A a = new A();
类似这种语句,而使用了IOC之后,会把系统中所有需要使用到的对象交给IOC容器去托管,当我们的程序中需要使用到某个对象时,也不是我们直接去容器获取这个对象,而是容器会主动的给我们这个对象,也就是说IOC容器会自动给程序中需要对象的地方给你注入对象,这就是反转,把获取对象的行为由主动变为被动,并且把控制权交给了容器。
DI代表的是依赖注入,什么是依赖注入?我们都知道,对象与对象之间一般都会有依赖关系的,例如对象A依赖于对象B,对象B又依赖于对象C,举个例子,对象A中有个方法a,a方法的实现需要调用B对象的b方法,这就是依赖,引入IOC之前,我们会在A对象中new一个B对象,再通过B对象去调用b方法;引入IOC之后,我们都不会去主动创建对象了,都交给IOC容器去处理了,我们只需要在对象A中声明一个B对象的成员变量,加上xml配置或者相关注解,IOC容器就可以我们自动实现注入。例如下面这段代码,在MVC架构中,Controller层依赖Service层,我们只需要在Controller层声明一个Service层的变量,并加上@Autowired注解,当IOC容器启动时加载ApplicationController这个Bean到容器中时,会自动把ApplicationService这个Bean也注入进来。
依赖注入的实现方式有三种:构造函数注入、set方法注入以及接口注入;接口注入用的比较少,一般都是使用构造函数注入和set方法注入,另外还有一种字段注入,如上图所示的注入方式就是字段注入,这种注入方式不是规范的,可以看到@Autowired注解会给出警告,不建议我们使用这种方式,但这种方式却是使用最多的,因为它很方便。
以下是构造函数注入:
以下是set方法注入:
2. Spring容器加载Bean/创建对象的时机
默认情况下,程序启动的时候,Spring就会把程序中需要用到的Bean加载到IOC容器中,也就是会一次性创建很多的对象到IOC容器中。当然,还有一种方式,就是当需要使用到某个Bean的时候,再去创建这个Bean到容器中,这就是延迟加载,或者叫延迟实例化。
如果我们希望某个Bean不要过早的被加载到容器中,使用到的时候再去创建,可以给这个Bean加上@Lazy注解。
关于@Lazy注解实现延迟加载,需要注意一个细节,我们给某个Bean加上@Lazy注解之后,并不一定就能实现延迟加载,有可能程序在启动的时候,这个Bean还是会在一开始就被创建,会产生这种情况是因为这个Bean有可能在别处被引用到,也就是说别的Bean可能依赖了这个Bean,而依赖的这个Bean又没有打上@Lazy注解,那么程序启动的时候,创建这个依赖的Bean,也会把它所依赖的Bean一并注入,这就是导致@Lazy注解有时没有生效的原因。
举个例子,ApplicationController这个Bean依赖了ApplicationServiceImpl,我们只给ApplicationServiceImpl这个Bean加上@Lazy注解,是无法实现延迟加载的,需要ApplicationController这个Bean也加上@Lazy注解。
3. @Autowired注解
@Autowired注解的作用就是实现依赖注入,大部分情况下,我们使用@Autowired这个注解就可以满足我们的需求了,但是我们需要知道这个注解的约定是什么,才能更好的使用它。首先,@Autowired注解实现的是按照类型注入,补充一个点:为了满足ocp原则,使得代码更加灵活便于扩展,我们倡导面向接口编程。例如下面这个例子,ApplicationService是一个接口,我们加上了@Autowired注解,按照类型注入的原则,IOC容器会去找ApplicationService接口的实现类去进行依赖注入,如果这个接口只有一个实现类,那么没有问题,可以实现注入,如果这个接口有多个实现类,那么IOC容器会知道给我们注入哪一个实现类吗,显然是不知道的,但是@Autowired还有一种规则,就是根据类型匹配时,如果能匹配多个,会再根据Bean的名称去匹配,如果Bean的名称能匹配上,那么没有问题,可以实现注入;如果Bean的名称匹配不上,程序会报错,因为IOC容器不知道给我们注入哪一个ApplicationService接口的实现类。
举个例子,假设现在ApplicationService接口有两个实现类ApplicationServiceImpl1(Bean名称:applicationServiceImpl1)和ApplicationServiceImpl2(Bean名称:applicationServiceImpl2),如果@Autowired注解作用的代码是private ApplicationService applicationServiceImpl1;,那么注入的是ApplicationServiceImpl1这个Bean,如果@Autowired注解作用的代码是private ApplicationService applicationServiceImpl2;,那么注入的是ApplicationServiceImpl2这个Bean,如果@Autowired注解作用的代码是private ApplicationService applicationServiceImpl;,那么程序报错,IOC容器知道为我们注入哪个Bean。
如果涉及一个接口有多个实现类的情况,我们建议使用@Autowired注解搭配@Qualifier注解,@Qualifier注解的实现机制是按照Bean的名称进行注入。
例如下面这个例子,首先@Autowired注解会根据类型找到ApplicationService的实现类,@Qualifier注解再找到名称为applicationServiceImpl的Bean进行注入。
PS:上面我们讲的是@Autowired注解如果匹配到多个Bean不知道注入哪一个的时候会报错,还有一种情况,如果@Autowired注解匹配不到任何对应的Bean也会报错。
4. @Configuration配置类
一个类被加上了@Configuration注解,就表明这个类是一个配置类,@Configuration注解底层注解是@Component,也就是说,注解类也会被加载到IOC容器中,在注解类中,通常会声明很多方法,这些方法的返回值是一个对象,并且方法加上了@Bean注解,这表明会将这些方法返回的对象加载到IOC容器中。例如:
@Configuration注解是用来替换Bean的xml配置,可以把@Configuration看作标签,把@Bean注解看作标签
传统的xml配置是这样的:
<beans> <bean id = "people", class = "com.test.People"> </bean> <bean id = "student", class = "com.test.Student"> </bean></beans>
思考一个问题:一般来说,把一个Bean加载到IOC容器中,不是通过@Component注解就可以实现吗?况且@Configuration注解的底层注解还是@Component,那为什么还需要@Configuation+@Bean这套注解来加载某些Bean到IOC容器中呢?换言之,@Configuation+@Bean的真正作用是什么?
@Configuation+@Bean的作用就是可以将一些定制化的Bean(特殊的Bean)注入到IOC容器中,什么是定制化的Bean?举个例子,有一个People类,有两个属性name和age,我们希望把People类加载到IOC容器中,并且使得IOC容器的这个Bean的name属性值是小明,age属性值是18。如果只是在People类上加@Component注解,是无法实现我们这个定制化的Bean的,因为IOC容器在启动的时候,创建Bean,默认调用的都是某个类的无参构造函数(也存在调用带参构造函数的情况,但是这种情况使用带参构造函数,只是为了实现依赖注入,也就是构造函数注入,并非是给属性赋值),也就是说无法给我们的属性赋特殊的值,而使用@Configuation+@Bean就可以实现。例如:
通常来说,我们不会直接把属性值写死在代码里,而是通过读取配置文件的形式,虽然说@Configuration注解表明这个类已经是一个配置类了,可以充当配置文件来使用,但是对于一些变化的属性,我们都会定义在普通配置文件里面,例如application.properties。
5. @Conditional条件注解
@Conditional注解的作用是满足某个条件时,才会将这个Bean注入到IOC容器中。我们可以通过自定义一个Condition类的方式定制某个规则,当满足这个规则才会注入Bean,例如:
通常,我们直接使用一些成品条件注解就可以满足我们的需求了。常用成品条件注解如下:
6. SpringBoot的自动配置/自动装配
自动装配是SpringBoot最核心的东西,学习自动装配,我们首先应该了解自动装配是什么,做了哪些事情?其次需要明确自动装配存在的意义,也就是说自动装配的好处是什么?最后再去了解自动装配的原理。按照这个流程下来,才能更好的掌握自动装配这个知识点。
a. 自动装配做了什么事情? 就是SpringBoot会自动把一些第三方的库或者SDK需要使用到的很多Bean都自动帮我们加载到IOC容器中。
b. 自动装配的好处是什么? 基于IOC思想,任何程序中使用到的Bean都需要注入IOC容器中进行托管,也就是说,我们引用一些第三方的库/依赖,例如Mongodb、Redis、Hadoop,也是需要把这些第三方的库涉及的Bean都加载到IOC容器中的。如果没有自动装配,那么如何把第三方的库里面的很多Bean都加载到我们的IOC容器中来,给每一个相关的类打上@Component注解?不是这样的,我们所引用的第三方库一般都是以jar包的形式存在,并不是直接给源码。即便是可以通过打上@Component注解的方式,试想我们的工作量会增加多少,引用一个第三方库可能涉及很多的Bean,并且有些Bean又可能是比较复杂的,我们不可能手动的将他们一个个注入容器,而SpringBoot都帮我们做好了,这就是自动装配的好处。
c. 自动装配的原理
首先,在SpringBoot程序的主启动类上,有一个非常重要的注解:@SpringBootApplication,进入到这个注解,可以发现底层由三个注解组成(元注解除外)
@SpringBootConfiguration
:表明SpringBoot程序的主启动类也是一个配置类
@ComponentScan
:指定包扫描路径,程序启动时,会根据指定的包扫描路径去加载Bean
@EnableAutoConfiguration
:最重要的一个注解,我们可以理解成这个注解的作用是开启自动装配
进入到@EnableAutoConfiguration,可以发现底层由两个注解组成(元注解除外)
我们主要研究@Import注解,这个注解有两种实现方式,第一种是指定一个或多个配置类,例如:@Import(TetsConfiguration.class) 那么IOC容器就会将这个配置类加载到容器中来,另一种实现方式是指定一个ImportSelector类的子类,例如@Import(AutoConfigurationImportSelector.class) 这个ImportSelector类的子类会去实现selectImports方法,该方法的返回值是一个字符串数组,代表要加载的配置类名称列表。
SpringBoot自动装配默认使用第二种方式,我们进到AutoConfigurationImportSelector这个类里面,发现它确实实现了selectImports方法。通过这个方法,SpringBoot就可以知道我们需要自动装配哪些配置类
我们再进入getAutoConfigurationEntry方法,这个方法里面有一个核心方法getCandidateConfigurations,翻译过来就是获取候选的配置类
getCandidateConfigurations方法实现逻辑就是:会去读取某一个配置文件,根据这个配置文件的值返回自动装配要装配的配置类名称列表
我们进入到SpringFactoriesLoader类,可以知道getCandidateConfigurations读取的配置文件就是META-INF下面的spring.factories
spring.factories的位置
spring.factories配置文件主要就声明了很多自动配置类的名称,截取部分如下:
总结一下,我们一路追踪源码下来,无非确定了一件事,SpringBoot自动装配时装配了哪些配置类,这些配置类的定义都存在于spring.factories配置文件中。
另外,我们需要明确,SpringBoot自动装配时也不是把所有自动配置类定义的Bean都加载到IOC容器中,因为这些Bean大部分都会加上条件注解,需要满足一定的条件才会被加载,例如,我们进入某一个自动配置类看一下
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注自由互联的更多内容!