从零开始 Spring Boot 36:注入集合

从零开始 Spring Boot 36:注入集合

从零开始 Spring Boot 36:注入集合_第1张图片

图源:简书 (jianshu.com)

在前面一篇文章从零开始 Spring Boot 27:IoC中,讨论过依赖注入集合(Java 容器)的内容,这里更深入地讨论注入集合的相关内容。

我们来看一个最基本的集合注入示例:

public record BookCategory(String name) {
}

@Configuration
public class WebConfig {
    //文学
    //文学理论
    @Bean
    BookCategory literaryTheory() {
        return new BookCategory("literary theory");
    }

    //外国文学
    @Bean
    BookCategory foreignLiterature() {
        return new BookCategory("foreign literature");
    }

    //中国文学
    @Bean
    BookCategory chineseLiterature() {
        return new BookCategory("chinese literature");
    }

    //历史
    //中国历史
    @Bean
    BookCategory chineseHistory() {
        return new BookCategory("chinese history");
    }

    //外国历史
    @Bean
    BookCategory foreignHistory() {
        return new BookCategory("foreign history");
    }
}

@RestController
@RequestMapping("/book")
public class BookController {
    @Autowired
    List<BookCategory> bookCategories;

    @GetMapping("/category/list")
    public Result<Object> listBookCategories() {
        System.out.println(bookCategories);
        return Result.success();
    }
}

运行示例会输出:

[BookCategory[name=literary theory], BookCategory[name=foreign literature], BookCategory[name=chinese literature], BookCategory[name=chinese history], BookCategory[name=foreign history]]

在这个示例中,@Autowired标记的是一个Java容器类型(List),而 Spring “智能地”用容器元素的类型BookCategory填充了一个List对象,并最终进行注入。

默认值

如果实际上并不存在任何容器元素的bean,会发生什么事?

比如删除配置类中的所有 bean 的工厂方法:

@Configuration
public class WebConfig {
	
}

程序将无法通过编译,报如下错误:

Field bookCategories in com.example.dicollections.controller.BookController required a bean of type 'java.util.List' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)

错误提示告诉我们无法完成依赖注入,因为缺少相关类型的 bean。可以看到,注入容器的时候,默认情况下,如果一个容器元素类型的 bean 都没有,就不会完成容器对象组装,并注入失败。

我们可以通过以下方式让这种情况发生时不报错:

public class BookController {
    @Autowired(required = false)
    List<BookCategory> bookCategories;
	// ...
}

此时bookCategories实际上并没有被注入,因此其值是Java对象初始化后的null

如果我们需要为其指定一个其它默认值,比如空列表,可以:

public class BookController {
    @Autowired(required = false)
    List<BookCategory> bookCategories = new ArrayList<>();
    // ...
}

要注意的是,如果是通过构造器注入,结果会有所不同,比如:

public class BookController {
    List<BookCategory> bookCategories;

    public BookController(List<BookCategory> bookCategories){
        this.bookCategories = bookCategories;
    }
	// ...
}

即使Spring 容器中没有任何可用于注入的 bean,bookCategories属性也会被一个空列表初始化。

但如果是通过Setter注入,就会报错,比如:

public class BookController {
    List<BookCategory> bookCategories;
    
    @Autowired
    public void setBookCategories(List<BookCategory> bookCategories) {
        this.bookCategories = bookCategories;
    }
    // ...
}

和之前的处理类似,如果要不报错和指定默认值,可以:

public class BookController {
    List<BookCategory> bookCategories = Collections.emptyList();

    @Autowired(required = false)
    public void setBookCategories(List<BookCategory> bookCategories) {
        this.bookCategories = bookCategories;
    }
    // ...
}

Spring 对 Setter 注入的处理是——如果缺少可以被注入的 bean,Setter 就不会被调用。

多个备选项

只存在容器元素类型的 bean 或只存在容器类型的 bean,注入的结果都是明确的,但是如果两者都存在,注入的结果会是什么?

看下面的示例:

@Configuration
public class WebConfig {
    //文学
    //文学理论
    @Bean
    BookCategory literaryTheory() {
        return new BookCategory("literary theory");
    }

  	// ...

    @Bean
    List<BookCategory> defaultBookCategories(){
        return List.of(chineseHistory(), foreignHistory());
    }
}

public class BookController {
    @Autowired(required = false)
    List<BookCategory> bookCategories = new ArrayList<>();
	// ...
}

输出:

[BookCategory[name=literary theory], BookCategory[name=foreign literature], BookCategory[name=chinese literature], BookCategory[name=chinese history], BookCategory[name=foreign history]]

注入的结果是使用容器元素类型的 bean 组装容器对象后注入,这种注入方式优先于容器类型的 bean

如果要用容器类型的 bean 完成注入,要怎么做?

可以使用@Resource实现:

public class BookController {
    @Resource(name = "defaultBookCategories")
    List<BookCategory> bookCategories;
	// ...
}

关于@Autowired@Resource的区别,可以阅读这篇文章。

@Value

可以用@Value从配置文件中“注入”容器,比如:

public class BookController {
    @Value("#{${book.categories}}")
    private List<String> categories;
    // ...
}

对应的配置文件:

book.categories={'literary theory','foreign literature','chinese literature','chinese history','foreign history'}

默认情况下我们只能通过这种方式生成常见类型的元素组成的容器,比如StringInteger等。这是因为Spring 是通过转换器(Converter)来处理字符串到相应类型的转换,而默认只包含一些基本类型的转换器。

换言之,如果我们需要处理自定义类型,想要从配置文件中将字符串形式的信息读取并创建我们需要的集合,可以建立相应的转换器并实现:

public class StringToBookCategoryConverter implements Converter<String, BookCategory> {
    @Override
    public BookCategory convert(String source) {
        return new BookCategory(source);
    }
}

