目录
[三、Spring IoC容器详解](#三、Spring IoC容器详解)
[@Configuration(配置存储)](#@Configuration(配置存储))
一、IoC&DI入门
在前面的文章,我们讲解了Spring Boot和Spring MVC的开发,可以完成一些基本功能的开发了,但是什么是Spring呢?Spring,Spring Boot和Spring MVC又有什么关系呢?咱们还是带着问题去学习
我们先看什么是Spring
Spring是什么?
通过前面的学习,我们知道了Spring是一个开源框架,他让我们的开发更加简单。他支持广泛的应用场景,有着活跃而庞大的社区,这也是Spring能够长久不衰的原因
但是这个概念相对来说,还是比较抽象
我们用一句更具体的话来概括Spring,那就是:Spring是包含了众多工具方法的IoC容器
那问题来了,什么是容器?什么又是IoC容器?接下来哦我们一起来看
什么是容器?
容器是用来容纳某种物品的装置。----百度百科
生活中的水杯、垃圾桶、冰箱等等这些都是容器
我们想想,之前Java学习中接触到的容器有哪些?
List/Map-->数据存储容器
Tomcat-->Web容器
什么是IoC?
IoC是Spring的核心思想,也是常见的面试题,那什么是IoC呢?
其实IoC我们在前面已经使用了,我们在前面文章讲到,在类上添加@RestController和@Controller注解,就是把这个对象交给Spring管理,Spring框架启动时就会加载该类。把对象交给Spring管理,就是IoC思想。
IoC(Inversion of Control,控制反转)
IoC是一种设计思想,其核心是将对象的创建、依赖关系的管理从代码中剥离,交由外部容器(如Spring 容器)负责。传统开发中,对象需主动通过 new 创建依赖对象,导致高耦合;而IoC通过容器反转控制权,实现解耦。
类比理解:
传统方式:自己做饭(需亲自买菜、切菜、烹饪)
IoC方式:点外卖(只需下单,由餐厅完成全部流程)
IoC介绍
接下来我们通过案例来了解一下什么是IoC
需求:造一辆车
传统程序开发
我们的实现思路是这样的:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据地盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子

最终程序的实现代码如下:
java
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);
}
}
}
问题分析
这样的设计看起来没问题,但是可维护性却很低
接下来需求有了变更:随着对车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎
那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:

修改之后,其他调用程序也会报错,我们需要继续修改



完整代码如下:
java
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);
}
}
}
从以上代码可以看出,当最底层代码改动之后,整个调用链上的所有代码都需要修改
程序的耦合度非常高(修改一处代码,会影响其他处的代码修改)
主要原因是,我们在每个类中自己创建下级类,像在Car类中,自己使用new来创建Framework类,在Framework类中,自己创建Bottom类.......,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。
解决方案
在上面的程序中,我们是根据轮子的尺寸设计的轮盘,轮子的尺寸一改,底盘的设计就得修改。同样因为我们是根据底盘设计的车身,那么底盘一改,车身也得改,同理汽车设计也得改,也就是整个设计几乎都得改
我们尝试换一种思路,我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车
这就类似我们打造一辆完整的汽车,如果所有的配件都是自己造,那么当客户需求发生改变的时候,比如轮胎的尺寸不再是原来的尺寸了,那我们要需要自己动手来改了。但如果我们是把轮胎外包出去,那么即时是轮胎的尺寸发生改变了,我们只需要向代理工厂下订单就行了,我们自身是不需要出力的

如何来实现呢?
从上述例子,我们知道,如果自己创建下级类就会出现 当下级类发生改变操作时,自己也要跟着修改
此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。
IoC程序开发
基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式
具体实现代码如下:
java
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("轮胎尺⼨:" + size);
}
}
}
代码经过以上调整后,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。
IoC优势
在传统的代码中,对象创建顺序是:Car-->Framework-->Bottom-->Tire
改进之后解耦的代码中对象创建顺序是:Tire-->Bottom-->Framework-->Car

