Spring常见错误 - 集合收集装配和直接装配的共存问题
一. 集合收集装配和直接装配的共存问题
首先,本篇文章针对性的是Spring
中关于集合属性的注入问题。我们从案例角度来看待这个问题会更直观。
1.1 案例
1.首先我们自定义一个简单的User
:
public class User {
private String name;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.自定义两个UserBean
:
@Configuration
public class MyBean {
@Bean
public User user1() {
User user = new User();
user.setName("UserOne");
return user;
}
@Bean
public User user2() {
User user = new User();
user.setName("UserTwo");
return user;
}
}
2.在Controller
类中注入,这里是收集装配的方式(注意,这里并没有写错,而是属性注入的一种方式,通过构造函数注入):
@Controller
public class MyController {
private List<User> users;
public MyController(List<User> users) {
this.users = users;
}
@GetMapping("/hello")
@ResponseBody
public String hello() {
return users.toString();
}
}
访问结果如下:
那么为啥我要通过构造函数注入的方式来完成属性注入呢?就是为了引出本文想讨论的一个重点,集合类型收集装配和直接装配的共存问题。
那么,往往我们在开发当中,可能更喜欢用这类方法(直接装配):注意把上面的两个BeanOne
和BeanTwo
注释掉。
@Bean
public List<User> users() {
ArrayList<User> users = new ArrayList<>();
User user = new User();
user.setName("UserThree");
User user2 = new User();
user2.setName("UserFour");
users.add(user);
users.add(user2);
return users;
}
Controller
引入:
@Autowired
private List<User> users;
访问结果如下:
可见能得到同样的效果,但是倘若我将两种装配方式同时存在,看看会怎么样,如图:
Controller
中依旧使用的@Autowired
的方式:
结果如下:
可见直接装配的bean
并没有生效,结果也并不是我们想要的。同时我们可以看出直接装配和收集装配的不同点:
- 直接装配:返回我们一次性指定好的几个对象,不用去容器中寻找了。
- 收集装配:根据某个类型,将容器中所有的类型收集起来,放入某个集合并返回。
1.2 原理分析
我们依旧来看下代码注入的入口:DefaultListableBeanFactory.doResolveDependency()
:
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
Object shortcut = descriptor.resolveShortcut(this);
if (shortcut != null) {
return shortcut;
}
Class<?> type = descriptor.getDependencyType();
// 针对@Value注解的处理
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
// 集合类型的处理
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
// 否则根据类型去注入
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
}
1.2.1 收集装配过程
在Spring常见问题解决 - @Value注解注入的值出错了?这篇文章中,主要说了@Value
注解处理的相关流程。那么针对本文的集合类型,如果使用的是收集装配方式。就需要看这行代码了:
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
Class<?> type = descriptor.getDependencyType();
if (descriptor instanceof StreamDependencyDescriptor) {
// ...装配Stream类型
}
else if (type.isArray()) {
// ...装配数组
}
else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
// 1.获取元素的集合类型
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
if (elementType == null) {
return null;
}
// 2.根据元素类型去寻找所有的Bean,这里和外部的调用是同一个方法,就是根据类型去自动装配
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
// 3.将所有查到的Bean进行类型转换并且返回
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
// ...
return result;
}
else if (Map.class == type) {
// ..解析Map类型
}
else {
return null;
}
}
简而言之就是:
- 获取当前集合的元素类型:
User
。 - 根据这个元素的类型,去容器中寻找所有该类型的
Bean
。 - 将匹配到的
Bean
进行类型转换,然后塞到集合结果中并返回。
而本案例当中,在对MyController
进行users
属性注入的时候。符合类型条件的两个单独的Bean
必定是加载好的。也就是下图中展示的部分:
因此匹配出来的结果就是:UserOne
和UserTwo
。
匹配出来后,根据源码的逻辑,直接返回:
而我们知道,我们代码中的users
属性,通过@Autowired
注入的。而它又是根据类型去自动匹配然后赋值的。也就是直接装配,它的执行逻辑主要在于:
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
但是问题在于:
- 集合类型的收集装配流程在于直接装配流程之前。
- 倘若收集匹配装配成功,会提前返回,不会执行后续流程。
- 即两种装配方式不可共存。
1.3 问题解决
问题解决最直接的办法就是:避免对于同种元素类型的集合采取多种注入方式。在对于同一个集合对象的注入上,混合多种注入方式是不可取的,Spring
只会返回容器中对应类型的所有元素。
一般用直接装配就可以了(同时不要在项目中存在单独的UserBean
):
@Bean
public List<User> users() {
ArrayList<User> users = new ArrayList<>();
User user = new User();
user.setName("UserThree");
User user2 = new User();
user2.setName("UserFour");
User user3 = new User();
user3.setName("UserOne");
User user4 = new User();
user4.setName("UserTwo");
users.add(user);
users.add(user2);
users.add(user3);
users.add(user4);
return users;
}
结果如下:
二. 总结
- 如果项目中对于集合类型的
Bean
,同时存在两种注入方式:收集装配和直接装配。那么后者会失效。 - 原因是因为
Spring
在进行属性注入的时候,会优先对集合类型进行装配(采用收集装配的方式):本质上就是从整个Spring
容器中去寻找和集合元素类型相同的Bean
。然后返回。 - 倘若能够匹配到结果,就会直接返回。不会再根据类型进行自动装配。因此直接装配的方式会失效。
另外,我们还注意到,1.1节案例中,对于收集装配方式,结果输出的是:
这里原因是因为Spring
加载Bean
是根据自然顺序来的。我们声明Bean
的自然顺序为:UserOne
在前,UserTwo
在后。
倘若希望UserTwo
先被加载,可以通过@Order
注解的方式来声明先后顺序,数字越小代表优先级越高。
@Bean
@Order(2)
public User user1() {
User user = new User();
user.setName("UserOne");
return user;
}
@Bean
@Order(1)
public User user2() {
User user = new User();
user.setName("UserTwo");
return user;
}
效果如图: