Spring IOC 基础篇

1. IOC依赖查找和依赖注入

1.1 依赖查找:byName和byType

public static void main(String[] args) throws Exception {
    BeanFactory factory = new ClassPathXmlApplicationContext("basic_dl/quickstart-byname.xml");
    // byName
  	Person person = (Person) factory.getBean("person");
    System.out.println(person);
  	// byType
  	Cat cat = factory.getBean(Cat.class);
    System.out.println(cat);
  	// 根据接口类型找到实现类,如果有多个实现类,需要使用到ApplicationContext.getBeansOfType()
  	DemoDao demoDao = factory.getBean(DemoDao.class);
    System.out.println(demoDao.findAll());
}

1.2 依赖注入:在xml中是用ref属性

<bean class="com.linkedbear.spring.basic_dl.b_bytype.dao.impl.DemoDaoImpl"/>

<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>
<bean id="cat" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Cat">
    <property name="name" value="test-cat"/>
    <!-- ref引用上面的person对象 -->
    <property name="master" ref="person"/>
</bean>

1.3 依赖查找中的ofType

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="demoMySQLDao" class="com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoMySQLDao"/>
    <bean id="demoOracleDao" class="com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoOracleDao"/>
    <bean id="demoPostgreDao" class="com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoPostgresDao"/>
</beans>
public class OfTypeApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("basic_dl/quickstart-oftype.xml");
        Map<String, DemoDao> beans = ctx.getBeansOfType(DemoDao.class);
        beans.forEach((beanName, bean) -> {
            System.out.println(beanName + " : " + bean.toString());
        });
    }
}

// demoMySQLDao : com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoMySQLDao@4883b407
// demoOracleDao : com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoOracleDao@7d9d1a19
// demoPostgreDao : com.linkedbear.spring.basic_dl.c_oftype.dao.impl.DemoPostgresDao@39c0f4a

这样就实现了传入一个接口 / 抽象类,返回容器中所有的实现类 / 子类。

1.4 withAnnotation

IOC 容器除了可以根据一个父类 / 接口来找实现类,还可以根据类上标注的注解来查找对应的 Bean 。下面咱来测试包含注解的 Bean 如何被查找。

public class WithAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("basic_dl/quickstart-withanno.xml");
        Map<String, Object> beans = ctx.getBeansWithAnnotation(Color.class);
        beans.forEach((beanName, bean) -> {
            System.out.println(beanName + " : " + bean.toString());
        });
    }
}

1.5 获取IOC容器中的所有Bean:getBeanDefinitionNames

public class BeannamesApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("basic_dl/quickstart-withanno.xml");
        String[] beanNames = ctx.getBeanDefinitionNames();
        // 利用jdk8的Stream快速编写打印方法
        Stream.of(beanNames).forEach(System.out::println);
    }
}

1.6 延迟查找:ObjectProvider

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("basic_dl/quickstart-lazylookup.xml");
        Cat cat = ctx.getBean(Cat.class);
        System.out.println(cat);
        // 下面的代码会报Bean没有定义 NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
    
        // 这一行代码不会报错
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);
      	// 只有调用 dogProvider 的 getObject ,真正要取包装里面的 Bean 时,才会报异常
      	dogProvider.getObject()
    }
}

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

Dog dog = dogProvider.getIfAvailable();
if (dog == null) {
    dog = new Dog();
}

ObjectProvider在jdk8中进行了升级,支持了函数式编程。

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

2. BeanFactory与ApplicationContext

BeanFactory接口提供了一种高级配置机制,能够管理任何类型的对象。ApplicationContext 是 BeanFactory 的子接口。它增加了:

  • 与 SpringFramework 的 AOP 功能轻松集成

  • 消息资源处理(用于国际化)

  • 事件驱动机制

  • 消息与国际化

  • 应用层特定的上下文,例如 Web 应用程序中使用的 WebApplicationContext

ApplicationContext是BeanFactory的子接口,ApplicationContext包含了BeanFactory的所有功能,并且还扩展了很多特性。(子类只做扩展,不改变父类的功能)

3. 注解驱动IOC

在jdk5之前使用xml进行配置bean,在jdk5支持注解之后就支持使用注解配置bean了,对应的就用AnnotationConfigApplicationContext

3.1 @Configuration

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

@Configuration
public class QuickstartConfiguration {

}

在 xml 中,声明 Bean 是通过 标签。

<bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"/>

在配置类中就是:

@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();
}

@Configuration也是bean,会被注册到IOC容器中。

3.2 启动类初始化注解IOC容器

public class AnnotationConfigApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(QuickstartConfiguration.class);
        Person person = ctx.getBean(Person.class);
        System.out.println(person);
    }
}

3.3 依赖注入

@Bean
public Person person() {
    Person person = new Person();
    person.setName("person");
    person.setAge(123);
    return person;
}

@Bean
public Cat cat() {
    Cat cat = new Cat();
    cat.setName("test-cat-anno");
    // 直接拿上面的person()方法作为返回值即可,相当于ref
    cat.setMaster(person());
    return cat;
}

相当于xml中的:

<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>

<bean id="cat" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Cat">
    <property name="name" value="test-cat"/>
    <property name="master" ref="person"/>
</bean>

3.4 组件注册:@Component

SpringFramework 整了几个注解出来,可以帮咱快速注册需要的组件,这些注解被成为模式注解 ( stereotype annotation )

在类上标注 @Component 注解,即代表该类会被注册到 IOC 容器中作为一个 Bean 。

@Component
public class Person {
    
}

相当于 xml 中的:

<bean class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"/>

如果想指定 Bean 的名称,可以直接在 @Component 中声明 value 属性即可:一般不会这么操作的吧

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

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

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

