《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 4

《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 4

2.5 依赖查找进阶与高级

2.2.1 节中介绍了最基础的两种依赖查找方式:基于 Bean 的 Name 或 Bean 的类型获取一个 Bean。下面继续介绍依赖查找的另外 4 种使用方式。

2.5.1 of Type

如果一个接口有多个实现,想一次性把这些实现类 Bean 都获取出来,那么 getBean 方法就不足以满足需求。ApplicationContext 在 BeanFactory 的基础上扩展了一些新的 获取 Bean 的方法,其中有一个方法叫 getBeansOfType,它可以一次性获取 IOC 容器中某一个类型的所有对象。

为了演示接口+实现类的场景,创建一个 DemoDao 接口和 3 个不同的实现类,并使用注解配置类将这 3 个类注册到IOC容器中。

java 复制代码
package com.yangjunbo.dl.exampleb;

public interface DemoDao {}
java 复制代码
package com.yangjunbo.dl.exampleb;

public class DemoMySQLDao implements DemoDao {}
java 复制代码
package com.yangjunbo.dl.exampleb;

public class DemoOracleDao implements DemoDao {}
java 复制代码
package com.yangjunbo.dl.exampleb;

public class DemoPostgresDao implements DemoDao {}
java 复制代码
package com.yangjunbo.dl.exampleb;

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

@Configuration
public class OfTypeConfiguration {
    @Bean
    public DemoDao demoMySQLDao() { return new DemoMySQLDao(); }
    @Bean
    public DemoDao demoOracleDao() { return new DemoOracleDao(); }
    @Bean
    public DemoDao demoPostgresDao() { return new DemoPostgresDao(); }
}

编写启动类来驱动 IOC 容器获取这些 DemoDao 的实现类,调用 ApplicationContext 的 getBeansOfType 方法,可以获得一个 Map,这个集合中的 key 就是每个 bean 对象的名称,value 则为这些 bean 对象本身。

运行 main 方法,控制台中成功打印了注解配置类中注册的 3 个对象,如此即实现了传入一个接口/抽象类,返回容器中所有的实现类/子类。

java 复制代码
package com.yangjunbo.dl.exampleb;

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

import java.util.Map;

public class OfTypeApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(OfTypeConfiguration.class);
        Map<String, DemoDao> beans = ctx.getBeansOfType(DemoDao.class);
        beans.forEach((beanName, bean) -> {
            System.out.println(beanName + " : " + bean.toString());
        });
    }
}

2.5.2 withAnnotation

IOC 容器还可以根据类上标注的注解来查找对应的 Bean,它借助的方法是 getBeansWithAnnotation。为了演示基于注解的获取,下面定义一个新的注解 @Color,并相应地创建几个与颜色相关的类 Black、Red 等;随后编写一个注解配置类 WithAnnoConfiguration,在其中注册 Black、Red 对象以及任意一个类。

java 复制代码
package com.yangjunbo.dl.examplec;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Color { }
java 复制代码
package com.yangjunbo.dl.examplec;

@Color
public class Red { }
java 复制代码
package com.yangjunbo.dl.examplec;

@Color
public class Black { }
java 复制代码
package com.yangjunbo.dl.examplec;

import com.yangjunbo.di.examplea.Person;

public class Cat {
    private String name;
    private Person master;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Person getMaster() {
        return master;
    }

    public void setMaster(Person master) {
        this.master = master;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", master=" + master +
                '}';
    }
}
java 复制代码
package com.yangjunbo.dl.examplec;

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

@Configuration
public class WithAnnoConfiguration {
    @Bean
    public Black black() { return new Black(); }
    @Bean
    public Red red() { return new Red(); }
    @Bean
    public Cat cat() { return new Cat(); }
}

编写一个启动类,驱动 IOC 容器后调用 getBeansWithAnnotation 方法获取所有标注了 @Color 注解的 bean 对象。

运行 main 方法,控制台中只打印了 black 和 red 两个 Bean,符合预期,证明成功获取 Bean 对象。

java 复制代码
package com.yangjunbo.dl.examplec;

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

import java.util.Map;

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

2.5.3 获取所有Bean

取出IOC容器中的所有Bean。要满足这种需求,使用上述学过的方法都无法实现,因为我们无法预知 IOC 容器中注册了哪些 Bean。在ApplicationContext 的父接口,即 BeanFactory 的派生接口 ListableBeanFactory 中有一个 getBeanDefinitionNames 方法,这个方法可以将 IOC 容器中所有 Bean 的名称全部返回,获取所有 Bean 的名称后,就可以循环调用 getBean 方法来逐个获取 IOC 容器中的所有 bean 对象。

使用 WithAnnoConfiguration 配置类驱动 IOC 容器,并打印其中所有 Bean 的名称。运行 main 方法,控制台可以成功打印出 Spring Framework 内置的一些组件名称,以及包含配置类在内的所有注册进去的 bean 对象的名称。

java 复制代码
package com.yangjunbo.dl.examplec;

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

