跳到主要内容
Spring IoC 与依赖注入 (DI) 核心解析 | 极客日志
Java java
Spring IoC 与依赖注入 (DI) 核心解析 综述由AI生成 深入解析 Spring IoC(控制反转)机制及其与 Servlet 的区别,阐述 IoC 容器管理的核心内容如 Bean 定义、实例及生命周期。详细介绍了 ApplicationContext 接口及五大类注解(@Component、@Controller、@Service、@Repository、@Configuration)的语义与功能。讲解了通过类型或名称获取 Bean 的方法,以及@Bean 注解在配置类中创建 Bean 的特性。重点对比了依赖注入(DI)的三种方式:构造方法注入、Setter 注入和字段注入的优缺点,并提供了处理候选 Bean 冲突的解决方案,包括@Primary、@Qualifier 和@Resource 注解的使用优先级。
监控大屏 发布于 2026/3/29 更新于 2026/5/25 29 浏览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( );
}
}
{
{
System.out.println( );
}
}
{
{
();
car.run();
}
}
"bottom init"
class
Tire
public
Tire
()
"tire init"
public
class
Main
public
static
void
main
(String[] args)
Car
car
=
new
Car
缺点:所有依赖都在类内部硬编码,缺乏灵活性难以修改组件的具体实现 (例如更换 Tire 的类型或参数),组件之间耦合度高。
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 {
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 实例,但在实际开发中应优先使用依赖注入。
@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 的类型获取
@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);
HelloController helloController = context.getBean(HelloController.class);
helloController.demo();
}
}
如果 HelloController 类没有交给 Spring 管理,会抛出异常:
[图片:NoSuchBeanDefinitionException 异常截图]
如果某接口 (类) 有多个实现类,此时根据类型来获取 Bean 会抛出异常:
public interface Func {}
@Service("dataBaseFunc")
class DataBaseFunc implements Func {}
@Service("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;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1 )) && Character.isUpperCase(name.charAt(0 ))) {
return name;
}
char [] chars = name.toCharArray();
chars[0 ] = Character.toLowerCase(chars[0 ]);
return new String (chars);
}
public interface Func {
void init () ;
}
@Service("dataBaseFunc")
class DataBaseFunc implements Func {
@Override
public void init () {
System.out.println("database init" );
}
}
@Service("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;
public UserObject () {
}
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;
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
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" );
}
}
查找候选 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 的一个替代方案,但在匹配机制上有重要区别。
2.6.4 优先级
当@Primary、@Qualifier、@Resource、@Autowired 同时存在时,优先级如下: Resource > Qualifier > Primary > Autowired
相关免费在线工具 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