深入浅出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 + @ResponseBody | REST 接口类 |
@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 | 注入配置值、表达式、常量 | 字段、方法参数 |
@Resource | JSR-250 注解,按名称注入 | 字段、setter |
@Inject | JSR-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构造函数注入的过程:
- 注册Bean:由于有
@Component
注解,Spring 会自动将该类注册为 Bean。 - 发现构造方法参数:由于
@Autowired
的存在,Spring 知道构造方法需要一个UserService
类型的参数。 - 查找 Bean:Spring 容器会查找有没有类型为
UserService
的 Bean。 - 自动注入:找到后,把这个
UserService
实例作为参数,调用构造方法创建OrderService
实例。 - 此时
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
方法注入的过程:
- 注册Bean:Spring 容器发现
@Service
注解,注册为 Bean。 - 实例化 OrderService:Spring会先调用无参构造方法(如果没写,会用默认的)
- 依赖注入阶段:Spring 发现有
@Autowired
的 setter 方法,会自动查找类型为UserService
的 Bean。 - 调用 setter:Spring 调用
setUserService(UserService userService)
方法,把查找到的UserService
实例传进去。 - 此时
userService
属性被赋值,可以正常使用。
2.2.3 字段注入
@Service
public class OrderService {@Autowiredprivate UserService userService;public void run() {userService.sayHello();}}
Spring字段注入的过程:
- 注册Bean:Spring容器发现这个类有
@Service
注解,注册为Bean。 - 实例化 OrderService:Spring用无参构造方法创建实例。
- 依赖注入阶段:Spring发现
userService
字段上有@Autowired
,就会去容器中查找类型为UserService
的Bean。 - 赋值注入:Spring通过反射直接把
UserService
的Bean赋值给userService
字段。 - 此时
userService
属性可以正常使用。
可以总结出三种依赖注入方式的特点:
- 构造函数注入:
- 依赖不可变:由于
final
变量的初始化只能在两个地方出现:变量定义处和构造方法或静态代码块中,这意味着只有构造函数注入可以将依赖声明为final
,依赖不可变,线程安全。 - 依赖强制注入:构造器参数明确表示哪些依赖项是必需的,否则对象无法被创建,避免遗漏。
- 便于单元测试:可以直接通过构造器传依赖项的Mock。
- 依赖不可变:由于
- Setter方法注入:
- 依赖可选,可以不注入(配合
@Autowired(required = false)
),可以在对象创建后动态修改依赖项,适用于一些特殊场景。 - 依赖可变,不安全。依赖可选容易遗漏必要依赖。
- 不利于单元测试,需要调用setter方法来注入Mock。
- 依赖可选,可以不注入(配合
- 字段注入:
- 写法简单,不需要写构造器或Setter。
- 依赖可变,不安全。由于依赖没有通过构造器签名或者Setter方法暴露出来,所以依赖不可见。
- 不利于单元测试,难以注入 mock,需要用反射。
由于构造器注入的依赖不可变性和强制性注入,使代码更健壮、更易于测试和维护,推荐优先使用构造器注入,其次是 setter 注入,需要动态修改依赖项的场景中可以使用。字段注入只建议用于简单项目中。
3. Spring Bean 生命周期和反射机制
刚才提到 DI 是 IoC 的实现方式,那么你知道 DI 的底层机制用到了什么技术吗?没错,就是反射。
不但动态代理的实现与反射有关,DI 也与反射密切相关。本节就讲讲Bean 的生命周期及其与反射机制的关系,这对于深入理解和高效使用 DI 很重要。
主要包括4/5个阶段:实例化Bean → 属性赋值 → 初始化Bean →(使用Bean)→ 销毁Bean。
-
实例化Bean
Spring 根据配置(比如注解、配置类等)知道了要创建哪个类的 Bean,然后调用这个类的构造方法,创建出一个实例对象,这个对象是否有属性被赋值取决于构造函数是否有参数。
反射体现:调用类的构造方法要基于 Java 的反射机制实现。
-
属性赋值
如果类中还依赖其他 Bean,比如通过Setter方法注入或者字段注入,Spring 会先找到这些被依赖的对象,再把被依赖的对象赋值给对象属性。
反射体现:把被依赖的对象赋值给对象属性基于反射机制实现。
-
初始化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 代理生成(动态代理)使用反射机制实现。
-
-
使用Bean
Bean 已经完全初始化好,可以被应用程序正常使用了。这时,可以通过依赖注入、Spring 容器的 getBean() 等方式获取 Bean 并调用其方法。
-
销毁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。
如果这篇文章对你有帮助,欢迎点赞、转发、留言!