Spring IoC & DI 终极指南:从造车模型到企业级开发实战

发布于:2025-08-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

关注我,学习更多企业开发和面试内容~

在这里插入图片描述

1. IoC & DI的概念

Spring是一个包含众多工具的IoC容器,容器能装东西。比如List/Map是数据存储容器,Tomcat是Web容器。

  • IoC:控制反转。
    • Spring也是一个容器,装的是对象。控制反转,又称为控制权反转,也就是获取依赖对象的过程被反转了。之前需要通过自己new创建对象,现在是直接把创建对象的任务交给容器,程序中只需要依赖注入(DI)就可以了。这个容器称为IoC容器,所以有时Spring也称为Spring容器。
    • 控制反装是一种思想,生活中比如,自动驾驶(比如驾驶员不需要驾驶,让自动化系统来控制)。
  • IoC容器:对类添加特定的注解,就是把这个对象交给Spring管理,Spring框架启动时会加载该类,把对象交给Spring管理,就是IoC思想。

【面试题】Spring两大核心思想:IoC和AOP。


2. IoC

以造车为例。

2.1 传统程序开发

轮胎(Tire)依赖底盘 (Bottom)依赖车身(Framework)依赖汽车(Car)。我们可以尝试不在每个类中自己创建new下级对象,因为如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。
在这里插入图片描述
只是给car类的构造方法加了一个参数,所有调用者都需要修改,这就是耦合太高了。

public class Tire {
    private Integer size;

    public Tire(Integer size) {
        this.size = size;
        System.out.println("Tire init...");
    }
}

public class Bottom {
    private Tire tire;
	//修改代码,添加size参数    //调用方第1次修改代码
    public Bottom(int size) {
        this.tire = new Tire(size);
        System.out.println("framework init...");
    }
}

public class Framework {
    private Bottom bottom;

//    public Framework() {
//        this.bottom = new Bottom();
//        System.out.println("framework init...");
//    }

    public Framework(int size) {
        this.bottom = new Bottom(size);  //调用方第1次修改代码
        System.out.println("framework init...");
    }
}

public class Car {
    private Framework framwork;

//    public Car() {
//        this.framwork = new Framework();
//        System.out.println("car init...");
//    }

    public Car(int size) {
        this.framwork = new Framework(size); //调用方第1次修改代码
        System.out.println("car init...");
    }
    public void run() {
        System.out.println("car run...");
    }
}

public class Main {
    public static void main(String[] args) {
//        Car car = new Car();
        Car car = new Car(4);
        car.run();
    }
}

综上所述,增加了1个参数,却需要修改3次代码。


2.2 解耦合形式开发(IoC)

此时,我们只需要将原来由自己创建下级类的对象,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。

public class Tire {
    private Integer size;
    private String color;

    public Tire(Integer size,String color) {
        this.size = size;
        this.color = color;
        System.out.println("Tire init...");
    }
}

public class Bottom {
    private Tire tire;

    public Bottom(Tire tire) {
        this.tire = tire;

    }
}

public class Framework {
    private Bottom bottom;

    public Framework(Bottom bottom) {
        this.bottom = bottom;
    }
}

public class Car {
    Framework framework;

    public Car(Framework framework) {
        this.framework = framework;
    }

    public void run() {
        System.out.println("car run...");
    }
}



public class Main {
    public static void main(String[] args) {
    	//Spring做的事情如下,创建对象
        Tire tire = new Tire(4,"red");
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
    }
}

在传统的代码中对象创建顺序是:Car->Framework->Bottom->Tire。
改进之后解耦的代码的对象创建顺序是:Tire->Bottom->Framework->Car。


2.3 DI形式开发(IoC)

IoC是思想,DI是实现方式。实现的IoC的方式很多,上面传统的解耦合代码是一种,DI也是一种解耦合。通过依赖注入(DI),将依赖关系的控制权交给外部容器(如 Spring),实现解耦。

  • 依赖:当前类需要的东西,比如jar包。

  • 注入:将依赖作为当前类的属性。

  • 将依赖注入当前类,使当前类和依赖注入的对象打配合,实现IoC,如下图。
    在这里插入图片描述

    • @Autowired:告诉Spring,从容器中取出这个对象,赋值给当前对象的属性。