3.5 组件扫描:@ComponentScan

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

在配置类上额外标注一个 @ComponentScan ,并指定要扫描的路径,它就可以扫描指定路径包及子包下的所有 @Component 组件

@Configuration
@ComponentScan("com.linkedbear.spring.annotation.c_scan.bean")
public class ComponentScanConfiguration {
    
}

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

另外,如果不写 @ComponentScan ,也是可以做到组件扫描的。在 AnnotationConfigApplicationContext 的构造方法中有一个类型为 String 可变参数的构造方法:

ApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring.annotation.c_scan.bean");

组件扫描可不是注解驱动 IOC 的专利,对于 xml 驱动的 IOC 同样可以启用组件扫描,它只需要在 xml 中声明一个标签即可:

<context:component-scan base-package="com.linkedbear.spring.annotation.c_scan.bean"/>
<!-- 注意标签是package,不是packages,代表一个标签只能声明一个根包 -->

3.6 注解驱动和xml驱动互通

如果一个应用中,既有注解配置,又有 xml 配置,这个时候就需要由一方引入另一方了。

3.6.1 xml引入注解

在 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/context 
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启注解配置 -->
    <context:annotation-config />
    <bean class="com.linkedbear.spring.annotation.d_importxml.config.AnnotationConfigConfiguration"/>
</beans>

3.6.2 注解引入xml

在注解配置中引入 xml ,需要在配置类上标注 @ImportResource 注解,并声明配置文件的路径:

@Configuration
@ImportResource("classpath:annotation/beans.xml")
public class ImportXmlAnnotationConfiguration {
    
}

4. 依赖注入

4.1 属性注入

4.1.1 setter属性注入

xml方式的setter注入:

<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>

注解方式的setter注入:

@Bean
public Person person() {
    Person person = new Person();
    person.setName("test-person-anno-byset");
    person.setAge(18);
    return person;
}

4.1.2 构造器注入

有一些 bean 的属性依赖,需要在调用构造器(构造方法)时就设置好;或者另一种情况,有一些 bean 本身没有无参构造器,这个时候就必须使用构造器注入了。

xml方式的构造器注入:

在 标签的内部,可以声明一个子标签:constructor-arg ,顾名思义,它是指构造器参数,由它可以指定构造器中的属性,来进行属性注入。

<bean id="person" class="com.linkedbear.spring.basic_di.b_constructor.bean.Person">
    <constructor-arg index="0" value="test-person-byconstructor"/>
    <constructor-arg index="1" value="18"/>
</bean>

注解式构造器属性注入:

@Bean
public Person person() {
    return new Person("test-person-anno-byconstructor", 18);
}

4.1.3 注解式属性注入

@Component下的属性注入:

实现注解式属性注入,可以直接在要注入的字段上标注 @Value 注解:

@Value("black-value-anno")
private String name;

@Value("0")
private Integer order;

外部配置文件引入:@PropertySource

在工程的 resources 目录下新建一个 red.properties ,用于存放 Red 的属性的配置:

red.name=red-value-byproperties
red.order=1

使用时,只需要将 @PropertySource 注解标注在配置类上,并声明 properties 文件的位置,即可导入外部的配置文件:

@Configuration
// 顺便加上包扫描
@ComponentScan("com.linkedbear.spring.basic_di.c_value_spel.bean")
@PropertySource("classpath:basic_di/value/red.properties")
public class InjectValueConfiguration {
    
}

Red类的属性注入:

@Value("${red.name}")
private String name;

@Value("${red.order}")
private Integer order;

对于 xml 中,占位符的使用方式与 @Value 是一模一样的:

<bean class="com.linkedbear.spring.basic_di.c_value_spel.bean.Red">
    <property name="name" value="${red.name}"/>
    <property name="order" value="${red.order}"/>
</bean>

作为一个 properties 文件,它加载到 SpringFramework 的 IOC 容器后,会转换成 Map 的形式来保存这些配置,而 SpringFramework 中本身在初始化时就有一些配置项,这些配置项也都放在这个 Map 中。占位符的取值就是从这些配置项中取

4.2 SpEL表达式

SpEL 全称 Spring Expression Language ,它从 SpringFramework 3.0 开始被支持,它本身可以算 SpringFramework 的组成部分,但又可以被独立使用。它可以支持调用属性值、属性参数以及方法调用、数组存储、逻辑计算等功能。

SpEL 的语法统一用 #{} 表示,花括号内部编写表达式语言。

4.2.1 SpEL属性注入

使用 @Value 配合 SpEL 完成字面量的属性注入,需要额外在花括号内部加单引号:

@Component
public class Blue {
    
    @Value("#{'blue-value-byspel'}")
    private String name;
    
    @Value("#{2}")
    private Integer order;
}

修改启动类,从 IOC 容器中取 Blue 并打印,可以发现字面量被成功注入:

Blue{name='blue-value-byspel', order=2}

4.2.2 Bean属性引用

SpEL 可以取 IOC 容器中其它 Bean 的属性。

@Component
public class Green {
    
    @Value("#{'copy of ' + blue.name}")
    private String name;
    
    @Value("#{blue.order + 1}")
    private Integer order;
}

修改启动类,测试运行,发现 Blue 的属性已经成功被取出了:

use spel bean property : Green{name='copy of blue-value-byspel', order=3}

xml的使用方式:

<bean class="com.linkedbear.spring.basic_di.c_value_spel.bean.Green">
    <property name="name" value="#{'copy of ' + blue.name}"/>
    <property name="order" value="#{blue.order + 1}"/>
</bean>

4.2.3 方法调用

