java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring IOC容器如何管理Bean

Spring IOC容器如何管理Bean?从入门到源码解析

作者:桃也在Coding

还在为Spring的IoC、DI和Bean管理头疼吗?本文用通俗易懂的例子,带你搞懂Spring、SpringMVC、SpringBoot的关系,掌握@Autowired、@Component等核心注解,解决多Bean注入冲突,轻松驾驭Spring框架

一、Spring、Spring MVC、Spring Boot三者关系

  1. Spring:底层框架,本质是IoC容器,提供IoC、AOP、事务、资源管理等基础能力;
  2. Spring MVC:Spring的子模块,专门做Web接口开发,提供@RestController@RequestMapping等Web注解;
  3. Spring Boot:Spring的脚手架,简化Spring繁琐XML配置,自动装配、内置服务器,快速搭建项目。

分层协作流程:

前端请求 → Spring MVC Controller → Spring Service业务层 → Spring Repository数据层 → 数据库

其中对象管理、依赖装配全部由Spring IoC容器实现。

二、IoC控制反转

2.1 定义

IoC全称Inversion of Control控制反转是一种设计思想

2.2 传统开发高耦合

需求:造一辆车,依赖关系Car → Framework车身 → Bottom底盘 → Tire轮胎

在每个类内部new下级依赖

static class Car {
    private Framework framework;
    public Car() {
        // 内部自己创建依赖,强耦合
        framework = new Framework();
    }
}
static class Framework {
    private Bottom bottom;
    public Framework() {
        bottom = new Bottom();
    }
}
static class Bottom {
    private Tire tire;
    public Bottom() {
        tire = new Tire(17);
    }
}

缺陷:底层类修改,例如轮胎增加尺寸参数,整条调用链Tire→Bottom→Framework→Car全部要修改,牵一发而动全身,耦合度极高。

2.3 IoC解耦

不再在类内部new依赖,而是外部传入依赖

public class Main {  
    public static void main(String[] args) {  
        Tire tire = new Tire(21);  
        Bottom bottom = new Bottom(tire);  
        Framework framework = new Framework(bottom);  
        Car car = new Car(framework);  
        car.run();  
    }  
}
public 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");  
    }  
}
public class Framework {  
    Bottom bottom ;  
    public Framework(Bottom bottom) {  
        this.bottom = bottom;  
        System.out.println("framework init...");  
    }  
}
public class Bottom {  
    Tire tire;  
    public Bottom( Tire tire) {  
        this.tire = tire;  
        System.out.println("bottom init...");  
    }  
}
public class Tire {  
    int size;  
    public Tire(int size){  
        this.size = size;  
        System.out.println("tire init,size: "+size);  
    }  
}

变化

  1. 对象创建顺序反转:传统Car先创建 → IoC模式Tire先创建
  2. 解耦:底层Tire修改,上层Car、Framework代码完全不用改动;
  3. 统一管理:所有对象由第三方容器统一创建、维护。

2.4 IoC容器两大核心能力

Spring就是标准IoC容器,只做两件事:

  1. 存Bean:把类交给Spring管理,Spring创建对象存入容器;
  2. 取Bean:需要使用对象时,从容器获取,自动装配依赖。

Bean:Spring容器中管理的所有对象,统一称为Bean。

在上面的例子中,Car,Framework,Bottom,Tire对象管理就可以交给Spring来完成

三、DI依赖注入

DI全称Dependency Injection依赖注入是实现IoC的具体技术手段

IoC容器运行时,自动将目标类需要的依赖对象,动态注入到类中。

IoC与DI关系区分:

四、Bean如何存入Spring容器

4.1 @Component 与 @Autowired

@Component : 把类交给 Spring 管理

@Component 是一个类级别的注解,告诉 Spring:“这个类由你来管理,创建它的实例(Bean)并放进 IoC 容器中。”

例如: BookServiceBookDao 加上 @Component

@Component
public class BookService {
@Component
@Data
public class BookDao {

加上 @Component 后,Spring 在启动时就会自动创建 BookServiceBookDao单例对象放在 IoC 容器中,不需要手动 new

注意@RestController 本身也包含了 @Component 的功能,所以加了 @RestController 的类也自动被 Spring 管理。

@Autowired :依赖注入

@Autowired 是一个字段/构造方法/Setter 级别的注解,告诉 Spring:“我需要一个该类型的对象,你从 IoC 容器中帮我注入进来。”

例如:在需要实例的代码中@Autowired ,就不需要手动创建实例,直接调用方法

@RestController  
@RequestMapping("/book")  
public class BookController {  
    @Autowired  
    private BookService bookService ;  
    @RequestMapping("/getList")  
    public List<BookInfo> getList() {  
        return bookService.getList();  
    }  
}
@Component  
public class BookService {  
    @Autowired  
    BookDao bookDao ;  
    public List<BookInfo> getList() {  
        bookDao.mockData();  
        return bookDao.getBookList();  
    }  
}
  1. BookController 需要调用 BookService 的方法,声明了 private BookService bookService 字段
  2. 加上 @Autowired 后,Spring 在创建 BookController 实例时,会去 IoC 容器中找到 BookService 类型的 Bean 并自动赋值给这个字段
  3. 同理,Spring 创建 BookService 时,也会自动把 BookDao 注入进去

这样就不需要写:

private BookService bookService = new BookService();  // ❌ 不需要手动创建

整体依赖链

BookController (@RestController)
    │  @Autowired
    ▼
BookService    (@Component)
    │  @Autowired
    ▼
BookDao        (@Component)

它们之间的依赖关系完全由 Spring 通过 @Autowired 自动装配,开发者只需要声明"我需要什么",不需要关心"怎么创建"。

注解角色解决的问题
@Component注册告知 Spring 帮我创建和管理这个类的实例
@Autowired注入告知 Spring 把我需要的依赖从容器中自动拿给我

两者配合,就实现了控制反转(IoC):对象的创建和组装不再由自己控制,而是交给 Spring 容器统一管理。

错误日志

