《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));
}
}