* Spring IoC和DI

Spring是一个IoC容器,能存和取对象。
Spring容器管理的主要是对象,这些对象,我们称之为"Bean"。我们把这些对象交由Spring管理,由Spring来负责对象的创建和销毁。我们程序只需要告诉Spring,哪些需要存,以及如何从Spring中取出对象。

IoC容器已经存在,它有IoC思想。所以只要学如何把对象交给Spring容器管理(如@Service等),如何把对象从容器取出来(如@Autowired等等),也就是DI。

3. Bean的存储

  1. 类注解: @Controller、@Service、@Repository、@Component、@Configuration。
  2. 方法注解: @Bean。

3.1 @Controller(存储控制器)

想被外界访问到,必须用@Controller,不能用其他注解替代,原因是实现原理不一样,注解声明实现看似一样,实则Spring源码完全不一样。

@Controller
public class UserController {

}

先不做验证,只在Service层做验证。

如果不写注解,会报NoSuchBeanDefinitionException,如下。

//不加@Controller
public class BookController {
@Autowired
    private BookService bookService;
}

3.2 @Service(存储服务)

@Service
public class UserService {
    public void doService(){
        System.out.println("do Service...");
    }
}

在这里插入图片描述
将类存后,取出Bean来验证,因为还没讲DI,先不用@Autowired,而是用其他方式获取。

按类型获取(推荐):

  • context.getBean(UserService.class)
  • 明确知道 Bean的类型时使用。
  • 类型必须唯一,否则抛NoUniqueBeanDefinitionException。

按名称获取(需转型):

  • (UserService) context.getBean(“userService”)
  • 需要动态获取或处理同名 Bean 时使用,需手动转型。
  • 名称错误抛 NoSuchBeanDefinitionException。

名称+类型获取(推荐):

  • context.getBean(“userService”, UserService.class)
  • 自动类型匹配。
  • 需要确保名称和类型双重匹配时使用。

名称默认为小驼峰形式,特殊情况:若类名前两位都为大写,bean的名称为类名(如EXam)。


3.3 @Repository(存储仓库)

@Repository
public class UserRepository {
    public void doRepository(){
        System.out.println("do Repository...");
    }
}

3.4 @Component(存储组件)

@Component
public class UserComponent {

    public void doComponent(){
        System.out.println("do Component...");
    }

}

3.5 @Configuration(存储配置)

@Configuration
public class UserConfig {

    public void doConfig(){
        System.out.println("do Config...");
    }
}

3.6 关于这些类注解

【为什么要这么多类注解】

这个也是和咱们前面讲的应用分层是呼应的。让程序员看到类注解之后,就能直接了解当前类的用途。

  • @Controller:控制层,接收请求,对请求进行处理,并进行响应。
  • @Service:业务逻辑层,处理具体的业务逻辑。
  • @Repository:数据访问层,也称为持久层。负责数据访问操作。
  • @Configuration:配置层。处理项目中的一些配置信息。
  • @Component:不属于标准分层(Controller/Service/Repository)的辅助类,所有类注解最终都是 @Component 的“特化版本”。

【类注解之间的关系】

在这里插入图片描述
源码里这些只是注解的声明,注解真正功能是在Spring源码里实现的,不在注解声明里。

五大注解只能加在类上,并且只能加在自己的代码上,不能加在。

五大注解取多少次,都是同一个地址。也就是说都是同一个对象。


3.7 方法注解@Bean

【使用@Bean的场景】

  • 五大注解只能加在类上,并且只能加在自己的代码上,如果我引入了一个第三方Jar包,也希望交给Spring管理,是没有办法加五大注解的。
  • 五大注解只能定义一个对象,而@Bean可以对同一个类定义多个对象。对于一个类,需要定义多个对象时,比如数据库操作,定义多个数据源。

@Bean是方法注解,必须搭配五大注解(类注解)来使用。原因:Spring最开始扫描注解时,只扫描类,不扫描方法,若类上有注解,才会扫描类内的方法。

@Configuration
public class BeanConfig {

    @Bean
    //存在Spring容器里的对象名默认是 name
    public String name(){
        return "塞尔达";
    }

