你真的会用 Spring 注入吗?别再把 @Autowired 当魔法符号了!
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言(为什么要认真学 DI)
写过 Java 的人都知道,Spring 的 DI(依赖注入)好像是“自带魔法”的那位同事:让对象自动被创建、组装、你只管写逻辑就好——听起来爽,但如果不懂原理,写出来的程序也会像加了胶水一样难以拆解、难以测试、上线后出问题也找不到北。本文目标:把 DI 的关键点、常见坑(循环依赖、错误作用域、原型与单例混用等)讲清楚,并给出可运行代码与练习,方便你立刻上手改造自己的项目。
1. Spring IoC 与 DI 概念速览(懒人读法)
IoC(Inversion of Control,控制反转)与 DI(Dependency Injection,依赖注入)是 Spring 的核心理念:由容器(ApplicationContext)来负责创建对象(Bean)并把需要的依赖“注入”进来,而不是对象自己 new 依赖。这样能把对象的构造、生命周期与依赖关系交给容器管理,便于解耦与测试。官方文档对 DI 的两种主要变体(构造器注入与 setter 注入)有明确说明。
2. 关键注解与 stereotype(角色型注解)
Spring 中常见的类级注解都是“立场声明”——告诉容器:这是个需要被管理的类型(bean)。主要有:
@Component:通用的组件(任何不属于其它专用角色的类都可以用它标注)。@Component本身会把类纳入组件扫描,成为 bean。@Service:语义上表示“服务层”,是@Component的细化,用于业务逻辑层(便于工具/切面识别)。@Repository:DAO / 持久层的 stereotype,并且会触发异常翻译(像把底层 JDBC/ORM 异常转换成 Spring 的 data access 异常)。@Controller/@RestController:用于 MVC 层,前者用于视图型控制器,后者是@Controller + @ResponseBody的组合,用于 REST。
小贴士:优先使用更语义化的注解(@Service / @Repository),而不是一律使用 @Component。这样有利于团队阅读和后续 AOP/工具识别。
3. 注入方式(构造器注入 vs 字段注入 vs setter 注入)
这部分是最常被讨论也容易踩坑的地方。来,逐个剖析,并给出示例代码。
3.1 构造器注入(推荐作为首选)
优点:
- 明确依赖:构造函数参数列出必要的依赖,编译期可见,更利于不可变对象设计(
final字段)。 - 更易于单元测试:直接 new 对象或用测试框架注入 mock 更方便。
- 避免隐式空值(依赖必传)。
示例(推荐在 Spring Boot 中最常见):
@Service
public class OrderService {
private final PaymentService paymentService;
// Spring 会调用这个构造器并注入 PaymentService
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
// ...
}
注:当只有一个构造器时,Spring 可以自动注入(不必写
@Autowired),如果有多个构造器,需要在目标构造器上标注@Autowired来明确注入点。
3.2 字段注入(方便但不推荐)
写法简单、代码少,但缺点明显:
- 隐式依赖(构造器中看不到依赖),不利于可测试性(需要用反射或 Spring 测试支持注入)。
- 难以保证不可变性。
示例:
@Service
public class FooService {
@Autowired
private BarService barService;
}
3.3 Setter 注入(适用于可选依赖)
优点:
- 适合可选依赖或需要在 bean 初始化后设置可变依赖的场景。
缺点: - 依赖可能在运行时被改变;如果依赖是必须的,使用 Setter 会让错误更迟钝(可能在运行中才发现 NPE)。
示例:
@Service
public class MailService {
private SmtpClient smtpClient;
@Autowired
public void setSmtpClient(SmtpClient smtpClient) {
this.smtpClient = smtpClient;
}
}
结论建议:统一使用构造器注入(必需依赖);对于确实是可选的或需要延迟设置的依赖使用 setter 注入;尽量避免字段注入。很多权威博客和实践也倾向推荐构造器注入。
4. @Autowired、@Qualifier、@Primary 的使用细节
实际项目中常会有多个实现类(同一个接口的多份 bean),这时 Spring 需要决策到底注入哪一份。常见策略:
@Primary:在多个候选 Bean 中指定首选者(当按类型注入且没有额外限定时,注入@Primary标注的 bean)。@Qualifier("beanName"):按名字或自定义限定器选择具体 bean。@Autowired:按类型自动注入(构造器上可省略,当只有一个构造器时)。详见官方@Autowired章节。
示例:
public interface PaymentProcessor { void pay(); }
@Component("paypal")
public class PaypalProcessor implements PaymentProcessor { ... }
@Component("stripe")
@Primary
public class StripeProcessor implements PaymentProcessor { ... }
@Service
public class CheckoutService {
private final PaymentProcessor paymentProcessor;
// 注入时会优先注入 @Primary 的 StripeProcessor
public CheckoutService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
// 如果想指定某个实现,可以用 @Qualifier
// public CheckoutService(@Qualifier("paypal") PaymentProcessor paymentProcessor) { ... }
}
5. Bean 生命周期(创建、初始化、销毁)
Spring 在管理 bean 时会经历多个阶段:实例化(instantiate)、依赖注入(populate)、初始化回调(init)、就绪使用、销毁(destroy)。常用回调方式包括:
- 实现
InitializingBean/DisposableBean(Spring 接口) - 在
@Bean上配置initMethod/destroyMethod - 使用 JSR-250 注解:
@PostConstruct/@PreDestroy(现代项目推荐,能减少对 Spring API 的耦合) - 使用
BeanPostProcessor可在 bean 初始化前后做通用处理(如 AOP、代理、增强)
官方文档把 @PostConstruct 和 @PreDestroy 列为常见且推荐的做法,因为它们减少了对 Spring 特性的耦合。
示例:
@Component
public class CacheManager {
@PostConstruct
public void init() {
// 加载缓存、建立连接池等
}
@PreDestroy
public void cleanup() {
// 释放资源
}
}
或者在 @Bean 中设定:
@Configuration
public class AppConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public EmbeddedServer server() {
return new EmbeddedServer();
}
}
6. @Configuration 与 @Bean(为啥不用全部 @Component)
@Configuration + @Bean 适用于你需要显式构造第三方类(无法加注解)或需要在创建 bean 时执行自定义初始化逻辑、或者需要控制依赖顺序、设置 initMethod/destroyMethod 的场景。@Bean 方法返回值会被 Spring 管理为 bean。@Configuration 会保证这些 @Bean 方法的增强(cglib 代理),从而保证在同一配置类中调用 bean 方法仍然走容器管理(单例保证)。
示例:
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:...");
return new HikariDataSource(cfg);
}
}
小提示:如果仅仅需要把普通类当 bean 使用,标注 @Component 更便捷;当你需要更多创建控制或第三方类型时,用 @Configuration+@Bean。
7. Bean 作用域(singleton、prototype、request、session、application、websocket)
默认作用域是 singleton(在容器中只存在一个共享实例)。Spring 还支持 prototype(每次注入/请求都会创建新实例),以及用于 web 应用的 request、session、application 等。官方文档对作用域做了详细说明。
示例:
@Component
@Scope("prototype")
public class UserSession { ... }
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean { ... }
注意:在单例 bean 中注入 prototype bean 时要小心——直接注入会在单例创建时只创建一次 prototype 实例(不是每次使用都新建)。常见解决办法有:ObjectFactory<T> / Provider<T> / 使用 @Lookup 方法或代理 (ScopedProxyMode) 来按需获取新实例。
8. 实战练习:实现多个实现类并使用 @Qualifier 切换(完整例子)
下面给出一个完整小示例:接口 Notifier,两个实现 EmailNotifier 和 SmsNotifier,演示 @Primary、@Qualifier、构造器注入与 @Configuration 下 @Bean 的几种用法。
项目结构(简化):
src/main/java/com/example/di/
Notifier.java
EmailNotifier.java
SmsNotifier.java
NotificationService.java
AppConfig.java
DemoApplication.java
核心代码:
// Notifier.java
package com.example.di;
public interface Notifier {
void notify(String to, String message);
}
// EmailNotifier.java
package com.example.di;
import org.springframework.stereotype.Component;
@Component("emailNotifier")
public class EmailNotifier implements Notifier {
@Override
public void notify(String to, String message) {
System.out.println("[EMAIL] to=" + to + ", msg=" + message);
}
}
// SmsNotifier.java
package com.example.di;
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Primary;
@Component("smsNotifier")
@Primary // 默认注入优先选择 SMS
public class SmsNotifier implements Notifier {
@Override
public void notify(String to, String message) {
System.out.println("[SMS] to=" + to + ", msg=" + message);
}
}
// NotificationService.java
package com.example.di;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Qualifier;
@Service
public class NotificationService {
private final Notifier notifier;
// 默认会注入 @Primary 标注的 SmsNotifier
public NotificationService(Notifier notifier) {
this.notifier = notifier;
}
// 如果你想强制注入 EmailNotifier:
// public NotificationService(@Qualifier("emailNotifier") Notifier notifier) { this.notifier = notifier; }
public void send(String to, String message) {
notifier.notify(to, message);
}
}
运行 Demo(Spring Boot main):
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
@Autowired
private NotificationService notificationService;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(String... args) {
notificationService.send("user@example.com", "Hello DI!");
}
}
你可以切换 @Primary 或在构造器上加 @Qualifier("emailNotifier") 来切换实现。实践证明,这种方式清晰且可测试。
9. 常见陷阱与对策(又到最有料的部分)
下面把常把人绊倒的坑列出来,并提供解决办法。
9.1 循环依赖(Circular dependency)
症状:容器启动时报 BeanCurrentlyInCreationException 或类似错误;或者在使用构造器注入时立即失败。
原因:A 依赖 B,B 又依赖 A。
解决办法:
- 最佳做法:重构代码,消除循环(把共同逻辑抽到第三个 bean,或用事件/消息解耦)。
- 临时解决:使用 setter 注入或字段注入(因为 Spring 可以先创建对象实例,再注入依赖),或者在构造器注入中使用
@Lazy注解延迟注入(让其中一个依赖延迟初始化)。注意:这些只是权宜之计,长期看仍应重构。详见社区讨论与官方建议。
示例(构造器注入会报错):
@Component
public class A {
public A(B b) {}
}
@Component
public class B {
public B(A a) {}
}
改为 setter 或使用 @Lazy:
@Component
public class A {
private B b;
@Autowired
public void setB(B b) { this.b = b; }
}
或
@Component
public class B {
private final A a;
public B(@Lazy A a) { this.a = a; }
}
9.2 错误的作用域选择
典型问题:在单例 bean 中直接注入 prototype bean,结果 prototype 只创建了一次;或者把 request/session scope 用在非 web 应用中导致上下文异常。
解决办法:如前所述,用 ObjectProvider<T> / ObjectFactory<T> / Provider<T> / @Lookup / 代理(proxyMode = ScopedProxyMode.TARGET_CLASS)来按需获取 prototype 或 request-scoped bean。
示例(使用 ObjectProvider):
@Component
public class Processor {
private final ObjectProvider<ExpensiveObject> provider;
public Processor(ObjectProvider<ExpensiveObject> provider) {
this.provider = provider;
}
public void process() {
ExpensiveObject obj = provider.getObject(); // 每次都能拿到新实例(对应 prototype)
}
}
9.3 把业务逻辑写进构造器或 @PostConstruct 中导致启动慢或失败
构造器/@PostConstruct 里不应该做太多阻塞或网络调用,建议把 I/O、外部依赖调用放在懒加载或初始化流程外,或者用健康检查/异步初始化策略。
10. 小结(把要点捋一遍)
核心建议总结:
- 优先使用构造器注入(可见依赖、易于测试)。
- 使用语义化 stereotype 注解(
@Service/@Repository/@Controller),而不是一律@Component。 - 理解 bean 生命周期并合理使用
@PostConstruct/@PreDestroy/initMethod。 - 对作用域(singleton/prototype/request/session)要心中有数,并使用 provider/lookup/proxy 来按需获取非单例 bean。
- 循环依赖优先重构解决,临时可用 setter 注入或
@Lazy。
推荐阅读(官方文档为主)
- Spring Framework — Dependency Injection(官方章节,DI 的核心概念)。
- Using
@Autowired(官方:构造器/Setter/Field 注入的介绍)。 - Bean Scopes(官方:作用域详解)。
- Using
@PostConstruct/@PreDestroy(官方:生命周期回调)。
练习(立刻上手,三步走)
- 在你的 Spring Boot 项目中建立
Notifier接口,两个实现(Email/SMS),用@Primary和@Qualifier切换。运行并确认不同注入策略的输出。 - 故意写一个循环依赖(构造器注入),观察启动时报错,再把其中一个改成 setter 注入或
@Lazy,观察差异。思考如何通过重构消除循环。 - 将一个
prototypebean 注入到单例中,尝试用ObjectProvider或@Lookup方式按需获取新实例,比较行为差异。
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)