《Spring Boot 3:高级与架构设计》第 2 章 IOC 容器的高级机制 编程式驱动 IOC 阅读笔记 7

《Spring Boot 3:高级与架构设计》第 2 章 IOC 容器的高级机制 编程式驱动 IOC 阅读笔记 7

在入门篇中曾多次提及一组概念:编程式与声明式。

编程式指的是编写代码命令程序如何去做事情,程序会按照编写的代码执行并得到结果;

声明式则指告诉程序期望得到什么结果,让程序完成。

声明式编程最大的特点是简单易用,比如在 JDBC 事务控制中,声明式事务只需要 @EnableTransactionManagement+@Transactional 即可完成对业务方法的事务控制;

而编程式事务控制最简单的方式依然要注入 TransactionTemplate 并在 Lambda 表达式中编写业务逻辑。

既然声明式编程驱动 Spring Framework 和 Spring Boot 应用已经如此简单方便,为什么还要了解编程式驱动?仁者见仁,智者见智,笔者在此列出以下 3 个观点。

(1)亲身接触编程式驱动 IOC 有助于理解 Spring Framework 在底层对声明式的处理。

(2)Spring Boot 的底层机制源自原生 Spring Framework 的编程式驱动,理解编程式驱动对探究 Spring Boot 有很大帮助。

(3)如果有基于 Spring 等框架进行二次封装的需求,编程式比声明式更好用。

简而言之,编程式驱动 IOC 通常不会在日常的业务开发中出现,但在了解、封装、定制基础组件时不可或缺。读者要想对这部分有更深刻的理解,本节内容可以起到引领的作用。本节内容不包含理论讲述,均为实操,侧重编码和应用,建议读者跟随练习。

2.4.1 简单的编程式驱动

从比较简单的编程式驱动开始,首先声明一个需求场景:不使用任何配置文件或配置类,也不使用组件扫描机制,向 IOC 容器注册一个 Person类型的 Bean 并注入属性值。

分析这个需求,结合入门篇和前述内容,读者是否可以想到以下几点:

(1)加载组件的方式有 XML 配置文件、注解配置类,或者利用注解驱动 IOC 容器的组件扫描;

(2)IOC 容器中 Bean 的来源包括 BeanDefinition 的构造和使用 DefaultSingletonBeanRegistry 直接注册;

(3)注入属性值除了借助 XML 配置文件和注解驱动编码,还可通过 BeanDefinition 中的 PropertyValues 完成。

结合上述几点,开始编码。

1. 模型类制作

首先是普通的 Person 类,只需要设计一个 name 属性即可,如代码所示。2.4 节中提到的所有 Person 均使用此类。

java 复制代码
package com.yangjunbo.spring.programmatic.examplea.bean;

public class Person {
    
    private String name;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }

}

2. 测试启动类

由于不允许使用配置文件和配置类,因此只需要编写测试启动类即可。又由于 XML 配置文件驱动的 IOC 容器很难做到 "无中生有"​,而注解驱动IOC 容器包含一个无参构造方法,因此选择使用 AnnotationConfigApplicationContext 作为 IOC 容器的实现载体,测试代码的骨架应当如代码所示。

紧接着是注册 Person Bean。结合前面的分析不难得知,可以从 AnnotationConfigApplicationContext 中获取到内部的DefaultListableBeanFactory,这样也就相当于获取到了 DefaultSingletonBeanRegistry,使用它就可以编程式构造一个 Person Bean 并完成属性赋值。诚然,使用该方式确实可以满足当前需求,但不具备通用性,缺乏抽象思维。应当借助 BeanDefinition 机制完成注册,所以相对合理的做法是使用 BeanDefinitionBuilder 构造一个 BeanDefinition,并注册到 AnnotationConfigApplicationContext 中,如代码所示。注意,代码调用的 registerBeanDefinition 方法来自 BeanDefinitionRegistry,使用 ApplicationContext 或者 ConfigurableApplicationContext 接口无法正常调用该方法,所以本示例中使用实现类本体(AnnotationConfigApplicationContext)声明类型,这样便可以调用子类具体的方法。

java 复制代码
package com.yangjunbo.spring.programmatic.examplea;

import com.yangjunbo.spring.programmatic.examplea.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * ClassName: programmaticAApplication
 * Package: com.yangjunbo.spring.programmatic.examplea
 * Description:
 *
 * @Author 杨钧博
 * @Create 2026/9/17 17:31
 * @Version 1.0
 */
public class programmaticAApplication {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class).getBeanDefinition();
        ctx.registerBeanDefinition("person", personDefinition);

        Person person = ctx.getBean(Person.class);
        System.out.println(person);
    }

}

3. 运行失败及原因分析

