SpringFramework —— IOC理解

小龙 527 2022-05-06

SpringFramework

IOC

IOC,控制反转(Inverse of Control,IOC)。在未引入IOC思想之前,在程序中需要使用其他对象时,必须使用关键字 “new” 去实例化对象。但是使用关键字 “new” 实例化的对象会和程序 强依赖/紧耦合。引入 IOC 思想之后,在运行时通过反射去创建对象,对象实例与程序之间变成了 弱依赖/松散耦合。使用关键字 “new” 主动声明的对象实例,只要编译通过,运行就一定没错;而引入了 IOC 思想后,不会指定实现类,而是由 BeanFactory 去帮助查找需要的对象。若没有找到则会抛出强制异常 “ClassCastException”。

IOC 思想就是将获取对象的方式交给了 BeanFactory 。这种控制权交给别人的思想就称为:控制反转,而根据指定的 对象名称 去获取和创建对象的过程,就称为:依赖查找(Dependency Lookuo,DL)

Spring 简述

  • IOC & AOP:是 SpringFramework 的两大核心,IOC(Inverse of Control) 控制反转、AOP(Aspect Oriented Programming) 面向切面编程
  • 轻量级:它的规模更小、消耗资源更少
  • 一站式:覆盖企业级开发中的所有领域
  • 第三方整合:SpringFramework 可以很方便的整合进其他第三方技术(如:Mybatis、Shiro等)
  • 容器:SpringFramework 的底层有一个管理对象和组件的容器,由它来支撑基于 SpringFramework 构建的应用的运行

如何概述SpringFramework

SpringFramework 是一个开源的、松耦合的、分层的、可配置的一站式企业级 Java 开发框架,它的核心是 IOC 和 AOP,它可以更容易的构建出企业级 Java 应用,并且它可以根据应用开发的组件需求,整合对应的技术。

为什么使用SpringFramework

  • IOC:组件之间的解耦
  • AOP:切面编程可以将应用业务做统一或特定的功能增强,能实现应用于增强逻辑的解耦
  • 容器与事件:管理应用中使用的组件 Bean、托管 Bean 的生命周期、事件与监听器的驱动机制
  • Web、事务控制、测试、与其它技术的整合

SpringFramework 包含的模块

  • beans、core、context、expression
  • aop 【面向切面编程】
  • jdbc 【整合 jdbc】
  • orm 【整合 ORM 框架】
  • tx 【事务控制】
  • web 【Web 层技术】
  • test 【整合测试】
  • … … … …

依赖查找和依赖注入的区别

  • 作用目标不同

    • 依赖注入的作用目标通常是类成员
    • 依赖查找的作用目标可以是方法体内,也可以是方法体外
  • 实现方式不同

    • 依赖注入通常借助一个上下文被动的接收
    • 依赖查找通常主动使用上下文搜索

BeanFactory 和 ApplicationContext

BeanFactory:在 org.springframework.beans 包下
ApplicationContext:在 org.springframework.context 包下

上面这两个包都是 IOC 容器的基础包。BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象、ApplicationContext 是 BeanFactory 的子接口,对 BeanFactory 的功能进行了扩展,新增了:与 AOP 功能的集成、国际化处理、事件发布、应用层特定上下文(WebApplicationContext)

Feature BeanFactory ApplicationContext
Bean instantiation/wiring —— Bean的实例化和属性注入 Yes Yes
Integrated lifecycle management —— 生命周期管理 No Yes
Automatic BeanPostProcessor registration —— Bean后置处理器的支持 No Yes
Automatic BeanFactoryPostProcessor registration —— BeanFactory后置处理器的支持 No Yes
Convenient MessageSource access (for internalization) —— 消息转换服务(国际化) No Yes
Built-in ApplicationEvent publication mechanism —— 事件发布机制(事件驱动) No Yes

BeanFactory与ApplicationContext的对比