SpEL 表达式不仅可以引用对象的属性,还可以直接引用类常量,以及调用对象的方法等。

@Component
public class White {
    
    @Value("#{blue.name.substring(0, 3)}")
    private String name;
    
    @Value("#{T(java.lang.Integer).MAX_VALUE}")
    private Integer order;
}

注意,直接引用类的属性,需要在类的全限定名外面使用 **T()** 包围。

xml 的方式,同样都是使用 value 属性:

<bean class="com.linkedbear.spring.basic_di.c_value_spel.bean.White">
    <property name="name" value="#{blue.name.substring(0, 3)}"/>
    <property name="order" value="#{T(java.lang.Integer).MAX_VALUE}"/>
</bean>

4.3 自动注入

4.3.1 @Autowired

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

首先,创建几个Bean用于演示:

@Component
public class Person {
    private String name = "administrator";
    // setter
}
@Component
public class Dog {
    
    @Value("dogdog")
    private String name;
    
    private Person person;
    // toString() ......
}

给Dog注入Person的三种方式:

对于 @Autowired 的使用,只需要在属性上标注即可:

@Component
public class Dog {
    // ......
    @Autowired
    private Person person;
}

也可以使用构造器注入方式:

@Component
public class Dog {
    // ......
    private Person person;
    
    @Autowired
    public Dog(Person person) {
        this.person = person;
    }
}

亦可以使用 setter 方法注入

@Component
public class Dog {
    // ......
    private Person person;
    
    @Autowired
    public void setPerson(Person person) {
        this.person = person;
    }
}

4.3.2 注入的Bean不存在

如果IOC容器中没有Person,那么正常情况下程序会抛出异常:

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: {} 

如果出现这种情况下又不想让程序抛异常,就需要在 @Autowired 注解上加一个属性:required = false

@Autowired(required = false)
private Person person;

再次运行启动类,可以发现控制台打印 person=null ,但没有抛出异常:

Dog{name='dogdog', person=null}

4.3.3 @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 注解来进行自动注入了。(其实不用标,SpringFramework 也知道自己得注入了)

4.3.4 多个相同类型Bean的自动注入:@Qualifier,@Primary

假设此时,两个 person (type是一样的),但是一个叫 master ,一个叫 administrator 。

下面咱直接运行测试启动类,可以发现控制台会报这样一个错误:

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: administrator,master

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

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

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

@Bean
@Primary
public Person master() {
    Person master = new Person();
    master.setName("master");
    return master;
}

@Qualifier 不受 @Primary 的干扰。

在 xml 中可以指定 标签中的 primary 属性为 true ,跟上面标注 @Primary 注解是一样的。

另外可以修改注入对象的变量名:

@Autowired
private Person administrator;

4.3.5 @Autowired注入的原理逻辑

先拿属性对应的类型,去 IOC 容器中找 Bean ,如果找到了一个,直接返回;如果找到多个类型一样的 Bean , 把属性名拿过去,跟这些 Bean 的 id 逐个对比,如果有一个相同的,直接返回;如果没有任何相同的 id 与要注入的属性名相同,则会抛出 NoUniqueBeanDefinitionException 异常。

优先级:类型 -> id

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

注入一个用单个对象接收,注入一组对象就用集合来接收

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

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

4.3.7 自动注入的规范:JSR250-@Resource

JSR 全程 Java Specification Requests ,它定义了很多 Java 语言开发的规范,有专门的一个组织叫 JCP ( Java Community Process ) 来参与定制。

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

@Resource 注解相当于标注 @Autowired @Qualifier。

@Component
public class Bird {
    
    @Resource(name = "master")
    private Person person;
}

4.3.8 JSR330-@Inject

JSR330 也提出了跟 @Autowired 一样的策略,它也是按照类型注入

<!-- 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;
}

4.3.9 @Autowired与@Inject对比

@Inject的包名是import javax.inject.Inject; ,@Autowired的包名是import org.springframework.beans.factory.annotation.Autowired;

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

4.4 【面试题】依赖注入的注入方式

注入方式
被注入成员是否可变
是否依赖IOC框架的API
使用场景

构造器注入

不可变

否(xml、编程式注入不依赖)

不可变的固定注入

参数注入

不可变

否(高版本中注解配置类中的 @Bean 方法参数注入可不标注注解)

注解配置类中 @Bean 方法注册 bean

属性注入

不可变

是(只能通过标注注解来侵入式注入)

通常用于不可变的固定注入

setter注入

可变

否(xml、编程式注入不依赖)

可选属性的注入

其实应该划分为两种形式——基于XML注入和基于注解注入,然后再细分为下面的形式:

img

基于XML注入的方式略过,直接看基于注解注入的方式。

通过构造方法注入:

@Service("userService")
public class UserServiceImpl implements UserService {
    
    private UserDao userDao;
    
    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    /**继承自UserService的方法**/
}

根据开发文档的说法,这种只有一个构造方法的情况,自Spring4.3以后,就不再需要添加 @Autowired标注,也可以。但是,如果有多个构造方法时,是必须要对其中一个方法标注 @Autowired,不然Spring会报出异常。

通过setter方法注入:

@Service("userService")
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    /**继承自UserService的方法**/
}

通过字段注入:

Fields are injected right after construction of a bean, before any config methods are invoked. Such a config field does not have to be public.

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    
    /**继承自UserService的方法**/
}

4.5 【面试题】自动注入的注解对比

注解

注入方式

是否支持@Primary

来源

Bean不存在时处理

根据类型注入

SpringFramework原生注解

可指定required=false来避免注入失败

根据名称注入

JSR250规范

容器中不存在指定Bean会抛出异常

根据类型注入

JSR330规范 ( 需要导jar包 )

