当前位置: 首页 > ops >正文

深入浅出Spring IoC与DI:设计思想、实现方式与反射技术详解

大家好,我是钢板兽!

在之前在一篇文章中(你真的懂 AOP 吗?动态代理与 AOP 全解析),我们探讨了Spring框架中的AOP特性。今天,将继续深入研究Spring的另外两个核心特性:控制反转(IoC)和依赖注入(DI),虽然 IoC 与 DI 可能看起来抽象,但它们在面试和实际开发中都是非常重要的概念。

本文将对IoC、DI以及反射技术之间的关系进行详细剖析。看完这篇文章,你将清晰地理解:IoC(设计思想)如何通过DI(实现方式)借助反射(技术手段)来实现 这一问题。

文章字数:4000+

阅读时长:10mins

文章目录

    • 1.了解IoC
      • 1.1 为什么需要IoC
      • 1.2 IoC的设计思想
    • 2. IoC的实现:DI
      • 2.1 注册Bean
        • 2.1.1 基于注解自动注册
        • 2.1.2 基于配置类注册
      • 2.2 依赖注入
        • 2.2.1 构造函数注入
        • 2.2.2 Setter方法注入
        • 2.2.3 字段注入
    • 3. Spring Bean 生命周期和反射机制

1.了解IoC

1.1 为什么需要IoC

如果不使用IoC的话,我们就需要在类中通过new关键字显式地在类中创建依赖对象,这样主动创建依赖的方式看起来很简单,但是这样一方面会使得业务代码依赖于具体实现,另一方面会导致对象的生命周期、依赖关系管理混乱。随着业务的发展、新增功能开发,代码的扩展性会越来越差。

(1)依赖具体实现→依赖抽象接口

举一个用户注册的场景业务,初始需求是在用户注册时,把用户信息保存在本地数据库。

public class UserService {private UserRepository userRepository = new MySQLUserRepository();public void register(String username) {userRepository.save(username);}
}

UserRepository是一个接口,而MySQLUserRepository是我们要用的实现类,MySQLUserRepository被写死在UserService中。

现在新增一个业务需求,要把用户数据集中存储到远程服务(如 MongoDB、分布式用户中心)。

由于要切换到实现类RemoteUserRepository,需要我们手动改代码

private UserRepository userRepository = new RemoteUserRepository();

仔细想一下我们就会知道,上面的程序由于是手动new创建对象,所以业务逻辑依赖于具体实现类,每当要更替别的实现类时,就要手动切换要new的对象。

那么有没有办法,可以让业务类只依赖抽象接口,而不是依赖具体实现呢?这时IoC就出现了。

(2)对象生命周期、依赖关系管理混乱→统一管理

在一个庞大的软件系统中,如果每个类都直接在自己的类中new对象,那么就会出现多个类可能重复创建相同对象(如HTTP调用、配置服务等),这就是对象生命周期、依赖关系、资源释放不统一管理的弊端。

比如在微服务架构中,服务之间常常通过 HTTP 调用进行通信。假设我们在系统中多个模块都需要调用远程用户中心服务(UserCenterClient),来查询用户资料。

在下面两个业务类中,每个类都自己 new 了一个 HTTP 客户端

public class OrderService {private final UserCenterClient userCenterClient = new UserCenterClient();public void createOrder(String userId) {UserInfo user = userCenterClient.getUserById(userId);...}
}public class CommentService {private final UserCenterClient userCenterClient = new UserCenterClient();public void postComment(String userId, String comment) {UserInfo user = userCenterClient.getUserById(userId);...}
}

要知道HttpClient是重量级对象,每个类中创建HTTP 客户端会导致对象无法复用,而且如果涉及到需要配置日志、认证、超时配置、重试机制的情况,无法集中配置。

那么有没有办法,可以让对象的生命周期、依赖关系得到统一管理呢?这时IoC又出现了。

1.2 IoC的设计思想

IoC(Inversion of Control,控制反转)是一种设计思想,设计目的就是将对象之间的依赖关系的控制权从代码内部“反转”给外部容器或框架管理。

更具体地说,传统方式是在代码里创建对象、管理依赖,而IoC是由框架或容器负责创建对象、注入依赖。

为什么会叫作“控制反转”这个名字呢?

public class A {private B b = new B(); // A 主动创建对 B 的依赖
}

这段代码中,类A对象控制了对类B对象的创建和依赖,这是正向控制。

而在IoC中,A类只将依赖关系声明出来,依赖注入由容器来执行。

public class A {private B b; //声明依赖关系,要依赖B类,但是不创建public A(B b) {this.b = b;}
}

A不再“控制”对B的依赖,而是由外部(Spring)来“反转”控制权,统一注入,这就是所谓的控制反转

