图源:简书 (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
从配置文件中“注入”容器,比如:
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
从配置文件中读取并创建容器:
@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,谢谢阅读。
本文的所有示例代码,可以从这里获取。