SpringFramework — IOC进阶

小龙 617 2022-05-09

依赖注入-回调注入&延迟注入

一、回调注入

回调的根源:Aware

回调注入的核心是一个叫 Aware 的接口,它来自 SpringFramework 3.1 :

public interface Aware {}

这是一个空接口,底下有一系列子接口
image

比较常用的几个回调接口

接口名 用途
BeanFactoryAware 回调注入 BeanFactory
ApplicationContextAware 回调注入 ApplicationContext(与上面不同,后续 IOC 高级讲解)
EnvironmentAware 回调注入 Environment(后续IOC高级讲解)
ApplicationEventPublisherAware 回调注入事件发布器
ResourceLoaderAware 回调注入资源加载器(xml驱动可用)
BeanClassLoaderAware 回调注入加载当前 Bean 的 ClassLoader
BeanNameAware 回调注入当前 Bean 的名称

这里面大部分接口,其实在当下的 SpringFramework 5 版本中,借助 @Autowired 注解就可以实现注入了,根本不需要这些接口,只有最后面两个,是因 Bean 而异的,还是需要 Aware 接口来帮忙注入。下面咱来演示两个接口的作用,剩余的接口小伙伴们可以自行尝试编写体会一下即可。

ApplicationContextAware的使用

创建Bean

新创建一个 AwaredTestBean ,用来实现这些 Aware 接口。咱先让它实现 ApplicationContextAware 接口:

public class AwaredTestBean implements ApplicationContextAware {
    
    private ApplicationContext ctx;
    
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }
}

这段代码就相当于,当这个 AwaredTestBean 被初始化好的时候,就会把 ApplicationContext 传给它,之后它就可以干自己想干的事了。

二、延迟注入

setter的延迟注入

之前在写 setter 注入时,直接在 setter 中标注 @Autowired ,并注入对应的 bean 即可。如果使用延迟注入,则注入的就应该换成 ObjectProvider :

@Component
public class Dog {
    
    private Person person;
    
    @Autowired
    public void setPerson(ObjectProvider<Person> person) {
        // 有Bean才取出,注入
        this.person = person.getIfAvailable();
    }
}

构造器的延迟注入

构造器的延迟注入与 setter 方式基本相同

@Component
public class Dog {
    
    private Person person;
    
    @Autowired // 可以省略
    public Dog(ObjectProvider<Person> person) {
        // 有Bean才取出,注入
        this.person = person.getIfAvailable();
    }
}

面试题

依赖注入的注入方式

注入方式 被注入成员是否可变 是否依赖IOC框架的API 注入时机 使用场景 支持延迟注入
构造器注入 不可变 否(xml、编程式注入不依赖) 对象创建时 不可变的固定注入
参数注入 不可变 是(只能通过标注注解来侵入式注入) 对象创建后 通常用于不可变的固定注入
setter注入 可变 否(xml、编程式注入不依赖) 对象创建后 可选属性的注入

依赖注入的目的和优点?

  1. 依赖注入作为IOC的实现方式之一,目的是解耦,使用了依赖注入我们就不再需要直接去 new 那些依赖的类对象(直接依赖会导致对象的创建机制、初始化过程难以统一控制);而且如果组件存在多级依赖,依赖注入可以将这些依赖的关系简化,开发之只需要定义好谁依赖谁的标准即可

  2. 依赖注入的另一个特点就是可配置,通过xml或注解声明,可以指定和调整组件注入的对象,借助Java 的多态特性,可以不需要大量批量的修改就可以完成依赖注入对象的替换(面向接口编程与依赖注入配合几乎完美)

Bean常见的几种类型与Bean的作用域

Bean的类型

在 SpringFramework 中,对于 Bean 的类型,一般有两种设计:普通 Bean工厂 Bean

普通Bean

@Component
public class Child {}

或者这样:

@Bean
public Child child() {
    return new Child();
}

亦或者这样:

<bean class="com.linkedbear.spring.bean.a_type.bean.Child"/>

上面这几种方式创建的 Bean 都是普通 Bean

FactoryBean

SpringFramework 考虑到一些特殊的设计:Bean 的创建需要指定一些策略,或者依赖特殊的场景来分别创建,也或者一个对象的创建过程太复杂,使用 xml 或者注解声明也比较复杂。这种情况下,如果还是使用普通的创建 Bean 方式,以咱现有的认知就搞不定了。于是,SpringFramework 在一开始就帮我们想了办法,可以借助 FactoryBean 来使用工厂方法创建对象。

FactoryBean是什么

FactoryBean 本身是一个接口,它本身就是一个创建 Bean 的工厂。如果 Bean 实现了 FactoryBean 接口,则它本身将不再是一个普通的 Bean ,不会在实际的业务逻辑中起作用,而是由创建的对象来起作用。

FactoryBean 接口有三个方法:

public interface FactoryBean<T> {
    // 返回创建的对象
    @Nullable
    T getObject() throws Exception;
    // 返回创建的对象的类型(即泛型类型)
    @Nullable
    Class<?> getObjectType();
    // 创建的对象是单实例Bean还是原型Bean,默认单实例
    default boolean isSingleton() {
        return true;
    }
}