我们发现了一个规律,通用程序的实现代码,类的创建顺序是反的,传统代码是Car控制并创建了
Framework,Framewoke控制并创建了Bottom,依次往下。而改进之后的控制权发生了反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了。
这样的话,即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是IoC的实现思想。
学到这里,我们大概就知道什么是控制发转了,那什么是控制反转容器呢?也就是IoC容器

这部分代码,就是IoC容器做的工作
从上面也可以看出来,IoC容器具备以下优点:
资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。
1.资源集中管理:IoC容器会帮我们管理一些资源(对象等),我们需要使用时,只需要从IoC容器中去取就可以了
2.我们在创建实例的时候不需要了解其中的细节,降低了使用资源双方的依赖程度,也就是耦合度
Spring就是一种IoC容器,帮助我们来做了这些资源管理
DI介绍
上面学习了IoC,那什么是DI呢?
DI(Dependency Injection,依赖注入)
DI是IoC的具体实现方式,指容器在运行时动态将依赖对象注入到目标对象中。通过构造函数、Setter方法或注解(如@Autowired)完成注入,避免硬编码依赖
程序运行时需要某个资源,此时容器就为其提供这个资源
从这点来看,依赖注入(DI)和控制反转(IoC)是从不同的角度描述同一件事情,依赖注入是从应用程序的角度来描述,就是指通过引入IoC容器,利用依赖关系注入的方式,实现对象之间的解耦
我们上述代码中,就是通过构造函数的方式,把依赖对象注入到需要使用的对象中的

IoC是一种思想,也是"目标",而思想只是一种指导原则,最终还是要有可行的落地方案,而DI就属于具体的实现。所以也可以说,DI是IoC的一种实现
关键点:
IoC是目标:解耦代码,提升可维护性
DI是手段:通过注入依赖实现IoC
二、为什么需要IoC&DI?
传统开发的痛点:
高耦合:修改一个类可能导致依赖链上的所有类需调整。
难测试:单元测试需模拟所有依赖对象
代码重复:多个类重复创建相同依赖对象
IoC&DI的优势:
解耦:对象不直接依赖具体实现,而是面向接口编程
易测试:可轻松注入模拟对象(Mock)进行测试
可维护性:修改依赖关系只需调整配置,无需改动代码
统一管理:容器集中管理对象生命周期和依赖
三、Spring IoC容器详解
IoC控制反转,就是将对象的控制权交给Spring的IoC容器,由IoC容器创建及管理对象,也就是Bean(对象)的存储
Bean的存储
我们要把某个对象交给IoC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的服务Web应用程序,提供了更丰富的注解
共有两类注解类型可以实现:
1.类注解:@Controller、@Service、@Repository、@Component、@Configuration
2.方法注解:@Bean
接下来我们分别来看上述注解
@Controller(控制器存储)
使用@Controller存储Bean的代码如下所示
java
@Controller //将对象存储到Spring中
public class UserController{
public void sayHi(){
System.out.println("hi,UserController...");
}
}
如何观察这个对象是否已经存储到Spring容器当中了呢?
接下来我们学习如何从Spring容器中获取对象
java
@SpringBootApplication
public class SpringIocDemoApplication{
public static void main(String[] args){
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplcation.class,args);
//从Spring上下文中获取对象
UserController userController=context.getBean(UserController.class);
//使用对象
userController.sayHi();
}
}
解析:
ApplicationContext翻译过来就是:Spring上下文
因为对象都交给Spring管理了,所以获取对象要从Spring中获取,那么就得先得到Spring的上下文
关于上下文的概念
在计算机领域,上下文这个概念,咱们最早是在学习线程时了解到过,比如我们应用进行线程切换的时候,切换前都会把线程的状态信息暂时存储起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间的时候,从上下文中拿到线程上次运行的信息
这个上下文,就是指当前的运行环境,也可以看作是一个容器,容器里存了很多内容,这些内容是当前运行的环境
观察运行结果,发现成功从Spring中获取到Controller对象,并执行Controller的sayHi方法

如果我们把UserController的@Controller删掉,再观察运行结果:


