您现在的位置是:首页 >技术教程 >从零开始 Spring Boot 27:IoC网站首页技术教程

从零开始 Spring Boot 27:IoC

魔芋红茶 2024-06-12 18:01:02
简介从零开始 Spring Boot 27:IoC

从零开始 Spring Boot 27:IoC

自从开始学习和从事Spring Boot开发以来,一个一直让我很迷惑的问题是IoC和Bean到底是什么东西。这个问题一直到我翻阅完Spring开发文档Core Technologies (spring.io)后才真正得到解惑。

虽然中文互联网上关于IoC的文章很多,但很少有和官方文档那样从零开始讲解并且非常全面的文章。所以学习Spring及相关技术的最好途径依然是官方文档。缺陷是篇幅很长…

关于IoC,Spring的官方文档已经介绍的相当全面了,但对从Spring Boot开始学习和接触Spring技术的开发者并不友好,因为其中相当一部分内容是关于基于XML配置的,这是因为Spring最早就是从这一途径发展起来的,而Spring Boot中更流行的基于注解的配置反而是后来的技术路线。所以,我打算在这篇文章中,基于Spring Boot的纯注解的配置来阐述Spring的IoC相关技术。

IoC容器

IoC技术的主要目的是通过提供一系列配置,让Spring框架来代替“我们”在必要时候创建对象。这样做的好处在于我们不需要考虑维护对象的生命周期,以及处理对象复杂的依赖关系。

container magic

图源:Core Technologies (spring.io)

而这其中最重要的是IoC容器,在使用Spring框架的时候,实际上大多数对象都由IoC容器创建。而这些由IoC容器创建的对象也叫做Spring Bean,这点在后边会进行说明。

IoC创建对象需要下面这几个步骤:

  1. 提供配置信息(XML或注解方式)给IoC容器。
  2. IoC容器根据配置信息生成Spring Bean的创建定义。
  3. 当需要某个对象的时候,IoC容器根据需要返回该对象,或者根据创建定义新建一个对象并返回。

下面通过实际示例演示如何在Spring中实现上边的步骤。这里我采用一个空白的Spring Boot项目进行演示。

如果不清楚如何构建一个Spring Boot项目,可以阅读从零开始Spring Boot 1:快速构建 - 红茶的个人站点 (icexmoon.cn)

从XML加载 Bean

IoC容器大致分为两种,从XML加载Bean的容器和从注解加载Bean的容器。当然,前者也可以在创建后以注解方式加载Bean,后者也可以在创建后从XML中加载Bean。但总之,IoC容器一般都会选择一种加载Bean的方式为主。

Spring框架提供一个IoC容器实现ClassPathXmlApplicationContext,可以用它以XML方式加载Bean定义和创建Bean对象。

假设这里我们定义两个类,分别作为简单的计算器和科学计算器:

package com.example.ioc;

public class Calculator {
    private double result = 0;

    public double add(double num) {
        return result += num;
    }

    public double subtract(double num) {
        return result -= num;
    }

    /**
     * 重置计算器
     * @return
     */
    public double reset() {
        result = 0;
        return result;
    }

    /**
     * 返回当前计算器运算结果
     * @return
     */
    public double print(){
        return result;
    }
    
    public double multiply(double num) {
        return result *= num;
    }
}
package com.example.ioc;

import javax.naming.OperationNotSupportedException;

public class SupperCalculator {
    private double result = 0;
    private Calculator calculator;

    /**
     * 次方运算
     *
     * @param num n次方
     * @return
     */
    public double power(double num) {
        calculator.reset();
        if (num < 0) {
            throw new RuntimeException(new OperationNotSupportedException());
        }
        if (num == 0) {
            return result = 1;
        }
        calculator.add(result);
        for (int i = 0; i < num - 1; i++) {
            calculator.multiply(result);
        }
        return result = calculator.print();
    }

    public double reset(){
        return result = 0;
    }

    public double print(){
        return result;
    }

    public void setCalculator(Calculator calculator) {
        this.calculator = calculator;
    }

    public double add(double num){
        calculator.reset();
        return result = calculator.add(num);
    }
}

创建一个XML文件来描述这两个类对应的Spring 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">
    <bean id="calculator" class="com.example.ioc.Calculator"/>
    <bean id="supperCalculator" class="com.example.ioc.SupperCalculator">
        <property name="calculator" ref="calculator"/>
    </bean>
</beans>

将该文件命名为app.xml并保存在resources目录下。

  • 本文依然会以注解为主进行介绍,这里只是展示一下XML配置方式下的IoC容器,并不会深入介绍,感兴趣的可以自行阅读核心技术 (springdoc.cn)
  • 值得注意的是,这里用于演示的两个类存在依赖关系,SupperCalculator中的属性calculator是一个Calculator类型的对象,并且该对象是对另一个Bean的引用。在XML配置中,通过ref属性进行表示。

在Spring应用的入口代码中实例化一个IoC容器,并从XML中加载Bean定义,然后创建相应的Bean对象:

package com.example.ioc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
        xmlApplicationContextTest();
        SpringApplication.run(IocApplication.class, args);
    }

    private static void xmlApplicationContextTest(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:app.xml");
        Calculator calculator = ctx.getBean(Calculator.class);
        calculator.add(1);
        calculator.add(2);
        System.out.println(calculator.print());
        SupperCalculator supperCalculator = ctx.getBean(SupperCalculator.class);
        supperCalculator.add(2);
        supperCalculator.power(3);
        System.out.println(supperCalculator.print());
    }

}

这里的ApplicationContext是一个更底层的IoC容器接口,一般用它来作为一个具体IoC容器实现的句柄。可以通过ApplicationContext.getBean()方法从IoC容器中获取Bean实例,该方法有多个重载版本,可以按需要根据Bean的名称或者类型来获取Bean。

从注解加载 Bean

类似的,Spring框架中有一个类AnnotationConfigApplicationContext,可以用它来创建基于注解方式加载Bean的IoC容器。

package com.example.ioc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
//        xmlApplicationContextTest();
        ApplicationContext ctx = new AnnotationConfigApplicationContext(Calculator.class, SupperCalculator.class);
        ctxTest(ctx);
        SpringApplication.run(IocApplication.class, args);
    }

    private static void xmlApplicationContextTest() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:app.xml");
        ctxTest(ctx);
    }

    private static void ctxTest(ApplicationContext ctx){
        Calculator calculator = ctx.getBean(Calculator.class);
        calculator.add(1);
        calculator.add(2);
        System.out.println(calculator.print());
        SupperCalculator supperCalculator = ctx.getBean(SupperCalculator.class);
        supperCalculator.add(2);
        supperCalculator.power(3);
        System.out.println(supperCalculator.print());
    }

}