当编写完毕后,运行 main 方法,发现控制台抛出了一个状态不合法的异常:org.springframework.context.annotation.AnnotationConfigApplicationContext@2077d4de has not been refreshed yet。这代表ApplicationContext 未刷新,无法使用。

为什么会出现这个问题?这是由于 ApplicationContext 的内部设计了一个 "刷新" 机制:ApplicationContext 的扩展接口ConfigurableApplicationContext 中定义了一个 refresh 方法,这个方法非常重要,整个 IOC 容器的初始化逻辑几乎全部位于 refresh 方法中。换言之,ApplicationContext 要想正常使用,就必须触发其 refresh 方法。可能读者看到这里会有疑惑:为什么之前编写的那么多测试案例都没有显式调用 refresh 方法,而 ApplicationContext 仍可以正常使用?答案源于 Spring Framework 中一个非常基础的设计:当驱动 IOC 容器,指定了需要加载的 XML 配置文件、注解配置类或组件扫描包时,Spring Framework 认定我们一定是 "有备而来"​,传入这些配置元信息后必然会初始化 IOC 容器,于是 Spring Framework 在底层自动调用 refresh 方法初始化 IOC 容器;而当出现代码所示未指定配置元信息的场景时,Spring Framework 不了解将会在何时使用 IOC 容器,所以需要我们手动调用 refresh 方法以 "通知" ApplicationContext 初始化 IOC 容器,这样才可以达到正常的效果。

结合 Spring Framework 的源码来验证,如代码所示,AnnotationConfigApplicationContex t的两个构造方法中,无参构造方法只会初始化内部组件,而传入 Class 类型可变参数的构造方法会额外调用注册配置类和 refresh 方法。

4. 手动 refresh 后测试

有了上一步的结论,下面修改 ProgrammaticQuickstartApplication 的代码,在注册 BeanDefinition 的动作之后追加 ApplicationContext 的refresh 方法调用,如代码所示。当添加 refresh 方法调用后,重新执行 main 方法,此时控制台可以打印 Person 的信息,本节的需求场景得以实现。

java 复制代码
package com.yangjunbo.spring.programmatic.examplea;

import com.yangjunbo.spring.programmatic.examplea.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * ClassName: programmaticAApplication
 * Package: com.yangjunbo.spring.programmatic.examplea
 * Description:
 *
 * @Author 杨钧博
 * @Create 2026/9/17 17:31
 * @Version 1.0
 */
public class programmaticAApplication {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class).getBeanDefinition();
        ctx.registerBeanDefinition("person", personDefinition);
        ctx.refresh();

        Person person = ctx.getBean(Person.class);
        System.out.println(person);
    }

}

5. 给 Person 的属性赋值

给 Person 属性赋值的方法也不复杂,在讲解 BeanFactoryPostProcessor 时已经演示过,只需要从 BeanDefinition 中获取到 PropertyValues 即可。这里介绍另外一种属性赋值方法,如代码所示。Person 的 BeanDefinition 由操纵 BeanDefinitionBuilder 构造而来,而BeanDefinitionBuilder 提供了一个构造过程中可以调用的 addPropertyValue 方法,它可以直接对 BeanDefinition 内部的 PropertyValues 进行属性赋值。编码完成后重新运行 main 方法,可以发现 name 属性被成功设置,本节的需求得以完整实现。

java 复制代码
package com.yangjunbo.spring.programmatic.examplea;

import com.yangjunbo.spring.programmatic.examplea.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * ClassName: programmaticAApplication
 * Package: com.yangjunbo.spring.programmatic.examplea
 * Description:
 *
 * @Author 杨钧博
 * @Create 2026/9/17 17:31
 * @Version 1.0
 */
public class programmaticAApplication {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
                .addPropertyValue("name", "zhangsan").getBeanDefinition();
        ctx.registerBeanDefinition("person", personDefinition);
        ctx.refresh();

        Person person = ctx.getBean(Person.class);
        System.out.println(person);
    }

}

2.4.2 编程式注入和初始化

编程式注册 Bean 完成后,下面来实现第二个需求:同时在IOC容器中注册 Person 和两个 Animal 的子类对象 Cat 和 Dog,并且 Cat 和 Dog 都依赖Person。不难看出,完成该需求的重点在于如何编程式地向 Cat 和 Dog 中注入 Person。

1. 模型类制作

首先声明 Animal 与它的子类 Cat 和 Dog,并在 Animal 中定义 name 和 person 属性,如代码所示。

java 复制代码
package com.yangjunbo.spring.programmatic.exampleb.bean;

public class Person {
    
    private String name;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }

}
java 复制代码
package com.yangjunbo.spring.programmatic.exampleb.bean;