报错信息显示:找不到类型是: 'com.fei.springiocdemo.controller.UserController'的bean
获取bean对象的其他方式
java
UserController userController=context.getBean(UserController.class);
上述代码是根据类型(UserController.class)来查找对象,如果Spring容器中,同一个类型存在多个bean的话,怎么来获取呢?
先来说说为什么会出现多个同类型Bean?
Spring允许为同一个接口或类注册多个Bean实例,常见场景包括:
1.多个实现类:一个接口有多个实现类,且均被Spring管理
java
@Service
public class EmailNotificationService implements NotificationService { ... }
@Service
public class SmsNotificationService implements NotificationService { ... }
这里的EmailNotficationService和SmsNotificationService即是NotificationService接口的实现类
当使用getBean(NotificationService.class)来获取Bean对象时,由于NotificationService接口有两个实现类,Spring容器无法确定应该返回哪一个,因此会抛出异常
2.重复配置:通过@Bean方法或XML配置重复定义了同类型的Bean
java
@Configuration
public class AppConfig {
@Bean
public NotificationService emailService() {
return new EmailNotificationService();
}
@Bean
public NotificationService smsService() {
return new SmsNotificationService();
}
}
冲突原因:
两个方法都返回NotificationService类型,Spring会为它们生成两个不同的Bean定义(名称默认为方法名:emailService和smsService)
当调用context.getBean(NotificationService.class)时,Spring发现容器中有两个同类型Bean(emailService和smsServic),无法确定应该返回哪一个,因此抛出异常
ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取Bean对象功能是由其父类BeanFactory提供的
java
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
char FACTORY_BEAN_PREFIX_CHAR = '&';
//1.根据bean名称获取bean
Object getBean(String name) throws BeansException;
//2.根据bean名称和类型获取bean
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
//3.按bean名称和构造函数参数动态创建bean,只适用于具有原型(prototype)作用域的bean
Object getBean(String name, Object... args) throws BeansException;
//4.根据类型获取bean
<T> T getBean(Class<T> requiredType) throws BeansException;
//5.按bean类型和构造函数参数动态创建bean,只适用于具有原型(prototype)作用域的bean
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
常用的是上述1、2、4种,这三种方式获取到的bean是一样的
其中1、2种都涉及到根据名称来获取对象。
那么bean的名称是什么呢?
Spring bean是Spring框架在运行时管理的对象,Spring会给管理的对象起一个名字
后续,Spring就可以根据Bean的名称(BeanId)就可以获取到对应的对象
Bean命名约定
大家可以看下官方文档的说明:Bean Overview :: Spring Framework

程序开发人员不需要为Bean指定名称(BeanId),如果没有显式的提供名称(BeanId),Spring容器将为该bean生成唯一的名称
命名约定使用Java标准约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写
比如:
类名:UserController-->Bean的名称为:userController
也有一些特殊情况,当有多个字符并且第一个和第二个都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize(Spring在这里使用的)定义的规则相同
比如:
类名:UController-->Bean的名称为:UController
根据这个命名规则,我们来获取Bean
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
//从Spring上下文中获取对象
//1.根据Bean类型获取
UserController userController1=context.getBean(UserController.class);
//2.根据Bean名称获取
UserController userController2=(UserController)context.getBean("userController");
//3.根据Bean类型+名称获取
UserController userController3=context.getBean("userController",UserController.class);
//使用对象
System.out.println(userController1);
System.out.println(userController2);
System.out.println(userController3);
}
}
运行结果:

地址一样,说明对象是一个
获取Bean对象,是父类BeanFactory提供的功能
常见面试题
ApplicationContext VS BeanFactory
继承关系和功能来说:Spring容器有两个顶级的接口:BeanFactory和ApplicationContext。其中BeanFactoryy提供了基础的访问容器的能力,而ApplicationContext属于BeanFactory的子类,它除了继承了BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面的支持
从性能方面来说:ApplicationContext是一次性加载并初始化所有的Bean对象,而BeanFactory是需要哪个采取加载哪个,因此更加轻量(空间换时间)
@Service(服务存储)
使用@Service存储bean的代码如下所示:
java
@Service
public class UserService {
public void sayHi(String name){
System.out.println("hi,"+name);
}
}
读取bean的代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
UserService userService=context.getBean(UserService.class);
userService.sayHi("world");
}
}
观察运行结果,发现成功从Spring中获取到UserService对象,并执行UserService的sayHi方法