这里重构了一下之前的示例代码,以展示如何使用AnnotationConfigApplicationContext

实现的方式相当简单,直接在AnnotationConfigApplicationContext的构造方法中加入相应的Class类即可。这样IoC容器就可以从相应的类文件中读取信息并生成相应的Bean定义,以用于后续Bean对象的创建。

需要注意的是,因为这里的两个示例类之间存在依赖关系,所以这里还需要以注解的方式进行依赖注入

package com.example.ioc;
// ...
public class SupperCalculator {
	// ...
    @Autowired
    public void setCalculator(Calculator calculator) {
        this.calculator = calculator;
    }
    // ...
}

这里以给setCalculator方法添加@Autowired注解的方式实现依赖注入,后边会对几种依赖注入方式进行说明。

自动扫描

显然,如果创建应用时都要像上面示例中的那样将所有需要注册为Spring Bean的类添加到IoC容器中将会相当麻烦。所以Spring框架提供一种方式以对特定目录进行扫描,并将符合条件的类自动注册到IoC容器。

这里先创建一个配置类AppConfig

package com.example.ioc;

// ...
@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
}

这里@ComponentScan注解可以实现自动扫描,其中的basePackages属性可以指定扫描的包名,该包名下的以@Component注解标记的类都会被注册到IoC容器。

实际上@Configuration注解就包含@Component注解,所以@Configuration标记的类也会被自动扫描。类似的注解还有@Repository@Service@Controller

给需要的类添加上@Component注解以自动扫描:

package com.example.ioc;
// ...
@Component
public class Calculator {
	// ...
}
package com.example.ioc;
// ...
@Component
public class SupperCalculator {
	// ...
}

修改入口代码,在创建IoC容器时加载AppConfig类:

package com.example.ioc;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
//        xmlApplicationContextTest();
//        annotationApplicationContextTest();
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        ctxTest(ctx);
//        SpringApplication.run(IocApplication.class, args);
    }

	// ...
}

这里注释掉了Spring Boot应用的启动代码SpringApplication.run(IocApplication.class, args);,因为会报错,可能是容器组件之间存在冲突,简单注释掉就不报错了,不影响对IoC内容的理解和学习。

除了这里展示的,自动扫描还可以添加更复杂的功能,以按照特定名称规则来添加或者排除类。这里不做过多说明,可以自行阅读官方文档核心技术 (springdoc.cn)

当然,在实际开发时,我们通常不需要自行添加@ComponentScan注解,只需要简单给需要注册到Spring Boot的IoC容器的类定义添加上@Component注解即可,这是因为Spring Boot的核心注解@SpringBootApplication本身就包含@ComponentScan注解:

package org.springframework.boot.autoconfigure;
// ...

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
	// ...
}

因此,默认情况下Spring Boot框架会自动扫描@SpringBootApplication注解标记的入口类目录和子目录下的所有@Component注解标记的类。

依赖注入

在前边我提到过,如果Spring Bean之间有依赖关系,就要用某种方式体现。具体来说,如果是以XML方式生成Bean定义,就要在XML中的property节点的ref属性中指定依赖的另一个Bean的名称。对于更常见的注解方式,则需要使用@Autowired注解来完成依赖注入,下面详细说明如何实现。

依赖注入(dependency injection DI),指的是将另一个IoC中的Spring Bean实例以某种方式注入到当前Spring Bean的属性中,以实现两个Spring Bean之间的依赖(关联)关系。

事实上通过注解方式实现依赖注入有三种途径:

  • 通过属性注入
  • 通过构造器注入
  • 通过setter注入

这三种方式略有一些区别,下面一一说明。

通过setter注入

这也是之前演示的方式:

package com.example.ioc;
// ...
@Component
public class SupperCalculator {
	// ...
    @Autowired
    public void setCalculator(Calculator calculator) {
        this.calculator = calculator;
    }
	// ...
}

具体来说,我们需要给需要注入的属性对应的Setter方法添加上@Autowired注解。

@Autowired注解的作用对象是方法或属性。

通过这种方式实现的注入,从语义上更倾向于可能存在的关联关系。这是因为以这种方式实现的依赖注入,实际上可以是缺少实际依赖的,这里以实际示例说明。

假设我们的IoC容器中并没有注册Calculator对应的Spring Bean。要实现这点很容易,删除对应的注解,让自动扫描排除Calculator类:

//@Component
public class Calculator {
	// ...
}

这样我们的IoC容器中就不存在Calculator类型的Spring Bean了。但如果你此时运行示例代码,就会出现一个报错:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'supperCalculator': Unsatisfied dependency expressed through method 'setCalculator' parameter 0: No qualifying bean of type 'com.example.ioc.Calculator' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

错误说明中说supperCalculator类的setCalculator方法需要至少一个Calculator类型的Bean,这和我们的设想有所出入。实际上这里还需要给@Autowired添加一个属性required,来表示这里是一个可有可无的依赖注入:

package com.example.ioc;
// ...
@Component
public class SupperCalculator {
	// ...
    @Autowired(required = false)
    public void setCalculator(Calculator calculator) {
        this.calculator = calculator;
    }

	// ,,,
}

默认情况下required的值是true

当然,还需要注释掉Calculator类对应的测试代码:

package com.example.ioc;
// ...
@SpringBootApplication
public class IocApplication {

    // ...

    private static void ctxTest(ApplicationContext ctx){
//        Calculator calculator = ctx.getBean(Calculator.class);
//        calculator.add(1);
//        calculator.add(2);
//        System.out.println(calculator.print());
        SupperCalculator supperCalculator = ctx.getBean(SupperCalculator.class);
        supperCalculator.add(2);
        supperCalculator.power(3);
        System.out.println(supperCalculator.print());
    }

}

再次运行就能出现类似下面的报错:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.example.ioc.Calculator.reset()" because "this.calculator" is null