IoC遵循了多个经典设计原则:

设计原则IoC 如何体现
依赖倒置原则(DIP)高层模块不依赖低层模块,二者都依赖于抽象
开闭原则(OCP)对扩展开放,对修改封闭
单一职责原则(SRP)对象不再负责管理其依赖,仅专注业务逻辑
好莱坞原则(Hollywood Principle)“Don’t call us, we’ll call you” ——你不主动调用,我们来调用你

2. IoC的实现:DI

IoC只是一种设计思想,它的落地通常是由DI(Dependency Injection,依赖注入)来实现的,它指的是由外部容器将对象所依赖的组件(如其他类、服务)注入进来,而不是由对象自己创建依赖。

简单来说就是,对象不再通过 new 的方式创建依赖,而是由框架(如 Spring 容器)负责创建依赖对象并把它“注入”到其它类中使用,这个依赖对象具体指的就是Bean

Spring 容器负责创建依赖对象的过程就是把一个类注册为Bean,依赖注入的过程就是把Bean注入到需要使用Bean的对象中。所以,我觉得 DI 可以分成两个核心问题:注册Bean和依赖注入

2.1 注册Bean

注册Bean有两种常用实现方式,一是基于注解自动注册,而是基于配置类注册,其他的方法还有基于XML配置注册、工厂方法注册等,这里不做讲解。

2.1.1 基于注解自动注册