public abstract class Animal {
    
    public Animal() {
        System.out.println("Animal constructor run ......");
    }
    
    protected String name;
    
    protected Person person;
    
    @Override
    public String toString() {
        return "Animal{" + "name='" + name + '\'' + ", person=" + person + '}';
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public Person getPerson() {
        return person;
    }
    
    public void setPerson(Person person) {
        this.person = person;
    }
}
java 复制代码
package com.yangjunbo.spring.programmatic.exampleb.bean;

public class Cat extends Animal {
    
    @Override
    public String toString() {
        return "Cat{" + "name='" + name + '\'' + ", person=" + person + '}';
    }
}
java 复制代码
package com.yangjunbo.spring.programmatic.exampleb.bean;

public class Dog extends Animal {
    
    @Override
    public String toString() {
        return "Dog{" + "name='" + name + '\'' + ", person=" + person + '}';
    }
}

2. 测试启动类

紧接着是测试启动类。依然使用 BeanDefinitionBuilder 构造 BeanDefinition,并注册到 IOC 容器,

只不过此处设置 Bean 之间的依赖的方式不再是调用 BeanDefinitionBuilder 的 addPropertyValue 方法,而是调用 addPropertyReference 方法,意为指定属性的引用,

这与编写 XML 配置文件时使用标签的 ref 属性效果一致。先注册一个 Cat 对象,编写完毕的启动类如代码所示。

java 复制代码
package com.yangjunbo.spring.programmatic.exampleb;

import com.yangjunbo.spring.programmatic.exampleb.bean.Cat;
import com.yangjunbo.spring.programmatic.exampleb.bean.Dog;
import com.yangjunbo.spring.programmatic.exampleb.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProgrammaticInjectApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        
        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
                .addPropertyValue("name", "老王").getBeanDefinition();
        ctx.registerBeanDefinition("laowang", personDefinition);
        
        BeanDefinition catDefinition = BeanDefinitionBuilder.rootBeanDefinition(Cat.class)
                .addPropertyValue("name", "咪咪").addPropertyReference("person", "laowang")//.setLazyInit(true)
                .getBeanDefinition();
        ctx.registerBeanDefinition("mimi", catDefinition);

        ctx.refresh();
        System.out.println("ApplicationContext refreshed ......");
        
        Cat cat = ctx.getBean(Cat.class);
        System.out.println(cat);
    }
}

运行 main 方法后,控制台可以正确打印出 "咪咪" 和它的主人 "老王"​,本节的需求得以实现。不过读者可能会有一些疑惑:这个需求的实现难度不大,为何放在这一节?请不要着急,继续往下阅读。

3. bean 的初始化时机探究

读者在之前的练习中是否思考过一个问题:通过编程式驱动的 Bean 都在何时被实例化和初始化?

为了得到答案,接下来要设计 3 个改造点加以测试和验证。

(1)refresh 方法的作用

在 Animal 中添加无参构造方法,并添加控制台打印,随后在 ProgrammaticInjectApplication 的 main 方法中 refresh 方法调用之后也添加控制台打印,如代码所示。

代码补充完毕后,重新运行测试启动类的 main 方法,观察控制台打印的顺序可以得出一个结论:refresh 方法会触发 Bean 的实例化。

(2)原型Bean的创建时机

以上结论是否可靠?可以通过扩展测试来验证。已创建的 Cat 属于单实例 Bean,下面再创建一个 Dog,并声明其作用域为 prototype(原型Bean)​。注册原型 Dog 和获取的方式如代码所示。重新运行 main 方法,发现原型 Dog 并没有在 refresh 方法触发后创建,所以以上结论要调整:refresh 方法会触发单实例 Bean 的实例化。

java 复制代码
package com.yangjunbo.spring.programmatic.exampleb;

import com.yangjunbo.spring.programmatic.exampleb.bean.Cat;
import com.yangjunbo.spring.programmatic.exampleb.bean.Dog;
import com.yangjunbo.spring.programmatic.exampleb.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProgrammaticInjectApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        
        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
                .addPropertyValue("name", "老王").getBeanDefinition();
        ctx.registerBeanDefinition("laowang", personDefinition);
        
        BeanDefinition catDefinition = BeanDefinitionBuilder.rootBeanDefinition(Cat.class)
                .addPropertyValue("name", "咪咪").addPropertyReference("person", "laowang")//.setLazyInit(true)
                .getBeanDefinition();
        ctx.registerBeanDefinition("mimi", catDefinition);

        BeanDefinition dogDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class)
                .addPropertyValue("name", "汪汪").addPropertyReference("person", "laowang")
                .setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
                //.setLazyInit(true)
                .getBeanDefinition();
        ctx.registerBeanDefinition("wangwang", dogDefinition);

        System.out.println("ApplicationContext before refresh ......");
        ctx.refresh();
        System.out.println("ApplicationContext refreshed ......");
        
        Cat cat = ctx.getBean(Cat.class);
        System.out.println(cat);

        Dog dog = ctx.getBean(Dog.class);
        System.out.println(dog);
    }
}

