理解 Spring 依赖注入:从构造器注入到集合与条件 Bean
在 Spring 项目中,我们经常会看到下面几种依赖注入方式:
java
@Autowired
private PaymentService paymentService;
或者:
java
@Resource
private PaymentService paymentService;
但在很多较新的 Spring 项目里,还会出现一种看起来"没有任何注入注解"的写法:
java
@RestController
public class PaymentController {
private final List<PaymentHandler> handlers;
private final Map<String, PaymentHandler> handlerMap;
public PaymentController(
List<PaymentHandler> handlers,
Map<String, PaymentHandler> handlerMap) {
this.handlers = handlers;
this.handlerMap = handlerMap;
}
}
这里既没有 @Autowired,也没有 @Resource,但 Spring 仍然能够把依赖传进来。
原因并不神秘:只要一个 Spring Bean 只有一个构造器,Spring 就可以直接把这个构造器当作依赖注入入口。 从 Spring 4.3 开始,单构造器场景通常不需要再显式标注 @Autowired。
这也是理解 Spring 依赖注入机制的一个很好的入口。
一、先区分两个概念:注册 Bean 与注入 Bean
Spring 的依赖注入通常可以拆成两个阶段:
- 先把对象交给 Spring 管理;
- 再把这些对象注入到需要它们的其他对象中。
1. 将对象注册为 Bean
常见方式包括:
@Component:通用组件;@Service:通常用于应用服务、领域服务等业务组件;@Repository:通常用于持久化访问组件,并提供部分数据访问异常转换能力;@Controller:Spring MVC 控制器;@RestController:REST 接口控制器;@Bean:通过配置类中的方法显式创建 Bean。
例如:
java
public interface MessageSender {
void send(String message);
}
java
@Service
public class EmailMessageSender implements MessageSender {
@Override
public void send(String message) {
// 发送邮件
}
}
此时 EmailMessageSender 会进入 Spring 容器,后续其他 Bean 才有可能注入它。
2. 从容器中解析并注入依赖
当 Spring 创建某个 Bean 时,会分析它需要哪些依赖,然后从容器中寻找匹配对象。
因此,依赖注入的本质可以简单理解为:
Spring 创建对象时,自动帮你把这个对象依赖的其他对象找出来,并传进去。
二、为什么构造器没有 @Autowired 也能注入
假设有下面的控制器:
java
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
只要 OrderController 本身是 Spring Bean,并且它只有这一个构造器,Spring 在创建它时就会发现:
要创建
OrderController,必须先获得一个OrderService。
于是 Spring 会从容器中寻找能够赋值给 OrderService 的 Bean,然后调用构造器:
text
Spring 容器
│
├── 找到 OrderService Bean
│
└── new OrderController(orderService)
因此下面两种写法在单构造器场景中效果通常相同:
java
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
java
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
实际项目中通常更推荐第一种,因为信息更少,也更容易保持类的简洁。
三、为什么通常推荐构造器注入
Spring 支持字段注入、Setter 注入和构造器注入,但业务代码中通常更推荐构造器注入。
例如:
java
@Service
public class CheckoutService {
private final InventoryService inventoryService;
private final PaymentService paymentService;
public CheckoutService(
InventoryService inventoryService,
PaymentService paymentService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
}
它有几个明显优势。
1. 依赖关系清晰
只看构造器就能知道这个类运行所需的依赖。
2. 必需依赖不能被遗漏
对象创建完成时,必需依赖已经全部提供,不需要等待字段二次注入。
3. 更适合不可变设计
依赖可以声明为 final:
java
private final PaymentService paymentService;
对象创建后依赖关系不会再被随意修改。
4. 更方便单元测试
测试时可以直接手工构造:
java
PaymentService paymentService = new FakePaymentService();
CheckoutService service = new CheckoutService(
new FakeInventoryService(),
paymentService
);
不必为了测试一个普通 Java 类启动 Spring 容器。
四、一个接口有多个实现时,Spring 怎么处理
真正值得理解的是这种场景:
java
public interface StorageService {
void store(String data);
}
有两个实现:
java
@Service
public class LocalStorageService implements StorageService {
@Override
public void store(String data) {
// 保存到本地
}
}
java
@Service
public class CloudStorageService implements StorageService {
@Override
public void store(String data) {
// 保存到云端
}
}
此时容器里存在两个 StorageService。
如果直接注入:
java
public FileService(StorageService storageService) {
this.storageService = storageService;
}
Spring 无法仅凭类型判断应该选择哪个实现,通常会出现候选 Bean 不唯一的问题。
解决这种问题有几种常见方式。
五、@Primary:提供默认实现
如果多个实现中有一个应该作为默认选择,可以使用 @Primary:
java
@Service
@Primary
public class CloudStorageService implements StorageService {
@Override
public void store(String data) {
// 默认保存到云端
}
}
此时:
java
public FileService(StorageService storageService) {
this.storageService = storageService;
}
Spring 会优先选择标记了 @Primary 的实现。
适合的语义是:
同一接口有多个实现,但其中一个是系统默认实现。
例如:
- 默认支付渠道;
- 默认对象存储;
- 默认大模型客户端;
- 默认消息发送器。
六、@Qualifier:明确指定 Bean
如果调用方明确知道自己需要哪个实现,通常使用 @Qualifier。
java
@Service("localStorage")
public class LocalStorageService implements StorageService {
}
java
@Service("cloudStorage")
public class CloudStorageService implements StorageService {
}
注入时:
java
public BackupService(
@Qualifier("cloudStorage") StorageService storageService) {
this.storageService = storageService;
}
可以把它理解为:
@Primary表示"默认选它";@Qualifier表示"这里明确要它"。
当业务中出现多个同类型实现时,这两个注解非常常见。
七、List<T> 注入:一次拿到某个接口的全部实现
Spring 不只能注入单个 Bean,还可以注入某个类型的所有候选 Bean。
例如:
java
@Component
public class ValidationEngine {
private final List<ValidationRule> rules;
public ValidationEngine(List<ValidationRule> rules) {
this.rules = rules;
}
}
假设系统中有:
text
EmailValidationRule
PhoneValidationRule
RiskValidationRule
它们都实现 ValidationRule,那么 Spring 会自动组装为:
text
List<ValidationRule>
├── EmailValidationRule
├── PhoneValidationRule
└── RiskValidationRule
这种方式特别适合:
- 责任链;
- 规则引擎;
- 多个 Handler 顺序执行;
- 多个扩展点依次处理;
- 插件式业务能力。
@Order 在这里有什么作用
如果集合中的实现需要固定顺序,可以使用 @Order:
java
@Component
@Order(10)
public class BasicValidationRule implements ValidationRule {
}
java
@Component
@Order(20)
public class RiskValidationRule implements ValidationRule {
}
那么注入 List<ValidationRule> 时,Spring 可以按照相应的顺序进行排列。
需要注意:
@Order主要用于影响有序集合、扩展点、拦截器等场景中的顺序,不应简单理解为"控制 Bean 的创建先后"。
如果确实存在 Bean 初始化依赖,应优先通过真实依赖关系或 @DependsOn 表达,而不是依赖 @Order。
八、Map<String, T> 注入:Bean 名称直接变成路由键
这是 Spring 中非常实用的一种能力。
例如定义支付接口:
java
public interface PaymentHandler {
PaymentResult pay(PaymentCommand command);
}
实现类:
java
@Component("wechat")
public class WechatPaymentHandler implements PaymentHandler {
}
java
@Component("alipay")
public class AlipayPaymentHandler implements PaymentHandler {
}
然后直接注入:
java
@Service
public class PaymentRouter {
private final Map<String, PaymentHandler> handlers;
public PaymentRouter(Map<String, PaymentHandler> handlers) {
this.handlers = handlers;
}
public PaymentResult pay(
String channel,
PaymentCommand command) {
PaymentHandler handler = handlers.get(channel);
if (handler == null) {
throw new IllegalArgumentException(
"Unsupported payment channel: " + channel
);
}
return handler.pay(command);
}
}
Spring 注入的内容可以理解为:
text
{
"wechat" -> WechatPaymentHandler,
"alipay" -> AlipayPaymentHandler
}
其中:
- Map 的 key 是 Bean 名称;
- Map 的 value 是对应的 Bean 实例。
这样就能把大量:
text
if channel == ...
else if channel == ...
else if channel == ...
替换为基于 Bean 注册表的路由。
这种模式非常适合:
- 支付渠道;
- 奖励发放策略;
- 文件解析器;
- 消息处理器;
- AI 模型适配器;
- 不同业务类型的策略实现。
本质上,它是一种非常轻量的 策略模式 + Spring 容器注册表。
九、Bean 名称从哪里来
如果显式指定:
java
@Service("wechat")
public class WechatPaymentHandler implements PaymentHandler {
}
Bean 名称就是:
text
wechat
如果没有显式指定:
java
@Service
public class WechatPaymentHandler implements PaymentHandler {
}
默认名称通常会根据类名生成,例如:
text
wechatPaymentHandler
因此,如果准备使用:
java
Map<String, PaymentHandler>
做业务路由,建议 Bean 名称本身就是稳定、明确的业务 key,而不要过度依赖默认类名转换规则。
十、可选依赖:不要把 required = false 理解成"自动防空指针"
Spring 支持可选依赖,例如:
java
@Autowired(required = false)
private AuditClient auditClient;
它的真实含义是:
如果容器中没有可注入的
AuditClient,Spring 不因为这次依赖解析而直接让 Bean 创建失败。
但字段本身仍然可能是 null。
也就是说:
java
auditClient.record();
依然可能出现空指针。
因此,现代 Spring 代码更推荐显式表达"这个依赖可能不存在"。
方式一:Optional<T>
java
public AuditService(Optional<AuditClient> auditClient) {
this.auditClient = auditClient;
}
使用:
java
auditClient.ifPresent(client -> client.record());
方式二:ObjectProvider<T>
java
public AuditService(ObjectProvider<AuditClient> provider) {
this.provider = provider;
}
需要时再获取:
java
AuditClient client = provider.getIfAvailable();
ObjectProvider 还适合延迟获取、多实现遍历等场景。
十一、@Bean:不是所有对象都必须写 @Component
有些对象来自第三方库,我们无法修改源码,也就不能在它们的类上添加 @Component。
这时可以通过配置类创建 Bean:
java
@Configuration
public class ClientConfiguration {
@Bean
public ApiClient apiClient(ApiProperties properties) {
return new ApiClient(
properties.getEndpoint(),
properties.getToken()
);
}
}
之后 ApiClient 和普通 Spring Bean 没有本质区别,可以继续被其他组件注入。
常见场景包括:
- 第三方 SDK;
- HTTP Client;
- Redis Client;
- 云服务客户端;
- 自定义线程池;
- 序列化器。
十二、@ConditionalOnMissingBean:只在用户没有提供实现时创建默认 Bean
Spring Boot 自动配置经常需要解决一个问题:
框架提供默认实现,但业务方如果自己实现了,就优先使用业务方的。
典型写法:
java
@Configuration
public class StorageAutoConfiguration {
@Bean
@ConditionalOnMissingBean(StorageService.class)
public StorageService defaultStorageService() {
return new LocalStorageService();
}
}
逻辑是:
text
容器里已经有 StorageService?
│
┌────┴────┐
│ │
有 没有
│ │
不创建 创建默认实现
这也是很多 Spring Boot Starter 能做到"开箱即用,同时允许覆盖默认实现"的关键机制。
适合:
- SDK;
- Starter;
- 基础设施组件;
- 默认策略实现;
- 可插拔框架。
十三、@ConditionalOnProperty:通过配置开关决定是否创建 Bean
有些组件只应该在配置开启时存在。
例如:
yaml
feature:
audit:
enabled: true
配置:
java
@Configuration
public class AuditConfiguration {
@Bean
@ConditionalOnProperty(
prefix = "feature.audit",
name = "enabled",
havingValue = "true"
)
public AuditClient auditClient() {
return new AuditClient();
}
}
当:
yaml
enabled: true
时创建 Bean;关闭时则不创建。
这比在业务代码中到处写:
java
if (auditEnabled) {
...
}
更适合控制一个完整组件是否进入 Spring 容器。
常见用途:
- 是否启用某个外部服务;
- 是否启用消息队列;
- 是否启用 AI 能力;
- 是否启用审计模块;
- Starter 功能开关。
十四、自定义 Condition:把复杂的创建规则封装起来
如果仅通过配置项已经无法描述 Bean 的创建条件,可以自定义 Condition。
例如:
java
public class LinuxEnvironmentCondition implements Condition {
@Override
public boolean matches(
ConditionContext context,
AnnotatedTypeMetadata metadata) {
String os = System.getProperty("os.name", "");
return os.toLowerCase().contains("linux");
}
}
使用:
java
@Bean
@Conditional(LinuxEnvironmentCondition.class)
public NativeWorker nativeWorker() {
return new NativeWorker();
}
这种方式适合处理:
- 操作系统;
- classpath 是否存在某个类;
- 多个配置组合;
- 特定运行环境;
- 自定义框架能力检测。
如果只是一个简单的布尔配置开关,优先使用 @ConditionalOnProperty,通常不需要自己实现 Condition。
十五、@Profile:根据运行环境启用不同 Bean
对于开发、测试、生产环境,有时需要使用不同实现。
例如:
java
@Service
@Profile("dev")
public class MockSmsSender implements SmsSender {
}
java
@Service
@Profile("prod")
public class RealSmsSender implements SmsSender {
}
配置:
yaml
spring:
profiles:
active: dev
则开发环境只启用 MockSmsSender。
典型应用包括:
- 开发环境使用模拟支付;
- 测试环境使用内存存储;
- 生产环境启用真实云服务;
- 不同部署环境使用不同实现。
需要注意:
如果某个 Bean 只在部分 Profile 下存在,那么依赖它的组件也要正确处理这种条件关系。可以通过:
- 同样限制 Profile;
Optional<T>;ObjectProvider<T>;- 条件配置;
来表达,而不是默认认为 required=false 就解决了所有问题。
十六、@Lazy:延迟 Bean 的创建
默认情况下,大多数单例 Bean 会在应用上下文初始化阶段创建。
使用:
java
@Service
@Lazy
public class HeavyModelClient {
}
后,Spring 可以延迟到真正需要这个 Bean 时再初始化。
适合:
- 初始化成本很高的对象;
- 启动时不一定会使用的功能;
- 某些外部 SDK;
- 大模型客户端或重量级资源。
不过不要把 @Lazy 当作常规性能优化手段。大量延迟初始化会让错误从"启动阶段"推迟到"运行阶段",反而降低问题暴露的及时性。
十七、@Scope("prototype"):每次获取都创建新实例
Spring Bean 默认通常是单例作用域:
text
ApplicationContext
│
└── 一个 Bean 实例被反复复用
如果定义:
java
@Component
@Scope("prototype")
public class PipelineContext {
}
那么每次向容器请求这个 Bean 时,Spring 都会创建新的实例。
适合:
- 每次任务需要独立状态;
- 每条工作流需要独立上下文;
- 每次构建责任链都需要新的可变对象。
但要注意一个常见问题:
如果把 prototype Bean 直接注入 singleton Bean 的构造器,通常只会在 singleton 创建时解析一次。
如果希望每次使用都获得新的 prototype 实例,可以配合 ObjectProvider:
java
public TaskRunner(ObjectProvider<PipelineContext> provider) {
this.provider = provider;
}
然后:
java
PipelineContext context = provider.getObject();
十八、@DependsOn:明确表达初始化先后依赖
某些 Bean 虽然没有直接的 Java 参数依赖,但初始化时确实要求另一个 Bean 已经完成创建,可以使用:
java
@Component
@DependsOn("schemaInitializer")
public class ReportService {
}
不过这通常属于基础设施级能力。
业务代码中,如果 A 真正依赖 B,优先让依赖直接出现在构造器中:
java
public ReportService(SchemaInitializer initializer) {
this.initializer = initializer;
}
这种依赖关系更显式,也更容易测试。
十九、Environment 与配置属性注入
Spring 可以注入 Environment:
java
@Component
public class RuntimeInfo {
private final Environment environment;
public RuntimeInfo(Environment environment) {
this.environment = environment;
}
}
然后读取:
java
String region = environment.getProperty("app.region");
不过,如果一组配置属于同一个业务组件,通常更推荐使用 @ConfigurationProperties:
java
@ConfigurationProperties(prefix = "storage")
public class StorageProperties {
private String endpoint;
private String bucket;
// getter / setter
}
相比零散调用 Environment#getProperty(),配置对象更加:
- 类型安全;
- 容易测试;
- 容易维护;
- 适合 IDE 提示;
- 能表达完整配置结构。
二十、@ImportResource 与 @PropertySource:兼容旧式配置
现代 Spring Boot 项目通常主要依赖:
- Java Configuration;
- 自动配置;
application.yml/application.properties。
但有些老项目或第三方组件仍然使用 XML:
java
@SpringBootApplication
@ImportResource("classpath:legacy/beans.xml")
public class Application {
}
这种方式允许 Spring Boot 项目继续加载传统 XML Bean 配置。
@PropertySource 则可以显式引入额外的 properties 文件。
这类机制更多用于:
- 老系统迁移;
- 兼容旧组件;
- 渐进式 Spring Boot 改造。
对于全新项目,一般没有必要主动回到 XML 配置方式。
二十一、@EnableScheduling 与 @Async 不属于依赖注入,但经常一起出现
这两个注解和 Bean 管理关系密切,但严格来说,它们不是"注入方式"。
@EnableScheduling
用于开启 Spring 定时任务能力:
java
@SpringBootApplication
@EnableScheduling
public class Application {
}
之后可以使用:
java
@Scheduled(fixedDelay = 60_000)
public void refreshCache() {
}
@Async
用于让符合条件的方法通过异步执行器运行。
例如:
java
@Async
public void exportReport(Long taskId) {
// 执行耗时导出
}
实际使用时还需要关注:
- 是否启用了异步能力;
- 使用哪个线程池;
- 异常如何处理;
- Spring AOP 代理是否生效;
- 同类内部直接调用是否绕过代理。
因此,不能简单把 @Async 理解成"加上注解就一定异步"。
二十二、Spring 注入 List 和 Map 时内部大致发生了什么
不需要一开始就深入 Spring 源码,可以先建立一个正确的整体模型。
假设有:
java
public PaymentRouter(
List<PaymentHandler> handlers,
Map<String, PaymentHandler> handlerMap) {
...
}
Spring 创建 PaymentRouter 时,大致会经历:
text
1. 读取 PaymentRouter 的 BeanDefinition
↓
2. 确定应该使用哪个构造器
↓
3. 分析构造器参数
↓
4. 发现需要 List<PaymentHandler>
↓
5. 找到所有 PaymentHandler Bean
↓
6. 组装成 List
↓
7. 发现需要 Map<String, PaymentHandler>
↓
8. 再按 Bean 名称 + Bean 实例组装成 Map
↓
9. 调用构造器创建 PaymentRouter
可以把它概括成一句话:
Spring 不只是"按类型找一个 Bean",它还会根据依赖声明的结构决定如何组织多个候选 Bean。
二十三、从源码角度看,核心职责分别在哪里
如果以后需要继续阅读 Spring 源码,可以重点关注几个职责层次。
1. BeanDefinition:描述"应该创建什么"
Spring 会把组件扫描、@Bean、XML 等配置转换为 Bean 定义信息。
它描述的不是最终对象本身,而是类似:
text
Bean 类型是什么
Bean 名称是什么
作用域是什么
构造方式是什么
是否 Lazy
有哪些依赖信息
2. 构造器解析:决定"用哪个构造器,参数从哪里来"
Spring 创建对象时,需要判断:
- 使用哪个构造器;
- 构造器每个参数需要什么类型;
- 参数是否唯一;
- 是否需要处理集合类型。
ConstructorResolver 等组件承担了这类工作。
3. BeanFactory:从容器中解析候选对象
DefaultListableBeanFactory 是 Spring 中非常核心的 BeanFactory 实现之一。
它负责完成类似:
text
给我一个 PaymentHandler
给我所有 PaymentHandler
给我名为 alipay 的 PaymentHandler
这类候选 Bean 解析工作。
4. AutowiredAnnotationBeanPostProcessor:处理注入元数据
它主要参与 @Autowired、@Value 等注入元数据的处理,并参与构造器候选识别等过程。
需要注意的是:
"单构造器无需
@Autowired也能注入"并不意味着 Spring 完全绕过了依赖解析机制,只是这个场景不再要求开发者显式写出@Autowired。
二十四、最值得掌握的几个组合
如果只准备记住实际项目中最常见的一部分,可以优先掌握下面几种。
1. 普通依赖:构造器注入
java
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
适用于绝大多数普通业务组件。
2. 多实现 + 默认实现:@Primary
java
@Primary
@Service
public class DefaultSearchService implements SearchService {
}
适用于"多个实现,但有默认值"。
3. 多实现 + 明确指定:@Qualifier
java
public Service(
@Qualifier("fastModel") ModelClient client) {
}
适用于调用方明确指定实现。
4. 多实现 + 全部执行:List<T>
java
public RuleEngine(List<Rule> rules) {
}
适用于责任链、规则集和插件集合。
5. 多实现 + 按 key 路由:Map<String, T>
java
public ParserRouter(Map<String, Parser> parsers) {
}
适用于策略选择和业务路由。
6. 可选 Bean:Optional<T> / ObjectProvider<T>
java
public Service(Optional<AuditClient> client) {
}
适用于功能可能关闭的情况。
7. 默认组件:@ConditionalOnMissingBean
适用于 Starter、SDK、基础设施组件。
8. 功能开关:@ConditionalOnProperty
适用于通过配置启停完整能力。
二十五、一个完整的小例子:不用 if-else 实现策略路由
假设系统支持三种文档解析方式:
java
public interface DocumentParser {
ParsedDocument parse(byte[] content);
}
实现:
java
@Component("pdf")
public class PdfDocumentParser implements DocumentParser {
}
java
@Component("markdown")
public class MarkdownDocumentParser implements DocumentParser {
}
java
@Component("html")
public class HtmlDocumentParser implements DocumentParser {
}
路由器:
java
@Component
public class DocumentParserRegistry {
private final Map<String, DocumentParser> parsers;
public DocumentParserRegistry(
Map<String, DocumentParser> parsers) {
this.parsers = parsers;
}
public DocumentParser get(String type) {
DocumentParser parser = parsers.get(type);
if (parser == null) {
throw new IllegalArgumentException(
"Unsupported document type: " + type
);
}
return parser;
}
}
调用方只需要:
java
DocumentParser parser = registry.get(type);
return parser.parse(content);
以后增加一种 docx:
java
@Component("docx")
public class DocxDocumentParser implements DocumentParser {
}
原来的路由代码完全不需要修改。
这正是集合注入真正有价值的地方:
把"新增策略"从修改旧代码,转变为注册一个新的实现类。
它不仅减少了 if-else,还天然更符合开闭原则。
二十六、常见误区
误区一:没有 @Autowired 就不是依赖注入
不是。
单构造器 Bean 通常可以直接进行构造器注入。
误区二:@Order 就是 Bean 的启动顺序
不准确。
@Order 更多用于排序,例如集合注入、拦截器或扩展链。初始化依赖应通过真实依赖关系、@DependsOn 等机制表达。
误区三:@Autowired(required = false) 可以避免空指针
不能。
它只能让"依赖不存在"不一定导致注入阶段失败,后续代码仍必须处理 null。更推荐 Optional 或 ObjectProvider。
误区四:所有可关闭的组件都应该用 required = false
通常不是最佳设计。
如果组件是否存在由配置决定,更适合使用:
text
@ConditionalOnProperty
@Profile
@ConditionalOnMissingBean
从 Bean 创建阶段就表达清楚。
误区五:Map 注入就是普通 Java Map
它当然是 Map,但它最重要的价值在于:
text
Bean 名称 -> Bean 实例
可以直接充当 Spring 管理的策略注册表。
二十七、总结
Spring 依赖注入并不只是 @Autowired 和 @Resource 两个注解。
真正需要理解的是:Spring 容器能够根据"依赖类型 + Bean 元数据 + 注入位置"自动解析对象之间的关系。
可以建立下面这个简单模型:
text
@Component / @Service / @Bean
│
▼
Bean 注册到容器
│
▼
Spring 创建其他 Bean
│
▼
分析构造器参数
│
┌────────┼──────────┐
▼ ▼ ▼
单个 T List<T> Map<String,T>
│ │ │
找一个 找全部 Bean 名称 -> Bean
└────────┴──────────┘
│
▼
调用构造器
在日常项目中,可以优先遵循下面的思路:
- 普通必需依赖优先使用构造器注入;
- 一个接口多个实现时,根据语义选择
@Primary、@Qualifier、List<T>或Map<String, T>; - 可选依赖优先考虑
Optional或ObjectProvider; - 功能是否存在,尽量在 Bean 创建阶段通过
Conditional或Profile表达; Map<String, T>非常适合策略路由、插件注册和消除大量分支判断;- 不要把 Bean 排序、Bean 创建顺序和依赖注入混为一谈。
当这些机制真正用熟以后,Spring 就不再只是一个"帮你自动 new 对象"的框架,而更像一个负责组件注册、依赖解析、策略装配和生命周期管理的运行时容器。