BeanFactory 接口提供了一个抽象的配置和对象的管理机制ApplicationContext 是BeanFactory的子接口,它简化了与AOP的整合、消息机制、事件机制。以及对Web环境的扩展(WebApplicationContext等),这些在BeanFactory中是没有的

ApplicationContext注入做了以下几个扩展

  • AOP的支持(AnnotationAwareAspectJAutoProxyCreator 用于与Bean初始化只会)

  • 配置元信息(BeanDefinition、Environment、注解等)

  • 资源管理(Rsource管理)

  • 事件驱动机制(ApplicationEvent,ApplicationListen)

  • 消息与国际化(LocaleResolver)

  • Environment 抽象

ApplicationContext常用API

getBeansWithAnnotation()

  • 根据指定的注解查找对于的 Bean
    getBeanDefinitionNames()
  • 获取当前 IOC 容器中的所有 bean ,获取到的时 Bean Id

延迟查找

对于一些特殊的场景,需要依赖容器中的某些特定的 Bean ,但当它们不存在时也能使用默认 / 缺省策略来处理逻辑

SpringFramework4.3中引入了一个新的Api ObjectProvider ,对应到 ApplicationContext 中的方法就是 getBeanProvider()

getBeanProvider():获取一个 Bean 的时候,先不报错,而是给一个包装类,回头用的时候再拆开看里面有还是没有,当调用getBeanProvider().getObject() 时 bean 不存在才会抛出异常

ObjectProvider 相当于延后了 Bean 的获取时机,也延后了异常可能出现的时机

ObjectProvider 中还有一个方法:getIfAvailable() ,它可以在找不到 Bean 时返回 null 而不抛出异常

随着 SpringFramework 5.0 基于 jdk8 的发布,函数式编程也被大量用于 SpringFramework 中。ObjectProvider 中新加了几个方法,可以使编码更佳优雅。

    Dog dog = dogProvider.getIfAvailable(() -> new Dog());

Or

Dog dog = dogProvider.getIfAvailable(Dog::new);

般情况下,取出的 Bean 都会马上或者间歇的用到,ObjectProvider 还提供了一个 ifAvailable 方法,可以在 Bean 存在时执行 Consumer 接口的方法:

    dogProvider.ifAvailable(dog -> System.out.println(dog)); // 或者使用方法引用

注解驱动IOC与组件扫描

在 xml 驱动的 IOC 容器中,用的是 ClassPathXmlApplicationContext 来加载类路径下的 xml 驱动。对于注解配置的驱动 AnnotationConfigApplicationContext 来加载对于的注解

对比于 xml 文件作为驱动,注解驱动需要的是配置类。一个配置类就可以类似的理解为一个 xml 。配置类没有特殊的限制,只需要在类上标注一个 @Configuration 注解即可。

@Configuration
public class QuickstartConfiguration {

}

在 xml 中,咱声明 Bean 是通过 标签。在配置类中,要想替换掉 标签,自然也能想到,它是使用 @Bean 注解

@Bean
public Person person() {
    return new Person();
}

这种使用方式,可以解释为:向 IOC 容器注册一个类型为 Person ,id 为 person 的 Bean 。方法的返回值代表注册的类型,方法名代表 Bean 的 id 。当然,也可以直接在 @Bean 注解上显式的声明 Bean 的 id ,只不过在注解驱动的范畴里,它不叫 id 而是叫 name :

@Bean(name = "aaa") // 4.3.3之后可以直接写value
public Person person() {
    return new Person();
}

组件注册与组件扫描

通过上面声明的方式,如果需要注册的组件特别多,那编写这些 @Bean 无疑是超多工作量,于是 SpringFramework 中给咱整了几个注解出来,可以帮咱快速注册需要的组件,这些注解被成为模式注解 ( stereotype annotation )

一切组件注册的根源:@Component

一切组件注册的根源:@Component,如果想指定 Bean 的名称,可以直接在 @Component 中声明 value 属性即可:

@Component("aaa")
public class Person { }