(3)延迟加载 Bean 的创建时机

除了作用域的区别之外,还有一种比较特殊的 Bean 是显式声明延迟加载的 Bean(lazy-init)​。

下面针对这种 Bean 继续改造测试启动类,在 Dog 的 BeanDefinition 中将其设置为延迟加载,并取消其原型 Bean 的声明,

如代码所示。改造完毕后再次运行 main 方法,控制台打印的效果与上一小节一致(没有在 refresh 方法触发之前打印 Dog 的创建动作)​。

通过以上 3 个测试环节,可以得出一个相对可靠的结论:refresh 方法的执行会触发非延迟加载的单实例 Bean 的实例化和初始化。

java 复制代码
package com.yangjunbo.spring.programmatic.exampleb;

import com.yangjunbo.spring.programmatic.exampleb.bean.Cat;
import com.yangjunbo.spring.programmatic.exampleb.bean.Dog;
import com.yangjunbo.spring.programmatic.exampleb.bean.Person;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProgrammaticInjectApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        
        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
                .addPropertyValue("name", "老王").getBeanDefinition();
        ctx.registerBeanDefinition("laowang", personDefinition);
        
        BeanDefinition catDefinition = BeanDefinitionBuilder.rootBeanDefinition(Cat.class)
                .addPropertyValue("name", "咪咪").addPropertyReference("person", "laowang")//.setLazyInit(true)
                .getBeanDefinition();
        ctx.registerBeanDefinition("mimi", catDefinition);

        BeanDefinition dogDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class)
                .addPropertyValue("name", "汪汪").addPropertyReference("person", "laowang")
                //.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
                .setLazyInit(true)
                .getBeanDefinition();
        ctx.registerBeanDefinition("wangwang", dogDefinition);

        System.out.println("ApplicationContext before refresh ......");
        ctx.refresh();
        System.out.println("ApplicationContext refreshed ......");
        
        Cat cat = ctx.getBean(Cat.class);
        System.out.println(cat);

        Dog dog = ctx.getBean(Dog.class);
        System.out.println(dog);
    }
}

补充,回顾 Bean 的作用域

作用域是一个很关键的概念,理解这个概念对学习 Spring Framework 中 Bean 的作用域很有帮助。

1 理解作用域

在学习 Java 语言基础的时候,读者肯定了解过一些基础概念:成员变量、方法变量、局部变量。

2 内置的作用域

Spring Framework 中内置了 6 种作用域(6.x版本)​。

作用域类型 概述
singleton 一个 IOC 容器中只有一个
proptype 每次获取新创建一个
request 一次请求创建一个
session 一个会话创建一个
application 一个 Web 应用创建一个
websocket 一个 websocket 会话创建一个

3 单实例 (singleton)

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

3.1 创建 Bean+配置类

先创建两个简单类 Child 和 Toy。

接下来,开始编写注解配置类,为了演示 Toy 在 IOC 容器中只产生了一个 Bean,在配置类 BeanScopeConfiguration 中注入两个 Child 类型的 Bean,都注入 Toy 类型的 Bean。

java 复制代码
package com.yangjunbo.bean.scope.examplea;

import org.springframework.stereotype.Component;

@Component
public class Toy { }
java 复制代码
package com.yangjunbo.bean.scope.examplea;

public class Child {
    
    private Toy toy;
    
    public void setToy(Toy toy) {
        this.toy = toy;
    }

    @Override
    public String toString() {
        return "Child{" +
                "toy=" + toy +
                '}';
    }

}
java 复制代码
package com.yangjunbo.bean.scope.examplea;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.yangjunbo.bean.scope.examplea")
public class BeanScopeConfiguration {
    
    @Bean
    public Child child1(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }
    
    @Bean
    public Child child2(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }
}
3.2 测试运行

启动类 BeanScopeAnnoApplication,使用注解驱动 IOC 容器,并从容器中取出所有 Child 对象,打印其内部的 Toy 对象并观察。运行 main 方法,控制台中打印的两个 Child 中持有的 Toy 是同一个,证明默认情况下 IOC 容器中的 Bean 作用域的确是单实例的。

java 复制代码
package com.yangjunbo.bean.scope.examplea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanScopeAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanScopeConfiguration.class);
        ctx.getBeansOfType(Child.class).forEach((name, child) -> {
            System.out.println(name + " : " + child);
        });
    }
}