容器中不存在指定Bean会抛出异常

4.6 复杂类型注入

创建一个复杂的对象:

public class Person {

    private String[] names;
    private List<String> tels;
    private Set<Cat> cats;
    private Map<String, Object> events;
    private Properties props;
    // setter
}

4.6.1 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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="com.linkedbear.spring.basic_di.g_complexfield.bean.Person"></bean>
</beans>

数组注入:

<property name="names">
    <array>
        <value>张三</value>
        <value>三三来迟</value>
    </array>
</property>

list注入:

<property name="tels">
    <list>
        <value>13888</value>
        <value>15999</value>
    </list>
</property>

Set注入:

<!-- 已经提前声明好的Cat -->
<bean id="mimi" class="com.linkedbear.spring.basic_di.g_complexfield.bean.Cat"/>
---

<property name="cats">
    <set>
        <bean class="com.linkedbear.spring.basic_di.g_complexfield.bean.Cat"/>
        <ref bean="mimi"/>
    </set>
</property>

Map注入:

<property name="events">
    <map>
        <entry key="8:00" value="起床"/>
        <!-- 撸猫 -->
        <entry key="9:00" value-ref="mimi"/>
        <!-- 买猫 -->
        <entry key="14:00">
            <bean class="com.linkedbear.spring.basic_di.g_complexfield.bean.Cat"/>
        </entry>
        <entry key="18:00" value="睡觉"/>
    </map>
</property>

Properties注入:

<property name="props">
    <props>
        <prop key="sex">男</prop>
        <prop key="age">18</prop>
    </props>
</property>

4.6.2 注解复杂注入

@Component("miaomiao")
public class Cat {
    private String name = "cat";
}
@Component
public class Person2 {
    
    @Value("#{new String[] {'张三', '张仨'}}")
    private String[] names;
    
    @Value("#{{'333333', '3333', '33'}}")
    private List<String> tels;
    
    // 引用现有的Bean,以及创建新的Bean
    @Value("#{{@miaomiao, new com.linkedbear.spring.basic_di.g_complexfield.bean.Cat()}}")
    private Set<Cat> cats;
    
    @Value("#{{'喵喵': @miaomiao.name, '猫猫': new com.linkedbear.spring.basic_di.g_complexfield.bean.Cat().name}}")
    private Map<String, Object> events;
    
    @Value("#{{'123': '牵着手', '456': '抬起头', '789': '我们私奔到月球'}}")
    private Properties props;
}

4.7 回调注入:Aware

public interface Aware {

}
img

常用的几个回调接口:

接口名
用途

BeanFactoryAware

回调注入 BeanFactory

ApplicationContextAware

回调注入 ApplicationContext(与上面不同,后续 IOC 高级讲解)

EnvironmentAware

回调注入 Environment(后续IOC高级讲解)

ApplicationEventPublisherAware

回调注入事件发布器

ResourceLoaderAware

回调注入资源加载器(xml驱动可用)

BeanClassLoaderAware

回调注入加载当前 Bean 的 ClassLoader

BeanNameAware

回调注入当前 Bean 的名称

这里面大部分接口,其实在当下的 SpringFramework 5 版本中,借助 @Autowired 注解就可以实现注入了,根本不需要这些接口,只有最后面两个,是因 Bean 而异的,还是需要 Aware 接口来帮忙注入。

4.7.1 ApplicationContextAware

public class AwaredTestBean implements ApplicationContextAware {
    
    private ApplicationContext ctx;
    
    public void printBeanNames() {
        Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
    }
    
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }
}

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

4.7.2 BeanNameAware

如果当前的 bean 需要依赖它本身的 name ,使用 @Autowired 就不好使了,这个时候就得使用 BeanNameAware 接口来辅助注入当前 bean 的 name 了。

public class AwaredTestBean implements ApplicationContextAware, BeanNameAware {
    
    private String beanName;
    private ApplicationContext ctx;
    
    public String getName() {
        return beanName;
    }
    
    public void printBeanNames() {
        Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
    }
    
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }
    
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }
}

4.7.3 NamedBean

其实,BeanNameAware 还有一个可选的搭配接口:NamedBean ,它专门提供了一个 getBeanName 方法,用于获取 bean 的 name 。

所以说,如果给上面的 AwaredTestBean 再实现 NamedBean 接口,那就不需要自己定义 getName 或者 getBeanName 方法,直接实现 NamedBean 定义好的 getBeanName 方法即可。

4.8 延迟注入:ObjectProvider

4.8.1 setter的延迟注入

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

4.8.2 构造器的延迟注入

@Component
public class Dog {
    
    private Person person;
    
    @Autowired
    public Dog(ObjectProvider<Person> person) {
        // 如果没有Bean,则采用缺省策略创建
        this.person = person.getIfAvailable(Person::new);
    }
}

构造器的延迟注入效果跟 setter 是一样的,只不过 setter 的注入时机是创建对象,而构造器的注入时机是创建对象

4.8.3 属性字段的延迟注入

属性直接注入是不能直接注入 Bean 的,只能注入 ObjectProvider ,通常也不会这么干,因为这样注入了之后,每次要用这个 Bean 的时候都得判断一次:

@Autowired
private ObjectProvider<Person> person;

@Override
public String toString() {
    // 每用一次都要getIfAvailable一次
    return "Dog{" + "person=" + person.getIfAvailable(Person::new) + '}';
}

4.8.4 【面试题】依赖注入的注入方式-扩展

注入方式
被注入成员是否可变
是否依赖IOC框架的API
注入时机
使用场景
支持延迟注入

构造器注入

不可变

否(xml、编程式注入不依赖)

对象创建时

不可变的固定注入