使用 @Component 或其语义注解(如 @Service, @Controller

注解说明常用于
@Component通用组件标记,注册为 Bean工具类、配置类
@Controller标识 Web 控制器MVC 控制器
@RestController@Controller + @ResponseBodyREST 接口类
@Service标识服务层 Bean(等价于 Component)业务逻辑层
@Repository标识 DAO 层 Bean,支持异常翻译数据访问层
@Mapper标识 DAO 层Bean,Mybatis框架专用数据访问层

如:

@Component
public class UserService { ... }

或者:

@Service
public class OrderService { ... }

使用注解自动注册的前提是Spring 必须扫描这个类所在的包

@SpringBootApplication // 内部包含了 @ComponentScan
public class Application { ... }

Spring 在启动时通过 ClassPathBeanDefinitionScanner 扫描带有 @Component 注解的类,为其创建 BeanDefinition,实例化后注册到 ApplicationContext,可通过 @Autowired 注入使用。

2.1.2 基于配置类注册

在配置类中用@Bean注解的方法返回对象,Spring会把返回对象注册为 Bean:

@Configuration
public class AppConfig {@Beanpublic RedisClient redisClient() {return new RedisClient("localhost", 6379);}
}

@Configuration 告诉 Spring这是一个配置类;@Bean 方法在容器初始化阶段被调用,其返回值注册为 Bean,方法名为默认 Bean 名,也可通过 @Bean("xxx") 指定。

这种方式常用于无法加注解的类(如第三方类、SDK、工具类)、构造逻辑复杂或带参数配置、Bean 需要手动控制依赖。

2.2 依赖注入

依赖注入即把Bean注入到需要使用Bean的对象中,Bean的注入方式基于注入驱动注解,Spring 支持以下几类注入驱动注解:

注解说明适用位置
@Autowired自动按类型注入 Bean,可配合 @Qualifier 精确指定 Bean构造器、字段、方法
@Value注入配置值、表达式、常量字段、方法参数
@ResourceJSR-250 注解,按名称注入字段、setter
@InjectJSR-330 注解,等价于 @Autowired字段、setter
@Required要求某属性必须注入(已弃用)setter 方法

AutowiredAnnotationBeanPostProcessor 是Spring框架中一个核心注入处理类,它负责解析和处理像@Autowired@Value@Resource这样的注解。

在 Spring 容器实例化 Bean 并填充属性时,如果BeanPostProcessor 发现某处有@Autowired等注入驱动注解,就会去 Spring 容器中查找对应类型的 Bean,并将其注入到目标对象中。

那么注入驱动注解应该加在什么地方呢?我们接着往下看。

假设有一个Bean(UserService):

@Service // 这个类会被注册为 Bean
public class UserService {public void sayHello() {System.out.println("Hello from UserService");}
}

现在要把这个Bean注入到OrderService类中,有三种常见的方式:构造函数注入、Setter方法注入、字段注入。

2.2.1 构造函数注入
@Component
public class OrderService {private final UserService userService;@Autowired // Spring 4.3+ 可省略(如果只有一个构造器)public OrderService(UserService userService) {this.userService = userService;}public void run() {userService.sayHello();}}

private final UserService userService;声明一个依赖,类型为 UserService

Spring构造函数注入的过程:

  1. 注册Bean:由于有 @Component 注解,Spring 会自动将该类注册为 Bean。
  2. 发现构造方法参数:由于@Autowired的存在,Spring 知道构造方法需要一个 UserService 类型的参数。
  3. 查找 Bean:Spring 容器会查找有没有类型为 UserService 的 Bean。
  4. 自动注入:找到后,把这个 UserService 实例作为参数,调用构造方法创建 OrderService 实例。
  5. 此时 orderService.userService 已经被正确赋值,可以直接使用。
2.2.2 Setter方法注入
@Service
public class OrderService {private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}public void run() {userService.sayHello();}
}

Spring Setter方法注入的过程:

  1. 注册Bean:Spring 容器发现 @Service 注解,注册为 Bean。
  2. 实例化 OrderService:Spring会先调用无参构造方法(如果没写,会用默认的)
  3. 依赖注入阶段:Spring 发现有 @Autowired 的 setter 方法,会自动查找类型为 UserService 的 Bean。
  4. 调用 setter:Spring 调用 setUserService(UserService userService) 方法,把查找到的 UserService 实例传进去。
  5. 此时userService 属性被赋值,可以正常使用。
2.2.3 字段注入
@Service
public class OrderService {@Autowiredprivate UserService userService;public void run() {userService.sayHello();}}

Spring字段注入的过程:

  1. 注册Bean:Spring容器发现这个类有 @Service 注解,注册为Bean。
  2. 实例化 OrderService:Spring用无参构造方法创建实例。
  3. 依赖注入阶段:Spring发现 userService 字段上有 @Autowired,就会去容器中查找类型为 UserService 的Bean。
  4. 赋值注入:Spring通过反射直接把 UserService 的Bean赋值给 userService 字段。
  5. 此时userService 属性可以正常使用。

可以总结出三种依赖注入方式的特点:

  1. 构造函数注入:
    • 依赖不可变:由于final变量的初始化只能在两个地方出现:变量定义处和构造方法或静态代码块中,这意味着只有构造函数注入可以将依赖声明为final,依赖不可变,线程安全。
    • 依赖强制注入:构造器参数明确表示哪些依赖项是必需的,否则对象无法被创建,避免遗漏。
    • 便于单元测试:可以直接通过构造器传依赖项的Mock。
  2. Setter方法注入:
    • 依赖可选,可以不注入(配合 @Autowired(required = false)),可以在对象创建后动态修改依赖项,适用于一些特殊场景。
    • 依赖可变,不安全。依赖可选容易遗漏必要依赖。
    • 不利于单元测试,需要调用setter方法来注入Mock。
  3. 字段注入:
    • 写法简单,不需要写构造器或Setter。
    • 依赖可变,不安全。由于依赖没有通过构造器签名或者Setter方法暴露出来,所以依赖不可见。
    • 不利于单元测试,难以注入 mock,需要用反射。

由于构造器注入的依赖不可变性和强制性注入,使代码更健壮、更易于测试和维护,推荐优先使用构造器注入,其次是 setter 注入,需要动态修改依赖项的场景中可以使用。字段注入只建议用于简单项目中。

3. Spring Bean 生命周期和反射机制

刚才提到 DI 是 IoC 的实现方式,那么你知道 DI 的底层机制用到了什么技术吗?没错,就是反射。

不但动态代理的实现与反射有关,DI 也与反射密切相关。本节就讲讲Bean 的生命周期及其与反射机制的关系,这对于深入理解和高效使用 DI 很重要。

主要包括4/5个阶段:实例化Bean → 属性赋值 → 初始化Bean →(使用Bean)→ 销毁Bean。

  1. 实例化Bean

    Spring 根据配置(比如注解、配置类等)知道了要创建哪个类的 Bean,然后调用这个类的构造方法,创建出一个实例对象,这个对象是否有属性被赋值取决于构造函数是否有参数。

    反射体现:调用类的构造方法要基于 Java 的反射机制实现。

  2. 属性赋值

    如果类中还依赖其他 Bean,比如通过Setter方法注入或者字段注入,Spring 会先找到这些被依赖的对象,再把被依赖的对象赋值给对象属性。

    反射体现:把被依赖的对象赋值给对象属性基于反射机制实现。

  3. 初始化Bean

    初始化阶段是Bean生命周期中最复杂、最关键的阶段之一,具体包括以下步骤:

    • 执行各种通知(Aware 接口回调)

      Aware 接口是Spring提供的一些列接口,允许 Bean 获取 Spring 容器的某些信息,比如 Bean 名称、容器引用、环境变量等。

      反射体现:Spring 通过反射检查 Bean 是否实现了特定的 Aware 接口,并调用相应方法注入信息

    • 执行初始化的前置工作

      Spring 提供了 BeanPostProcessor 接口,允许开发者在 Bean 初始化前进行额外的处理或修改,例如修改属性值、检查,甚至替换Bean实例,Spring 会遍历所有注册的BeanPostProcessor

      这个BeanPostProcessor 接口有点抽象,但是Spring框架底层使用了大量BeanPostProcessor的实现类,很多Spring的重要功能,如@Autowired注解的自动注入、@Scheduled注解的任务调度等功能都是基于BeanPostProcessor实现的。

      反射体现:开发者实现的 BeanPostProcessor 可以通过反射修改 Bean 的属性或状态。

    • 进行初始化工作

      在属性注入完成后,Spring会调用Bean的初始化方法,这个方法可以是Bean实现的InitializingBean接口中的afterPropertiesSet方法,或者是通过@PostConstruct注解标记的方法。允许开发者在Bean完全配置后执行一些自定义初始化逻辑,比如打开数据库连接、加载缓存等。

      反射体现:通过反射调用 Bean 中标记为 @PostConstruct 的方法或实现了 InitializingBean 接口的 afterPropertiesSet() 方法。

    • 执行初始化的后置工作

      Spring 会再次遍历所有 BeanPostProcessor,这次是在初始化之后,允许开发者对 Bean 做进一步增强,比如生成代理对象(AOP)、自动包装等。

      反射体现:Spring AOP 代理生成(动态代理)使用反射机制实现。

  4. 使用Bean

    Bean 已经完全初始化好,可以被应用程序正常使用了。这时,可以通过依赖注入、Spring 容器的 getBean() 等方式获取 Bean 并调用其方法。

  5. 销毁Bean

    当 Spring 容器关闭时,会销毁 Bean,释放占用的资源。销毁过程包括调用 @PreDestroy 注解的方法、实现 DisposableBean 接口的 destroy() 方法以及自定义的销毁方法(destroy-method)。这一步通常用于释放资源,比如关闭数据库连接、线程池等。

    反射体现:使用反射机制调用标记为@PreDestroy的方法,或调用实现DisposableBean接口的destroy()方法。

Spring Bean 的整个生命周期与 Java 的反射机制密不可分。从 Bean 的实例化、属性赋值,到初始化阶段,再到最终的销毁阶段,反射技术都在其中发挥着关键作用。

由于时间原因,我对于Spring Bean 生命周期的理解尚浅,没有看源码,如果你想学习Spring源码,更深一步掌握Bean的生命周期,可以看这篇文章:https://cloud.tencent.com/developer/article/1691346。


如果这篇文章对你有帮助,欢迎点赞、转发、留言!

http://www.xdnf.cn/news/19779.html

相关文章:

  • Excel 电影名匹配图片路径教程:自动查找并写入系统全路径
  • PostgreSQL 中唯一索引的工作原理
  • 分布式AI算力系统番外篇-----超体的现实《星核》
  • Vue基础知识-重要的内置关系:vc实例.__proto__.__proto__ === Vue.prototype
  • 股指期货可以通过移仓长线持有吗?
  • AppInventor2 如何自定义包名?
  • 华为云云原生架构赋能:大腾智能加速业务创新步伐
  • 【深度学习新浪潮】视觉大模型在预训练方面有哪些关键进展?
  • 鸿蒙UI开发实战:解决布局错乱与响应异常
  • 企业实用——MySQL的备份详解
  • 基于机器学习的Backtrader波动性预测与管理
  • Kubernetes ConfigMap 更新完整指南:原理、方法与最佳实践
  • PyTorch实战——ResNet与DenseNet详解
  • Huggingface终于没忍住,OpenCSG坚持开源开放
  • flume拓扑结构详解:从简单串联到复杂聚合的完整指南
  • Linux 的信号 和 Qt 的信号
  • IO_HW_9_3
  • MySQL数据库恢复步骤(基于全量备份和binlog)
  • 揭秘ArrowJava核心:IndexSorter高效排序设计
  • Cookie、Session、登录
  • 一个工业小白眼中的 IT/OT 融合真相:数字化工厂的第一课
  • SQL Server核心架构深度解析
  • AlexNet:计算机视觉的革命性之作
  • PostgreSQL性能调优-优化你的数据库服务器
  • JVM调优与常见参数(如 -Xms、-Xmx、-XX:+PrintGCDetails) 的必会知识点汇总
  • 【学Python自动化】 9.1 Python 与 Rust 类机制对比学习笔记
  • 【WPS】WPSPPT 快速抠背景
  • 通过SpringCloud Gateway实现API接口镜像请求(陪跑)网关功能
  • 进攻是最好的防守 在人生哲学中的应用
  • 百度智能云「智能集锦」自动生成短剧解说,三步实现专业级素材生产