跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Spring IoC 与依赖注入 (DI) 核心解析

深入解析 Spring IoC(控制反转)机制及其与 Servlet 的区别,阐述 IoC 容器管理的核心内容如 Bean 定义、实例及生命周期。详细介绍了 ApplicationContext 接口及五大类注解(@Component、@Controller、@Service、@Repository、@Configuration)的语义与功能。讲解了通过类型或名称获取 Bean 的方法,以及@Bean 注解在配置类中创建 Bean 的特性。重点对比了依赖注入(DI)的三种方式:构造方法注入、Setter 注入和字段注入的优缺点,并提供了处理候选 Bean 冲突的解决方案,包括@Primary、@Qualifier 和@Resource 注解的使用优先级。

监控大屏发布于 2026/3/29更新于 2026/7/2048 浏览
Spring IoC 与依赖注入 (DI) 核心解析

1. IoC(控制反转)

1.1 Spring IoC VS Servlet

在上文:Java 外功基础 (1)——Spring Web MVC 中,很形象地模拟出使用 Spring'建造房子'的大概流程。使用 Spring 建造房子不需要像 Servlet 那样烧制每一块砖,只需要从 Spring 中取出一个个提前预制好的组件然后组装即可。换言之:Spring 是包含了大量工具的 IoC 容器

[图片:Spring IoC 容器示意图]

1.2 IoC 解析

1.2.1 IoC 概述

概念:IoC(Inversion of Control,控制反转),是一种设计原则,用于减少代码间的直接依赖关系。传统编程中,调用者通常主动创建和管理被调用者的生命周期,而 IoC 将这种控制权交给外部容器或框架,由容器负责对象的创建、依赖注入和管理。

示例一:传统编程模式

class Car {
    protected Framework framework;
    public Car() {
        framework = new Framework();
        System.out.println("car init");
    }
    public void run() {
        System.out.println("car run");
    }
}

class Framework {
    protected Bottom bottom;
    public Framework() {
        bottom = new Bottom();
        System.out.println("framework init");
    }
}

class Bottom {
    protected Tire tire;
    public Bottom() {
        tire = new Tire();
        System.out.println("bottom init");
    }
}

class Tire {
    public Tire() {
        System.out.println("tire init");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }
}

缺点:所有依赖都在类内部硬编码,缺乏灵活性难以修改组件的具体实现 (例如更换 Tire 的类型或参数),组件之间耦合度高。

示例二:IoC

class Car {
    protected Framework framework;
    public Car(Framework framework) {
        this.framework = framework;
        System.out.println("car init");
    }
    public void run() {
        System.out.println("car run");
    }
}

class Framework {
    protected Bottom bottom;
    public Framework(Bottom bottom) {
        this.bottom = bottom;
        System.out.println("framework init");
    }
}

class Bottom {
    protected Tire tire;
    public Bottom(Tire tire) {
        this.tire = tire;
        System.out.println("bottom init");
    }
}

class Tire {
    protected int size;
    protected String color;
    public Tire(int size, String color) {
        this.size = size;
        this.color = color;
        System.out.println("tire init");
    }
}

public class Main {
    // main 方法中实例化并保存了多个对象,理论上可以视为一种简单的容器实现
    public static void main(String[] args) {
        Tire tire = new Tire(10, "blue");
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}

优点:依赖通过构造函数从外部注入 (依赖注入),组件之间解耦,可以灵活定制组件的具体实现 (例如 Tire 的 size 和 color 可动态配置)。

1.2.2 IoC 容器管理

Spring IoC 容器保存了以下几类核心内容: Bean 定义 (Bean Definitions):这是容器保存的最核心的元数据。它就像是对象的'蓝图'或'配方',告诉容器如何创建一个 Bean。容器在启动时 (比如 ApplicationContext 被刷新时) 会加载并解析这些定义。 Bean 实例 (Bean Instances):Bean 定义创建出来的实际对象。 Bean 之间的依赖关系 (Dependencies):IoC 的核心是'控制反转',即由容器来注入依赖。因此,容器内部维护着一个依赖关系图。 Bean 生命周期 (Lifecycle State):管理 Bean 的整个生命周期,已创建/未创建、依赖注入是否完成、初始化方法是否已执行、是否已被销毁。

1.3 ApplicationContext

定义:ApplicationContext 是 Spring IoC 容器的核心接口,并有多种实现类。它实现了 BeanFactory 接口,因此可以通过 getBean() 方法获取 Bean 实例,但在实际开发中应优先使用依赖注入。

//标识该类为 Spring 项目的启动类
@SpringBootApplication
public class SpringIocApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);
    }
}

1.4 类注解