参数注入

不可变

是(只能通过标注注解来侵入式注入)

对象创建后

通常用于不可变的固定注入

setter注入

可变

否(xml、编程式注入不依赖)

对象创建后

可选属性的注入

4.9【面试题】依赖注入4连问

4.9.1 依赖注入的目的和优点?

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

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

4.9.2 谁把什么注入给谁了?

IOC 容器把需要依赖的对象注入给待注入的组件

4.9.3 依赖注入具体是如何注入的?

4.9.4 使用setter注入还是构造器注入?

  • SpringFramework 4.0.2 及之前是推荐 setter 注入,理由是一个 Bean 有多个依赖时,构造器的参数列表会很长;而且如果 Bean 中依赖的属性不都是必需的话,注入会变得更麻烦

  • 4.0.3 及以后官方推荐构造器注入,理由是构造器注入的依赖是不可变的、完全初始化好的,且可以保证不为 null

  • 当然 4.0.3 及以后的官方文档中也说了,如果真的出现构造器参数列表过长的情况,可能是这个 Bean 承担的责任太多,应该考虑组件的责任拆解

5. Bean

5.1 FactoryBean

Bean的类型:普通 Bean 、工厂 Bean ,普通Bean就不多说了,常见的都是普通Bean。

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

FactoryBean是什么

FactoryBean 本身是一个接口,它本身就是一个创建对象的工厂。

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

public interface FactoryBean<T> {
    // 返回创建的对象
    @Nullable
    T getObject() throws Exception;

    // 返回创建的对象的类型(即泛型类型)
    @Nullable
    Class<?> getObjectType();

    // 创建的对象是单实例Bean还是原型Bean,默认单实例
    default boolean isSingleton() {
        return true;
    }
}

FactoryBean的使用

场景:小孩子要买玩具,由一个玩具生产工厂来给这个小孩子造玩具。

public class Child {
    // 当前的小孩子想玩球
    private String wantToy = "ball";
    
    public String getWantToy() {
        return wantToy;
    }

创建几个玩具,包括一个抽象类和两个实现类:

public abstract class Toy {
    
    private String name;
    
    public Toy(String name) {
        this.name = name;
    }
}

public class Ball extends Toy { // 球
    
    public Ball(String name) {
        super(name);
    }
}

public class Car extends Toy { // 玩具汽车
    
    public Car(String name) {
        super(name);
    }
}

创建一个 ToyFactoryBean 玩具工厂,让它实现 FactoryBean 接口:

public class ToyFactoryBean implements FactoryBean<Toy> {
    
    @Override
    public Toy getObject() throws Exception {
        return null;
    }
    
    @Override
    public Class<Toy> getObjectType() {
        return Toy.class;
    }
}

希望能让它根据小孩子想要玩的玩具来决定生产哪种玩具,那咱就得在这里面注入 Child 。由于咱这里面使用的不是注解式自动注入,那咱就用 setter 注入吧:

public class ToyFactoryBean implements FactoryBean<Toy> {
    
    private Child child;
    
    @Override
    public Toy getObject() throws Exception {
        // 根据 Child 中的 wantToy 属性,来决定创建哪个玩具:
        switch (child.getWantToy()) {
            case "ball":
                return new Ball("ball");
            case "car":
                return new Car("car");
            default:
                // SpringFramework2.0开始允许返回null
                // 之前的1.x版本是不允许的
                return null;
        }
    }
    
    @Override
    public Class<Toy> getObjectType() {
        return Toy.class;
    }
    
    public void setChild(Child child) {
        this.child = child;
    }
}

然后注入工厂类和小孩子的类:

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

@Bean
public ToyFactoryBean toyFactory() {
    ToyFactoryBean toyFactory = new ToyFactoryBean();
    toyFactory.setChild(child());
    return toyFactory;
}

或者xml的方式:

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

<bean id="toyFactory" class="com.linkedbear.spring.bean.a_type.bean.ToyFactoryBean">
  <property name="child" ref="child"/>
</bean>

FactoryBean创建Bean的时机

FactoryBean 本身的加载是伴随 IOC 容器的初始化时机一起的

FactoryBean 生产 Bean 的机制是延迟生产

FactoryBean 默认生成的 Bean 是单实例的

BeanFactory 与 FactoryBean 的区别是什么?

  1. BeanFactory :SpringFramework 中实现 IOC 的最底层容器(此处的回答可以从两种角度出发:从类的继承结构上看,它是最顶级的接口,也就是最顶层的容器实现;从类的组合结构上看,它则是最深层次的容器,ApplicationContext 在最底层组合了 BeanFactory )

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

5.2 Bean的作用域

5.2.1 内置的作用域

作用域类型
概述

singleton

一个 IOC 容器中只有一个【默认值】

prototype

每次获取创建一个

request

一次请求创建一个(仅Web应用可用)

session

一个会话创建一个(仅Web应用可用)

application

一个 Web 应用创建一个(仅Web应用可用)

websocket

一个 WebSocket 会话创建一个(仅Web应用可用)

5.2.2 singleton:单实例Bean

5.2.3 prototype:原型Bean

每次对原型 Bean 提出请求(任何依赖查找、依赖注入的动作)时,都会创建一个新的 Bean 实例。

如果连续 getBean() 两次,那就应该创建两个不同的 Bean 实例;向两个不同的 Bean 中注入两次,也应该注入两个不同的 Bean 实例。

img

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

可以通过**@Scope("prototype")**声明为原型类型。也可以通过常量**ConfigurableBeanFactory.**SCOPE_PROTOTYPE定义。

原型Bean的创建时机:

单实例 Bean 的创建咱已经知道,是在 ApplicationContext 被初始化时就已经创建好了,那这些原型 Bean 又是什么时候被创建的呢?其实也不难想出,它都是什么时候需要,什么时候创建(类似懒加载,饿汉式)。

5.2.4 Web应用的作用域们