@Configuration
public class MVCConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        WebMvcConfigurer.super.addFormatters(registry);
        registry.addConverter(new StringToBookCategoryConverter());
    }
}

@RestController
@RequestMapping("/book")
public class BookController {
    @Value("#{${book.categories}}")
    private List<BookCategory> bookCategories;
	// ...
}

关于转换器的相关内容,可以阅读我的另一篇文章。

@ConfigurationProperties

当然,同样可以借助@ConfigurationProperties从配置文件中读取并创建容器:

@Configuration
@ConfigurationProperties(prefix = "my.book.categories")
@Getter
@Setter
public class BookCategories {
    private List<BookCategory> list;
}

@RestController
@RequestMapping("/book")
public class BookController {
    @Autowired
    private BookCategories bookCategories2;
    // ...
    @GetMapping("/category/list")
    public Result<Object> listBookCategories() {
        // ...
        System.out.println(bookCategories2.getList());
        return Result.success();
    }
}

对应的配置文件:

my.book.categories.list[0]=literary theory
my.book.categories.list[1]=foreign literature
my.book.categories.list[2]=chinese literature
my.book.categories.list[3]=chinese history
my.book.categories.list[4]=foreign history

同样的,这里使用到了前面示例中提到的自定义转换器StringToBookCategoryConverter。除此之外,也可以不使用自定义转换器,而是通过在配置文件中通过结构化语法指定每个对象的每个属性,比如:

my.book.categories.list[0].name=literary theory
my.book.categories.list[1].name=foreign literature
# ...

泛型

就像前面演示的,Spring 在进行依赖注入时,可以识别容器类的泛型参数。我们可以利用这一点来限制和筛选注入容器中的 bean。

比如下面这个示例:

@Getter
@Setter
@AllArgsConstructor
@ToString
public abstract class Vehicle {
    private String name;
    private String manufacturer;
}

@Getter
@Setter
@ToString(callSuper = true)
public class Car extends Vehicle {
    private String engineType;

    public Car(String name, String manufacturer, String engineType) {
        super(name, manufacturer);
        this.engineType = engineType;
    }
}

@Getter
@Setter
@ToString(callSuper = true)
public class Motorcycle extends Vehicle{
    private boolean twoWheeler;

    public Motorcycle(String name, String manufacturer, boolean twoWheeler) {
        super(name, manufacturer);
        this.twoWheeler = twoWheeler;
    }
}

@Configuration
public class WebConfig {
    @Bean
    public Car bmp2() {
        return new Car("bmp2", "俄罗斯", "V8");
    }

    @Bean
    public Car t90() {
        return new Car("T-90", "俄罗斯", "V9");
    }

    @Bean
    public Car a99() {
        return new Car("99A", "中国", "V10");
    }

    @Bean
    public Motorcycle f90() {
        return new Motorcycle("f90", "9号电动车", true);
    }
    // ...
}

@RestController
@RequestMapping("/vehicle")
public class VehicleController {
    @Autowired
    private List<Vehicle> vehicles;

    @GetMapping("/print")
    public Result<Object> print(){
        System.out.println(vehicles);
        return Result.success();
    }
}

执行相应的HTTP请求,就能看到下面的测试输出:

[Car(super=Vehicle(name=bmp2, manufacturer=俄罗斯), engineType=V8), Car(super=Vehicle(name=T-90, manufacturer=俄罗斯), engineType=V9), Car(super=Vehicle(name=99A, manufacturer=中国), engineType=V10), Motorcycle(super=Vehicle(name=f90, manufacturer=9号电动车), twoWheeler=true)]

这说明如果我们要注入一个泛型容器,且该容器的泛型参数是一个基类型,则该类型的所有子类(包括该类型本身)的 bean 实例都将作为容器元素被注入。

当然,如果有需要,我们可以利用@Qualifier注解(或利用@Qualifier实现的组合注解)来进一步限定和筛选用于注入的 bean,这点在从零开始 Spring Boot 27:IoC中的相关章节中有过明确说明,这里不再赘述。

如果我们仅需要某个具体类型的 bean,而非某个基类的所有派生类,可以用具体类型作为容器类的泛型参数:

@RestController
@RequestMapping("/vehicle")
public class VehicleController {
	// ...
    @Autowired
    private List<Motorcycle> motorcycles;

    @GetMapping("/print")
    public Result<Object> print(){
        // ...
        System.out.println(motorcycles);
        return Result.success();
    }
}

输出:

[Motorcycle(super=Vehicle(name=f90, manufacturer=9号电动车), twoWheeler=true)]

实际上,Spring 是通过ResolvableType来确定泛型参数的具体类型的:

ResolvableType resolvableType = ResolvableType.forField(this.getClass().getDeclaredField("vehicles"));
System.out.println(resolvableType);
ResolvableType generic = resolvableType.getGeneric();
System.out.println(generic);
Class<?> resolve = generic.resolve();
System.out.println(resolve);

输出:

java.util.List
com.example.dicollections.entity.Vehicle
class com.example.dicollections.entity.Vehicle

The End,谢谢阅读。

本文的所有示例代码,可以从这里获取。

参考资料

  • 从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)
  • Spring - Injecting Collections | Baeldung
  • Wiring in Spring: @Autowired, @Resource and @Inject | Baeldung
  • Spring Boot 教程3:在 Spring Boot 中使用 application.yml 与 application.properties - 红茶的个人站点 (icexmoon.cn)
  • Inject Arrays & Lists from Spring Property Files | Baeldung
  • 从零开始 Spring Boot 29:类型转换 - 红茶的个人站点 (icexmoon.cn)
  • Spring Autowiring of Generic Types | Baeldung

你可能感兴趣的:(JAVA,java,集合,依赖注入)