  1. 假设BookDao没有@Component注解,直接使用@Autowired实例化这个类:
Field bookDao in com.daisy.springbookdemo.service.BookService required a bean of type 'com.daisy.springbookdemo.dao.BookDao' that could not be found.

表示需要一个com.daisy.springbookdemo.dao.BookDao这样类型的对象,但是在IoC容器中找不到,因为缺少了@Component

  1. 假设BookDao使用了@Component,BookService中没有使用@Autowired注解,直接实例化这个类
java.lang.NullPointerException: Cannot invoke "com.daisy.springbookdemo.dao.BookDao.mockData()" because "this.bookDao" is null

抛出空指针异常,因为@Autowired在Spring中没有取出BookDao,也就无法进行赋值,因此这个字段就是null

4.2 五大类注解

1. 注解层级关系

@Controller@Service@Repository@Configuration底层都标注了@Component,是@Component衍生注解:

注解使用分层作用
@Controller控制层接收前端请求,Web接口
@Service业务逻辑层处理业务逻辑
@Repository数据层操作数据库DAO
@Configuration配置层项目配置类
@Component通用组件不属于以上分层的工具类

这五个注解的共同作用都是把类交给Spring管理。

区别:

2. Bean命名规则

  1. 默认规则:类名首字母小写,如 BookService → bean名称bookService
  2. 特殊规则:类名前两个字母大写,名称不变,如UControllerUController
  3. 自定义名称:在注解中加上字符串,@Service("book")手动指定Bean名称。

3. 容器获取Bean三种方式

通过ApplicationContext(Spring上下文,容器入口)获取:

// 1. 根据类型获取
BookService service = context.getBean(BookService.class);
// 2. 根据Bean名称获取
BookService service = (BookService) context.getBean("bookService");
// 3. 名称+类型双重匹配
BookService service = context.getBean("bookService", BookService.class);

4.3 @Bean方法注解

@Bean方法注解,写在配置类的方法上,将当前方法返回的对象注册为 Spring IoC 容器中的 Bean,交给 Spring 统一管理。

1. 适用场景

场景1:第三方外部类,无法加类注解

比如 DataSource、Redis客户端、Mybatis工具类,源码不在自己项目里,不能给类加 @Service/@Component,只能用 @Bean 创建对象交给容器。

@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://xxx");
        return ds;
    }
}
场景2:同一个类需要多个不同配置的 Bean

比如多数据源、多个 Redis 实例,一个类要创建多个对象,类注解只能生成一个Bean,@Bean 可以定义多个方法生成多个实例:

@Component  
public class StudentComponent {  
    @Bean  
    public Student s1 () {  
        return new Student("zhangsan", 18);  
    }  
    @Bean  
    public Student s2 () {  
        return new Student("lisi", 18);  
    }  
}

2. 使用规范

@Bean 不能单独生效,必须配合@Component或者其他类注解配置类使用,否则Spring扫描不到这个方法,无法注册Bean。

推荐使用 @Configuration,专门用于配置类,Spring会做CGLIB代理优化。

3. 命名规则

  1. 默认名称:不加name属性时,Bean名称 = 方法名
// bean名称:user
@Bean
public User user(){...}
  1. 自定义名称:通过 name 属性指定,支持多个别名
// bean别名:u1、userOne
@Bean({"u1","userOne"})//使用数组接收
public User user(){...}

// 简写
@Bean("u1")
public User user(){...}

4.4 Bean扫描范围与@ComponentScan

默认扫描规则

默认扫描启动类所在包 + 所有子包

举例子:

如果Bean类放在启动类同级包/上级包,超出默认扫描范围,Spring找不到,直接报 No qualifying bean

@ComponentScan

@ComponentScan 是Spring的包扫描注解,自动扫描指定包下所有标注了 @Component@Controller@Service@Repository@Configuration 的类,将这些类注册为Bean存入IoC容器。告诉Spring去哪里找带类注解的Bean

与 @SpringBootApplication 的关系

@SpringBootApplication 是复合注解,源码内部已经包含 @ComponentScan

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan // 内置了包扫描
public @interface SpringBootApplication {}