在 Spring 中有五大类注解,主要用于类级别的声明。它们都源自 @Component,它们在功能上几乎一致,主要区别在于语义和用途,用于标识不同架构层次的组件,让代码结构更清晰。

@Component - 组件:最基础的通用注解,用于标记任何一个需要被 Spring 管理的组件,当你不确定一个类属于@Controller、@Service、@Repository 中的哪一类时,或者它就是一个通用的工具类、管理器等,就可以使用它。

@Controller - 控制器:专门用于标记 MVC 模式中的控制器,用于接收 Web 请求、处理业务逻辑并返回视图或数据。派生注解:@RestController(@Controller + @ResponseBody)。

@Service - 业务逻辑层:专门用于标记业务逻辑层的组件。它本身不提供额外功能,但通过这个名称,开发者可以清晰地知道这个类包含了核心业务规则。

@Repository - 数据访问层:专门用于标记数据访问层的组件,用于封装对数据库等数据源进行增删改查的类。特殊功能:自动将平台特定的异常 (如 SQLException) 转换为 Spring 统一的 DataAccessException 层次结构中的非受查异常;统一异常体系提供更有意义的异常信息,如 BadSqlGrammarException(SQL 语法错误);简化异常处理,由于转换后的异常都是非受查异常,不再需要写大量的 try-catch 块,可以选择在适当的层次统一处理。

@Configuration - 配置类:用于标记配置类,其内部可以定义 Bean(通常使用@Bean 注解)。特殊功能:它确保了其中使用@Bean 注解的方法的单例性。Spring 会通过 CGLIB 代理来拦截对@Bean 方法的调用,确保每次返回的是同一个实例 (单例作用域下)。

1.5 获取 Bean

1.5.1 通过 Bean 的类型获取
//将 HelloController 类交给 Spring 管理
@Controller
public class HelloController {
    public void demo() {
        System.out.println("Hello HelloController");
    }
}
@SpringBootApplication
public class SpringIocApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);
        //通过 Bean 的类型获取
        HelloController helloController = context.getBean(HelloController.class);
        helloController.demo();
    }
}

如果 HelloController 类没有交给 Spring 管理,会抛出异常:

[图片:NoSuchBeanDefinitionException 异常截图]

如果某接口 (类) 有多个实现类,此时根据类型来获取 Bean 会抛出异常:

public interface Func {}
@Service("dataBaseFunc") //设置 Bean 的名称为 dataBaseFunc
class DataBaseFunc implements Func {}
@Service("fileFunc") //设置 Bean 的名称为 fileFunc
class FileFunc implements Func {}

[图片:MultipleBeansException 异常截图]

1.5.2 通过 Bean 的名称 (ID) 获取

在 Java 中,Bean 的命名规范如下: Bean 命名约定 约定是在命名 bean 时使用标准的 Java 约定作为实例字段名。也就是说,bean 名称以小写字母开头,然后使用驼峰式大小写。此类名称的示例包括 accountManager,accountService,userDao,loginController。

以 JDK17 为例,在 java.beans.Introspector 类中提供了如下方法用于 Bean 的命名:

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    //如果类名的前两个字母都是大写,那么 Bean 的名字就是类名
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) {
        return name;
    }
    char[] chars = name.toCharArray();
    //将类名的第一个字母转为小写作为 Bean 的名称
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}

示例 1:

public interface Func {
    void init();
}
@Service("dataBaseFunc") //设置 Bean 的名称为 dataBaseFunc
class DataBaseFunc implements Func {
    @Override
    public void init() {
        System.out.println("database init");
    }
}
@Service("fileFunc") //设置 Bean 的名称为 fileFunc
class FileFunc implements Func {
    @Override
    public void init() {
        System.out.println("file init");
    }
}
@SpringBootApplication
public class SpringIocApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);
        Func dataBaseFunc = (Func) context.getBean("dataBaseFunc");
        Func fileFunc = (Func) context.getBean("fileFunc");
        dataBaseFunc.init();
        fileFunc.init();
    }
}

运行结果: database init file init

示例 2:以上述 HelloController 为例

@SpringBootApplication
public class SpringIocApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);
        HelloController helloController = (HelloController) context.getBean("helloController");
    }
}

运行结果: Hello HelloController

1.6 方法注解 - @Bean

1.6.1 特性解析

作用:在五大类注解标记的类的方法上使用,告诉 Spring 该方法返回的对象应该被注册为 Bean。 使用场景:配置类中定义第三方库的 Bean或需要复杂初始化逻辑的 Bean。

package org.example.springioc.model;

@Data
public class UserObject {
    private String name;
    private Integer age;
    private Integer id;

    public UserObject() {}

    public UserObject(String name) {
        this.name = name;
    }

