Spring常见错误 - 集合收集装配和直接装配的共存问题

30 篇文章 3 订阅

一. 集合收集装配和直接装配的共存问题

首先,本篇文章针对性的是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();
    }
}

访问结果如下:
在这里插入图片描述

那么为啥我要通过构造函数注入的方式来完成属性注入呢?就是为了引出本文想讨论的一个重点,集合类型收集装配和直接装配的共存问题。

那么,往往我们在开发当中,可能更喜欢用这类方法(直接装配):注意把上面的两个BeanOneBeanTwo注释掉。

@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;
	}
}

简而言之就是:

  1. 获取当前集合的元素类型:User
  2. 根据这个元素的类型,去容器中寻找所有该类型的Bean
  3. 将匹配到的Bean进行类型转换,然后塞到集合结果中并返回。

而本案例当中,在对MyController进行users属性注入的时候。符合类型条件的两个单独的Bean必定是加载好的。也就是下图中展示的部分:
在这里插入图片描述
因此匹配出来的结果就是:UserOneUserTwo
在这里插入图片描述
匹配出来后,根据源码的逻辑,直接返回:
在这里插入图片描述

而我们知道,我们代码中的users属性,通过@Autowired注入的。而它又是根据类型去自动匹配然后赋值的。也就是直接装配,它的执行逻辑主要在于:

Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);

但是问题在于:

  1. 集合类型的收集装配流程在于直接装配流程之前。
  2. 倘若收集匹配装配成功,会提前返回,不会执行后续流程。
  3. 即两种装配方式不可共存。

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

结果如下:
在这里插入图片描述

二. 总结

  1. 如果项目中对于集合类型的Bean,同时存在两种注入方式:收集装配和直接装配。那么后者会失效。
  2. 原因是因为Spring在进行属性注入的时候,会优先对集合类型进行装配(采用收集装配的方式):本质上就是从整个Spring容器中去寻找和集合元素类型相同的Bean。然后返回。
  3. 倘若能够匹配到结果,就会直接返回。不会再根据类型进行自动装配。因此直接装配的方式会失效。

另外,我们还注意到,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;
}

效果如图:
在这里插入图片描述

根据提供的引用内容,spring-boot-starter-data-redis是Spring Boot中用于自动装配Redis的starter包。它包含了自动装配所需的类和注解等。当我们在项目的pom.xml文件中引入spring-boot-starter-data-redis包时,Spring Boot会自动根据配置文件中的相关配置信息来完成Redis的自动装配。 具体来说,spring-boot-starter-data-redis使用了RedisAutoConfiguration类来实现自动装配。该类通过读取配置文件中的相关配置信息,例如主机名、端口号、密码等,来创建Redis连接工厂和RedisTemplate等实例。这些实例可以在应用程序中直接使用,而无需手动配置和初始化。 下面是一个示例代码,展示了如何使用spring-boot-starter-data-redis进行自动装配: ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.redis.core.RedisTemplate; @SpringBootApplication public class RedisApplication { private final RedisTemplate<String, String> redisTemplate; public RedisApplication(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } public static void main(String[] args) { SpringApplication.run(RedisApplication.class, args); } // 在需要使用Redis的地方,可以直接注入RedisTemplate实例,并进行操作 // 例如: // redisTemplate.opsForValue().set("key", "value"); // String value = redisTemplate.opsForValue().get("key"); } ``` 通过上述代码,我们可以看到,在Spring Boot应用程序中,我们只需要在需要使用Redis的地方注入RedisTemplate实例,就可以直接使用Redis的相关操作方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值