如果 FactoryBean 创建的 Bean 在容器中已经存在了,在项目启动时就会抛出 NoUniqueBeanDefinitionException 异常,提示有两个相同的 Bean 了,说明 FactoryBean 创建的 Bean 是直接放在 IOC 容器中了。

FactoryBean创建Bean的时机

ApplicationContext 初始化 Bean 的时机默认是容器加载时就已经创建,FactoryBean 本身的加载是伴随 IOC 容器的初始化时机一起的,但 FactoryBean 中要创建的 Bean 在FactoryBean创建时是没有加载的,要在使用到的时候才会加载,也就是 FactoryBean 生产 Bean 的机制是延迟生产。

FactoryBean创建Bean的实例数

FactoryBean 接口中有一个默认的方法 isSingleton ,默认是 true ,代表默认是单实例的。可以通过该方法查看创建的Bean是否为单例的

取出FactoryBean本体

直接本体去取,取到的都是 FactoryBean 生产的 Bean 。一般情况下咱也用不到 FactoryBean 本体,但如果真的需要取,使用的方法也很简单:要么直接传 FactoryBean 的 class (很容易理解),也可以传 ID 。不过,如果真的靠传 ID 的话,传配置文件 / 配置类声明的 ID 就不好使了,因为那样只会取出生产出来的 Bean 。取 FactoryBean 的方式,需要在 Bean 的 id 前面加 “&” 符号:

ApplicationContext.getBean("&toyFactory")

BeanFactory与FactoryBean的区别

BeanFactory: SpringFramework中实现IOC最底层的容器,它是一个顶层接口,提供实例化 Bean 和获取 Bean 的基本功能,这个接口我们并不会直接去使用它,而是使用它的子接口 ApllicationContext。

FactoryBean: 创建 Bean 的工厂,可以使用它来直接创建一些初始化流程比较复杂的对象

Bean的作用域

作用域的概念

思考一个问题:为什么会出现多种不同的作用域呢?肯定是它可以被使用的范围不同了。那为什么不都统一成一样的作用范围呢?说白了,资源是有限的,如果一个资源允许同时被多个地方访问(如全局常量),那就可以把作用域提的很高;反之,如果一个资源伴随着一个时效性强的、带强状态的动作,那这个作用域就应该局限于这一个动作,不能被这个动作之外的干扰。这段话理解起来可能有点困难,接下来咱配合着 SpringFramework 的作用域来学习,会更容易理解一些。

SpringFramework中内置的作用域

SpringFramework 中内置了 6 种作用域(5.x 版本):

作用域类型 概述
singleton 一个 IOC 容器中只有一个【默认值】
prototype 每次获取创建一个
request 一次请求创建一个(仅Web应用可用)
session 一个会话创建一个(仅Web应用可用)
application 一个 Web 应用创建一个(仅Web应用可用)
websocket 一个 WebSocket 会话创建一个(仅Web应用可用)

singleton:单实例Bean

SpringFramework 官方文档中有一张图,解释了单实例 Bean 的概念:
image-1652077050774

图中左边的几个定义的 Bean 同时引用了右边的同一个 accountDao ,对于这个 accountDao 就是单实例 Bean 。

SpringFramework 中默认所有的 Bean 都是单实例的,即:一个 IOC 容器中只有一个

prototype:原型Bean

Spring 官方的定义是:每次对原型 Bean 提出请求时,都会创建一个新的 Bean 实例。这里面提到的 ”提出请求“ ,包括任何依赖查找、依赖注入的动作,都算做一次 提出请求 。由此咱也可以总结一点:如果连续 getBean() 两次,那就应该创建两个不同的 Bean 实例向两个不同的 Bean 中注入两次,也应该注入两个不同的 Bean 实例。SpringFramework 的官方文档中也给出了一张解释原型 Bean 的图:
image-1652077225401

图中的 3 个 accountDao 是 3 个不同的对象,由此可以体现出原型 Bean 的意思。

其实对于原型这个概念,在设计模式中也是有对应的:原型模式。原型模式实质上是使用对象深克隆,乍看上去跟 SpringFramework 的原型 Bean 没什么区别,但咱仔细想,每一次生成的原型 Bean 本质上都还是一样的,只是可能带一些特殊的状态等等,这个可能理解起来比较抽象,可以跟下面的 request 域结合着理解。

修改 Bean 的作用域

SpringFramework中提供了一个注解:@Scope

@Component
@Scope("prototype")
public class Toy {}

注意,这个 prototype 不是随便写的常量,而是在 ConfigurableBeanFactory 中定义好的常量:

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";

	String SCOPE_PROTOTYPE = "prototype";
}

原型Bean的创建时机

单实例 Bean 是在 ApplicationContext 被初始化时就已经创建好了;原型 Bean 的创建时机是什么时候需要,什么时候创建。


# IOC