  • request :请求Bean,每次客户端向 Web 应用服务器发起一次请求,Web 服务器接收到请求后,由 SpringFramework 生成一个 Bean ,直到请求结束。

  • session :会话Bean,每个客户端在与 Web 应用服务器发起会话后,SpringFramework 会为之生成一个 Bean ,直到会话过期。

  • application :应用Bean,每个 Web 应用在启动时,SpringFramework 会生成一个 Bean ,直到应用停止(有的也叫 global-session )

  • websocket :WebSocket Bean ,每个客户端在与 Web 应用服务器建立 WebSocket 长连接时,SpringFramework 会为之生成一个 Bean ,直到断开连接。

5.3 Bean的实例化方式

实例化指调用构造方法,创建新的对象初始化指创建好新的对象后的属性赋值、组件注入等后续动作。

5.3.1 普通Bean实例化

普通 Bean 的对象,它们默认是单实例的,在 IOC 容器初始化时就已经被初始化了。

5.3.2 FactoryBean创建Bean

注册 Bean 时,只需要注入 FactoryBean ,IOC 容器会自动识别,并默认在第一次获取时创建对应的 Bean 并缓存(针对默认的单实例 FactoryBean )。

5.3.3 静态工厂创建Bean

CarStaticFactory 类,加上静态方法:

public class CarStaticFactory {
    
    public static Car getCar() {
        return new Car();
    }
}

静态工厂的使用通常运用于 xml 方式比较多,创建一个 bean-instantiate.xml 文件,在这里面编写关于静态工厂的使用方法:

<bean id="car1" class="com.linkedbear.spring.bean.c_instantiate.bean.Car"/>
<bean id="car2" class="com.linkedbear.spring.bean.c_instantiate.bean.CarStaticFactory" factory-method="getCar"/>

上面的注册方式是普通的 Bean 注册方式,下面的方式会直接引用静态工厂,并声明要创建对象的工厂方法 factory-method 即可。SpringFramework 会依照这个 xml 的方式,解析出规则并调用静态工厂的方法来创建实际的 Bean 对象。

静态工厂本身不会被注册到 IOC 容器中

如果使用注解方式,由于 SpringFramework 中并没有提供关于静态工厂相关的注解,所以只能使用注解配置类+编程式使用静态工厂了,而这个使用方式相当的简单:

@Bean
public Car car2() {
    return CarStaticFactory.getCar();
}

5.3.4 实例工厂创建Bean

创建一个 CarInstanceFactory 代表实例工厂:

public class CarInstanceFactory {
    
    public Car getCar() {
        return new Car();
    }
}

对于实例工厂,要想调用对象的方法,那自然得先把对象实例化才行了,所以咱就需要先在 xml 中注册实例工厂,随后才能创建真正的目标 Bean :

<bean id="carInstanceFactory" class="com.linkedbear.spring.bean.c_instantiate.bean.CarInstanceFactory"/>
<bean id="car3" factory-bean="carInstanceFactory" factory-method="getCar"/>

可以发现, 标签可以不传入 class 属性,用 factory-bean 和 factory-method 属性也可以完成 Bean 的创建。

编程式使用实例工厂的方式:

@Bean
public Car car3(CarInstanceFactory carInstanceFactory) {
    return carInstanceFactory.getCar();
}

5.4 Bean的生命周期

img

一个对象从被创建,到被垃圾回收,可以宏观的划分为 5 个阶段:

  • 创建 / 实例化阶段:调用类的构造方法,产生一个新的对象。

  • 初始化阶段:对象已经创建好,但还没有被正式使用,可能这里面需要做一些额外的操作(如预初始化数据库的连接池)。

  • 运行使用期:此时对象已经完全初始化好,程序正常运行,对象被使用。

  • 销毁阶段:此时对象准备被销毁,已不再使用,需要预先的把自身占用的资源等处理好(如关闭、释放数据库连接)。

  • 回收阶段:此时对象已经完全没有被引用了,被垃圾回收器回收。

5.4.1 Spring能干预的生命周期阶段:初始化和销毁两个阶段

5.4.2 初始化阶段:init-method和销毁阶段:destroy-method

为了方便演示 xml 与注解的方式,咱创建两个类分别演示,这样咱搞一个 Cat 和一个 Dog 。

public class Cat {
    
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void init() {
        System.out.println(name + "被初始化了。。。");
    }
    public void destroy() {
        System.out.println(name + "被销毁了。。。");
    }
}
public class Dog {
    
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void init() {
        System.out.println(name + "被初始化了。。。");
    }
    public void destroy() {
        System.out.println(name + "被销毁了。。。");
    }
}

xml配置:

在 标签中,有两个属性,就是上面的 init-method 和 destroy-method 。

<bean class="com.linkedbear.spring.lifecycle.a_initmethod.bean.Cat"
      init-method="init" destroy-method="destroy">
  <property name="name" value="mimi"/>
</bean>

注解配置:

@Bean(initMethod = "init", destroyMethod = "destroy")
public Dog dog() {
    Dog dog = new Dog();
    dog.setName("wangwang");
    return dog;
}

5.4.3 初始化销毁方法的要求特征

  • 方法访问权限无限制要求( SpringFramework 底层会反射调用的)。

  • 方法无参数(如果真的设置了参数,SpringFramework 也不知道传什么进去)。

  • 方法无返回值(返回给 SpringFramework 也没有意义)。

