文章目录
- IoC & DI 介绍
- IoC介绍
- DI 介绍
- 组件注册
- Bean 命名约定
- 方法注解 @Bean
- 总结
- 扫描路径
- DI 详解
- 属性注入
- 构造方法注入
- Setter 注入
- 三种注入优缺点分析
- 当同一类型存在多个Bean时,直接使用@Autowired会存在问题
- 使用@Primary注解
- 使用@Qualifier注解
- 使用Bean的名称
- 使用@Resource注解
IoC & DI 介绍
IoC介绍
Spring 是什么?
Spring 是一个开源框架,让我们的开发更加简单。我们用一句更具体的话来概括 Spring,那就是:Spring 是包含了众多工具方法的 IoC 容器
那么问题来了,什么是容器?什么是 IoC 容器?接下来我们一起来看
什么是容器?
容器是用来容纳某些物品的装置。生活中的水壶,冰箱都是容器。Java中 List,Map 等集合类(数据存储容器),Tomcat(Web 容器) 也是容器
什么是 IoC: Inversion of Control (控制反转)?
IoC 是 Spring 的核心思想。其实在类上面添加 @RestController
或者@Controller
注解,就是把这个对象交给 Spring 管理,Spring 框架启动时就会加载该类。而把对象交给 Spring 管理这个理念,就是 IoC 思想
什么是控制反转呢?
也就是控制权反转(获得依赖对象的过程被反转了)
当需要某个对象时,传统开发模式中需要自己 new 对象,而 IoC 不需要自己创建,是把创建对象的任务交给容器,程序中只需要依赖注入 (Dependency Injection,DI) 就可以了,这个容器称为 IoC 容器, Spring 就是一个 IoC 容器,所以有时 Spring 也称为 Spring 容器
示例:造一辆车(分为四部分:轮子,底盘,车身,汽车)
传统程序开发的实现思路是这样的:先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子
实现代码如下:
public class NewCarExample {public static void main(String[] args) {Car car = new Car();car.run();}//汽车static class Car {private Framework framework;public Car() {framework = new Framework();System.out.println("Car init....");}public void run() {System.out.println("Car run...");}}//车身static class Framework {private Bottom bottom;public Framework() {bottom = new Bottom();System.out.println("Framework init...");}}//底盘static class Bottom {private Tire tire;public Bottom() {this.tire = new Tire();System.out.println("Bottom init...");}}//轮胎static class Tire {// 尺寸private int size;public Tire() {this.size = 17;System.out.println("轮胎尺寸: " + size);}}
}
这样设计看起来没问题,但是可维护性很低
比如接下来需求有了变更:我们需要加工多种尺寸的轮胎。那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:
public class NewCarExample {public static void main(String[] args) {Car car = new Car(20);car.run();}//汽车static class Car {private Framework framework;public Car(int size) {framework = new Framework(size);System.out.println("Car init...");}public void run() {System.out.println("Car run...");}}//车身static class Framework {private Bottom bottom;public Framework(int size) {bottom = new Bottom(size);System.out.println("Framework init...");}}//底盘static class Bottom {private Tire tire;public Bottom(int size) {this.tire = new Tire(size);System.out.println("Bottom init...");}}//轮胎static class Tire {// 尺寸private int size;public Tire(int size) {this.size = size;System.out.println("轮胎尺寸:" + size);}}
}
在上面的程序中,我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改。同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改,也就是整个设计几乎都得改
问题就是:当最底层类创建或减少参数之后,整个调用链上的所有类都需要修改。程序的耦合度非常高 (修改一处代码,影响其他处的代码修改)
此时,我们可以把上级类自己创建下级类的方式,改为传递的方式(也就是注入的方式),这样即使下级类发生变化(创建或减少参数),当前类也无需修改任何代码
基于以上思路,我们把示例改造一下,把创建子类的方式,改为注入传递的方式,具体实现代码如下:
public class IocCarExample {public static void main(String[] args) {Tire tire = new Tire(20);Bottom bottom = new Bottom(tire);Framework framework = new Framework(bottom);Car car = new Car(framework);car.run();}static class Car {private Framework framework;public Car(Framework framework) {this.framework = framework;System.out.println("Car init...");}public void run() {System.out.println("Car run...");}}static class Framework {private Bottom bottom;public Framework(Bottom bottom) {this.bottom = bottom;System.out.println("Framework init...");}}static class Bottom {private Tire tire;public Bottom(Tire tire) {this.tire = tire;System.out.println("Bottom init...");}}static class Tire {private int size;public Tire(int size) {this.size = size;System.out.println("Tire init, size: " + size);}}
}
代码经过以上调整,无论底层类如何变化,整个调用链都不用做任何改变,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了
IoC 优势
传统代码中对象创建顺序是:Car->Framework->Bottom->Tire
改进后的代码的对象创建顺序是:Tire->Bottom->Framework->Car
传统代码是 Car 控制并创建了 Framework,Framework 控制并创建了 Bottom,依次往下,而改进之后的控制权发生反转,不再是使用方创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了,这样的话,即使依赖类发生改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想
从上面可以看出,loC 容器的资源不由资源双方管理,而由不使用资源的第三方管理,这可以带来很多好处:
- 资源集中管理: loC 容器会帮我们管理一些资源 (对象等),我们需要使用时,只需要从 loC 容器中去取就可以了,实现资源的可配置和易管理
- 我们在创建实例时不需要了解其中的细节,降低了资源双方的依赖程度,也就是耦合度
总结
IoC (控制反转),就是将对象的控制权交给 Spring 的 IoC 容器,由 IoC 容器创建及管理对象
DI 介绍
上面学习了 loC, 那么什么是 DI: Dependency Injection (依赖注入)呢?
容器在运行期间,程序需要某个资源,此时容器就为其提供这个资源。动态的为应用程序提供运行时所依赖的资源,称之为依赖注入
从这点来看,依赖注入(DI)和控制反转(IoC)其实是从不同的角度描述同一件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦
上述代码中,是通过构造函数的方式,把依赖对象注入到需要使用的对象中的:轮胎注入到底盘,底盘再注入到车身,最后车身注入到汽车中
IoC 是一种思想,也是 “目标”,而思想只是一种指导原则,最终还是要有可行的落地方案,而 DI 就属于具体的实现。所以也可以说,DI 是 IoC 的一种实现
对 IoC 和 DI 有了初步的了解,接下来我们具体学习 Spring IoC 和 DI 的代码实现。既然 Spring 是一个 IoC(控制反转)容器,作为容器,那么它就具备两个最基础的功能:存和取
Spring 容器管理的主要是对象,这些对象,我们称之为 “Bean”。 我们把这些对象交由 Spring 管理,由 Spring 负责对象的创建和销毁。我们程序只需要告诉 Spring 哪些需要存,以及如何从 Spring 中取出对象并注入到需要使用的类中就行了
- 组件注册(交给 Spring 管理 —— 存):使用
@Component
及其派生注解(如@Controller
、@Service
、@Repository
、@Configuration
)可以将类标记为 Spring 管理的 Bean。这些注解会触发组件扫描机制,使 Spring 自动发现并注册这些类 - 依赖注入(取):使用
@Autowired
注解可以自动注入依赖的 Bean。该注解可用于构造函数、Setter 方法或字段,让 Spring 自动解析并注入所需的依赖对象
组件注册
要把某个对象交给 IoC 容器管理,共有两类注解可以实现:
- 类注解:
@Controller
、@Service
、@Repository
、@Component
、@Configuration
- 方法注解:
@Bean
接下来我们分别来看
使用 @Controller
存储 Bean 的代码如下所示:
//把 TestController 交给 Spring 管理,由 Spring 来管理对象
@org.springframework.stereotype.Controller
public class TestController {public void sayHi() {System.out.println("Hi");}
}
如何观察这个对象已经存在 Spring 容器中了呢?
接下来我们学习如何从 Spring 容器中获取对象
更改启动类代码如下,注意把类名换成自己的
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);//从Spring上下文中获取对象TestController bean = context.getBean(TestController.class);//使用对象System.out.println(bean);bean.sayHi();}
}
ApplicationContext 翻译就是: Spring 上下文
因为对象都交给 Spring 管理了,所以对象要从 Spring 中获取,那么就得先拿到 Spring 的上下文
关于上下文的概念
上下文就是指当前的运行环境。比如进行线程切换的时候,切换前会把线程的状态信息暂时储存起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到 CPU 的时候,从上下文中拿到线程上次运行的信息
运行启动类,观察运行结果,发现成功从 Spring 中获取到 TestController 对象,并执行 TestController 的 sayHi 方法
如果把@Controller
注释掉,就会报错
也就是没有org.example.j20250624.TestController
这个对象
那么自动创建的 Bean 的名称是什么呢?
Bean 是 Spring 框架在运行时管理的对象,Spring 会给管理的对象起一个名字方便管理,根据 Bean 的名称就可以获取到对应的对象
Bean 命名约定
我们看下官方文档的说明: Bean Overview :: Spring Framework
程序开发人员不需要为 Bean 指定名称, 如果没有显式的提供名称,Spring容器自动为该 Bean 生成唯一的名称
命名约定使用Java标准约定作为实例字段名。也就是说,Bean 名称以小写字母开头,然后使用驼峰式大小写
比如:
- 类名:UserController,Bean的名称为:userController
- 类名:AccountManager,Bean的名称为:accountManager
- 类名:AccountService,Bean的名称为:accountService
也有一些特殊情况,当类名有多个字符并且第一个和第二个字符都是大写时,将保留原始的大小写。规则与 java.beans.Introspector.decapitalize
定义的规则相同
比如:
- 类名:UController,Bean的名称为:UController
- 类名:AManager,Bean的名称为:AManager
public class Test {public static void main(String[] args) {String className = "UserController";System.out.println(Introspector.decapitalize(className));}
}
public class Test {public static void main(String[] args) {String className = "UController";System.out.println(Introspector.decapitalize(className));}
}
根据这个命名规则,我们来获取 Bean
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);// 从 Spring 上下文中获取对象// 方式1:根据 bean 类型获取TestController testController1 = context.getBean(TestController.class);// 方式2:根据 bean 名称获取(需强制类型转换)TestController testController2 = (TestController) context.getBean("testController");// 方式3:根据 bean 名称和类型获取TestController testController3 = context.getBean("testController", TestController.class);// 验证获取的对象实例System.out.println(testController1);System.out.println(testController2);System.out.println(testController3);}
}
地址一样,说明对象是同一个。能获取 Bean 对象,其实是 BeanFactory 提供的功能
ApplicationContext vs BeanFactory(常见面试题)
- 从继承关系和功能方面来说:Spring 容器有两个顶级接口: BeanFactory 和 ApplicationContext。其中 BeanFactory 提供了基础的访问容器能力,而 ApplicationContext 属于 BeanFactory 的子接口,它继承了 BeanFactory 的所有功能之外,还添加了国际化、事件发布、AOP 等方面的支持
- 从性能方面来说:ApplicationContext 是一次性加载并初始化所有的 Bean 对象,而 BeanFactory 是需要哪个才去加载哪个,因此 BeanFactory 更加轻量。两者本质是时间与空间的权衡
除了@Controller
,使用 @Service
,@Repository
,@Configuration
,@Component
一样也可以把类交给spring管理
为什么要这么多类注解?
这个也是和应用分层相呼应的。让程序猿看到类注解之后,就能直接了解当前类的用途
@Controller
:控制层。接收请求,对请求进行处理,并进行响应@Service
:业务逻辑层。处理具体的业务逻辑@Repository
:数据访问层,也称为持久层。负责数据访问操作@Configuration
:配置层。处理项目中的一些配置信息@Component
:通用组件层。当一个类不好明确归类到其他层的时候就可以用@Component
程序的应用分层,调用流程如下:
类注解之间的关系
查看 @Controller
/ @Service
/ @Repository
/ @Configuration
的源码发现这些注解里面都有 @Component
@Component
是一个元注解,也就是说可以注解其他类注解,用于标识一个类为 Spring 管理的 Bean。当 Spring 进行组件扫描时,会自动发现并注册被 @Component
标记的类。如 @Controller
,@Service
,@Repository
等。这些注解被称为 @Component
的衍生注解
方法注解 @Bean
类注解是添加到某个类上的,但是存在两个问题:
- 使用外部包里的类,没办法添加类注解
- 五大注解交给Spring管理的都是单例的。一个类,可能需要多个对象,比如多个数据源
这些场景,我们就需要使用方法注解 @Bean
我们先来看看方法注解如何使用:
public class User {private String name;private int age;public User(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
public class BeanConfig {@Beanpublic User user(){return new User("zhangsan", 23);}
}
然而,当我们写完以上代码,尝试获取 Bean 对象中的 User 却发现,根本获取不到:
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);// 从 Spring 上下文中获取对象User user = context.getBean(User.class);//使用对象System.out.println(user);}
}
这是因为@Bean
必须搭配五大注解使用
@Component
public class BeanConfig {@Beanpublic User user(){return new User("zhangsan", 23);}
}
再次执行,运行结果如下:
定义多个对象
比如多数据源的场景,类是同一个,但是配置不同,指向不同的数据源
如果让Spring再管理一个User对象,用类型取会报错
@Component
public class BeanConfig {@Beanpublic User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);// 从 Spring 上下文中获取对象User user = context.getBean(User.class);//使用对象System.out.println(user);}
}
报错信息显示:期望只有一个匹配,结果发现了两个,user1,user2。@Bean
管理的对象的名称就是方法名
接下来我们根据名称来获取 bean 对象
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);//根据bean名称,从Spring上下文中获取对象User user1 = (User) context.getBean("user1");User user2 = (User) context.getBean("user2");//使用对象System.out.println(user1);System.out.println(user2);}
}
运行结果:
可以看到,@Bean
可以针对同一个类,定义多个对象
重命名 Bean
可以通过设置 name 属性给 Bean 对象进行重命名,如下代码所示:
@Component
public class BeanConfig {@Bean(name = {"u1","user1"})public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
此时我们使用 u1 也可以获取到 User 对象了,如下代码所示:
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);//根据bean名称,从Spring上下文中获取对象User u1 = (User) context.getBean("u1");User user1 = (User) context.getBean("user1");User user2 = (User) context.getBean("user2");//使用对象System.out.println(u1);System.out.println(user1);System.out.println(user2);}
}
name=
可以省略,如下代码所示:
@Component
public class BeanConfig {@Bean({"u1","user1"})public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
只有一个名称时, {}
也可以省略, 如:
@Component
public class BeanConfig {@Bean("u1")public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
这样原来的user1
对象就没有了
总结
Bean 的命名
- 五大注解存储的 Bean
① 前两位字母均为大写,Bean 名称为类名
② 其他的为类名首字母小写
③ 重命名通过 value 属性设置 @Controller (value = "user")
@Bean
注解存储的 Bean
① Bean 名称为方法名
② 重命名通过 name 属性设置 @Bean (name = {"u1","user1"})
如果存在多个Bean, 根据类型去拿就会有问题,通过名称不会有问题(名称不会重复)
扫描路径
Q: 五大注解和@Bean
声明的 bean,一定会生效吗?
A: 不一定(原因: bean 想要生效,还需要被 Spring 扫描)
下面我们通过修改项目工程的目录结构,来测试 Bean 对象是否生效:
记得把代码改回正确的再测试
原本的目录结构:
因为现在启动类测试的是@Bean
创建的bean,把启动类移动到configuration包下还是能正确运行的,所以就把启动类放到controller包下进行测试
@SpringBootApplication
内部已默认包含 @ComponentScan
(默认扫描当前主启动类所在包及其子包的组件),可以显式添加 @ComponentScan
重新配置扫描路径, 但默认的扫描逻辑会被覆盖
@ComponentScan("org.example.j20250624.configuration")
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);//根据bean名称,从Spring上下文中获取对象User u1 = (User) context.getBean("u1");User user1 = (User) context.getBean("user1");User user2 = (User) context.getBean("user2");//使用对象System.out.println(u1);System.out.println(user1);System.out.println(user2);}
}
这样就能扫描到configuration包了
加上{} 可以配置多个包路径
@ComponentScan({"org.example.j20250624.configuration", "org.example.j20250624.model"})
DI 详解
依赖注入是一个过程,是指 IoC 容器在创建 Bean 时,去提供运行时所依赖的资源,而资源指的就是对象。简单来说,就是把对象取出来放到某个类的属性中。
在一些文章中,依赖注入也被称为 “对象注入”,“属性装配”,具体含义需要结合文章的上下文来理解
依赖注入是使用 @Autowired
实现的,Spring 给我们提供了三种方式:
- 属性注入 (Field Injection)
- 构造方法注入 (Constructor Injection)
- Setter 注入 (Setter Injection)
下面我们按照实际开发中的模式,将 Service 类注入到 Controller 类中
属性注入
Service 类的实现代码如下:
@Service
public class UserService {public void sayHi() {System.out.println("Hi,UserService");}
}
Controller 类的实现代码如下:
//注入方法1: 属性注入@Controller
public class UserController {@Autowiredprivate UserService userService;public void sayHi(){System.out.println("hi,UserController...");userService.sayHi();}
}
调用 UserController 中的 sayHi 方法:
@SpringBootApplication
public class J20250624Application {public static void main(String[] args) {//获取Spring上下文对象,可以理解成拿到Spring容器ApplicationContext context = SpringApplication.run(J20250624Application.class, args);//从Spring上下文中获取对象UserController userController = (UserController) context.getBean("userController");//使用对象userController.sayHi();}
}
去掉@Autowired
, 再运行一下程序看看结果
构造方法注入
构造方法注入是在类的构造方法中实现注入,如下代码所示:
//注入方法2: 构造方法@Controller
public class UserController {private UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}public void sayHi(){System.out.println("hi,UserController2...");userService.sayHi();}
}
注意事项:如果类只有一个构造方法,那么 @Autowired
可以省略,Spring 会自动使用该构造方法进行依赖注入;如果类中有多个构造方法,那么需要加上 @Autowired
来明确指定使用哪个构造方法
@Controller
public class UserController {private UserService userService;private String name;// 构造方法1:注入 UserService@Autowired //必须指定public UserController(UserService userService) {this.userService = userService;}// 构造方法2:注入 UserService 和 namepublic UserController(UserService userService, String name) {this.userService = userService;this.name = name;}public void sayHi(){System.out.println("hi,UserController2...");userService.sayHi();}
}
Setter 注入
Setter 注入和属性的 Setter 方法实现类似,只不过在设置 Setter 方法的时候加上 @Autowired
,如下代码所示:
//注入方法3: Setter方法注入@Controller
public class UserController {private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}public void sayHi(){System.out.println("hi,UserController3...");userService.sayHi();}
}
三种注入优缺点分析
- 属性注入
- 优点:代码简洁直观,通过
@Autowired
直接标记字段即可注入,开发效率高 - 缺点:强依赖 IoC 容器。无法注入 final 修饰的字段(因为字段初始化依赖容器注入,而 final 要求构造时赋值)
- 构造函数注入
-
优点:支持注入 final 修饰的字段(构造方法执行时初始化,符合 final 语义)。
依赖在类创建阶段(构造方法执行)就完成注入,保证对象使用时依赖已完全初始化。
通用性强:构造方法是 JDK 基础特性,不依赖框架,切换框架时无需修改注入逻辑。
注入的对象状态稳定(依赖不可变),避免后续被意外修改。 -
缺点:若类依赖多个对象,构造方法参数会增多,代码略显繁琐。
- Setter 注入
- 优点:支持对象创建后动态修改依赖,灵活度高,适合处理 “可选依赖”(依赖可后续配置,不影响类初始化)
- 缺点:无法注入 final 修饰的字段(依赖通过 Setter 方法赋值,晚于 final 字段初始化时机)。
依赖可能被多次修改(Setter 方法可被外部调用),存在状态变更风险。
当同一类型存在多个Bean时,直接使用@Autowired会存在问题
@Component
public class BeanConfig {@Bean("u1")public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
@Controller
public class UserController {//注入user@Autowiredprivate User user;
}
只需要一个Bean,但找到两个
如何解决上述问题呢?Spring提供了以下几种解决方案:
- @Primary
- @Qualifier
- 改成 Bean的名称
- @Resource
使用@Primary注解
@Primary
注解的作用是在存在多个相同类型的 Bean 时,指定某个 Bean 作为默认注入选择
@Component
public class BeanConfig {@Primary@Bean("u1")public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
@Controller
public class UserController {//注入user@Autowiredprivate User user;
}
使用@Qualifier注解
在@Qualifier
的value属性中,指定当前要注入的Bean的名称。@Qualifier
注解不能单独使用,必须配合@Autowired
使用
@Component
public class BeanConfig {@Bean("u1")public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
@Controller
public class UserController {//注入user@Qualifier("u1")@Autowiredprivate User user;
}
使用Bean的名称
@Component
class BeanConfig {@Beanpublic User user1(){return new User("zhangsan",18);}@Beanpublic User user2() {return new User("lisi",19);}
}
@Controller
public class UserController {@Autowiredprivate User user1;
}
使用@Resource注解
通过name属性指定要注入的Bean的名称
@Component
public class BeanConfig {@Bean("u1")public User user1(){return new User("zhangsan", 18);}@Beanpublic User user2(){return new User("lisi", 19);}
}
@Controller
public class UserController {//注入user@Resource(name = "u1")private User user;
}