4 原型 (prototype)

连续两次调用 getBean 方法,得到两个不同的 Bean;

4.1 原型 Bean 效果演示

Toy 类上标注一个额外的注解 @Scope,并声明为原型类型。

在接口 ConfigurableBeanFactory 中定义好了常量,如果担心声明作用域时拼错单词,建议直接引用对应的常量。

其他代码不需要改变,直接运行 BeanScopeAnnoApplication 的 main 方法,发现控制台打印的两个 Toy 确实不同。

4.2 原型 Bean 的创建时机

对于单实例 Bean 的创建时机已经了解,在 ApplicationContext 被初始化时就已经创建完毕,那么原型 Bean 的创建时机是什么?

原型 Bean 都是在获取时创建的。验证方法,可以给 Toy 类中添加一个无参构造方法,构造方法中打印信息到控制台。

修改启动类 BeanScopeAnnoApplication,如果只做组件扫描,不加载配置类,这样就相当于只有一个 Toy 类被扫描进去了,Child 不会注册到 IOC 容器中;Toy 类的构造方法没有打印,说明容器初始化时没有创建 Toy Bean。

而如果不修改启动类,以 BeanScopeConfiguration 配置类驱动 IOC 容器,则控制台中可以看到两次 Toy 构造方法的打印。

java 复制代码
package com.yangjunbo.bean.scope.examplea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanScopeAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanScopeConfiguration.class);
        ctx.getBeansOfType(Child.class).forEach((name, child) -> {
            System.out.println(name + " : " + child);
        });
        System.out.println("---------------------不加载配置类---------------------");
        ApplicationContext ctx2 = new AnnotationConfigApplicationContext();
        System.out.println("---------------------加载配置类---------------------");
        ApplicationContext ctx3 = new AnnotationConfigApplicationContext(BeanScopeConfiguration.class);
    }
}

5 Web 中的扩展作用域

  • request:每次客户端向 Web 应用服务器发起一次请求,Web 服务器接收到请求后由 Spring Framework 生成一个 Bean,直到请求结束后销毁。
  • session:每个客户端在与 Web 应用服务器发起会话后,Spring Framework 会为之生成一个 Bean,直到会话过期后销毁。
  • application:每个 Web 应用在启动时,Spring Framework 会生成一个 Bean,直到应用停止后销毁。
  • websocket:每个客户端在与 Web 应用服务器建立 WebSocket 长连接时,Spring Framework 会为之生成一个 Bean,直到断开连接后销毁。

2.4.3 内置组件的编程式使用

Spring Framework 的内置组件主要包含 ApplicationContext 中组合的关键 API(包括扫描器、加载器、解析器等)​。本节简单演示内置组件的编程式使用方式。

1. 组件扫描器

先声明需求:不主动构造 BeanDefinition 和实例化 Animal 对象,也不能改变 Cat 和 Dog 的代码,将 Cat 与 Dog 注册到 IOC 容器中,并对name 和 person 属性进行注入。

这个需求的限定条件更多,不主动构造 BeanDefinition 意味着 2.4.1~2.4.2 节所述的方式均无法使用;而不允许实例化对象则意味着不能使用AnnotationConfigApplicationContext 的 scan 方法通过组件扫描将 Cat 和 Dog 注册到 IOC 容器中。因为 scan 方法有一个限定条件:默认情况下 scan 会扫描标注 @Component 及派生注解的类,这与需求是矛盾的。

经过上述分析,发现利用现有知识几乎无法解决问题,所以接下来介绍内置 API:ClassPathBeanDefinitionScanner。这是组合在ApplicationContext 中的类路径扫描器,可以根据给定的扫描规则,将扫描到的类封装为 BeanDefinition,该功能能满足我们的需求。下面演示ClassPathBeanDefinitionScanner 的使用方法。

(1)组件扫描器的创建与规则设置

在创建 ClassPathBeanDefinitionScanner 时,需要传入 BeanDefinitionRegistry。

由于 ApplicationContext 的所有落地实现类都实现了BeanDefinitionRegistry 接口,因此在构造 ClassPathBeanDefinitionScanner 时可以直接传入 ApplicationContext 本身,如代码所示。

紧接着要给组件扫描器设置过滤规则。既然要扫描的目标是 Cat 和 Dog,而它们的父类都是 Animal,那么就可以添加一个 include 类型的过滤规则,即检测被扫描类的父类是否为 Animal。

注意 addIncludeFilter 方法中传入的 Lambda 表达式,它的类型为 TypeFilter。在入门篇 4.6.2 节自定义过滤组件扫描规则时,就利用 TypeFilter接口完成了特殊 Bean 的扫描过滤,读者可以翻阅入门篇以回顾。