这里@ComponentScan的值就是启动类所在的目录

手动自定义扫描路径

当Bean不在启动类包下,手动指定扫描包:

//手动指定包
@ComponentScan("com.example.demo")

// 扫描多个包
@ComponentScan({"com.example.demo", "com.example.other"})
@SpringBootApplication
public class DemoApplication {}

注意:

  1. 数组可以填多个包路径,逗号分隔;
  2. 这只是一种解决扫描路径的方案,但是很繁琐,最优的方案是 把启动类放在项目根包,依靠默认扫描。
BeanFactory 与 ApplicationContext

五、依赖注入方式

5.1 属性注入

Field注入,@Autowired写在字段上

@RestController
public class BookController {
    // 属性注入
    @Autowired
    private BookService bookService;
}

优点:代码极简,开发快;

缺点:

  1. 仅能在IoC容器中使用,脱离容器直接new会空指针;
  2. 无法注入final常量;
  3. 依赖隐藏,无法直观看到类依赖哪些对象。

5.2 构造方法注入

@RestController
public class BookController {
    private final BookService bookService;
    // 类只有一个构造方法,@Autowired可省略
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
}

优点:

  1. 支持final修饰,依赖不可变;
  2. 实例化时依赖必须全部注入,避免空指针;
  3. JDK支持,不依赖Spring容器,单元测试可直接new传入Mock对象;

缺点:依赖较多时,构造方法参数冗长。

5.3 Setter方法注入

@Autowired写在set方法

@RestController
public class BookController {
    private BookService bookService;
    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
}

六、多Bean注入冲突与解决方案

多Bean注入冲突

当同一个类创建了多个Bean,@Autowired按类型查找会报错。

@Component  
public class StudentComponent {  
//创建两个Bean
    @Bean  
    public Student s1 () {  
        return new Student("zhangsan", 18);  
    }  
  
    @Bean  
    public Student s2 () {  
        return new Student("lisi", 19);  
    }  
}

public class UserService {  
//Autowired按照Student类型查找
    @Autowired  
    private Student stu;  
    public void print() {  
        System.out.println("do Service!");  
        System.out.println(stu);  
    }  
}

报错信息:

Field student in com.daisy.springiocdemo.service.UserService required a single bean, but 2 were found:
	- s1: defined by method 's1' in class path resource [com/daisy/springiocdemo/component/StudentComponent.class]
	- s2: defined by method 's2' in class path resource [com/daisy/springiocdemo/component/StudentComponent.class]

This may be due to missing parameter name information

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

UserService中的student属性需要一个Bean,但是找到了两个对象,不知道该使用哪个对象,就会报错

Action中提供了两种解决方案。

解决方案

1. @Primary:设置默认Bean

在其中一个Bean上添加注解,Spring默认优先注入该对象:

@Configuration
public class StudentComponent {  
    @Primary  
    @Bean    public Student s1 () {  
        return new Student("zhangsan", 18);  
    }  
  
    @Bean  
    public Student s2 () {  
        return new Student("lisi", 19);  
    }  
}

2. @Qualifier + @Autowired:指定Bean名称

@Service
public class UserService {  
//指定Bean名称为s1的对象
    @Qualifier("s1")  
    @Autowired  
    private Student student;  
    public void print() {  
        System.out.println("do Service!");  
        System.out.println(student);  
    }  
}

@Qualifier 也可以标注在 @Bean 方法参数上,精确指定注入哪个 Bean

举个例子:

@Component  
public class StudentComponent {  
    @Bean  
    public String name1() {  
        return "zhangsan";  
    }  
    @Bean  
    public String name2() {  
        return "lisi";  
    }  
    @Bean  
    public Student s1 (@Qualifier("name1") String n) {  
        return new Student(n, 18);  
    }  
  
    @Bean  
    public Student s2 () {  
        return new Student("lisi", 19);  
    }  
}

其中

@Bean
public Student s1 (@Qualifier("name1") String n) {
    return new Student(n, 18);
}

s1() 方法需要注入一个 String 类型的参数 n。但容器里有两个 String 类型的 bean(name1"zhangsan"name2"lisi")。

NoUniqueBeanDefinitionException: 找到 2 个 String 类型的 Bean,不知道用哪个

3. @Resource:按名称注入

@Resource是Jakarta提供的注解,可以直接通过name属性指定Bean名称:

@Service
public class UserService {    
    @Resource(name = "s1")  
    private Student student;  
    public void print() {  
        System.out.println("do Service!");  
        System.out.println(student);  
    }  
}

对比第一中方式,后两种方式更灵活,不同场景下可以拿到不同的对象

@Autowired 和 @Resource 区别

对比项@Autowired@Resource
来源Spring框架专属注解JSR-250 Java官方标准注解
默认匹配规则优先按类型优先按名称,找不到再匹配类型
指定Bean名称必须配合@Qualifier自带name属性直接指定
required属性支持,可设required=false允许为空无该属性,依赖必须存在

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文