    public UserObject(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public UserObject(String name, Integer age, Integer id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
}
package org.example.springioc.configuration;

@Configuration
public class UserConfiguration {
    @Bean
    public UserObject getUserObject1() {
        //你完全控制对象的创建过程
        return new UserObject(); //无参构造
    }

    @Bean
    public UserObject getUserObject2() {
        //你可以传递任意参数
        return new UserObject("李四"); //带参构造
    }
}

优点:完全控制对象创建,可以传递参数、调用方法、进行复杂初始化。

@Component //或其他四大注解
public class UserObject {
    private String name;
    private Integer age;

    //Spring 只会调用无参构造方法
    public UserObject() {
        //Spring 通过反射创建对象时,只会调用这个构造方法
        //你无法传递"张三",18 这样的参数
    }

    public UserObject(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

问题:Spring 通过反射创建 Bean 时,默认调用无参构造方法,你无法传递特定参数。

1.6.2 通过 Bean 的类型获取

与五大类注解一致。

1.6.3 通过 Bean 的名称 (ID) 获取

与五大注解不同,使用@Bean 标注方法时方法名就是该 Bean 的名称。

@SpringBootApplication
public class SpringIocApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);
        UserObject getUserObject1 = (UserObject) context.getBean("getUserObject1");
        System.out.println(getUserObject1);
        UserObject getUserObject2 = (UserObject) context.getBean("getUserObject2");
        System.out.println(getUserObject2);
    }
}

运行结果: UserObject(name=null, age=null, id=null) UserObject(name=李四,age=null, id=null)

1.7 @ComponentScan - 扫描路径

概念:@ComponentScan是 Spring 框架中一个非常核心的注解,它负责告诉 Spring 容器去哪里扫描那些被@Component、@Service、@Repository、@Controller 等注解标记的类,并将它们自动注册为 Bean。如果一个类没有添加 Spring 的五大注解,那么即使它位于扫描路径下,它也不会被注册为 Spring 容器中的 Bean。

在启动类中,@SpringBootApplication 注解包含了@ComponentScan 注解,默认扫描路径是启动类所在的包及其子包(可以显式配置来指定扫描路径,如@ComponentScan(basePackages = {"org.example.springioc"}))。

[图片:ComponentScan 扫描范围示意图]

2. DI(依赖注入)

2.1 概述

依赖注入是一种设计模式,它将对象的依赖关系 (即它所需要的其他对象) 从外部注入,而不是由对象自己在内部创建。

2.2 构造方法注入 (Constructor Injection)

@Controller
public class HelloController {
    private HelloService helloService;
    private HelloComponent helloComponent;

    //如果只有一个构造方法,只能使用该构造方法
    //当存在多个构造方法时,如果有无参构造方法,默认调用无参构造方法
    //当存在多个构造方法时,如果没有无参构造方法,Spring 就不知道该用哪个构造方法,需要使用@Autowired 指定一个默认的构造方法
    public HelloController() {}

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    @Autowired
    public HelloController(HelloService helloService, HelloComponent helloComponent) {
        this.helloService = helloService;
        this.helloComponent = helloComponent;
    }

    public void demo() {
        helloService.demo();
        helloComponent.demo();
        System.out.println("Hello HelloController");
    }
}

构造方法使用规范:当添加有参构造方法时,显式添加无参构造方法;如果存在多个构造方法,使用@Autowired 指定一个默认的构造方法。

2.3 Setter 方法注入 (Setter Injection)

@Controller
public class HelloController {
    private HelloService helloService;
    private HelloComponent helloComponent;

    //搭配@Autowired 使用
    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }

    @Autowired
    public void setHelloComponent(HelloComponent helloComponent) {
        this.helloComponent = helloComponent;
    }

    public void demo() {
        helloService.demo();
        helloComponent.demo();
        System.out.println("Hello HelloController");
    }
}

2.4 字段/属性注入 (Field Injection)

@Controller
public class HelloController {
    @Autowired
    private HelloService helloService;

    @Autowired
    private HelloComponent helloComponent;

    public void demo() {
        helloService.demo();
        helloComponent.demo();
        System.out.println("Hello HelloController");
    }
}

默认行为:按类型匹配 (byType)

  • 查找候选 Bean:Spring 会首先在 IoC 容器中查找类型匹配的 Bean。
  • 判断并注入:
    • 找到 0 个:如果 required 属性为 true(默认),则会抛出 NoSuchBeanDefinitionException 异常。
    • 找到 1 个:直接注入,这是最理想的情况。
    • 找到多个:此时按类型匹配失败,Spring 会尝试后备策略,即按名称匹配 (byName)。它会将变量名 (或属性名) 作为 Bean 的 ID 去容器中查找。