同样的,把注解@Service删掉,再观察运行结果,如下图:

@Repository(仓库存储)
使用@Repository存储bean的代码如下
java
@Repository
public class UserRepository {
public void sayHi(){
System.out.println("Hi,Repository");
}
}
读取bean的代码
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
UserRepository userRepository=context.getBean(UserRepository.class);
userRepository.sayHi();
}
}
观察运行结果,发现成功从Spring中获取到UserRepository对象,并执行UserRepository的sayHi方法

同样的,把注解@Repository删掉,运行结果类似上面@Service
@Component(组件存储)
使用@Component存储bean的代码如下:
java
@Component
public class UserComponent {
public void sayHi(){
System.out.println("Hi,Component");
}
}
读取bean的代码
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
UserComponent userComponent=context.getBean(UserComponent.class);
userComponent.sayHi();
}
}
运行结果同上述@Repository
@Configuration(配置存储)
和上述四个注解类似,这里就不再复述了
为什么要这么多的类注解?
这个其实就涉及到了应用分层。
简单来说,如果按照某一合理的项目结构去将代码分类,项目代码会简洁美观,便于查找
实例:

让程序员看到类注解之后,就能直接了解当前类的用途
@Controller:控制层,接收请求,对请求进行处理,并返回响应
@Service:业务逻辑层,处理具体的业务逻辑
@Repository:数据访问层,也成为持久层。负责数据访问操作
@Configuration:配置层。处理项目中的一些配置信息
类注解之间的关系
这里我们先查看@Controller/@Service/@Repository/@Configuration等注解的源码:


其实这些注解里面都有一个@Component,说明它们本身就是属于@Component的"子类"。
@Component是一个元注解,也就是说可以注解其他类注解,如@Controller,@Service,@Repository等。这些注解被称为@Component的衍生注解
@Controller,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果我们要在业务逻辑层使用@Component或@Service,显然@Service是更好的选择
方法注解@Bean
上面我们讲解了五大类注解,知道类注解是添加到某个类上的,但是存在两个问题:
1.使用外部包里的类,没办法添加类注解
2.一个类,需要多个对象,比如多个数据源
这种场景,我们就需要使用方法注解@Bean
我们先来看看方法注解如何使用:
java
public class BeanConfig {
@Bean
public User user(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
}
然而,当我们写完上述代码,尝试获取bean对象中的user时却发现,根本获取不到:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
User user=context.getBean(User.class);
System.out.println(user);
}
}
程序执行结果如下:

显示没有User类型的bean对象
这是为什么呢?
结论是:方法注解要配合类注解使用
方法注解要配合类注解使用
在Spring框架的设计中,方法注解@Bean要配合类注解才能将对象正常的存储到Spring容器中,代码如下所示:
java
@Component
public class BeanConfig {
@Bean
public User user(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
}
再次执行获取User对象的代码,结果如下:

定义多个对象
对于同一个类,如何定义多个对象呢?
比如多数据源的场景,类是同一个,但是配置不同,指向不同的数据源
我们看下@Bean的使用
java
@Component
public class BeanConfig {
@Bean
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
@Bean
public User user2(){
User user=new User();
user.setName("lisi");
user.setAge(19);
return user;
}
}
定义了多个对象的话,我们根据类型获取对象,获取的是哪个对象呢?
运行结果:

报错信息显示:期望只有一个匹配,结果发现了两个,user1,user2
从报错信息中,可以看出来,@Bean注解的对象名称就是它的方法名
接下来我们根据名称来获取bean对象
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
User user1=(User) context.getBean("user1");
User user2=(User) context.getBean("user2");
System.out.println(user1);
System.out.println(user2);
}
}
运行结果:

可以看到,@Bean可以针对同一个类,定义多个对象
重命名Bean
可以通过设置name属性给Bean对象进行重命名操作,代码如下:
java
@Bean(name = {"u1","user1"})
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
此时我们使用u1就可以获取到User1对象了,代码如下:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
User u1=(User) context.getBean("u1");
System.out.println(u1);
}
}
运行结果如下:

注意:这里name={}可以省略,如下所示:
java
@Bean({"u1","user1"})
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
只有一个名称时,{}也可以省略,如:
java
@Bean("u1")
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
扫描路径
使用前面学习的四个注解声明的Bean,一定会生效吗?
答案:不一定(原因:Bean想要生效,还需要被Spring扫描)
下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:

再运行代码:
java
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
User u1=(User) context.getBean("u1");
System.out.println(u1);
}
}
运行结果:

解释:没有bean的名称为u1
为什么没有找到bean对象呢?
使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些注解
也就是通过@ComponentScan来配置扫描路径
java
@ComponentScan({"com.fei.springiocdemo"})
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
User u1=(User) context.getBean("u1");
System.out.println(u1);
}
}
{}里可以配置多个包路径
这种做法仅做了解,不推荐使用
再次运行:

那为什么前面在没有配置@ComponentScan注解也可以正确运行呢?
原先@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中了
默认的扫描范围是SpringBoot启动类所在包及其子包
在配置类上添加@ComponentScan注解,该注解默认会扫描该类所在的包下所有配置类

推荐做法:
把启动类放在我们希望扫描的包的路径下,这样我们定义的Bean就都可以被扫描到
即:原先的包结构

Bean的生命周期
实例化:容器通过反射创建Bean实例
属性填充:注入依赖对象(DI阶段)
初始化:调用@PostConstruct注解方法或自定义init-method
使用:Bean处于就绪状态,供应用程序调用
销毁:容器关闭时调用@PreDestroy或destory-method
Bean的作用域
Singleton(默认):整个容器中仅一个实例
Prototype:每次请求创建新实例
Request(Web环境):每个HTTP请求一个实例
Session(Web环境):每个用户会话一个实例
四、DI详解
上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入DI的细节
依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象
举例:
java
@Controller //将对象存储到Spring中
public class UserController{
//注入UserService
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
}
}
在上面程序案例中,我们使用@Autowired这个注解,完成了依赖注入的操作
简单来说,就是把对象取出来放到某个类的属性中
关于依赖注入,Spring也给我们提供了三种方式:
1.属性注入(Field Injection)
2.构造方法注入(Constructor Injection)
3.Setter注入(Setter Injection)
java
@Controller //将对象存储到Spring中
public class UserController{
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
}
}
接下来,我们分别来看
下面我们按照实际开发中的模式,将Service类注入到Controller类中
属性注入
属性注入是使用@Autowired实现的,将Service类注入到Controller类中。
Service类的实现代码如下:
java
@Service
public class UserService {
public void sayHi(){
System.out.println("hi,UserService");
}
}
Controller类的实现代码如下:
java
@Controller //将对象存储到Spring中
public class UserController{
//注入方法1:属性注入
@Autowired
private UserService userService;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();//注意这里调用UserService中的sayHi方法
}
}
获取Controller中的sayHi方法:
java
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
UserController userController=(UserController) context.getBean("userController");
userController.sayHi();
}
运行结果如下:

这里,我们去掉UserService的@Autowired注解,再运行程序观察结果:

构造方法注入
构造方法注入是在类的构造方法中实现注入,代码如下所示:
java
@Controller //将对象存储到Spring中
public class UserController2{
//注入方法2:构造方法
private UserService userService;
public UserController2(UserService userService){
this.userService=userService;
}
public void sayHi(){
System.out.println("hi,UserController2...");
userService.sayHi();
}
}
运行结果:

注意:如果类只有一个构造方法,那么@Autowired注解可以省略;如果类中有多个构造方法,那么需要添加上@Autowired来明确指定到底使用哪个构造方法
Setter注入
Setter注入和属性的Setter方法实现类似,只不过在设置set方法的时候需要加上@Autowired注解,代码如下所示:
java
@Controller
public class UserController3 {
//注入方法3:setter方法注入
private UserService userService;
@Autowired
public void setUserService(UserService userService){
this.userService=userService;
}
public void sayHi(){
System.out.println("hi,UserController3...");
userService.sayHi();
}
}
这里如果移除了@Autowired注解后,Spring无法自动通过setter方法注入UserService,导致依赖缺失而报错。
三种注入优缺点分析
属性注入
优点:简洁,使用方便
缺点:
只能用于IoC容器,非IoC容器不可用,并且只有在使用的时候才会出现空指针异常
不能注入一个final修饰的属性
构造函数注入(Spring 4.X推荐)
优点:
可以注入final修饰的属性
注入的对象不会被修改
依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法
通用性好,构造方法是JDK支持的,所以无论更换任何框架,它都是适用的
缺点:
注入多个对象时,代码会比较繁琐
Setter注入(Spring 3.X推荐)
优点:
方便在类实例之后,重新对该对象进行配置或者注入
缺点::
不能注入一个final修饰的属性
注入对象可能会被改变,因为setter方法可能会被多次调用,就有被修改的风险
@Autowired存在的问题
当同一类型存在多个Bean时,使用@Autowired会存在问题
java
@Component
public class BeanConfig {
@Bean(name = {"u1","user1"})
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
@Bean
public User user2(){
User user=new User();
user.setName("lisi");
user.setAge(19);
return user;
}
}
java
@Controller //将对象存储到Spring中
public class UserController{
//注入方法1:属性注入
@Autowired
private UserService userService;
@Autowired
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
System.out.println(user);
}
}
java
@ComponentScan({"com.fei.springiocdemo"})
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//获取Spring上下文
ApplicationContext context=SpringApplication.run(SpringIocDemoApplication.class, args);
UserController u1=(UserController) context.getBean("userController");
u1.sayHi();
}
}
运行结果:

报错原因是:非唯一的Bean对象
主要是因为在BeanConfig中有两个Bean对象,并且它们的返回类型都为User,而在UserController中需要注入User对象,此时Spring就不知道到底该注入BeanConfig的哪个对象了
这里我们只需明确将BeanConfig中的哪个Bean对象注入给UserController即可
如何解决上述问题呢?Spring提供了以下几种解决方案:
1.@Primary
2.@Qualifier
3.@Resource
使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现
java
@Component
public class BeanConfig {
@Primary //指定该bean为默认bean的实现
@Bean(name = {"u1","user1"})
public User user1(){
User user=new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
@Bean
public User user2(){
User user=new User();
user.setName("lisi");
user.setAge(19);
return user;
}
}
使用@Qualifier注解:指定当前要注入的bean对象。在@Qualifier的value属性中,指定注入的bean的名称
注意:@Qualifier注解不能单独使用,必须配合@Autowired使用
java
@Controller //将对象存储到Spring中
public class UserController{
//注入方法1:属性注入
@Autowired
private UserService userService;
@Qualifier("user2") //指定bean的名称
@Autowired
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
System.out.println(user);
}
}
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称
java
@Controller //将对象存储到Spring中
public class UserController{
//注入方法1:属性注入
@Autowired
private UserService userService;
@Resource(name="user2")
private User user;
public void sayHi(){
System.out.println("hi,UserController...");
userService.sayHi();
System.out.println(user);
}
}
注意:与@Qualifier不同的时,在使用@Resource注解时,不能与@Autowired同时使用
@Autowired与@Resource的区别
@Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解
@Autowired默认是按照类型注入,而@Resource是按照名称注入。相比于@Autowired来说,@Resource支持更多的参数设置,例如name设置,根据名称获取bean
Autowired注入顺序

五、实战案例:从零搭建Spring项目
1.添加依赖(Maven)
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
2.定义接口和实现类
java
public interface UserRepository {
void save(String name);
}
@Repository
public class JdbcUserRepository implements UserRepository {
@Override
public void save(String name) {
System.out.println("Saving user: " + name);
}
}
3.使用构造函数注入依赖
java
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public void addUser(String name) {
repository.save(name);
}
}
4.运行测试
java
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserService userService = context.getBean(UserService.class);
userService.addUser("Alice");
}
}
输出结果:
Saving user: Alice