目录
一. 前言
二. IoC 基础
2.1. IoC 是什么
2.2. IoC 能做什么
2.3. IoC 和 DI 是什么关系
三. IoC 配置的三种方式
3.1. XML 配置
3.2. Java 配置
3.3. 注解配置
四. 依赖注入的三种方式
4.1. 属性注入(setter 注入)
4.2. 构造方法注入(Construct 注入)
4.3. 注解注入
五. IoC 和 DI 使用问题总结
5.1. 为什么推荐构造器注入方式?
5.2. 在使用构造器注入方式时注入了太多的类导致 Bad Smell 怎么办?
5.3. @Autowired、@Resource、@Inject 等注解注入有何区别?
5.3.1. @Autowired 注解
5.3.2. @Resource 注解
5.3.3. @Inject 注解
5.4. 小结
一. 前言
IoC(Inversion of Control),即控制反转,不是什么技术,而是一种设计思想。在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。IoC Container 管理的是 Spring Bean, 那么 Spring Bean 是什么呢?Spring 里面的 Bean 就类似于定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的 Bean 就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能。
如果你有精力看英文,首推 Martin Fowler 大师的 《Inversion of Control Containers and the Dependency Injection pattern》;其次 IoC 作为一种设计思想,不要过度解读,而是应该简化理解。
在《通过一个 Spring 的 HelloWorld 引入 Spring 要点》中向你展示了 IoC 的基础含义,同时以此发散了一些 IoC 相关知识点;本节将在此基础上进一步解读 IoC 的含义以及 IoC 的使用方式。
二. IoC 基础
2.1. IoC 是什么
IoC(Inversion of Control),即控制反转,不是什么技术,而是一种设计思想。在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
我们来深入分析一下:
1. 谁控制谁,控制什么?
传统 Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器来控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象,包括比如文件等)。
2. 为何是反转,哪些方面反转了?
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
图例说明:
传统程序设计下,都是主动去创建相关对象然后再组合起来:
当有了 IoC/DI 的容器后,在客户端类中不再主动去创建这些对象了,如图:
2.2. IoC 能做什么
IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。
传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试。有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
其实 IoC 对编程带来的最大改变不是从代码上,而是从思想上,发生了主从换位的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IoC/DI 思想中,应用程序就变成被动的了,被动的等待 IoC 容器来创建并注入它所需要的资源了。
IoC 很好的体现了面向对象设计法则之一 —— 好莱坞法则:“别找我们,我们找你”;即由 IoC 容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。关于好莱坞法则可参见《面向对象的五大基本原则(SOLID)及扩展原则》。
2.3. IoC 和 DI 是什么关系
控制反转(IoC)是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是 IoC 是设计思想,DI 是实现方式。
DI(Dependency Injection)即依赖注入:组件之间依赖关系由容器在运行期决定,形象地说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
我们来深入分析一下:
1. 谁依赖于谁?
当然是应用程序依赖于 IoC 容器。
2. 为什么需要依赖?
应用程序需要 IoC 容器来提供对象需要的外部资源。
3. 谁注入谁?
很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象。
4. 注入了什么?
就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
5. IoC 和 DI 有什么关系呢?
其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物 Martin Fowler 又给出了一个新的名字:依赖注入。相对 IoC 而言,依赖注入明确描述了被注入对象依赖 IoC 容器配置依赖对象。通俗来说就是 IoC 是设计思想,DI 是实现方式。
三. IoC 配置的三种方式
在《通过一个 Spring 的 HelloWorld 引入 Spring 要点》已经给出了三种配置方式,这里再总结下;总体上目前的主流方式是 注解 + Java 配置。
3.1. XML 配置
顾名思义,就是将 Bean 的信息配置在 xml 文件里,通过 Spring 加载文件为我们创建 Bean。这种方式出现在很多早前的 SSM 项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持 Spring 注解。
- 优点: 可以使用于任何场景,结构清晰,通俗易懂。
- 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差。
举例:
配置 xx.xml 文件,声明命名空间和配置 Bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="com.lm.it.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
3.2. Java 配置
将类的创建交给我们配置的 JavaConfig 类来完成,Spring 只负责维护和管理,采用纯 Java 创建方式。其本质上就是把在 XML 上的配置声明转移到 Java 配置类中。
- 优点:适用于任何场景,配置方便,因为是纯 Java 代码,扩展性高,十分灵活。
- 缺点:由于是采用 Java 类的方式,声明不明显,如果大量配置,可读性比较差。
举例:
1. 创建一个配置类, 添加 @Configuration 注解声明为配置类。
2. 创建方法,方法上加上 @Bean,该方法用于创建实例并返回,该实例创建后会交给 Spring 管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解:
/**
* BeansConfig
*/
@Configuration
public class BeansConfig {
/**
* @return user dao
*/
@Bean("userDao")
public UserDaoImpl userDao() {
return new UserDaoImpl();
}
/**
* @return user service
*/
@Bean("userService")
public UserServiceImpl userService() {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao());
return userService;
}
}
3.3. 注解配置
通过在类上加注解的方式,来声明一个类交给 Spring 管理,Spring 会自动扫描带有@Component、@Controller、@Service、@Repository 这四个注解的类,然后帮我们创建并管理,前提是需要先配置 Spring 的注解扫描器。
- 优点:开发便捷,通俗易懂,方便维护。
- 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用 XML 或 JavaConfig 的方式配置。
举例:
1. 对类添加 @Component 相关的注解,比如@Controller、@Service、@Repository。
2. 设置 ComponentScan 的 basePackage,比如 <context:component-scan base-package="com.lm.it.springframework"> 或者 @ComponentScan("com.lm.it.springframework") 注解,或者 new AnnotationConfigApplicationContext("com.lm.it.springframework") 指定扫描的basePackage:
/**
* UserService
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
@Autowired
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return userDao.findUserList();
}
}
四. 依赖注入的三种方式
常用的注入方式主要有三种:属性注入(setter 注入)、构造方法注入(Construct 注入)、基于注解的注入(接口注入)。
4.1. 属性注入(setter 注入)
在 XML 配置方式中,property 都是 setter 方式注入,比如下面的 xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="com.lm.it.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
本质上包含两步:
- 第一步,需要 new UserServiceImpl() 创建对象,所以需要默认构造函数。
- 第二步,调用 setUserDao() 函数注入 userDao 的值,所以需要 setUserDao() 函数。
所以对应的 Service 类是这样的:
/**
* UserService
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private UserDaoImpl userDao;
/**
* init.
*/
public UserServiceImpl() {
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
/**
* set dao.
*
* @param userDao user dao
*/
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
在 Java 配置和注解方式下:
/**
* UserService
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
/**
* set dao.
*
* @param userDao user dao
*/
@Autowired
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
在 Spring3.x 刚推出的时候,推荐使用注入的就是这种,但是这种方式比较麻烦,所以在 Spring4.x 版本中推荐构造函数注入。
4.2. 构造方法注入(Construct 注入)
在 XML 配置方式中,<constructor-arg> 是通过构造函数参数注入,比如下面的 xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="com.lm.it.springframework.service.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
本质上是 new UserServiceImpl(userDao) 创建对象, 所以对应的 Service 类是这样的:
/**
* UserService
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
public UserServiceImpl(UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
在 Java 配置和注解方式下:
/**
* UserService
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
@Autowired // 这里@Autowired也可以省略
public UserServiceImpl(final UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
在 Spring4.x 版本中推荐的注入方式就是这种。
4.3. 注解注入
以 @Autowired(自动注入)注解注入为例,修饰符有三个属性:byType、byName、Constructor。默认按照 byType 注入。
- byType:查找所有的 set 方法,将符合参数类型的 Bean 注入。
- byName:被注入 Bean 的 id 名必须与 set 方法后半截匹配,并且 id 名称的第一个单词首字母必须小写,这一点与手动 set 注入有点不同。
- constructor:通过构造方法进行自动注入,Spring 会匹配与构造方法参数类型一致的 Bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的 Bean,那么 Spring 会优先将 Bean 注入到多参数的构造方法中。
示例:
/**
* UserService
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
@Autowired
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return userDao.findUserList();
}
}
五. IoC 和 DI 使用问题总结
5.1. 为什么推荐构造器注入方式?
先来看看 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.
简单的翻译一下:这个构造器注入的方式能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。
下面来简单的解释一下:
- 依赖不可变:其实说的就是 final 关键字。
- 依赖不为空(省去了我们对其检查):当要实例化 UserServiceImpl 的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要 Spring 容器传入所需要的参数,所以就两种情况:1. 有该类型的参数 --> 传入,OK ;2. 无该类型的参数 --> 报错。
- 完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在 Java 类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。
所以通常是这样的:
/**
* UserService
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
public UserServiceImpl(final UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
}
如果使用 setter 注入,缺点显而易见,对于 IoC 容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现 NPE 的存在。
// 这里只是模拟一下,正常来说我们只会暴露接口给客户端,不会暴露实现。
UserServiceImpl userService = new UserServiceImpl();
userService.findUserList(); // -> NullPointerException, 潜在的隐患
循环依赖的问题:使用 field 注入可能会导致循环依赖,即 A 里面注入 B,B 里面又注入 A:
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private A a;
}
如果使用构造器注入,在 Spring 项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是 field 注入的话,启动的时候不会报错,在使用那个 Bean 的时候才会报错。
5.2. 在使用构造器注入方式时注入了太多的类导致 Bad Smell 怎么办?
比如当你一个 Controller 中注入了太多的 Service 类,Sonar 会给你提示相关告警:
对于这个问题,说明你的类当中有太多的责任,那么你要好好想一想是不是自己违反了类的单一性职责原则,从而导致有这么多的依赖要注入。
5.3. @Autowired、@Resource、@Inject 等注解注入有何区别?
5.3.1. @Autowired 注解
在 Spring 2.5 引入了 @Autowired 注解:
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
从 Autowired 注解源码上看,可以使用在下面这些地方:
@Target(ElementType.CONSTRUCTOR) #构造函数
@Target(ElementType.METHOD) #方法
@Target(ElementType.PARAMETER) #方法参数
@Target(ElementType.FIELD) #字段、枚举的常量
@Target(ElementType.ANNOTATION_TYPE) #注解
还有一个 required 属性,默认是 true。
总结:
- @Autowired 是 Spring 自带的注解,通过 AutowiredAnnotationBeanPostProcessor 类实现的依赖注入;
- @Autowired 可以作用在 CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE;
- @Autowired 默认是根据类型(byType )进行自动装配的;
- 如果有多个类型一样的 Bean 候选者,需要指定按照名称(byName )进行装配,则需要配合 @Qualifier。
指定名称后,如果 Spring IoC 容器中没有对应的组件,Bean 抛出NoSuchBeanDefinitionException。也可以将 @Autowired 中 required 配置为 false,如果配置为false 之后,当没有找到相应 Bean 的时候,系统不会抛异常。
代码示例:
在字段属性上
@Autowired
private HelloDao helloDao;
或者
private HelloDao helloDao;
public HelloDao getHelloDao() {
return helloDao;
}
@Autowired
public void setHelloDao(HelloDao helloDao) {
this.helloDao = helloDao;
}
或者
private HelloDao helloDao;
// @Autowired
public HelloServiceImpl(@Autowired HelloDao helloDao) {
this.helloDao = helloDao;
}
// 构造器注入也可不写@Autowired,也可以注入成功。
将 @Autowired 写在被注入的成员变量上,setter 或者构造器上,就不用在 xml 文件中配置了。
如果有多个类型一样的 Bean 候选者,则默认根据设定的属性名称进行获取。如 HelloDao 在Spring 中有 helloWorldDao 和 helloDao 两个 Bean 候选者。
@Autowired
private HelloDao helloDao;
首先根据类型获取,发现多个 HelloDao,然后根据 helloDao 进行获取,如果要获取限定的其中一个候选者,结合 @Qualifier 进行注入。
@Autowired
@Qualifier("helloWorldDao")
private HelloDao helloDao;
注入名称为 helloWorldDao 的 Bean组件。@Qualifier("XXX") 中的 XX 是 Bean 的名称,所以 @Autowired 和 @Qualifier 结合使用时,自动注入的策略就从 byType 转变成 byName 了。
多个类型一样的 Bean 候选者,也可以 @Primary 进行使用,设置首选的组件,也就是默认优先使用哪一个。
注意:使用 @Qualifier 时候,如果设置的指定名称的 Bean 不存在,则会抛出异常,如果防止抛出异常,可以使用:
@Qualifier("xxxxyyyy")
@Autowired(required = false)
private HelloDao helloDao;
在 SpringBoot 中也可以使用 @Bean+@Autowired 进行组件注入,将 @Autowired 加到参数上,其实也可以省略。
@Bean
public Person getPerson(@Autowired Car car) {
return new Person();
}
// @Autowired 其实也可以省略
5.3.2. @Resource 注解
@Resource 注解源码:
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
String name() default "";
// 其他省略
}
从 @Resource 注解源码上看,可以使用在下面这些地方:
@Target(ElementType.TYPE) #接口、类、枚举、注解
@Target(ElementType.FIELD) #字段、枚举的常量
@Target(ElementType.METHOD) #方法
name 指定注入指定名称的组件。
总结:
- @Resource 是 JSR250 规范的实现,在 javax.annotation 包下;
- @Resource 可以作用 TYPE、FIELD、METHOD 上;
- @Resource 是默认根据属性名称进行自动装配的,如果有多个类型一样的 Bean 候选者,则可以通过 name 进行指定进行注入。
代码示例:
@Component
public class SuperMan {
@Resource
private Car car;
}
按照属性名称 car 注入容器中的组件。如果容器中除了 BMW 还有 BYD 两种类型组件。指定加入 BMW。如下代码:
@Component
public class SuperMan {
@Resource(name = "BMW")
private Car car;
}
name 的作用类似 @Qualifier。
5.3.3. @Inject 注解
@Inject 注解源码:
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {}
从 @Inject 注解源码上看,可以使用在下面这些地方:
@Target(ElementType.CONSTRUCTOR) #构造函数
@Target(ElementType.METHOD) #方法
@Target(ElementType.FIELD) #字段、枚举的常量
总结:
- @Inject 是 JSR330(Dependency Injection for Java)中的规范,需要导入 javax.inject.Inject jar 包 ,才能实现注入;
- @Inject 可以作用 CONSTRUCTOR、METHOD、FIELD 上;
- @Inject 是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合 @Named。
代码示例:
@Inject
private Car car;
指定加入 BMW 组件
@Inject
@Named("BMW")
private Car car;
@Named 的作用类似 @Qualifier。
5.4. 小结
1. @Autowired 是 Spring 自带的,@Resource 是 JSR250 规范实现的,@Inject 是 JSR330 规范实现的。
2. @Autowired、@Inject 用法基本一样,不同的是 @Inject 没有 required 属性。
3. @Autowired、@Inject 是默认按照类型匹配的,@Resource 是按照名称匹配的。
4. @Autowired 如果需要按照名称匹配需要和 @Qualifier 一起使用,@Inject 和 @Named 一起使用,@Resource 则通过 name 进行指定。