2.5 优缺点对比

构造方法注入:优点:可以注入 final 修饰的属性;注入的依赖在调用前一定会完全初始化,因为构造方法在类加载阶段执行;在 Spring 以外的其他框架中同样支持,构造方法由 JDK 提供。缺点:注入多个依赖时代码量庞大。

Setter 方法注入:优点:Setter 方法可以被多次调用,这意味着该依赖可以多次注入。缺点:不能注入 final 修饰的属性;多次注入可能导致依赖被修改。

字段/属性注入:优点:代码简洁,使用方便。缺点:不能注入 final 修饰的属性;在非 IoC 容器中不可用。

2.6 候选 Bean 问题

在使用@Autowired 注入时,优先按照类型匹配,如果匹配到多个 Bean,会将变量名 (或属性名) 作为 Bean 的 ID 去容器中查找。当变量名 (或属性名) 匹配失败时,程序无法运行成功。 在上述代码中,UserController 类需要注入一个单例 Bean,却发现多个候选 Bean,这叫做候选 Bean 问题。

2.6.1 @Primary 注解

作用:指定当存在多个相同类型的 Bean 时,优先使用被标记的 Bean。

运行结果:UserObject(name=李四,age=null, id=null)

2.6.2 @Qualifier 注解

作用:是 Spring 框架中用于精确指定要注入哪个 Bean的注解,它解决了当存在多个相同类型 Bean 时的依赖注入歧义问题。

运行结果:UserObject(name=王五,age=18, id=102)

2.6.3 @Resource 注解

作用:@Resource 注解是 Java 标准注解,Spring 框架提供了对它的支持。它是@Autowired 的一个替代方案,但在匹配机制上有重要区别。

也可以明确指定名称,按名称精确匹配。

[图片:@Resource 按名称匹配示意图]

优先按照 Bean 的名称匹配。

[图片:@Resource 按类型匹配示意图]

2.6.4 优先级

当@Primary、@Qualifier、@Resource、@Autowired 同时存在时,优先级如下: Resource > Qualifier > Primary > Autowired

目录

  1. 1. IoC(控制反转)
  2. 1.1 Spring IoC VS Servlet
  3. 1.2 IoC 解析
  4. 1.2.1 IoC 概述
  5. 1.2.2 IoC 容器管理
  6. 1.3 ApplicationContext
  7. 1.4 类注解
  8. 1.5 获取 Bean
  9. 1.5.1 通过 Bean 的类型获取
  10. 1.5.2 通过 Bean 的名称 (ID) 获取
  11. 1.6 方法注解 - @Bean
  12. 1.6.1 特性解析
  13. 1.6.2 通过 Bean 的类型获取
  14. 1.6.3 通过 Bean 的名称 (ID) 获取
  15. 1.7 @ComponentScan - 扫描路径
  16. 2. DI(依赖注入)
  17. 2.1 概述
  18. 2.2 构造方法注入 (Constructor Injection)
  19. 2.3 Setter 方法注入 (Setter Injection)
  20. 2.4 字段/属性注入 (Field Injection)
  21. 2.5 优缺点对比
  22. 2.6 候选 Bean 问题
  23. 2.6.1 @Primary 注解
  24. 2.6.2 @Qualifier 注解
  25. 2.6.3 @Resource 注解
  26. 2.6.4 优先级
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Dify MCP Server 插件实战:将工作流发布为第三方服务
  • C++ STL set 系列:底层原理、核心接口与实战场景
  • OpenClaw 开源 AI 助手中文发行版部署指南:Docker/NPM 安装与配置
  • 万方 AIGC 检测工具对比与选择指南
  • 无人机路径规划算法详解:原理与实战应用
  • C++ 异常处理机制详解
  • 用 ImGui 快速搭一个 C++ 调试面板
  • 工信部发布工业互联网开源行动方案 推动生态跃迁
  • ChatGPT 辅助 SEO 实战技巧与提示词指南
  • Spring AI MCP Server 技术解析与实战示例
  • Linux 命令行参数与环境变量详解
  • AI 产品经理完整学习路径:从入门到进阶指南
  • 前端安全实战:防范 XSS、CSRF 及敏感信息泄露
  • Edge 边栏 Copilot 图标消失的修复方案
  • Wi-Fi 7 轻量化演进:智能家居与物联网的连接新机遇
  • 大模型时代的技术趋势与产业机遇
  • Eino Embedding 组件核心解析:文本向量化与语义检索
  • MCPHost:命令行下利用大模型与外部工具交互
  • Ubuntu 24.04 更换国内软件源配置方法
  • Java 设计模式:单例、工厂与代理模式

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online