import java.util.Map;
import java.util.stream.Stream;

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

        ApplicationContext ctx2 = new AnnotationConfigApplicationContext(WithAnnoConfiguration.class);
        String[] beanNames = ctx2.getBeanDefinitionNames();
        // 利用JDK 8的Stream快速编写打印方法
        Stream.of(beanNames).forEach(System.out::println);
    }
}

2.5.4 延迟查找

如果有一种机制,能够支持我们在获取一个 Bean 时,无论 Bean 是否存在都不会抛出异常,而是返回一个包装器,只有在我们真正打开包装器时,才能得知其内部的 Bean 是否真的存在,由此可以避免 IOC 容器在获取 Bean 时抛出异常。自 Spring Framework 4.3 版本开始引入了一个新的 API:ObjectProvider。基于这个 API 就可以实现 Bean 的延迟查找。

实际使用时,对于获取不确定是否存在的 Bean,我们不再使用 getBean 方法,而是改用 getBeanProvider 方法,这个方法会返回一个ObjectProvider,其内部的泛型就是所要获取的 Bean 的类型。使用 ObjectProvider 之后,运行 main 方法时程序并不会抛出异常,只有调用dogProvider 的 getObject 方法,真正要取包装器中的 Dog 对象时才会抛出异常。总结下来,ObjectProvider 的作用相当于延后了 Bean 的获取时机,也延后了异常可能出现的时机。

java 复制代码
package com.yangjunbo.dl.exampled;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("dl/exampled/inject-lazy.xml");
        // 运行下面的代码会抛出Bean没有定义的异常NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
        
        // 运行这一行代码不会抛出异常
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);
    }
}
java 复制代码
<?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">

</beans>
java 复制代码
package com.yangjunbo.dl.exampled;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("dl/exampled/inject-lazy.xml");
        // 运行下面的代码会抛出Bean没有定义的异常NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
        
        // 运行这一行代码不会抛出异常
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);
    }
}

虽然 ObjectProvider 可以让 Bean 的获取时机延后,但是 Bean 不存在时调用 getObject 方法还是会抛出异常。ObjectProvider 中还有一个方法getIfAvailable,它可以在找不到 Bean 时返回 null 而不抛出异常。

java 复制代码
package com.yangjunbo.dl.exampled;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("dl/exampled/inject-lazy.xml");
        // 运行下面的代码会抛出Bean没有定义的异常NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
        
        // 运行这一行代码不会抛出异常
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);

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

随着 Spring Framework 5.0 基于 Java 8 的发布,函数式编程也被大量用于 Spring Framework 中,其中 ObjectProvider 中新增加了几个方法,可以使编码风格更加优雅。Spring Framework 5.0 后给 ObjectProvider 重载了一个带 Supplier 参数的 getIfAvailable 方法,它可以在 IOC 容器中没有指定 Bean 时调用 Supplier 产生一个默认对象。

java 复制代码
package com.yangjunbo.dl.exampled;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("dl/exampled/inject-lazy.xml");
        // 运行下面的代码会抛出Bean没有定义的异常NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
        
        // 运行这一行代码不会抛出异常
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);

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

        Dog dog2 = dogProvider.getIfAvailable(() -> new Dog()); // 或使用更简单的 Dog::new
        System.out.println(dog2);
    }
}

除了获取 bean 对象本身,还有一种实用的场景:当 Bean 存在时,执行一段代码,不存在时则不执行。Spring Framework 5.0 后给ObjectProvider 扩展了一个带 Consumer 参数的 ifAvailable 方法,这个方法内的 Consumer 表达式只有在 Bean 存在时才会被回调。

java 复制代码
package com.yangjunbo.dl.exampled;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("dl/exampled/inject-lazy.xml");
        // 运行下面的代码会抛出Bean没有定义的异常NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
        
        // 运行这一行代码不会抛出异常
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);

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

        Dog dog2 = dogProvider.getIfAvailable(() -> new Dog()); // 或使用更简单的 Dog::new
        System.out.println(dog2);

        dogProvider.ifAvailable(dog3 -> System.out.println(dog3));
    }
}
相关推荐
周GZ4 小时前
简单讲解Java中静态方法与 实例方法
java·开发语言
疯狂打码的少年5 小时前
【数据结构】树的基本概念与二叉树定义
java·数据结构·笔记·算法
马上去吃汉堡王5 小时前
Windows11安装Geant4
笔记
气泡水。5 小时前
RHCSA笔记实记
linux·笔记
盗理者5 小时前
AI Agent 技能分享|SQL 性能诊断与优化
java·sql·spring·skill
GitLqr5 小时前
Java 26 终于原生支持 HTTP/3 了:告别 Netty,直接用 QUIC
java·netty·http3
用户40966601317515 小时前
Jackson 序列化:@JsonIgnore / @JsonProperty / @JsonFormat / @JsonInclude / @JsonUnwrapped 一次讲清楚
java·后端
码路漫漫5 小时前
用了 Caffeine,消息为什么还是被处理了两次?
java
云和数据.ChenGuang6 小时前
fastapi项目拆分实战数据模型
java·服务器·数据库·人工智能·深度学习·fastapi·强化学习
祈禾6 小时前
Redis三大特殊数据类型
运维·服务器·数据库·redis·笔记·缓存