在Spring中注入Set集合的方法详解
作者:步行cgn
一、什么是 Set 注入?
Set 注入是指将一个 Set 类型的集合注入到 Bean 的属性中。Set 与 List 最大的区别是:Set 中的元素不允许重复,且通常不保证顺序(除非使用 LinkedHashSet 或 TreeSet)。
@Component
public class MyBean {
private Set<String> servers; // 字符串 Set
private Set<DataSource> dataSources; // Bean Set
private Set<Handler> handlers; // 策略/处理器 Set
}
在 Spring 注入场景中,Set 常用于:
- 需要去重的多个同类型依赖
- 配置文件中定义的多个值(不关心顺序)
- 收集所有实现类时避免重复
二、XML 配置中注入 Set
2.1 注入字符串 Set
<bean id="myBean" class="com.example.MyBean">
<property name="servers">
<set>
<value>server1.example.com</value>
<value>server2.example.com</value>
<value>server3.example.com</value>
</set>
</property>
</bean>2.2 注入 Bean Set
<bean id="myBean" class="com.example.MyBean">
<property name="dataSources">
<set>
<ref bean="dataSource1"/>
<ref bean="dataSource2"/>
<ref bean="dataSource3"/>
</set>
</property>
</bean>2.3 注入基本类型 Set
<bean id="myBean" class="com.example.MyBean">
<property name="ports">
<set>
<value>8080</value>
<value>8081</value>
<value>8082</value>
</set>
</property>
</bean>Spring 会自动将字符串值转换为 Integer 类型。
2.4 注入对象 Set
<bean id="myBean" class="com.example.MyBean">
<property name="users">
<set>
<bean class="com.example.User">
<property name="name" value="张三"/>
<property name="age" value="25"/>
</bean>
<bean class="com.example.User">
<property name="name" value="李四"/>
<property name="age" value="30"/>
</bean>
</set>
</property>
</bean>2.5<set>与<list>的 XML 差异
<!-- Set 类型属性 -->
<property name="servers">
<set>
<value>server1</value>
<value>server2</value>
</set>
</property>
<!-- List 类型属性 -->
<property name="servers">
<list>
<value>server1</value>
<value>server2</value>
</list>
</property>虽然 XML 标签不同,但 Spring 会根据目标属性的类型自动选择合适的集合实现。如果属性是 Set,即使写 <list>,Spring 也会注入 Set(通常是 LinkedHashSet)。
三、@Value 注入 Set
3.1 从配置文件读取
@Component
public class MyBean {
// 配置文件:app.servers=server1,server2,server3
@Value("${app.servers}")
private Set<String> servers;
// 带默认值
@Value("${app.ports:8080,8081,8082}")
private Set<Integer> ports;
}
配置文件:
app: servers: server1,server2,server3 ports: 8080,8081,8082
Spring 会自动将逗号分隔的字符串拆分为 Set。
3.2 使用 SpEL 拆分
@Value("#{'${app.servers}'.split(',')}")
private Set<String> servers;
注意:split() 返回数组,Spring 会自动转换为 Set。如果数组中有重复元素,Set 会自动去重。
3.3 直接指定多个值
@Value("server1,server2,server3")
private Set<String> servers;
四、@ConfigurationProperties 绑定 Set
4.1 简单 Set
@Component
@ConfigurationProperties(prefix = "app")
@Getter
@Setter
public class AppProperties {
private Set<String> servers;
private Set<Integer> ports;
}
app:
servers:
- server1
- server2
- server3
ports: [8080, 8081, 8082]4.2 对象 Set
@Component
@ConfigurationProperties(prefix = "app")
@Getter
@Setter
public class AppProperties {
private Set<User> users;
@Getter
@Setter
public static class User {
private String name;
private int age;
private String email;
}
}
app:
users:
- name: 张三
age: 25
email: zhangsan@example.com
- name: 李四
age: 30
email: lisi@example.com4.3 嵌套 Set
@Component
@ConfigurationProperties(prefix = "app")
@Getter
@Setter
public class AppProperties {
private Set<Cluster> clusters;
@Getter
@Setter
public static class Cluster {
private String name;
private Set<String> nodes; // 嵌套 Set
}
}
app:
clusters:
- name: cluster-a
nodes:
- node1
- node2
- name: cluster-b
nodes:
- node3
- node4五、自动注入 Bean Set(最常用)
Spring 支持将容器中所有同类型的 Bean 自动注入到 Set 中。
5.1 基本用法
public interface PaymentStrategy {
void pay(double amount);
}
@Component
public class AliPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount);
}
}
@Component
public class WechatPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("微信支付:" + amount);
}
}
@Component
public class PaymentService {
@Autowired
private Set<PaymentStrategy> strategies; // 自动注入所有实现
public void payAll(double amount) {
strategies.forEach(s -> s.pay(amount));
}
}
5.2 构造器注入 Set
@Service
public class PaymentService {
private final Set<PaymentStrategy> strategies;
public PaymentService(Set<PaymentStrategy> strategies) {
this.strategies = strategies;
}
}
5.3 Set 的默认实现类型
Spring 注入 Set 时,默认使用 LinkedHashSet。LinkedHashSet 内部维护了一个链表,保留了元素的插入顺序,因此注入到 Set 中的 Bean 顺序通常是 Bean 定义的顺序。
如果需要去重且不关心顺序,可以声明为 Set。如果需要排序,可以声明为 SortedSet 或 TreeSet:
@Autowired private SortedSet<PaymentStrategy> strategies; // 按自然顺序排序 @Autowired private TreeSet<PaymentStrategy> strategies; // 按 Comparator 或自然顺序
5.4 使用 @Order 控制顺序
虽然 Set 通常不保证顺序,但 LinkedHashSet 会保留插入顺序。配合 @Order 注解,可以控制注入顺序:
@Component
@Order(1)
public class FirstStrategy implements PaymentStrategy { }
@Component
@Order(2)
public class SecondStrategy implements PaymentStrategy { }
@Component
@Order(3)
public class ThirdStrategy implements PaymentStrategy { }
注入的 LinkedHashSet 中,元素按 @Order 的值从小到大排列。
六、Set 的自动去重特性
Set 最重要的特性是元素去重。当容器中有多个相同类型的 Bean 时,如果 Bean 的 equals() 和 hashCode() 判定为相同,Set 会自动去重。
@Component
public class HandlerA implements Handler {
@Override
public boolean equals(Object obj) {
return obj instanceof HandlerA;
}
@Override
public int hashCode() {
return HandlerA.class.hashCode();
}
}
@Component
public class HandlerA2 implements Handler {
// 如果 equals 和 hashCode 与 HandlerA 相同,Set 会去重
}
但在实际开发中,不同的 Bean 通常不会互相 equals,所以 Set 注入的去重效果主要体现在同一个 Bean 被多次注册的场景(比如通过不同的配置方式重复注册)。
七、Set 为空时的处理
7.1 默认行为
当容器中没有匹配类型的 Bean 时,@Autowired 注入 Set 不会抛异常,而是注入一个空 Set。
@Autowired private Set<Handler> handlers; // 没有 Handler 时,注入空 Set(不是 null)
这与 List 注入行为一致。数组注入在找不到 Bean 时会抛异常,而 Set 和 List 会返回空集合。
7.2 使用 ObjectProvider 控制
@Autowired
private ObjectProvider<Set<Handler>> handlersProvider;
public void doSomething() {
Set<Handler> handlers = handlersProvider.getIfAvailable(Collections::emptySet);
}
7.3 校验 Set 不为空
@PostConstruct
public void init() {
if (strategies.isEmpty()) {
throw new IllegalStateException("没有可用的策略实现");
}
}
八、Set 注入 vs List 注入 vs 数组注入
| 对比维度 | Set | List | 数组 |
|---|---|---|---|
| 是否去重 | ✅ 自动去重 | ❌ 允许重复 | ❌ 允许重复 |
| 是否有序 | ⚠️ 取决于实现类 | ✅ 有序 | ✅ 有序 |
| 默认实现 | LinkedHashSet | ArrayList | 无 |
| 找不到 Bean 时 | 注入空 Set | 注入空 List | 抛异常 |
| 排序支持 | 支持 @Order(LinkedHashSet) | 支持 @Order | 支持 @Order |
| 适用场景 | 需要去重的多实现 | 需要顺序遍历 | 固定长度、性能敏感 |
选择建议:
- 需要去重且不关心顺序 → Set
- 需要顺序遍历 → List
- 固定长度且性能敏感 → 数组
- 需要按名称查找 → Map
九、Set 注入的底层实现
Spring 在 populateBean() 阶段处理 Set 注入时,会根据目标属性的类型选择合适的集合实现类。
简化流程:
- Spring 扫描到
Set<PaymentStrategy>类型的依赖 - 调用
DefaultListableBeanFactory.resolveMultipleBeans()获取所有PaymentStrategy类型的 Bean - 根据目标类型
Set选择合适的实现类(默认LinkedHashSet) - 遍历所有匹配的 Bean,依次添加到 Set 中
- 将构建好的 Set 赋值给目标字段
如果 Bean 的数量为 0,返回空 Set,不抛异常。
十、常见问题
10.1 Set 注入为 null
原因:没有使用 @Autowired 或没有 @Component 注解。
解决:确保类被 Spring 管理,字段标注 @Autowired。
10.2 Set 顺序不符合预期
原因:HashSet 不保证顺序,如果 Spring 注入的是 LinkedHashSet,则按 Bean 定义的顺序。
解决:使用 @Order 注解明确顺序,或声明为 SortedSet、TreeSet。
10.3 Set 去重导致 Bean 丢失
原因:多个 Bean 的 equals() 和 hashCode() 判定为相同,Set 自动去重。
解决:检查 Bean 的 equals() 和 hashCode() 实现,避免误判。如果不希望去重,改用 List 或数组。
10.4 @Value 注入 Set 时逗号被拆分
原因:配置值中的逗号被当作分隔符。
解决:使用 @ConfigurationProperties 绑定,或用 SpEL 自定义拆分。
10.5 Set 与 @Order 的配合
Set 本身不保证顺序,但 LinkedHashSet 保留了插入顺序。如果使用 HashSet,@Order 可能失效。
解决:声明为 Set 时 Spring 默认注入 LinkedHashSet,配合 @Order 可以控制顺序。
十一、最佳实践
优先使用自动注入 Set。 当需要收集同一接口的所有实现且需要去重时,@Autowired Set<T> 是最简洁的方式。
用 @Order 控制顺序。 当 Set 中的元素有执行顺序要求时,给实现类加 @Order 注解。
配置文件中的 Set 用 @ConfigurationProperties。 比 @Value 更类型安全,支持嵌套对象。
处理空 Set。 在 @PostConstruct 中校验关键 Set 是否为空,避免业务逻辑出错。
注意 Set 的去重特性。 如果多个 Bean 的 equals() 判定相同,Set 会去重。确认这是否符合业务预期。
选择合适的具体实现。 如果需要排序,声明为 SortedSet 或 TreeSet;如果需要保留插入顺序,使用默认的 LinkedHashSet。
十二、总结
| 维度 | 核心要点 |
|---|---|
| XML 注入 | 使用 <set> 标签,支持嵌套、对象、基本类型 |
| @Value 注入 | 逗号分隔字符串自动拆分,或使用 SpEL |
| @ConfigurationProperties | 原生支持复杂 Set 绑定,推荐用于配置文件 |
| 自动注入 Bean Set | 注入容器中所有同类型 Bean,自动去重 |
| 默认实现 | LinkedHashSet(保留插入顺序) |
| 顺序控制 | @Order 注解配合 LinkedHashSet |
| 空 Set 处理 | 默认注入空集合,需要时用 @PostConstruct 校验 |
| Set vs List | Set 去重,List 允许重复;Set 顺序取决于实现,List 天然有序 |
| 最佳实践 | 自动注入用 Set,配置绑定用 @ConfigurationProperties,需要排序用 SortedSet |
Set 注入是 Spring 集合注入的重要形式,特别适合需要去重的多实现场景。理解 Set 与 List 的区别、默认实现类的选择以及 @Order 的配合,能让你在业务开发中更准确地选择集合类型,写出更符合语义的代码。
以上就是在Spring中注入Set集合的方法详解的详细内容,更多关于Spring注入Set集合的资料请关注脚本之家其它相关文章!