如果不指定 Bean 的名称,它的默认规则是 “类名的首字母小写”(例如 Person 的默认名称是 person ,DepartmentServiceImpl 的默认名称是 departmentServiceImpl )。

组件扫描

只声明了组件,咱在写配置类时如果还是只写 @Configuration 注解,随后启动 IOC 容器,那它是感知不到有 @Component 存在的,一定会报 NoSuchBeanDefinitionException 。需要引入一个新的注解:@ComponentScan

@Configuration
@ComponentScan("扫描的路径")
public class ComponentScanConfiguration {
  
}

如果不指定扫描路径,则默认扫描本类所在包及子包下的所有 @Component 组件。

SpringFramework 为了迎合咱在进行 Web 开发时的三层架构,它额外提供了三个注解:@Controller@Service @Repository ,分别代表表现层、业务层、持久层。这三个注解的作用与 @Component 完全一致,其实它们的底层也就是 @Component

IOC驱动方式小结

IOC容器中,有两种驱动方式,分别是:

  1. ClassPathXmlApplicationContext 类路径下xml驱动
  2. AnnotationConfigApplicationContext 注解配置驱动
  • 注解驱动:

    • @Configuration 配置类类似xml文件,加了该注解的类也会被视为bean注册
    • @Bean 方法返回bean
    • @Component 类会被注册为bean
  • 组件扫描:

    • 注解:@ComponentScan 组件扫描,可以指定扫描路径,默认为本类所在包及子包
  • xml:

    • 注解驱动与xml驱动互通:
    • xml需要开启注解配置引入注解
    • 注解配置引入xml需要在配置类标注@ImportResource

依赖注入-自动注入&复杂类型注入

自动注入

xml 中的 ref 属性可以在一个 Bean 中注入另一个 Bean ,注解同样也可以这样做,它可以使用的注解有很多种

@Autowired

在 Bean 中直接在 属性 / 构造方法 / setter 方法 上标注 @Autowired 注解,IOC 容器会按照属性对应的类型,从容器中找对应类型的 Bean 赋值到对应的属性上,实现自动注入。

当我们注入一个不存在的对象时会抛出异常

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.linkedbear.spring.basic_di.d_autowired.bean.Person' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

异常解释:想找一个类型为 Person 的 Bean ,但一个也没找到!

如果出现这种情况下又不想让程序抛异常,就需要在 @Autowired 注解上加一个属性:required = false ,加上之后,程序不会抛出异常,而是将 bean 设置为 null

@Autowired在配置类的使用

@Autowired 不仅可以用在普通 Bean 的属性上,在配置类中,注册 @Bean 时也可以标注:

@Configuration
@ComponentScan("com.linkedbear.spring.basic_di.d_complexfield.bean")
public class InjectComplexFieldConfiguration {

  @Bean
  @Autowired // 高版本可不标注
  public Cat cat(Person person) {
      Cat cat = new Cat();
      cat.setName("mimi");
      cat.setPerson(person);
      return cat;
  }
}

由于配置类的上下文中没有 Person 的注册了(使用了 @Component 模式注解),自然也就没有 person() 方法给咱调,那就可以使用 @Autowired 注解来进行自动注入了。

多个相同类型Bean的自动注入

在容器中存在两个类型相同的 bean 实例时,程序启动时就会抛出异常

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.linkedbear.spring.basic_di.d_complexfield.bean.Person' available: expected single matching bean but found 2: bean名称1,bean名称2

IOC 容器发现有两个类型相同的 Bean ,它也不知道注入哪一个了,索性直接 “我选择死亡” ,就挂了。

@Qualifier:指定注入Bean的名称

@Qualifier 注解的使用目标是要注入的 Bean ,它配合 @Autowired 使用,可以显式的指定要注入哪一个 Bean :

 @Autowired
  @Qualifier("administrator")
  private Person person;

@Primary:默认Bean

