你真的会用 Spring 注入吗?别再把 @Autowired 当魔法符号了!

举报
喵手 发表于 2026/01/15 17:55:13 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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 应用的 requestsessionapplication 等。官方文档对作用域做了详细说明。

示例:

@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,两个实现 EmailNotifierSmsNotifier,演示 @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(官方:生命周期回调)。

练习(立刻上手,三步走)

  1. 在你的 Spring Boot 项目中建立 Notifier 接口,两个实现(Email/SMS),用 @Primary@Qualifier 切换。运行并确认不同注入策略的输出。
  2. 故意写一个循环依赖(构造器注入),观察启动时报错,再把其中一个改成 setter 注入或 @Lazy,观察差异。思考如何通过重构消除循环。
  3. 将一个 prototype bean 注入到单例中,尝试用 ObjectProvider@Lookup 方式按需获取新实例,比较行为差异。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。