控制反转(IOC)
从【基础|第一个spring项目】中的实例分析,如果没有Spring框架,就需要手动去创建User/Dao/Service等;有了Spring框架后,可以将原有的Bean的创建工作转给框架,需要用的时候从Bean的容器中获取即可。
1 | // create and configure beans |
-
Spring Bean
Spring里边的bean就类似是定义的一个组件,而这个组件的作用就是是心啊某个功能。这里所定义的bean就相当于给了你一个更为简便的方法去调用组件来实现你的业务。
-
IOC
Inversion of Control,即控制反转,是一种设计思想。
-
谁控制谁,控制什么?
传统的Java SE程序设计,直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是由专门的一个容器来创建这些对象,即由IOC容器来控制对象的创建;
所以,IOC容器控制了对象。主要控制了外部资源获取。
-
为什么是反转?
传统的由自己在对象中主动控制去获取依赖对象的就是正转;通过容器来帮忙创建以及注入依赖对象的,叫反转。因为由容器将依赖对象注入,对象是被动的接收依赖对象的,所以称为反转。
-
传统方式:

IOC:

IOC和DI
控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度的描述。简单来说IOC是设计思想,DI是实现方式。
DI(Dependency Injection),依赖注入。组件之间依赖关系由容器在运行期间决定。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
我们来深入分析一下:
- 谁依赖于谁?
当然是应用程序依赖于IoC容器;
- 为什么需要依赖?
应用程序需要IoC容器来提供对象需要的外部资源;
- 谁注入谁?
很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
- 注入了什么?
就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
- IoC和DI有什么关系呢?
其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。通俗来说就是IoC是设计思想,DI是实现方式。
IOC三种配置方式
xml配置
顾名思义,就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。
- 优点: 可以使用于任何场景,结构清晰,通俗易懂
- 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差
举例:
- 配置xx.xml文件
- 声明命名空间和配置bean
1 |
|
Java配置
将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中
- 优点:适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活
- 缺点:由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差
举例:
- 创建一个配置类, 添加@Configuration注解声明为配置类
- 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解
1 |
|
注解配置
通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。
- 优点:开发便捷,通俗易懂,方便维护。
- 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置
举例:
- 对类添加@Component相关的注解,比如@Controller,@Service,@Repository
- 设置ComponentScan的basePackage,例如
<context:component-scan base-package='x'>,或者@ComponentScan("x")注解,或者new AnnotationConfigApplicationContext("x");都是在指定扫描的basePackage。
DI的三种方式
setter
1 |
|
本质上包含两步:
- 需要new UserServiceImpl()创建对象,所以需要默认构造函数
- 调用setUserDao()函数注入userDao的值,所以需要setUserDao()函数
对应的service类:
1 | public class UserServiceImpl { |
注解Java配置:
1 | public class UserServiceImpl { |
构造函数
1 |
|
注解输入
以@Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。默认按照byType注入。
- constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。
- byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。
- byType:查找所有的set方法,将符合符合参数类型的bean注入。
1 |
|
IOC和DI使用问题小结
为什么推荐构造器注入方式?
Spring文档中,有这么一段描述:
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.
Spring团队通常提倡构造函数注入,因为它允许将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。
- 不可变对象:其实说的就是final关键字
- 依赖项不为空:当实例化UserServiceImpl的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造参数,那么就需要Spring容器传入所需要的此参数,这时就会出现两种情况:有该类型的参数,传入;没有,报错。
- 完全初始化的状态:这个可以跟上面的依赖项不为空组合起来看,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前那如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。
1 |
|
如果使用setter注入,缺点显而易见,对于IOC容器以外的环境,除了使用反射来提供它需要的以来之外,无法无用该类的实现类。而且将一直是个潜在隐患,因为你不调用将一直无法确定NullPointerException异常是否存在。
循环依赖问题
使用field植入可能导致循环依赖,即A里面注入B,B里面注入A:
1 | public class A{ |
如果使用构造器注入,在项目启动的时候,就会抛出BeanCurrentlyCreationException:Requested bean is currently in creation:Is there an unresolvable circular reference?从而在项目启动之前处理掉循环依赖的问题。
DI的三种注解
@Autowired
Spring2.5引入了@Autowired注解:
1 |
|
从元注解@Target注解上看,可以使用在:
ElementType.CONSTRUCTOR构造函数ElementType.METHOD方法ElementType.PARAMETER方法参数ElementType.FIELD字段、枚举的常量ElementType.ANNOTATION_TYPE注解
还有一个required,默认是true,代表注入bean的时候该bean必须存在,不然就会注入失败;设置成false,代表注入bean时,bean存在,则注入成功;不存在就忽略跳过,启动时不报错,会将NullPointerException隐藏。
@Autowired总结:
-
是Spring自带的注解,通过AutowiredAnnotationBeanPostProcessor类实现的依赖注入。
-
可以作用在构造函数,方法,方法参数,字段、枚举的常量,注解
-
默认根据类型(byType)进行自动装配
-
🔶 如果有多个类型一样的Bean候选者,需要指定按照名称(byName)进行装配,则需要配合@Qualifier
1
2
3
private HelloDao helloDao;
@Resoure
1 | @Target({TYPE, FIELD, METHOD}) |
从元注解@Target注解上看,可以使用在:
TYPE接口、类、枚举、注解FIELD字段、枚举的常量METHOD方法
name指定注入指定名称的组件。
@Resoure总结:
-
是JSR250规范的是实现,在javax.annotation包下
-
可以作用在TYPE, FIELD, METHOD
-
默认根据属性名称进行自动装配的,如果有多个类型一样的Bean候选者,则可以通过name进行指定进行注入
1
2
3
4
5
6
public class SuperMan {
private Car car;
}
// name的作用类似于@Qualifier
@Inject
1 |
|
从元注解@Target注解上看,可以使用在:
CONSTRUCTOR构造函数FIELD字段、枚举的常量METHOD方法
@Inject总结:
-
是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject jar包 ,才能实现注入
-
可以作用CONSTRUCTOR、METHOD、FIELD上
-
根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named;
1
2
3
// 作用类似于@Qualifier
private Car car;
总结
- @Autowired是Spring自带的,@Resource是JSR250规范实现的,@Inject是JSR330规范实现的
- @Autowired、@Inject用法基本一样,不同的是@Inject没有required属性
- @Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的
- @Autowired如果需要按照名称匹配需要和@Qualifier一起使用,@Inject和@Named一起使用,@Resource则通过name进行指定
- 🔗 Spring源码分析@Autowired、@Resource注解的区别