也就是说IoC容器在创建SupperCalculator对象时,本应当调用setCalculator方法注入一个Calculator类型的Bean实例,但因为当前IoC容器中并不存在对应的实例,所以就没有调用该方法进行依赖注入。显而易见的是最终创建的SupperCalculator对象的calculator属性为null,因此在后续调用时产生了这个null引用异常

这么做是有意义的,比如我们可以判断是否真的存在相应依赖,以实现不同方式的处理逻辑:

package com.example.ioc;
// ...
@Component
public class SupperCalculator {
    private double result = 0;
    private Calculator calculator;

    /**
     * 次方运算
     *
     * @param num n次方
     * @return
     */
    public double power(double num) {
        if (calculator == null) {
            //缺少依赖,用数学函数完成计算
            return result = Math.pow(result, num);
        }
        //存在依赖,利用简单计算器累加实现
        calculator.reset();
        if (num < 0) {
            throw new RuntimeException(new OperationNotSupportedException());
        }
        if (num == 0) {
            return result = 1;
        }
        calculator.add(result);
        for (int i = 0; i < num - 1; i++) {
            calculator.multiply(result);
        }
        return result = calculator.print();
    }

    // ...

    public double add(double num) {
        if (calculator == null) {
            //缺少依赖,直接实现加法
            return result += num;
        }
        //存在依赖,利用简单计算器实现加法
        calculator.reset();
        calculator.add(result);
        calculator.add(num);
        return result = calculator.print();
    }
}

再次运行就不会报错了,即使缺少相应依赖,代码任然可以正常执行。更妙的是,如果存在依赖,代码就会利用相应的Bean完成业务代码。

这里更优雅的方式是将简单计算器类Calculator的关键方法抽象成一个接口,具体的Calculator类仅作为接口的一个实现,如果不存在相应的Bean,可以创建一个默认的实例作为缺省实现。

通过属性注入

这也是最为广泛应用的一种方式:

package com.example.ioc;
// ...
@Component
public class SupperCalculator {
    private double result = 0;
    @Autowired
    private Calculator calculator;

    // ...

//    @Autowired(required = false)
//    public void setCalculator(Calculator calculator) {
//        this.calculator = calculator;
//    }
	// ...
}

通过属性注入并不需要有对应的Setter方法,因为具体的赋值操作是Spring框架通过反射实现的。

如果IoC容器中缺少Calculator类型的Bean,运行时就会产生一个错误:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'supperCalculator': Unsatisfied dependency expressed through field 'calculator': No qualifying bean of type 'com.example.ioc.Calculator' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

内容和之前的类似,都是因为缺少相应的依赖导致SupperCalculator类型的Bean创建失败。

实际上这个错误可以通过@Autowired(required = false)进行规避,但并不推荐这么做,一般而言通过属性注入都被认为是必须存在的依赖,可有可无的依赖通过Setter方法注入。

通过构造器注入

实际上,如果你的Bean对应的类只有一个构造器,那么通过该构造器注入的依赖会自动实现,你不需要做任何事:

@Component
public class SupperCalculator {
    private double result = 0;
    private Calculator calculator;

    public SupperCalculator(Calculator calculator) {
        this.calculator = calculator;
    }
    // ...
}

这是很自然而然的事情,显然,IoC容器在创建SupperCalculator对象的时候,会先找到一个Calculator类型的Bean实例,然后作为SupperCalculator构造器的参数传入并创建相应的对象。

通过构造器实现注入的一个额外好处是,我们可以将相应的属性声明为final,以确保一定会被正确赋值:

@Component
public class SupperCalculator {
    private double result = 0;
    private final Calculator calculator;

    public SupperCalculator(Calculator calculator) {
        this.calculator = calculator;
    }
	// ...
}

通过属性注入是不能这样做的,因为无法通过Java的静态代码检查,编译器会认为相应属性是final类型,但并没有在构造器中进行初始化,这是一个语法错误。

这也是为什么Spring官方更推荐通过构造器注入而不是通过属性注入

这也是通过属性注入时IDE会存在相应提醒的原因。

如果存在多个构造器,你就需要指定一个构造器并使用@Autowired注解指名IoC用于创建Bean实例时需要使用的构造器,比如:

@Component
public class SupperCalculator {
    private double result = 0;
    private final Calculator calculator;

    @Autowired
    public SupperCalculator(Calculator calculator) {
        this.calculator = calculator;
    }

    public SupperCalculator(Calculator calculator, double result){
        this.calculator = calculator;
        this.result = result;
    }
    // ...
}

实际上以注解的方式通过构造器注入存在一个问题,因为Java语法本身不支持函数参数默认值,导致如果构造器中存在一些基本类型的参数,就无法让IoC容器在调用时传递相应的值。在XML方式定义中则可以通过配置相应的预设值来解决。但大多数情况下我们不会遇到或者可以规避该问题,所以这里不做过多解释。

此外,还可以通过@Autowired指定多个构造器作为IoC创建Bean实例时的候选,IoC容器会选择一个可用的最长参数列表的构造器进行实例创建。这种方式很少见,这里不做演示。

最后做个总结,对于必须存在的依赖,我们应当使用属性注入构造器注入,并且更推荐使用构造器注入的方式。对于可有可无的依赖,使用setter注入

当然,不能教条地遵循任何建议,比如日常开发中因为效率和dead line的关系,代码中充斥着大量的属性注入,或者存在一个参数列表巨长的构造器注入(通常被认为设计有问题,需要重构),如果是必要的妥协,那么就让它存在下去。

实际上这种妥协出现的概率在我的工作中高达100%…

无论如何,会不会做与是否知道最佳实践是两码事,后者依然重要

Bean

实际上,在学习Spring一开始,我就被各种Bean概念弄得一头雾水。在学习Java时,我们知道如果一个类有属性,且存在Setter和Getter方法,那这可以被认为是一个Java Bean。实际上,在Spring框架中,我们通常谈到的Bean是指Spring Bean

而所谓的Spring Bean,实际上是指由IoC管理的类定义,这些通过XML或注解方式注册到IoC容器中的类定义(Bean Define),会在需要的时候被创建成对应的实例。

之前的示例已经展示了如何通过自动扫描和@Component注解向IoC容器注册Bean,并通过ApplicationContext.getBean()方法从IoC容器中获取Bean实例。

每个Bean都有一个在当前IoC容器中唯一的名称,默认情况下该名称是对应类的首字母小写形式,比如SupperCalculator类的Bean名称就是supperCalculator,我们可以通过名称获取Bean实例:

