抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

控制反转(IOC)

从【基础|第一个spring项目】中的实例分析,如果没有Spring框架,就需要手动去创建User/Dao/Service等;有了Spring框架后,可以将原有的Bean的创建工作转给框架,需要用的时候从Bean的容器中获取即可。

1
2
3
4
5
6
7
// create and configure beans
ApplicationContext context =
new ClassPathXmlApplicationContext("aspects.xml", "daos.xml", "services.xml");
// retrieve configured instance
UserServiceImpl service = context.getBean("userService", UserServiceImpl.class);
// use configured instance
List<User> userList = service.findUserList();
  • 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注解。

  • 优点: 可以使用于任何场景,结构清晰,通俗易懂
  • 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差

举例

  1. 配置xx.xml文件
  2. 声明命名空间和配置bean
1
2
3
4
5
6
7
8
9
10
<?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="x.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
</beans>

Java配置

将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中

  • 优点:适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活
  • 缺点:由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差

举例

  1. 创建一个配置类, 添加@Configuration注解声明为配置类
  2. 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解
1
2
3
4
5
6
7
8
9
@EnableAspectJAutoProxy
@Configuration
public class BeanConfig {
@Bean("userDao")
public UserDaoImpl userDao(){
return new UserDaoImpl();
}
//...
}

注解配置

通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。

  • 优点:开发便捷,通俗易懂,方便维护。
  • 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置

举例

  1. 对类添加@Component相关的注解,比如@Controller,@Service,@Repository
  2. 设置ComponentScan的basePackage,例如<context:component-scan base-package='x'>,或者@ComponentScan("x")注解,或者new AnnotationConfigApplicationContext("x");都是在指定扫描的basePackage。

DI的三种方式

setter

1
2
3
4
5
6
7
8
9
10
<?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="x.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
</beans>

本质上包含两步:

  1. 需要new UserServiceImpl()创建对象,所以需要默认构造函数
  2. 调用setUserDao()函数注入userDao的值,所以需要setUserDao()函数

对应的service类:

1
2
3
4
5
6
7
8
9
10
11
public class UserServiceImpl {
private UserDaoImpl userDao;
public UserServiceImpl() {
}
public List<User> findUserList(){
return this.userDao.findUserList();
}
public void setUserDao(UserDaoImpl userDao){
this.userDao = userDao;
}
}

注解Java配置:

1
2
3
4
5
6
7
8
9
10
public class UserServiceImpl {
private UserDaoImpl userDao;
public List<User> findUserList() {
return this.userDao.findUserList();
}
@Autowired
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
<?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="x.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>

注解输入

以@Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。默认按照byType注入。

  • constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。
  • byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。
  • byType:查找所有的set方法,将符合符合参数类型的bean注入。
1
2
3
4
5
6
7
8
@Service
public class UserServiceImpl {
@Autowired
private UserDaoImpl userDao;
public List<User> findUserList() {
return userDao.findUserList();
}
}

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
2
3
4
5
6
7
@Service
public class UserServiceImpl{
private final UserDaoImpl userDao;
public UserServiceImpl(final UserDaoImpl userDaoImpl){
this.userDao = userDaoImpl;
}
}

如果使用setter注入,缺点显而易见,对于IOC容器以外的环境,除了使用反射来提供它需要的以来之外,无法无用该类的实现类。而且将一直是个潜在隐患,因为你不调用将一直无法确定NullPointerException异常是否存在。

循环依赖问题

使用field植入可能导致循环依赖,即A里面注入B,B里面注入A:

1
2
3
4
5
6
7
8
public class A{
@Autowired
private B b;
}
public class B{
@Autowired
private A a;
}

如果使用构造器注入,在项目启动的时候,就会抛出BeanCurrentlyCreationException:Requested bean is currently in creation:Is there an unresolvable circular reference?从而在项目启动之前处理掉循环依赖的问题。

DI的三种注解

@Autowired

Spring2.5引入了@Autowired注解:

1
2
3
4
5
6
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

从元注解@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
    @Qualifier("xxxxyyyy")
    @Autowired(required = false)
    private HelloDao helloDao;

@Resoure

1
2
3
4
5
6
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
String name() default "";
// ...
}

从元注解@Target注解上看,可以使用在:

  • TYPE 接口、类、枚举、注解
  • FIELD 字段、枚举的常量
  • METHOD 方法

name指定注入指定名称的组件。

@Resoure总结

  • 是JSR250规范的是实现,在javax.annotation包下

  • 可以作用在TYPE, FIELD, METHOD

  • 默认根据属性名称进行自动装配的,如果有多个类型一样的Bean候选者,则可以通过name进行指定进行注入

    1
    2
    3
    4
    5
    6
    @Component
    public class SuperMan {
    @Resource(name = "BMW")
    private Car car;
    }
    // name的作用类似于@Qualifier

@Inject

1
2
3
4
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {}

从元注解@Target注解上看,可以使用在:

  • CONSTRUCTOR 构造函数
  • FIELD 字段、枚举的常量
  • METHOD 方法

@Inject总结

  • 是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject jar包 ,才能实现注入

  • 可以作用CONSTRUCTOR、METHOD、FIELD上

  • 根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named;

    1
    2
    3
    @Inject
    @Named("BMW") // 作用类似于@Qualifier
    private Car car;

总结

  1. @Autowired是Spring自带的,@Resource是JSR250规范实现的,@Inject是JSR330规范实现的
  2. @Autowired、@Inject用法基本一样,不同的是@Inject没有required属性
  3. @Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的
  4. @Autowired如果需要按照名称匹配需要和@Qualifier一起使用,@Inject和@Named一起使用,@Resource则通过name进行指定
  5. 🔗 Spring源码分析@Autowired、@Resource注解的区别

评论