java 复制代码
package com.yangjunbo.spring.programmatic.examplec;

import com.yangjunbo.spring.programmatic.examplec.bean.Animal;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

public class ProgrammaticComponentsApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx);
        scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
            return metadataReader.getClassMetadata().getSuperClassName().equals(Animal.class.getName());
        });
        
        int count = scanner.scan("com.yangjunbo.spring.programmatic.examplec.bean");

    }
}

设置扫描规则后,须使包扫描器工作。当调用 ClassPathBeanDefinitionScanner 的 scan 方法后,得到的不是 BeanDefinition,而是一个 int 类型的数值,这似乎与需求不符。是调用的 scan 方法有问题,还是 ClassPathBeanDefinitionScanner 的设计有问题?

(2)组件扫描的扫描动作对比

一般情况下,Spring Framework 内部的 API 设计没有问题。由 javadoc 可以得知,scan 方法的返回值是 "number of beans registered" --- 注册进 IOC 容器中 Bean 的数量。所以使用 scan 方法后,匹配的类会被直接封装为 BeanDefinition 并注册进 IOC 容器(整个过程自动化)​。

按照需求,不能直接扫描 Cat 和 Dog 并注册到 IOC 容器,因为这两个类中并没有写入依赖注入的注解,所以需要先获取到 BeanDefinition,完成依赖注入的动作后才可以注册到 IOC 容器中。幸运的是,ClassPathBeanDefinitionScanner 的 findCandidateComponents 方法可以将扫描后的所有 BeanDefinition 一次性返回,从而满足需求。利用该方法就可以完成编码工作,如代码所示。

java 复制代码
package com.yangjunbo.spring.programmatic.examplec;

import com.yangjunbo.spring.programmatic.examplec.bean.Animal;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

import java.beans.Introspector;
import java.util.Set;

public class ProgrammaticComponentsApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx);
        scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
            return metadataReader.getClassMetadata().getSuperClassName().equals(Animal.class.getName());
        });
        
        //int count = scanner.scan("com.yangjunbo.spring.programmatic.examplec.bean");

        Set<BeanDefinition> animalDefinitions = scanner
                .findCandidateComponents("com.yangjunbo.spring.programmatic.examplec.bean");
        animalDefinitions.forEach(definition -> {
            MutablePropertyValues propertyValues = definition.getPropertyValues();
            String beanClassName = definition.getBeanClassName();
            propertyValues.addPropertyValue("name", beanClassName);
            propertyValues.addPropertyValue("person", new RuntimeBeanReference("laowang"));
            ctx.registerBeanDefinition(Introspector.decapitalize(beanClassName.substring(beanClassName.lastIndexOf("."))), definition);
        });

    }
}

(3)测试运行

最后测试时主动刷新 IOC 容器,随后从 IOC 容器中获取 Cat 并打印,控制台打印的内容正常(两次 Animal constructor run 分别由 Cat 和 Dog的实例化引发)​,证明借助 ClassPathBeanDefinitionScanner 已经完成了本节的需求。

java 复制代码
package com.yangjunbo.spring.programmatic.examplec;

import com.yangjunbo.spring.programmatic.examplec.bean.Animal;
import com.yangjunbo.spring.programmatic.examplec.bean.Person;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

import java.beans.Introspector;
import java.util.Set;

public class ProgrammaticComponentsApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
                .addPropertyValue("name", "老王").getBeanDefinition();
        ctx.registerBeanDefinition("laowang", personDefinition);

        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx);
        scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
            return metadataReader.getClassMetadata().getSuperClassName().equals(Animal.class.getName());
        });
        
        //int count = scanner.scan("com.yangjunbo.spring.programmatic.examplec.bean");

        Set<BeanDefinition> animalDefinitions = scanner
                .findCandidateComponents("com.yangjunbo.spring.programmatic.examplec.bean");
        animalDefinitions.forEach(definition -> {
            MutablePropertyValues propertyValues = definition.getPropertyValues();
            String beanClassName = definition.getBeanClassName();
            propertyValues.addPropertyValue("name", beanClassName);
            propertyValues.addPropertyValue("person", new RuntimeBeanReference("laowang"));
            ctx.registerBeanDefinition(Introspector.decapitalize(beanClassName.substring(beanClassName.lastIndexOf("."))), definition);
        });

        ctx.refresh();

    }
}

2. 配置文件解析器

上一节的需求并没有对 Person 的构造提出限制。本节将借助 Spring Framework 的另一内置组件 XmlBeanDefinitionReader 完成 XML 配置文件的加载和 BeanDefinition 读取,以代替编程式构造 BeanDefinition。