SupperCalculator supperCalculator = (SupperCalculator) ctx.getBean("supperCalculator");

显然,通过名称获取实例无法利用泛型实现自动类型转换,所以getBean方法返回的是一个Object对象,这里需要类型强制转换。

有一点我们要清楚地认识——类定义Bean定义是两码事,虽然后者是利用前者生成的。实际上通过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">
    <bean id="calculator" class="com.example.ioc.Calculator"/>
    <bean id="supperCalculator" class="com.example.ioc.SupperCalculator">
<!--        <property name="calculator" ref="calculator"/>-->
        <constructor-arg ref="calculator"/>
    </bean>
    <bean id="supperCalculator1" class="com.example.ioc.SupperCalculator">
        <constructor-arg ref="calculator"/>
    </bean>
    <bean id="supperCalculator2" class="com.example.ioc.SupperCalculator">
        <constructor-arg ref="calculator"/>
    </bean>
    <bean id="supperCalculator3" class="com.example.ioc.SupperCalculator">
        <constructor-arg ref="calculator"/>
    </bean>
</beans>

在这个示例中,通过XML向IoC容器中添加了4个SupperCalculator类型的Bean,其名称分别是supperCalculatorsupperCalculator1supperCalculator2supperCalculator3

但如果是通过注解的方式,就需要利用配置类的@Bean方法实现:

package com.example.ioc;
// ...
@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
    @Bean
    public SupperCalculator supperCalculator1(Calculator calculator) {
        return new SupperCalculator(calculator);
    }

    @Bean
    public SupperCalculator supperCalculator2(Calculator calculator) {
        return new SupperCalculator(calculator);
    }

    @Bean
    public SupperCalculator supperCalculator3(Calculator calculator) {
        return new SupperCalculator(calculator);
    }
}

@Configuration标记的配置类中,使用@Bean注解标记的方法可以将对应的返回值注册到IoC中作为Bean定义存在。这种情况下产生的Bean的名称就是@Bean注解的方法的名称。比如这里的示例生成的三个Bean名称分别是supperCalculator1supperCalculator2supperCalculator3

这种方式可以看作是利用工厂方法来创建Bean,在这里supperCalculator1supperCalculator2supperCalculator3实际上就是工厂方法。

此外,@Bean标记的方法在调用时,IoC会自动向其中的参数进行相应的注入,也就是说,在IoC容器调用supperCalculator1方法创建名称为supperCalculator1的Bean的实例时,会自动查找一个类型为Calculator的Bean实例注入到supperCalculator1方法的实参中,这样就实现了依赖关系。

如果不想使用方法名,也可以通过@Bean注解指定名称:

@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
    @Bean("sc1")
    public SupperCalculator supperCalculator1(Calculator calculator) {
        return new SupperCalculator(calculator);
    }
    // ...
}

实际上注解@Beanvalue属性可以接收一个字符串数组,所以可以指定多个值,第一个是Bean名称,后续的则作为Bean的别名:

    @Bean({"sc1","supperCalculator1"})
    public SupperCalculator supperCalculator1(Calculator calculator) {
        return new SupperCalculator(calculator);
    }

实际上,创建Bean是@Configuration标记的配置类的主要工作。

除此之外,@Configuration标记的配置类的@Bean方法存在一些特殊性,比如下面这个示例:

package com.example.ioc;
// ...
@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
    @Bean({"sc1", "supperCalculator1"})
    public SupperCalculator supperCalculator1() {
        Calculator calculator = calculator();
        System.out.println(calculator);
        return new SupperCalculator(calculator);
    }

    @Bean("sc2")
    public SupperCalculator supperCalculator2() {
        Calculator calculator = calculator();
        System.out.println(calculator);
        return new SupperCalculator(calculator);
    }

    @Bean("sc3")
    public SupperCalculator supperCalculator3() {
        Calculator calculator = calculator();
        System.out.println(calculator);
        return new SupperCalculator(calculator);
    }

    @Bean("cal")
    public Calculator calculator() {
        return new Calculator();
    }
}