  • 可以抛出异常(异常不由自己处理,交予 SpringFramework 可以打断 Bean 的初始化 / 销毁步骤)。

5.4.4 测试

这里接收的类型不再用 ApplicationContext ,而是用实现类本身,目的是为了调用 close 方法对容器进行关闭,以触发 Bean 的销毁动作。

public class InitMethodXmlApplication {
    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("lifecycle/bean-initmethod.xml");
        System.out.println("IOC容器初始化完成。。。");
        
        System.out.println();
        
        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
    }
}

运行 main 方法,控制台会打印如下内容:

准备初始化IOC容器。。。
mimi被初始化了。。。
IOC容器初始化完成。。。

准备销毁IOC容器。。。
mimi被销毁了。。。
IOC容器销毁完成。。。

说明在 IOC 容器初始化之前,默认情况下 Bean 已经创建好了,而且完成了初始化动作;容器调用销毁动作时,先销毁所有 Bean ,最后 IOC 容器全部销毁完成。

5.4.5 Bean的初始化流程顺序探究

上面的编码中,只能看出来 Bean 在 IOC 容器初始化阶段就创建并初始化好,那每个 Bean 的初始化动作又是如何呢?

public class Cat {
    
    private String name;
    
    public Cat() {
        System.out.println("Cat 构造方法执行了。。。");
    }
    
    public void setName(String name) {
        System.out.println("setName方法执行了。。。");
        this.name = name;
    }
}

重新运行启动类的 main 方法,控制台会打印如下内容:(省略销毁部分)

准备初始化IOC容器。。。
Cat 构造方法执行了。。。
setName方法执行了。。。
mimi被初始化了。。。
IOC容器初始化完成。。。

由此可以得出结论:Bean 的生命周期中,是先对属性赋值,后执行 init-method 标记的方法

5.4.6 【重要】JSR250规范

上面的方法,都是咱手动声明注册的 Bean ,对于那些使用模式注解的 Bean ,这种方式就不好使了,因为没有可以让你声明 init-method destroy-method 的地方了,****@Component 注解上也只有一个 value 属性而已。这个时候咱就需要学习一种新的方式,这种方式专门配合注解式注册 Bean 以完成全注解驱动开发,那就是如标题所说的 JSR250 规范

JSR250 规范中除了有 @Resource 这样的自动注入注解,还有负责生命周期的注解,包括 @PostConstruct@PreDestroy 两个注解,分别对应 init-method 和 destroy-method 。

这次咱模拟另一种场景:钢笔与墨水,刚买来的动作代表实例化,加墨水的动作代表初始化,倒掉所有墨水的动作代表销毁,于是这个 Pen 可以这样设计:

@Component
public class Pen {
    
    private Integer ink;
    
    public void addInk() {
        this.ink = 100;
    }
    
    public void outwellInk() {
        this.ink = 0;
    }
    
    @Override
    public String toString() {
        return "Pen{" + "ink=" + ink + '}';
    }
    