既然是用 XML 配置文件代替手动构造,那么下面先制作一个简单的只包含 Person 的配置文件,如代码所示。

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

    <bean id="laowang" class="com.yangjunbo.spring.programmatic.examplec.bean.Person">
        <property name="name" value="老王"/>
    </bean>
</beans>

接下来的工作是借助 XmlBeanDefinitionReader 读取 XML 配置文件并注册其中定义的 Bean,如代码所示。

只需要手动构造一个 XmlBeanDefinitionReader,并调用其 loadBeanDefinitions 方法传入配置文件即可。

与上一节不同的是,XML 配置文件的内容本身就是完全体,所以只需注册即可,无须再提取 BeanDefinition 进行二次处理。

最后运行 main 方法,控制台仍然可以正确打印,证明已经正确使用 XmlBeanDefinitionReader 加载了 XML 配置文件并完成了装配。

java 复制代码
package com.yangjunbo.spring.programmatic.examplec;

import com.yangjunbo.spring.programmatic.examplec.bean.Animal;
import com.yangjunbo.spring.programmatic.examplec.bean.Person;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.io.ClassPathResource;

import java.beans.Introspector;
import java.util.Set;

public class ProgrammaticComponentsApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

//        BeanDefinition personDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class)
//                .addPropertyValue("name", "老王").getBeanDefinition();
//        ctx.registerBeanDefinition("laowang", personDefinition);

        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx);
        reader.loadBeanDefinitions(new ClassPathResource("programmatic/examplec/programmatic-components.xml"));

        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx);
        scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
            return metadataReader.getClassMetadata().getSuperClassName().equals(Animal.class.getName());
        });
        
        //int count = scanner.scan("com.yangjunbo.spring.programmatic.examplec.bean");

        Set<BeanDefinition> animalDefinitions = scanner
                .findCandidateComponents("com.yangjunbo.spring.programmatic.examplec.bean");
        animalDefinitions.forEach(definition -> {
            MutablePropertyValues propertyValues = definition.getPropertyValues();
            String beanClassName = definition.getBeanClassName();
            propertyValues.addPropertyValue("name", beanClassName);
            propertyValues.addPropertyValue("person", new RuntimeBeanReference("laowang"));
            ctx.registerBeanDefinition(Introspector.decapitalize(beanClassName.substring(beanClassName.lastIndexOf("."))), definition);
        });

        ctx.refresh();

    }
}

2.4.4 编程式资源管理

2.4.1~2.4.3 节的内容都是对 IOC 容器和 IOC 容器中的 Bean 进行编程式驱动和开发,然而资源管理同样不容忽视。

前面的代码已经涉及编程式资源加载的方式,即借助 ClassPathResource 实现类路径下的文件加载。入门篇 4.7.1 节已经简单介绍过资源管理机制,本节会更加详细深入地介绍 Spring Framework 中的资源模型,以及 Spring Framework 加载资源的方式。

1. 设计起源

读者在接触 Spring Framework 设计的资源管理模型时可能会产生疑惑:

利用 ClassLoader 的 getResource 和 getResourceAsStream 方法本来就可以加载类路径下的文件,使用 java.io 和 java.net 系列的类也可以完成文件系统和网络的资源加载,为什么 Spring Framework 要"另起炉灶"​?

要回答这个问题,可以查阅 Spring Framework 官方文档有关资源管理概述的内容,原文比较长,简而言之是因为 Spring Framework 的开发者认为 JDK 原生的资源加载方式,尤其是使用 URL 加载类路径或 ServletContext 下的资源,没有标准的处理手段,实现起来相对烦琐,于是开发者决定自行封装一套更实用、更贴合 Spring Framework 应用的资源管理机制。

2. 资源模型 API

Spring Framework 用于表示资源的顶层接口并不是之前接触过的 Resource,而是 InputStreamSource,该接口的定义如代码所示。该接口的设计表示仅提供可读取的一条 InputStream,并不透露也不限制其来源。

关于 InputStreamSource 的子接口 Resource,文档中值得重点关注的是:

Resource 是资源描述符的接口,它可以从基础资源的实际类型中抽象出来,例如文件或类路径资源。

从其中的 "文件或类路径的资源" 可知,Resource 确实更适合 Spring Framework 做资源加载(配置文件通常都放到类路径下)​。

具体到 Resource 的实现类,Spring Framework 主要考虑了以下 3 种资源加载的方式,其中每一行最后的方括号代表资源路径的前缀:

如果是以 classpath 开头的资源路径,Spring Framework 解析后会自动去类路径下寻找;

如果是以 file 开头的资源路径,Spring Framework 会去文件系统中寻找;

如果是 URL 支持的协议开头,底层会使用对应的协议去尝试获取相应的资源文件。