    @Bean
    //存在Spring容器里的对象名默认是 name333
    public String name333(){
        return "达";
    }
}

【多个@Bean】

  • 当一个类型存在多个Bean时,就不能用类型获取对象了,会报NoUniqueBeanDefinitionException。对象名默认是@Bean修饰的方法名,这时可以用名称获取,或名称加类型,唯独不能用类型获取对象。
  • 因为两个对象是同类型,Spring不知道取哪个使用名称,不会出错。

根据方法加类型获取,不需要强转。
根据类型获取,不需要强转。
根据方法获取,不需要强转。

3.8 重命名Bean

可以通过设置name属性给Bean对象进行重命名操作。

//重命名的对象名可以有多个
@Bean(name = {"u1","user1"})
public User user1(){
	 User user = new User();
	 user.setName("zhangsan");
	 user.setAge(18);
	 return user;
}
//简化版本
@Bean({"u1","user1"})
public User user2(){
	 User user = new User();
	 user.setName("zhangsan");
	 user.setAge(18);
	 return user;
}
//只有一个名称时,{}可以省略
@Bean("u1")
public User user3(){
	 User user = new User();
	 user.setName("zhangsan");
	 user.setAge(18);
	 return user;
}

3.9 更改扫描路径@ComponentScan

启动类(Application)的扫描范围:默认路径是启动类所在的目录极其子目录。
将启动类放入子目录,再次启动,会报错。

src/main/java
└── com
    └── example
        └── myapp  ← 这是项目的根包(root package)
            ├── Application.java  ← 启动类应该放在这
            ├── controller
            │   └── UserController.java
            ├── service
            │   └── UserService.java 
            └── repository
                └── UserRepository.java
src/main/java
└── com
    └── example
        └── myapp
            ├── config
            │   └── Application.java  ← 错误:启动类在子包
            ├── controller
            ├── service
            └── repository

【扫描范围对比】

  • 当启动类在 com.example.myapp 时:

    • 自动扫描:com.example.myapp 及其所有子包(controller/service/repository)
  • 当启动类在 com.example.myapp.config 时:

    • 自动扫描:com.example.myapp.config 及其子包
    • 不会扫描:com.example.myapp.controller 等兄弟包

用注解指定路径:@ComponentScan(“com.example”)
在这里插入图片描述
属于Spring的注解才会被Spring管理,@Data不是Spring的注解。
先扫描@ComponentScan(“com.example”)确定路径。


4. DI

4.1 属性注入

不写@Autowired会报异常。

@Controller
public class UserController {
    @Autowired
    private UserService userService;

    public void doController(){
        userService.doService();
        System.out.println("do Controller...");
    }

}

4.2 构造方法注入

只有一个,可以省略掉@Autowired。如果存在多个构造方法,必须用@Autowired指定。

@Controller
public class UserController2 {
    //构造方法注入
    private final UserService userService;

    @Autowired // Spring 4.3+ 可省略@Autowired(当只有一个构造方法时)
    public UserController2(UserService userService) {
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("hi, UserController2...");
        userService.sayHi(); 
    }
}

有无参构造方法时Spring会优先用无参。


4.3 Setter注入

就算只有一个set方法也不能省略@Autowired。

【@Autowired 的作用】
明确告诉 Spring 此方法需要依赖注入。若省略,Spring会认为这是一个普通方法,不会自动调用它来注入依赖。

【Setter 注入的本质】
需要 Spring 主动调用 Setter 方法并传入依赖对象,而 @Autowired就是触发这一行为的标记。

@Controller
public class UserController3 {
    // 注入方式3:Setter方法注入
    private UserService userService;

    @Autowired // 必须保留注解(即使只有一个Setter方法)
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("hi, UserController3...");
        userService.sayHi(); // 调用Service方法
    }
}

4.4 @Autowired存在的问题

为什么官方不推荐用属性注入?
属性注入以类型进行匹配,与注入的属性名称无关。
无法注入final修饰的属性,定义时就需要赋值或者构造方法中赋值。

如何解决:

  • @Primary
  • @Qualifier
  • @Resource

4.5 三种方式的区别

static类型的变量与类无关,就算没有类他也存在,只是指定了一个路径而已。


网站公告

今日签到

点亮在社区的每一天
去签到