    @PostConstruct
    public void addInk() {
        System.out.println("钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    @PreDestroy
    public void outwellInk() {
        System.out.println("钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
}

对于 JSR250 规范的这两个注解的使用,直接标注在 Bean 的方法上即可:

@PostConstruct @PreDestroy 注解标注的方法,与 init-method / destroy-method 方法的声明要求是一样的,访问修饰符也可以是 private 。

5.4.7 JSR250规范与init-method共存

如果不使用 @Component 注解来注册 Bean 而转用 / @Bean 的方式,那 @PostConstruct 与 @PreDestroy 注解是可以与 init-method / destroy-method 共存的。

新建 Pen2 :

public void open() {
    System.out.println("init-method - 打开钢笔。。。");
}

public void close() {
    System.out.println("destroy-method - 合上钢笔。。。");
}

@PostConstruct
public void addInk() {
    System.out.println("@PostConstruct - 钢笔中已加满墨水。。。");
    this.ink = 100;
}

@PreDestroy
public void outwellInk() {
    System.out.println("@PreDestroy - 钢笔中的墨水都放干净了。。。");
    this.ink = 0;
}

之后使用注解式注册这个 Pen2 :

@Configuration
public class JSR250Configuration {

    @Bean(initMethod = "open", destroyMethod = "close")
    public Pen2 pen() {
        return new Pen2();
    }
}

之后修改启动类,驱动这个配置类,观察控制台的打印:

准备初始化IOC容器。。。
@PostConstruct - 钢笔中已加满墨水。。。
init-method - 打开钢笔。。。
IOC容器初始化完成。。。

准备销毁IOC容器。。。
@PreDestroy - 钢笔中的墨水都放干净了。。。
destroy-method - 合上钢笔。。。
IOC容器销毁完成。。。

可以得出结论:@PostConstruct init-method

5.4.8 【重要】InitializingBean&DisposableBean

InitializingBean&DisposableBean实际上是两个接口,而且是 SpringFramework 内部预先定义好的两个关于生命周期的接口。他们的触发时机与上面的 init-method / destroy-method 以及 JSR250 规范的两个注解一样,都是在 Bean 的初始化和销毁阶段要回调的。

依然使用 Pen 作为演示对象,这次咱让 Pen 实现这两个接口:

@Component
public class Pen implements InitializingBean, DisposableBean {
    
    private Integer ink;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("钢笔中已加满墨水。。。");
        this.ink = 100;
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("钢笔中的墨水都放干净了。。。");
        this.ink = 0;
    }
    
    @Override
    public String toString() {
        return "Pen{" + "ink=" + ink + '}';
    }
}

直接使用注解驱动 IOC 容器扫描这个 Pen ,其余编写内容与上面一致:

public class InitializingDisposableAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.linkedbear.spring.lifecycle.c_initializingbean.bean");
        System.out.println("IOC容器初始化完成。。。");
        System.out.println();
        System.out.println("准备销毁IOC容器。。。");
        ctx.close();
        System.out.println("IOC容器销毁完成。。。");
    }
}

运行 main 方法,控制台同样打印了触发生命周期的内容:

准备初始化IOC容器。。。
钢笔中已加满墨水。。。
IOC容器初始化完成。。。

准备销毁IOC容器。。。
钢笔中的墨水都放干净了。。。
IOC容器销毁完成。。。

5.4.9 三种生命周期并存

当一个 Bean 同时使用这三种生命周期共同控制时,执行顺序是怎样的。

public void open() {
    System.out.println("init-method - 打开钢笔。。。");
}

public void close() {
    System.out.println("destroy-method - 合上钢笔。。。");
}

@PostConstruct
public void addInk() {
    System.out.println("@PostConstruct - 钢笔中已加满墨水。。。");
    this.ink = 100;
}

@PreDestroy
public void outwellInk() {
    System.out.println("@PreDestroy - 钢笔中的墨水都放干净了。。。");
    this.ink = 0;
}

@Override
public void afterPropertiesSet() throws Exception {
    System.out.println("InitializingBean - 准备写字。。。");
}

@Override
public void destroy() throws Exception {
    System.out.println("DisposableBean - 写完字了。。。");
}

之后使用注解驱动方式注册这个 Pen :

@Bean(initMethod = "open", destroyMethod = "close")
public Pen pen() {
    return new Pen3();
}

之后让注解 IOC 容器驱动这个配置类,运行 main 方法,观察控制台的打印:

准备初始化IOC容器。。。
@PostConstruct - 钢笔中已加满墨水。。。
InitializingBean - 准备写字。。。
init-method - 打开钢笔。。。
IOC容器初始化完成。。。

准备销毁IOC容器。。。
@PreDestroy - 钢笔中的墨水都放干净了。。。
DisposableBean - 写完字了。。。
destroy-method - 合上钢笔。。。
IOC容器销毁完成。。。

这个顺序又有点怪怪的,咱不要关注那些,总结执行顺序才是最关键的:@PostConstruct InitializingBean init-method

5.4.10 【重要】原型Bean的生命周期

对于原型 Bean 的生命周期,使用的方式跟上面是完全一致的,只是它的触发时机就不像单实例 Bean 那样了。

单实例 Bean 的生命周期是陪着 IOC 容器一起的,容器初始化,单实例 Bean 也跟着初始化(当然不绝对,后面会介绍延迟 Bean );容器销毁,单实例 Bean 也跟着销毁。原型 Bean 由于每次都是取的时候才产生一个,所以它的生命周期与 IOC 容器无关。

@Configuration
public class PrototypeLifecycleConfiguration {
    
    @Bean(initMethod = "open", destroyMethod = "close")
    @Scope(ConfigurableBeanFactroy.SCOPE_PROTOTYPE)
    public Pen pen() {
        return new Pen();
    }
}

编写启动类,只初始化 IOC 容器:

public class PrototypeLifecycleApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                PrototypeLifecycleConfiguration.class);
        System.out.println("IOC容器初始化完成。。。");
    }
}

运行 main 方法,控制台只打印了 IOC容器初始化完成。。。 这一句话,证明原型 Bean 的创建不随 IOC 的初始化而创建

在 main 方法中添加如下几行代码,来获取一次 Pen 实例:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
            PrototypeLifecycleConfiguration.class);
    System.out.println("准备获取一个Pen。。。");
    Pen pen = ctx.getBean(Pen.class);
    System.out.println("已经取到了Pen。。。");
}

运行,控制台打印 Pen 的初始化动作:

准备获取一个Pen。。。
@PostConstruct - 钢笔中已加满墨水。。。
InitializingBean - 准备写字。。。
init-method - 打开钢笔。。。
已经取到了Pen。。。

三种初始化的动作都执行了,证明原型Bean的初始化动作与单实例Bean完全一致

在 main 方法中再添加几行代码,将这个 Pen 销毁掉:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
            PrototypeLifecycleConfiguration.class);
    System.out.println("准备获取一个Pen。。。");
    Pen pen = ctx.getBean(Pen.class);
    System.out.println("已经取到了Pen。。。");
    System.out.println("用完Pen了,准备销毁。。。");
    ctx.getBeanFactroy().destroyBean(pen);
    System.out.println("Pen销毁完成。。。");
}

再次运行 main 方法,发现控制台中只打印了 @PreDestroy 注解和 DisposableBean 接口的执行,没有触发 destroy-method 的执行:

用完Pen了,准备销毁。。。
@PreDestroy - 钢笔中的墨水都放干净了。。。
DisposableBean - 写完字了。。。
Pen销毁完成。。。

得出结论:原型 Bean 在销毁时不处理 destroy-method 标注的方法

5.4.11【面试题】SpringFramework中控制Bean生命周期的三种方式

以下的对比维度不是最终最全的,不过在现在的阶段已经是足够了:

init-method & destroy-method
@PostConstruct & @PreDestroy
InitializingBean & DisposableBean

执行顺序

最后

最先

中间

组件耦合度

无侵入(只在 和 @Bean 中使用)

与 JSR 规范耦合

与 SpringFramework 耦合

容器支持

xml 、注解原生支持

注解原生支持,xml需开启注解驱动

xml 、注解原生支持

单实例Bean

原型Bean

只支持 init-method

到这里,Bean 的基本生命周期就讲解完毕了(恩,仅仅是基本的),更复杂的生命周期知识咱放到后面的 IOC 高级中讲解。

最后更新于