(1)借助ClassLoader加载类路径下的资源:ClassPathResourceclasspath:/ 。

(2)借助File加载文件系统中的资源:FileSystemResourcefile: 。

(3)借助URL和不同的协议加载本地/网络上的资源:UrlResourcexxx:/ 。

除了上述 3 种方式,Spring Framework 还考虑到了一种上下文的资源加载(比如 ServletContext 这样的域)​,这种资源加载的模型是ContextResource,对应 ServletContext 的实现方式即是 ServletContextResource。这种特殊的方式仅供读者了解,实际开发中极少用到。

3. 加载资源

加载资源的方式也不难,代码中利用 ClassPathResource 读取类路径文件就是很基础的一种。

针对其他几种资源的加载方式如代码所示。这些加载资源的方式都有一个局限性:每次只能加载一个文件;如果一个目录下有多个文件,或者有多层目录,那么加载资源会变得比较麻烦。

java 复制代码
package com.yangjunbo.spring.programmatic.exampled;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;

public class ResourceLoadApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathResource classPathResource = new ClassPathResource("/resource/dog.txt");
        FileSystemResource fileSystemResource = new FileSystemResource("D:/dog.txt");
        UrlResource urlResource = new UrlResource("file:D:/dog.txt");
        UrlResource htmlResource = new UrlResource("https://spring.io/projects/spring-boot/");
    }
}

针对加载多个资源的问题,Spring Framework 提供了一个更好用的组件 --- ResourceLoader,它可以借助一些机制来实现一次性加载多个资源。另外,在入门篇的 4.2.1 节中曾经提过,ApplicationContext 具备资源加载的能力,而该能力就来自 ResourceLoader 接口,读者可以查阅入门篇以回顾。

借助 ApplicationContext 加载文件的方式更加简单,只需要遵循上一小节讲解的加载资源的前缀表达式规则即可。

另外 ApplicationContext 还支持 Ant 风格的模式匹配加载,它可以做到加载一个或多个目录下的多个资源文件,如代码所示。读者可以结合注释理解和体会 ApplicationContext 加载资源的强大功能。

java 复制代码
package com.yangjunbo.spring.programmatic.exampled;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;

public class ResourceLoadApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathResource classPathResource = new ClassPathResource("/resource/dog.txt");
        FileSystemResource fileSystemResource = new FileSystemResource("D:/dog.txt");
        UrlResource urlResource = new UrlResource("file:D:/dog.txt");
        UrlResource htmlResource = new UrlResource("https://spring.io/projects/spring-boot/");

        ApplicationContext ctx = new AnnotationConfigApplicationContext();
        Resource classpathResource = ctx.getResource("classpath:resource/dog.txt");
        Resource filesystemResource = ctx.getResource("D:/dog.txt");
        // 加载指定路径下的所有文件
        Resource[] dirResources = ctx.getResources("classpath:resource/*");
        // 加载类路径下的所有txt文件
        Resource[] alltxtResources = ctx.getResources("classpath:/**/*.txt");
    }
}

到此为止,演示了多种编程式驱动 IOC 的内容,这并不是全部。

仅通过这些案例启示读者,一切通过声明式和配置式等快速简单易懂的方式完成的工作,底层都是由编程式来支撑实现,

所以完全可以以编程式驱动达到同样的效果。这种方式在平时的业务开发中几乎不会用到,但当读者从事的工作比较深入,或者参与应用底层封装和二次开发,那么这些技能会起到很大的帮助。

相关推荐
IT枫斗者枫哥1 小时前
UPDATE影响0行,接口却返回成功:把版本冲突接回业务
java·后端
带金箍的至尊宝1 小时前
系统架构设计师笔记 03:CPU 结构、Cache 与总线怎么考
笔记·系统架构
霸道流氓气质1 小时前
AI应用成本优化完全指南:Token压缩、语义缓存与模型路由的Java生产级实战
java·人工智能·缓存
2401_850122041 小时前
高温耐油热缩套管选型:材料、温度和油液要匹配
经验分享·笔记·科技·学习
mmm.c1 小时前
Redis 两级缓存 + Pub/Sub 广播:一次为抗高并发做的缓存改造
java
yexianglunbai1 小时前
中间件(Middleware)详解:概念、作用与实战
java·开发语言·中间件
叶总没有会1 小时前
OpenFeign
java·开发语言·springcloud·openfeign
小七在进步1 小时前
类和对象(四)
java·javascript·ajax
白帽攻防录1 小时前
SRC 挖洞:Apache Tomcat 加密拦截器绕过深度复盘,CVE-2026-34486 fail-open 一行代码怎么打穿集群通信
java·网络安全·tomcat·apache