从零开始 Spring Boot 36:注入集合
图源:简书 (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'}
默认情况下我们只能通过这种方式生成常见类型的元素组成的容器,比如String
、Integer
等。这是因为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
# ...
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)