在这个示例中supperCalculator1等方法没有从参数中接收依赖,而是通过calculator()方法创建一个Calculator对象来实现依赖关系,只不过``calculator()方法本身是一个@bean方法,会在IoC中生成一个名称为cal`的Bean。

特别的是,示例中supperCalculator1等方法会分别打印calculator()方法返回的对象,实际运行代码会看到类似下面的输出:

com.example.ioc.Calculator@77192705
com.example.ioc.Calculator@77192705
com.example.ioc.Calculator@77192705

这说明IoC容器在调用supperCalculator1等方法时,通过calculator()方法获取到的是同一个Calculator对象,这与我们通常的理解是不符的,本来应该每次new都产生一个新的对象才对。

实际上这是因为Spring框架对@Configuration标记的配置类进行了特殊处理,当在这种类中调用自身的@Bean方法时,会进行CGLIB代理,实际上并不是真正调用对应方法,而是从IoC容器中获取对应@Bean方法产生的Bean实例。因为默认是单例作用域,所以获取到的是同一个Calculator实例。

在实例应用中,会在配置类中见到大量的自身方法调用,也正是因为上面的原因,它们实际上都是获取的Bean实例,因此并不存在重复构建的问题。

如果你不想某个配置类在调用自己的方法时使用代理,可以通过@ConfigurationproxyBeanMethods属性关闭:

@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	//...
}

此时你会观察到调用每次调用calculator()方法都会产生一个新的实例,就像一般的Java程序表现的那样。

除了@Configuration标记的类,实际上@Bean方法也可以在其他的@Component类中使用,只不过这些类不会被特殊处理,不存在自身方法调用会通过CGLIB代理实现的问题,因此一般不会用@Configuration以外的类进行配置工作。

除了常见的在@Configuration类中定义的@Bean方法外,还可以在接口中定义@Bean方法,并被@Configuration类实现,这样同样可以创建Spring Bean:

public interface AppConfigImpl {
    @Bean("cal2")
    default Calculator calculator2(){
        return new Calculator();
    }
}

@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	//...
}

接口中的@Bean方法是default的,这是自然而然的,因为要实现方法体内的代码,而不是一个空的接口方法。

继承的影响

使用@bean方法时有一点容易被忽视——如果返回的不是具体类型而是一个底层类型,可能导致依赖注入的失败。比如:

public class Vehicle {
}

public class Car extends Vehicle{
}

public class Bus extends Car{
}

@Configuration()
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	// ...
    @Bean
    public Car car(){
        return new Car();
    }
}

@Service
public class HelloService {
    @Autowired
    private Car car;
	// ...
}

这样是没有问题的,因为IoC容器知道有一个类型为Car的bean,甚至你可以在注入时选择一个更底层的类型,同样可以正常查找到bean:

@Service
public class HelloService {
	// ...
	@Autowired
    private Vehicle car;
    // ...
}

但如果@bean方法返回的是一个更底层的类型:

@Configuration()
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	// ...
	@Bean
    public Vehicle car(){
        return new Car();
    }
}

@Service
public class HelloService {
    @Autowired
    private ClientMessage clientMessage;
    @Autowired
    private Car car;
	// ...
}

就会导致程序错误,因为IoC容器中定义的bean类型是Vehicle而不是Car,虽然@Bean方法真正被调用时返回的会是一个Car类型的对象,但这要到bean实例被真正创建时才能被IoC容器知道,在IoC容器的bean定义中,bean的类型是由@Bean方法签名中的返回类型决定的。

作用域

Bean是有作用域的,主要有以下几种:

Scope说明
singleton(默认情况下)为每个Spring IoC容器将单个Bean定义的Scope扩大到单个对象实例。
prototype将单个Bean定义的Scope扩大到任何数量的对象实例。
request将单个Bean定义的Scope扩大到单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的Bean实例,该实例是在单个Bean定义的基础上创建的。只在Web感知的Spring ApplicationContext 的上下文中有效。
session将单个Bean定义的Scope扩大到一个HTTP Session 的生命周期。只在Web感知的Spring ApplicationContext 的上下文中有效。
application将单个Bean定义的 Scope 扩大到 ServletContext 的生命周期中。只在Web感知的Spring ApplicationContext 的上下文中有效。
websocket将单个Bean定义的 Scope 扩大到 WebSocket 的生命周期。仅在具有Web感知的 Spring ApplicationContext 的上下文中有效。

下面介绍几种常用的作用域。

singleton

singleton即单例作用域,其意义与单例模式相同。换言之,在一个IoC容器中,同一时间最多只会有同一个Bean的一个实例产生。

更多关于单例模式的内容,可以阅读设计模式 with Python 5:单例模式 - 红茶的个人站点 (icexmoon.cn)

这也是Spring Bean的默认作用域,可以使用@Scope注解显式声明或者改变这一行为:

@Component
@Scope("singleton")
public class Calculator {
	//...
}

值得注意的是,这里的@Scope注解的value属性是字符串,在使用中很容易写错,这点可以通过引入自定义常量来解决:

public class ScopeValue {
    public static final String SINGLETON = "singleton";
    public static final String PROTOTYPE = "prototype";
    public static final String REQUEST = "request";
    public static final String SESSION = "session";
    public static final String APPLICATION = "application";
    public static final String WEBSOCKET = "websocket";
}
@Component
@Scope(ScopeValue.SINGLETON)
public class Calculator {
	//...
}

实际上Spring框架提供了部分常量:

@Scope(WebApplicationContext.SCOPE_APPLICATION)
@Scope(WebApplicationContext.SCOPE_REQUEST)
@Scope(WebApplicationContext.SCOPE_SESSION)

比较让人费解的是我只找到了这三个可以用作@Scope值的常量,并没有找到其它常量。

除了上面这种做法,我们也可以使用组合注解的方式新建一个自定义注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("singleton")
public @interface SingletonScope {
}

要证明在单例作用域下每次获取到的都是同一个对象也很简单:

        Calculator calculator1 = (Calculator) ctx.getBean("calculator");
        Calculator calculator2 = (Calculator) ctx.getBean("calculator");
        if (calculator1 == calculator2){
            System.out.println("get same calculator.");
        }

prototype

prototype是原型的意思,也就是说有原型作用域的Bean,将会作为一个原始模版,不停地产生新的实例。通俗的说,被声明为原型作用域的Bean,每次尝试从IoC容器中获取时,都将获取一个新的对象。

实际上我们这里作为示例的CalculatorSupperCalculator更应当使用原型作用域而非单例,否则会存在一些问题,比如:

        calculator1.add(2);
        calculator2.add(3);
        System.out.println(calculator1.print());
        System.out.println(calculator2.print());

你会吃惊地看到输出既不是2也不是3,更不是5,而是一个别的什么数字。这实际上是因为无论是calculator1calculator2还是SupperCalculator对象中注入的calculator属性,它们关联的都是同一个对象,自然而然的,任何对该对象的调用都会改变其中的result值。

所以,对于像计算器这样,拥有自己维护的属性的类,应当被声明为原型作用域而非单例作用域。这也是判断一个类应当使用单例还是原型作用域的一般性指导原则。

改为单例作用域很容易:

@Component
@Scope(ScopeValue.PROTOTYPE)
public class Calculator {
	// ...
}

request

request和session作用域都只能用于Web相关的Spring应用。这也很容易理解,前者的作用范围是每次Http请求,后者的作用范围是当前请求对应的Session,对于非Web应用,自然就没有Http请求或Session。

所以这里要想演示这两种作用域下的Bean,就需要搭建一个简单的Spring Boot Web应用。

关于如何使用Spring Boot搭建一个简单的Web应用,可以阅读从零开始 Spring Boot 2:处理请求 - 红茶的个人站点 (icexmoon.cn)

下面是一个简单的示例:

@RestController()
@RequestMapping("/hello")
public class HelloController {
    @Autowired
    private HelloService helloService;
    @Autowired
    private ClientMessage clientMessage;

    @GetMapping("")
    public String hello(@RequestParam("msg") String msg){
        clientMessage.setMessage(msg);
        helloService.doSomething();
        return Result.success("hello").toString();
    }
}

@Service
public class HelloService {
    @Autowired
    private ClientMessage clientMessage;

    public void doSomething() {
        String msg = clientMessage.getMessage();
        System.out.println(String.format("get msg from client: %s", msg));
    }
}

@Data
@Component
@Scope(ScopeValue.REQUEST)
public class ClientMessage {
    String message;
}

这个示例中,HelloController负责接收请求,请求中会包含一个msg参数,表示由客户端发送的一段信息。然后Controller会调用HelloService.doSomething()方法,执行一些处理工作。假设该方法需要获取那段客户端信息,最简单的方式无疑是通过方法参数传递,但这里为了演示,我们选择更复杂的方式,用一个request范围的Bean ClientMessage来保存该信息,并在HelloService.doSomething()方法中进行读取。

看起来似乎没有问题,但实际运行这段代码,会出现一个错误:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'helloController': Unsatisfied dependency expressed through field 'helloService': Error creating bean with name 'helloService': Unsatisfied dependency expressed through field 'clientMessage': Error creating bean with name 'clientMessage': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton

错误信息说了,创建bean helloControllerhelloService出错,因为依赖clientMessage不能满足条件,并且提示我们如果要在单例bean中注入,最好使用代理。

这类问题其实是不同作用域的Bean互相依赖的问题,request范围的Bean只会在一次Http请求内生效,而单例作用域的Bean则会长期有效。这就导致了一个问题,依赖注入的时候注入helloControllerclientMessage对象到底是哪一个?

实际上在这个示例中,负责初始化框架代码的主线程本身显然并没有接收任何Http请求,所以也不存在相应的request范围的Bean,所以直接报错,无法完成相应的依赖注入。

所以我们可以试着按照提示为request 范围的Bean创建一个singleton范围的Bean代理来解决:

@Component
public class ClientMessageProxy extends ClientMessage{
    @Autowired
    private ApplicationContext ctx;

    @Override
    public void setMessage(String message) {
        ClientMessage clientMessage = (ClientMessage) ctx.getBean("clientMessage");
        clientMessage.setMessage(message);
    }

    @Override
    public String getMessage() {
        ClientMessage clientMessage = (ClientMessage) ctx.getBean("clientMessage");
        return clientMessage.getMessage();
    }
}

相应的,在Controller和Service中都使用该代理,而不是直接使用request范围的Bean。

@RestController()
@RequestMapping("/hello")
public class HelloController {
    @Autowired
    private HelloService helloService;
    @Autowired
    private ClientMessageProxy clientMessage;
	//...
}

@Service
public class HelloService {
    @Autowired
    private ClientMessageProxy clientMessage;
	// ...
}

现在用浏览器访问测试就能看到控制台正确的输出信息,表示客户端信息有成功的保存到request范围的bean中,并随后正确读取。

虽然这样做是可行的,但我们需要在代码中注入IoC容器,并通过IoC容器来实现代理类,这样做不符合Spring框架非侵入的宗旨,且需要额外付出努力来实现代理类(这还不包括被代理类修改可能导致的代理类的维护开销)。

幸运的是这一切都是非必须的,我们可以利用注解简单地完成代理工作:

@Data
@Component
@Scope(value = ScopeValue.REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ClientMessage {
    String message;
}

通过这种方式注入到HelloControllerHelloService中的ClientMessage对象,实际上是一个代理。所有对该对象公开方法的请求都会被代理到request范围的ClientMessage对象。

修改Controller和Service,注入被代理后的ClientMessage对象:

@RestController()
@RequestMapping("/hello")
public class HelloController {
    @Autowired
    private HelloService helloService;
    @Autowired
    private ClientMessage clientMessage;
	// ...
}

@Service
public class HelloService {
    @Autowired
    private ClientMessage clientMessage;
	// ...
}

不要忘记注释掉我们的代理类,让它不要被扫描到IoC容器中,因为该类与ClientMessage有相同的基类,会影响到获取Bean的结果:

//@Component
public class ClientMessageProxy extends ClientMessage{
	//...
}

重启应用进行测试,就会发现程序像之前我们手动实现代理一样可以正常执行。

@ScopeproxyMode属性有以下几个选项:

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,
    TARGET_CLASS;
}

默认情况下ScopedProxyMode.DEFAULT其实就是ScopedProxyMode.NO,除非我们修改配置进行变更。INTERFACES使用JDK动态代理来创建代理对象,TARGET_CLASS则使用CGLIB来创建代理对象。

  • 关于动态代理相关内容可以阅读深入理解Java动态代理 - 知乎 (zhihu.com)
  • 通俗地说,如果你不知道使用那个代理,那就用CGLIB生成代理。因为JDK生成代理有一些额外限制,比如JDK代理是基于接口生成的代理,所以被代理类必须要至少实现一个接口,并且调用方要通过接口来访问被代理类。

综上所述,如果我们要为一个单例Bean注入一个request或session作用域的Bean,就需要指定代理。鉴于这种需求相当普遍,Spring框架提供了一个组合注解方便我们使用:

package org.springframework.web.context.annotation;

//...

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("request")
public @interface RequestScope {
    @AliasFor(
        annotation = Scope.class
    )
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

@RequestScope注解默认会使用CGLIB创建动态代理。

我们只需要用@RequestScope注解取代@Scope注解即可:

@Data
@Component
@RequestScope
public class ClientMessage {
    String message;
}

session

session作用域与request作用域是类似的,区别是其作用范围是session。下面通过实际演示进行说明。

其实session作用域的Bean的用法与session本身的用途是类似的,我们很容易利用它来实现一个简单的登录逻辑。一般Web开发中会用session来保存登录信息,比如登录名,这里我们用一个session作用域的Bean来代替:

@Data
@Component
@SessionScope
public class LoginUser {
    String userName;

    public boolean isLogined() {
        return userName != null;
    }
}

这里使用的是@SessionScope这个注解,该注解已经默认采用代理。

创建一个基于LoginUser实现的登录用Service:

package com.example.ioc.web;

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * Created with IntelliJ IDEA.
 *
 * @author : 魔芋红茶
 * @version : 1.0
 * @Project : ioc
 * @Package : com.example.ioc.web
 * @ClassName : .java
 * @createTime : 2023/5/6 19:15
 * @Email : icexmoon@qq.com
 * @Website : https://icexmoon.cn
 * @Description :
 */
@Service
public class LoginService {

    @AllArgsConstructor
    private class Account {
        String name;
        String password;
    }

    @Autowired
    private LoginUser loginUser;
    private Map<String, Account> accounts = Collections.synchronizedMap(new HashMap<>());

    public LoginService() {
        this.accounts.put("hello", new Account("hello", "123"));
        this.accounts.put("LiLei", new Account("LiLei", "123"));
    }

    /**
     * 执行登录操作
     *
     * @param name     用户名
     * @param password 密码
     * @return 成功或失败
     */
    public boolean login(String name, String password) {
        //注销用户
        loginUser.setUserName(null);
        //检查密码是否正确
        if (!accounts.containsKey(name)) {
            return false;
        }
        Account localAccount = accounts.get(name);
        if (localAccount == null || !localAccount.password.equals(password)) {
            return false;
        }
        //执行登录操作
        loginUser.setUserName(name);
        return true;
    }

    public boolean isLogined() {
        return loginUser.isLogined();
    }

    public String getUserName() {
        return loginUser.getUserName();
    }


    /**
     * 注销用户
     */
    public void exit(){
        loginUser.setUserName(null);
    }

}

简单起见,这里使用一个内存字典来存放账户信息。

最后,在Controller中引入Service,就能很容易的实现登录和注销操作:

@RestController()
@RequestMapping("/hello")
public class HelloController {
    @Autowired
    private HelloService helloService;
    @Autowired
    private ClientMessage clientMessage;
    @Autowired
    private LoginService loginService;

    @GetMapping("")
    public String hello(@RequestParam("msg") String msg){
        clientMessage.setMessage(msg);
        helloService.doSomething();
        String userName = "guest";
        if (loginService.isLogined()){
            userName = loginService.getUserName();
        }
        return Result.success("hello, %s".formatted(userName)).toString();
    }

    @GetMapping("/login")
    public String login(@RequestParam("name") String name, @RequestParam("password") String password){
        if (!loginService.login(name, password)){
            return Result.fail("login operation is fail, please check your password.").toString();
        }
        return Result.success(null).toString();
    }

    @GetMapping("/exit")
    public String exit(){
        loginService.exit();
        return Result.success(null).toString();
    }
}

为了让示例更有趣一点,我修改了hello()方法,用户未登录时显示hello, guest,如果登陆了,返回的欢迎信息中就会包含用户名。

生命周期

Spring Bean的生命周期是由IoC容器控制的,因此我们需要有一种方式来介入,以便在Bean实例的创建或销毁时添加上一些处理逻辑。

有个例外,原型范围的Bean的创建依然由IoC容器完成,但在创建后容器不会继续跟踪,所以IoC 容器并不会对其进行销毁。

Spring本身定义了一些和Bean生命周期相关的接口,比如InitializingBean

package org.springframework.beans.factory;

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

从方法名称可以很容易猜测到,这个接口定义的方法afterPropertiesSet会在Bean实例被创建,并且完成属性依赖注入后被调用(如果有的话)。

对应的,还有会在Bean实例销毁前会调用的接口DisposableBean

package org.springframework.beans.factory;

public interface DisposableBean {
    void destroy() throws Exception;
}

任何实现了这两个接口的Bean,在创建和销毁时相应的方法会被调用,比如:

@Data
@Component
@RequestScope
public class ClientMessage implements InitializingBean, DisposableBean {
    String message;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("ClientMessage Instance is build.");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("ClientMessage Instance is will destroyed, message is %s.".formatted(message));
    }
}

示例中的ClientMessage作用域是request,所以会在一次Http请求中创建和销毁,这点可以通过验证得到证实,请求http://localhost:8080/hello?msg=111后就能看到类似下面的输出:

ClientMessage Instance is build.
get msg from client: 111
ClientMessage Instance is will destroyed, message is 111.

虽然这样做是可行的,但是Spring官方并不推荐,因为这样做会让你的业务代码与Spring框架绑定,并不符合Spring非侵入的宗旨。

正确的方式是使用一些注解来标记应当在实例创建和销毁时应该被调用的方法:

@Data
@Component
@RequestScope
public class ClientMessage {
    String message;

    @PostConstruct
    public void afterPropertiesSet() {
        System.out.println("ClientMessage Instance is build.");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("ClientMessage Instance is will destroyed, message is %s.".formatted(message));
    }

}

两个注解:

  • @PostConstruct,用于标记会在Bean实例初始化后被调用的方法。
  • @PreDestroy,用于标记会在Bean实例被销毁前调用的方法。

对于由@Bean方法创建的Bean,可以通过@Bean注解的属性来指定方法:

@Component
@Scope(ScopeValue.PROTOTYPE)
public class Calculator {
	// ...
    public void construct(){
        
    }

    public void destroy(){

    }
}

@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
	// ...
    @Bean(value = "cal", initMethod = "construct", destroyMethod = "destroy")
    public Calculator calculator() {
        return new Calculator();
    }
}

实际上如果Bean中有名称为closeshutdownpublic方法,会在相应的Bean销毁时被自动调用,如果需要阻止,可以通过@Bean(destroyMethod = "")来禁用。

当然,也可以直接在Java代码中的对象创建后手动进行调用:

@Configuration
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig {
	// ...
    @Bean(value = "cal", initMethod = "", destroyMethod = "destroy")
    public Calculator calculator() {
        Calculator calculator = new Calculator();
        calculator.construct();
        return calculator;
    }
}

实际上这几种控制bean生命周期的方式可以组合使用,组合后的方法调用先后顺序可以参考核心技术 (springdoc.cn)

对于非Web应用,Spring还提供一个Lifecycle接口,用于控制bean实例的启动和关闭回调。这并不常见,想了解可以阅读核心技术 (springdoc.cn)

Aware接口

Spring提供一系列Aware接口,用于在Bean实例创建时从IoC容器获取一些信息。实现了特定的Aware接口,将会在Bean实例创建的特定阶段被调用。

比如ApplicationContextAware,通过这个接口,Bean可以获取到创建它的IoC容器的引用:

@Data
@Component
@RequestScope
public class ClientMessage implements ApplicationContextAware {
	// ...
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("ctx is get.");
        System.out.println(applicationContext.getBean("cal"));
    }
}

获取到IoC容器引用后,可以实现一些特殊的需求。

虽然这样做很有效,但正如本文其它地方所说的,这种将业务代码与Spring框架直接绑定的侵入式编码,并不符合Spring官方的宗旨,是不推荐的。如果有需要,可以直接通过@Autowired进行注入。

另一个BeanNameAware接口,可以用于获取Bean的名称:

@Component
public class SupperCalculator implements BeanNameAware {
    // ...
	@Override
    public void setBeanName(String name) {
        System.out.println("bean name is get: %s".formatted(name));
    }
}

这个回调会在Bean实例的属性被设置,但InitializingBean.afterPropertiesSet()等初始化回调没有被调用前完成。

更多的Aware接口可以阅读核心技术 (springdoc.cn)

扩展容器

如果要为IoC容器扩展一些功能,通常并不需要自定义类并从ApplicationContext继承,Spring提供了一些特殊的接口,实现这些接口的bean会被IoC容器特殊处理,并用于特定用途,这样我们就可以为IoC容器集成一些特殊功能。

BeanPostProcessor

接口BeanPostProcessor定义的回调方法,会在初始化bean实例时被调用,我们可以利用这些方法实现自定义的bean实例初始化逻辑(甚至覆盖原本Spring的初始化逻辑)。

实际上Spring本身就依赖这个接口完成bean实例的初始化,比如:

package org.springframework.beans.factory.annotation;
// ...
public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware {
	//...
}

AutowiredAnnotationBeanPostProcessor类的用途就是解析bean对应的类定义中的@Autowired@Value注解,并进行注入。

这里用一个示例说明如何使用。假设我们需要在bean实例创建后打印其名称和toString()方法:

@Component
public class PrintBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        try {
            System.out.println("bean %s is created: %s".formatted(beanName, bean));
        } catch (ScopeNotActiveException e) {
            ;
        }
        return bean;
    }
}

postProcessAfterInitialization方法会在bean实例初始化后被调用,在这里简单地打印一些信息。

需要说明的是,这里拦截了ScopeNotActiveException异常,是因为初始化Spring框架的主线程并不负责处理Http请求,自然也不能用于获取requestsession范围的bean实例,所以如果IoC容器中存在此类作用域的bean定义,就会产生类似于当前线程的request范围不生效的错误提示以及一个ScopeNotActiveException异常。当然这样做可能不太对,暂时我只能想到这样处理了,有更好的方式可以指出,谢谢。

启动Web应用后,就能看到诸如以下的输出:

bean liveReloadServer is created: org.springframework.boot.devtools.livereload.LiveReloadServer@2567cb27
...
bean optionalLiveReloadServer is created: org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer@6447290

这些都是Spring框架自己定义的bean。

当然,实现BeanPostProcessor接口的bean同样可以通过@Bean方法创建,比如:

@Configuration()
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	// ...
    @Bean
    public static PrintBeanPostProcessor printBeanPostProcessor(){
        return new PrintBeanPostProcessor();
    }
}

这里的方法定义是static的,这样做是有意义的,可以保证在框架初始化时就注册这类特殊的bean。

就像前边说的,可以定义多个实现了BeanPostProcessor接口的bean,这些bean之间的调用关系可以用Ordered接口表示:

@Component
public class PrintBeanPostProcessor implements BeanPostProcessor, Ordered {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        try {
            System.out.println("#%d bean %s is created: %s".formatted(getOrder(), beanName, bean));
        } catch (ScopeNotActiveException e) {
            ;
        }
        return bean;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

@Component
public class PrintBeanPostProcessor1 extends PrintBeanPostProcessor {
    @Override
    public int getOrder() {
        return 1;
    }
}

可以看到类似下面的输出:

#0 bean scopedTarget.loginUser is created: LoginUser(userName=null)
#1 bean scopedTarget.loginUser is created: LoginUser(userName=null)

BeanFactoryPostProcessor

BeanPostProcessor的用途是在bean创建时对其进行修改,BeanFactoryPostProcessor则可以对BeanFactory进行修改。BeanFactory是一个底层接口,就像字面意思那样,表示用于创建bean的工厂,实际上表示IoC容器的ApplicationContext接口就是一个BeanFactory

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
	//...
}

public interface ListableBeanFactory extends BeanFactory {
	//...
}

Spring定义了一个实现了BeanFactoryPostProcessor接口的PropertyOverrideConfigurer类,可以利用这个类完成从配置文件中加载默认值到bean属性:

@Configuration()
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
    // ...
	    @Bean
    public static PropertyOverrideConfigurer propertyOverrideConfigurer(){
        PropertyOverrideConfigurer propertyOverrideConfigurer = new PropertyOverrideConfigurer();
        propertyOverrideConfigurer.setLocation(new ClassPathResource("override.properties"));
        return propertyOverrideConfigurer;
    }
}

配置文件override.properties中的内容:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

实际测试就能发现,注入的名为dataSource的bean的driverClassName属性内容是com.mysql.jdbc.Driverurl属性的内容是jdbc:mysql:mydb

FactoryBean

FactoryBean是Spring定义的一个表示工厂的接口:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

如果一个bean实现了这个接口,那么通过相应的名称获取到的bean实例就是工厂类产生的对象(getObject方法返回的)。

比如下面这个例子:

public class SimpleClock {
    private LocalDateTime time;
    private int num;
    private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;

    public SimpleClock(LocalDateTime time, int num) {
        this.time = time;
        this.num = num;
    }

    @Override
    public String toString() {
        return "Clock#%d(%s)".formatted(num, dateTimeFormatter.format(time));
    }
}

@Component
public class ClockFactoryBean implements FactoryBean<SimpleClock> {
    private static int i = 0;
    @Override
    public SimpleClock getObject() throws Exception {
        return new SimpleClock(LocalDateTime.now(), i++);
    }

    @Override
    public Class<?> getObjectType() {
        return SimpleClock.class;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

简单进行测试:

        System.out.println(ctx.getBean("clockFactoryBean"));
        System.out.println(ctx.getBean("&clockFactoryBean"));

输出:

Clock#0(2023-05-08T21:15:08.6452618)
com.example.ioc.web.ClockFactoryBean@5f57ced3

可以看到用clockFactoryBean获取到的Bean实例实际上是一个SimpleClock类型的对象,也就是工厂生产的对象。而&clockFactoryBean返回的才是工厂类本身的对象。

更常见的是通过@Bean方法创建一个工厂bean:

@Configuration()
@ComponentScan(basePackages = "com.example.ioc")
public class AppConfig implements AppConfigImpl{
	// ...
    @Bean
    public ClockFactoryBean clockFactoryBean(){
        return new ClockFactoryBean();
    }
}

效果和之前的是一样的。

好了,大致上就这些了,有缺陷以后再修改和补充,谢谢阅读。

本文的所有示例代码可以从learn_spring_boot/ch27/ioc获取。

参考资料

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。