@Primary 注解的使用目标是默认被注入的 Bean ,在一个应用中,一个类型的 Bean 注册只能有一个,它配合 @Bean 使用,可以指定默认注入的 Bean :

  @Bean
  @Primary // 可以保证IOC容器中存在多个类型相同的Bean时,默认使用标记有 @Primary 注解的 Bean
  public Person master() {
      Person master = new Person();
      master.setName("master");
      return master;
  }

除了 @Qualifier 可以注入指定名称的Bean ,使用 @Autowired 也可以实现

@Autowired
private Person administrator; // 直接在类后面指定对于的 Bean 名称即可

@Autowired注入的原理逻辑

@Autowired 首先根据对象的类型,去IOC容器中查找指定的 Bean ,如果只找到了一个,直接返回;如果找到多个类型一样的 Bean , 把属性名拿过去,跟这些 Bean 的 id 逐个对比,如果有一个相同的,直接返回;如果没有任何相同的 id 与要注入的属性名相同,去查找使用了 @Primary 的注解,找到了就直接返回,没有找到就抛出 NoUniqueBeanDefinitionException 异常。

多个相同类型Bean的全部注入

上面都是注入一个 Bean 的方式,通过两种不同的办法来保证注入的唯一性。但如果需要一下子把所有指定类型的 Bean 都注入进去应该怎么办呢?其实答案也挺简单的,注入一个用单个对象接收,注入一组对象就用集合来接收:

@Component
public class Dog {
  // ......
  
  @Autowired
  private List<Person> persons;

如上就可以实现一次性把所有的 Person 都注入进来

JSR250-@Resource

@Resource 也是用来属性注入的注解,它与 @Autowired 的不同之处在于:@Autowired 是按照类型注入,@Resource 是直接按照属性名 / Bean的名称注入。

这个 @Resource 注解相当于标注 @Autowired 和 @Qualifier 了!实际开发中,@Resource 注解也是用的很多的,可以根据情况来进行选择。

JSR330-@Inject

JSR330 也提出了跟 @Autowired 一样的策略,它也是按照类型注入。不过想要用 JSR330 的规范,需要额外导入一个依赖:

<!-- jsr330 -->
<dependency>
  <groupId>javax.inject</groupId>
  <artifactId>javax.inject</artifactId>
  <version>1</version>
</dependency>

剩下的使用方式就跟 SpringFramework 原生的 @Autowired + @Qualifier 一样了:

@Component
public class Cat {
  
  @Inject // 等同于@Autowired
  @Named("admin") // 等同于@Qualifier
  private Person master;
}

@Autowired与@Inject对比

如果万一项目中没有 SpringFramework 了,那么 @Autowired 注解将失效,但 @Inject 属于 JSR 规范,不会因为一个框架失效而失去它的意义,只要导入其它支持 JSR330 的 IOC 框架,它依然能起作用。

依赖注入的注入方式

注入方式 被注入成员是否可变 是否依赖IOC框架的API 使用场景
构造器注入 不可变 否(xml、编程式注入不依赖) 不可变的固定注入
参数注入 不可变 否(高版本中注解配置类中的 @Bean 方法参数注入可不标注注解) 注解配置类中 @Bean 方法注册 bean
属性注入 不可变 是(只能通过标注注解来侵入式注入) 通常用于不可变的固定注入
setter注入 可变 否(xml、编程式注入不依赖) 可选属性的注入

自动注入的注解对比

注解 注入方式 是否支持@Primary 来源 Bean不存在时处理
@Autowired 根据类型注入 SpringFramework原生注解 可指定required=false来避免注入失败
@Resource 根据名称注入 JSR250规范 容器中不存在指定Bean会抛出异常
@Inject 根据类型注入 JSR330规范 ( 需要导jar包 ) 容器中不存在指定Bean会抛出异常

@Qualifier :如果被标注的成员/方法在根据类型注入时发现有多个相同类型的 Bean ,则会根据该注解声明的 name 寻找特定的 bean

@Primary :如果有多个相同类型的 Bean 同时注册到 IOC 容器中,使用 “根据类型注入” 的注解时会注入标注 @Primary 注解的 bean


# IOC