0


Spring IoC和DI

Spring可以总结为:包含了众多方法工具的IoC容器

什么是 IoC ?什么是容器

我们通过本篇文章来进行学习

IoC是什么?

理解IoC

IoC(Inversion of Control)控制反转,即控制权反转,指在传统的程序设计中,流程的控制权通常由程序内部实现,而使用IoC,对象对自己所依赖的资源不再负责获取和管理,而是把这些控制权交给外部容器来实现。

什么是容器?

容器:也就是用来容纳物品的装置(如生活中的箱子)。在计算机中,我们容纳的是应用程序和相关组件,因此,容器是一种用于封装应用程序及其所有相关组件的技术

什么是控制权反转?

在传统的程序开发过程中, 当我们需要某个对象时,需要自己通过new创建对象,而IoC有专门的容器来创建这些对象,即IoC容器来控制对象的创建,此时,我们可以将创建对象的任务交给容器,程序只需要依赖注入(DI)就可以了。在此过程中,我们获取对象的过程,由主动(自己创建)变为了被动(从IoC容器中获取),控制权颠倒,因此叫做控制反转

我们通过一个例子来进一步理解IoC:

示例

需求:造一辆车

在进行传统程序开发时,我们的思路是这样的:要造一辆车,首先要有轮胎

然后将Tire类主动注入到用Car中

简单代码演示:

Tire:


public class Tire {
    private int size;
    public Tire(){
        this.size = 20;
        System.out.println("轮胎尺寸:" + size);
        System.out.println("Tire init");
    }
    public void run(){
        System.out.println("Tire");
    }
}

Car:

public class Car {
    private Tire tire;
    public Car(){
        tire = new Tire();
        System.out.println("Car init");
    }
    public void run(){
        System.out.println("Car");
    }
}

然而,当轮胎的尺寸发生变化,需要通过用户的指定来造轮胎,此时,我们需要修改代码:

对应的,Car也需要进行修改:

此时,修改底层代码,调用该类的代码都需要修改, 程序的耦合性非常高

在上述程序中,我们在造车时根据车轮的大小设计汽车,车轮一修改,汽车的设计就得修改

我们可以换一种思路:我们不需要完成汽车和轮胎的所有设计,将轮胎外包出去,当轮胎的尺寸发生改变时,我们只需向工厂下订单,工厂就会帮我们造好轮胎

在代码中体现为:我们不在类中创建依赖的对象,而是通过传递(也就是注入)的方式

这样,即使Tire中发生改变,Car本身也不需要修改任何代码

底层类发生变化,调用它的类不用做任何代码,也就实现了代码之间的解耦,从而程序设计更加灵活

在传统的代码中,对象创建的顺序是:Car -> Tire

而改进后的代码,对象创建的顺序是:Tire -> Car

我们可以发现:传统开发中,Car控制并创建了Tire,而改进后,控制权发生了反转,不再是由使用方创建并控制依赖对象了,而是将依赖对象注入到对象中,依赖对象的控制权不再由使用方控制

而 IOC有专门的容器来创建这些对象,即IoC容器来控制对象的创建,使用方需要时,只需将依赖注入就可使用依赖对象

为什么要使用IOC

通过上述过程,我们可以看出,使用IoC容器,资源不再由使用资源的双方管理,而是由不使用资源的第三方管理,此时:

  1. 能够降低耦合度:IoC容器可以帮助管理对象之间的依赖关系,将对象的创建和管理交给容器来实现,从而降低了使用资源双方的依赖程度,使得代码更易于理解、维护和扩展。

  2. 简化配置和管理:IoC容器可以集中管理资源,统一进行配置和管理,减少了对资源的手动管理工作,提高了系统的可维护性和管理效率。

  3. 提高了灵活性:通过IoC容器管理对象的依赖关系,可以使系统更加灵活,能够更方便地进行替换、升级或扩展,而不需要修改大量的代码。

DI是什么?

DI(Dependency Injection):依赖注入,在容器运行期间,动态的为应用程序提供运行时所依赖的资源,即程序在运行时需要资源,此时容器就为其提供这个资源

因此,依赖注入可以看做是实现控制反转的一种方式

在示例的改进代码中,是通过构造函数的方式,将依赖对象注入到需要使用的对象中的:

在了解了 IoC 和 DI的基本概念后,我们来学习 Spring IoC 和 DI 的代码实现

Spring 是一个 IoC容器,作为容器,就具备两个功能:存 和 取

Spring容器管理的主要是对象,而这些被管理的对象,我们称之为 Bean。我们将对象交给 Spring 进行管理,由 Spring 来负责对象的创建和销毁,程序只需要告诉 Spring,哪些需要存取,以及如何取

IoC

要将对象交给IoC容器进行管理,需要使用注解,而Spring框架为更好的服务web应用程序,提供了丰富的注解:

类注解:@Controller @Service @Repository @Component @Configuration

方法注解:@Bean

我们先学习类注解

@Controller(控制器存储)

@Controller的使用

使用@Controller存储bean:

@Controller

public class UserController {
    public void hello(){
        System.out.println("hello...");
    }
}

如何观察这个对象是否已经存在Spring容器中了呢?

若是能从Spring容器中获取这个对象,则这个对象就已经存在Spring容器中了

我们使用ApplicationContext来帮助我们获取对象

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        UserController bean = context.getBean(UserController.class);
        bean.hello();
    }
}

我们观察运行结果,发现成功从Spring中获取到Controller对象,且执行了Controller的hello方法:

若我们将@Controller删除,再观察运行结果:

此时程序报错:找不到类型为:com.example.demo.controller.UserController 的bean

ApplicationContext

ApplicationContext是什么呢?

ApplicationContext,即Spring上下文

对象交由Spring进行管理,要从Spring中获取对象,首先要拿到Spring的上下文

我们来理解上下文的概念:

在做语文阅读理解的时候,常会遇见这样的问题:请根据上下文,分析你对...的理解 其中的上下文,指的是文章中与某一词语或文句相连的上文和下文

在学习多线程时,应用进行线程切换的时候,在切换之前会将线程的状态信息暂时存储起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间时,就能从上下文中拿到线程上次运行的信息

而在Spring框架中,上下文指的是 Spring IoC 容器管理的对象之间的环境和状态。其中包括 bean 的定义、依赖注入、AOP配置等。

在上述代码中,我们是通过类型(DemoApplication.class)来查找对象的,还有其他的方式获取bean

获取bean的方式

ApplicationContext提供了许多获取bean的方式,而ApplicationContext获取bean对象的功能是父类 BeanFactory 提供的功能

在上述获取bean的方法中,常用的是第1,2,4种

其中涉及到根据名称获取bean,bean的名称是什么呢?

Spring bean 是Spring框架在运行时管理的对象,Spring会给管理的对象起一个名字(例如,学校会给每个学生分配一个学号,根据学号就能够找到对应的学生)

而在Spring中也是如此,为每个对象起一个名字,根据bean的名称,就可以找到对应对象,从而获取对应对象

在分配学号时,学校会根据学生的入学年份、专业、班级等信息分配学号,也就是学号的制定规则

在Spring,是如何命名的呢?Bean的命名约定是什么呢?

命名约定使用Java标准约定作为实例字段名,即 使用小驼峰命名规则

例如:UserController,bean的名称为:userController

根据这个命名规则,我们来获取bean:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        UserController bean = context.getBean(UserController.class);//根据 类型 获取
        UserController userController = (UserController) context.getBean("userController");//根据 名称 获取
        UserController userController1 = context.getBean("userController", UserController.class);//根据名称 + 类型 获取
        System.out.println(bean);
        System.out.println(userController);
        System.out.println(userController1);
    }
}

运行结果:

根据结果我们也可以发现:三种方法获取的对象地址一样,这说明获取的对象是同一个

但是,命名时也有一些特殊情况:

@Controller
public class UController {
    public void run(){
        System.out.println("UController");
    }
}
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        UController uController = (UController) context.getBean("uController");
    }
}

此时,运行结果为:

错误为:没有名称为 uController 的 bean

这是因为,当有多个字符,且第一个和第二个字符都是大写时,要保留原始大小写

因此,UController,bean 的名称为 UController

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        
        UController uController = (UController) context.getBean("UController");
    }
}

我们来总结一下bean的命名约定:

当有多个字符且第一个和第二个字符都是大写时,保留原始大小写

其他情况,则使用小驼峰命名规则

接下来,我们来看@Service

@Service(服务存储)

使用@Service 存储 bean:

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

获取 bean:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        UserService userService = context.getBean(UserService.class);
        userService.runService();
    }
}

@Repository(仓库存储) @Component(组件存储) @Configuration(配置存储)的使用也是类似的,这里就不再一一演示了

类注解总结

为什么要有这么多的类注解呢?

这与 应用分层 类似,为了在看到类注解后,就能直接了解当前类的用途

@Controller:控制层,接收请求,对请求进行处理,并进行响应

@Service:业务逻辑层,处理具体的业务逻辑

@Repository:数据访问层,也称为持久层,处理数据访问操作

@Configuration:配置层,处理项目中的一些配置信息

@Component:泛指组件,当组件不好归类时,可以使用这个注解

类注解之间的关系:

我们观察 @Controller @Service @Repository @Configuration 和 @Component 的源码:

@Controller @Service @Repository @Configuration 这些注解里面都有一个注解 @Component,说明 它们 属于 @Component 的 “子类”

@Component 是一个 元注解,也就是可以注解其他类的注解,@Controller @Service等,这些注解被称为@Component的衍生注解

类注解是添加到某个类上的,但在某些情况下也会出现问题问题:

  1. 使用外部包里的类时,没办法添加类注解

  2. 一个类需要多个对象时

此时,我们就需要使用方法注解

@Bean

方法注解的使用

@Data

public class User {
    private int id;
    private String name;
    public User(){}
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
public class Users {
    @Bean
    public User user() {
        return new User(1, "aaa");
    }
}

尝试获取:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user = context.getBean(User.class);
        System.out.println(user);
    }
}

运行结果:

程序报错:找不到 类型为 com.example.demo.model.User 的 bean

为什么会报错呢?

这是因为方法注解要配合类注解使用

我们加上类注解:

@Component
public class Users {
    @Bean
    public User user() {
        return new User(1, "aaa");
    }
}

运行结果:

定义多个对象

若此时同一个类中有多个对象呢?

@Component
public class Users {
    @Bean
    public User user1() {
        return new User(1, "aaa");
    }
    @Bean
    public User user2(){
        return new User(2, "bbb");
    }
}

我们根据类型来获取对象:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user = context.getBean(User.class);
        System.out.println(user);
    }
}

运行结果:

报错显示:期望只有一个匹配,结果发现了两个:user1,user2

我们可以报错信息中看出:@Bean注解的bean,bean的名称就是它的方法名

我们根据名称来获取bean:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user1 = (User) context.getBean("user1");
        User user2 = (User) context.getBean("user2");
        System.out.println(user1);
        System.out.println(user2);
    }
}
        

运行结果:

根据 名称 + 类型来获取bean:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user1 = (User) context.getBean("user1", User.class);
        User user2 = (User) context.getBean("user2", User.class);
        System.out.println(user1);
        System.out.println(user2);
    }
}

运行结果:

由此可以看出:@Bean可以针对同一个类定义多个对象

Bean的重命名

可以通过设置name属性对Bean进行重命名:

@Component
public class Users {
    @Bean(name = {"us1", "user1"})
    public User user1() {
        return new User(1, "aaa");
    }
    @Bean
    public User user2(){
        return new User(2, "bbb");
    }
}

此时就可以使用 us1来获取User对象了:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user1 = (User) context.getBean("us1");
        User user2 = (User) context.getBean("user2", User.class);
        System.out.println(user1);
        System.out.println(user2);
    }
}

运行结果:

其中 name = 可以省略:

    @Bean({"us1", "user1"})
    public User user1() {
        return new User(1, "aaa");
    }

而当只有一个名称时,{}也可以省略:

    @Bean("us1")
    public User user1() {
        return new User(1, "aaa");
    }

扫描路径

bean想要生效,需要被Spring扫描

我们修改项目工程的目录结构:

此时再运行代码:

程序报错:没有找到名称为us1的bean

为什么没有找到呢?

使用注解声明的bean想要生效需要配置扫描路径,让Spring能够扫描到这些注解

通过 @ComponentScan 来配置扫描路径:

@SpringBootApplication
@ComponentScan({"com.example.demo"})
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        User user1 = (User) context.getBean("us1");
        User user2 = (User) context.getBean("user2", User.class);
        System.out.println(user1);
        System.out.println(user2);
    }
}

{} 中可以配置多个包路径

为什么前面没有配置 @ComponentScan 注解也能够扫描?

@ComponentScan 虽然没有显示配置,但其实已经包含在启动类声明注解 @SpringBootApplication 中了

其中,扫描的默认范围是 SpringBoot 启动类所在的包及其子包

(在配置类上添加 @ComponentScan 注解,该注解默认会扫描该类所在包下的所有配置类)

因此,将启动类放在我们所希望扫描的包的路径下,这样,定义的bean就可以被扫描到了

DI

在进一步学习了控制反转IoC后,我们来学习依赖注入DI

依赖注入是一个过程,在IoC容器创建bean时,提供运行时所依赖的资源

使用 @Autowired 注解来完成依赖注入

Spring 为我们提供了三种注入方式:

  1. 属性注入 (Field Injection)

  2. 构造方法注入 (Constructor Injection)

  3. Setter注入(Setter Injection)

属性注入

属性注入是通过在类的属性上使用 @Autowired 注解或在配置文件中进行配置,将依赖对象注入到类的属性中

我们将UserService类注入到UserController类中:

UserService:

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

UserController:

@Controller
public class UserController {
    @Autowired
    private UserService userService;
    public void hello(){
        System.out.println("hello...");
        userService.runService();
    }
}

获取UserController中的hello方法:

@SpringBootApplication
@ComponentScan({"com.example.demo"})
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        UserController userController = context.getBean(UserController.class);
        userController.hello();
    }
}

运行结果:

若去掉 @Autowired:

程序报错:不能调用 com.example.demo.service.UserService.runService(),因为 this.userService 为空

构造方法注入

构造方法注入是在类的构造方法中实现注入:

@Controller
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }
    
    public void hello(){
        System.out.println("hello...");
        userService.runService();
    }
}

运行结果:

若此时类中只有一个构造方法,则 @Autowired 注解可以省略:

@Controller
public class UserController {

    private UserService userService;

   // @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }

    public void hello(){
        System.out.println("hello...");
        userService.runService();
    }
}

但若类中有多个构造方法,此时就需要添加 @Autowired 注解来明确指定使用哪个构造方法

@Controller
public class UserController {

    private UserService userService;
    public UserController(){}

    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }

    public void hello(){
        System.out.println("hello...");
        userService.runService();
    }
}

若此时去掉 @Autowired 注解,程序报错:

Setter注入

Setter注入与属性的Setter方法类似,只不过需要在设置set方法时添加上 @Autowired 注解:

@Controller
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService){
        this.userService = userService;
    }

    public void hello(){
        System.out.println("hello...");
        userService.runService();
    }
}

此时,若 去掉 @Autowired:

同样的,程序报错

三种注入方法分析

属性注入:

优点:

简洁方便,可以直接在属性上进行注入,代码简洁,不需要编写额外的构造方法或 Setter 方法

**缺点: **

  1. 只能用于IoC容器

  2. 不能注入 final 修饰的属性

构造函数注入:

优点:

  1. 可以注入 final 修饰的属性

  2. 注入的对象不会被修改

  3. 依赖对象在使用前一定会被完全初始化(因为依赖是在类的构造方法中执行的,而构造方法在类加载阶段就会执行)

  4. 通用性好,构造方法是JDK支持的,更换框架后也是适用的

缺点:

注入多个对象时,代码量相对较多

Setter注入:

优点:

  1. 方便在类实例后,重新对该对象进行配置或注入

  2. 可选依赖,可以只提供部分 Setter 方法,不影响其他依赖的注入

缺点:

  1. 不能注入 final修改的属性

  2. 注入对象可能会发生改变(setter方法可能被多次调用,就有被修改的风险)

  3. 需要为每个需要注入的属性编写相应的 Setter 方法,增加了代码量

因此, 选择使用哪种依赖注入方式取决于具体的需求和场景。

@Autowired存在的问题

若同一类型中存在多个bean,此时适用@Autowired会存在问题:

@Controller
public class UserController {
    @Autowired

    private User user;

    public void hello(){
        System.out.println("hello...");
        System.out.println(user);;
    }
}

运行结果:

程序报错:UserController 需要一个 bean,但是发现了两个,即非唯一的bean

如何解决该问题呢?

其实报错信息下面就给出了解决方法:

@Primary

当多个类型相同的bean注入时,加上 @Primary 注解,来确定默认的实现:

@Component
public class Users {
    @Primary//指定 该 bean 为默认bean实现
    @Bean
    public User user1() {
        return new User(1, "aaa");
    }
    @Bean
    public User user2(){
        return new User(2, "bbb");
    }
}

运行结果:

@Qualifier

使用 @Qualifier 注解,指定当前要注入的对象,在 @Qualifier 的 value属性中,指定注入的 bean的名称,@Qualifier注解不能单独使用,必须配合 @Autowired 使用

@Controller
public class UserController {
    @Qualifier("user2")
    @Autowired

    private User user;

    public void hello(){
        System.out.println("hello...");
        System.out.println(user);;
    }
}

运行结果:

@Resource

@Resource注解,是按照 bean 的名称进行注入,通过 name 属性指定要注入的 bean 的名称

@Controller
public class UserController {
    @Resource(name = "user1")
    private User user;

    public void hello(){
        System.out.println("hello...");
        System.out.println(user);;
    }
}

运行结果:

@Autowired 与 @Resource 的区别

@Autowired 是 Spring 框架提供的注解,而 @Resource 是JDK 提供的注解

@Autowired 默认是按照类型注入的,而 @Resource 是按照名称注入的,相比于 @Autowired,@Resource 支持更多的参数设置

标签: spring java IoC

本文转载自: https://blog.csdn.net/2301_76161469/article/details/137093682
版权归原作者 楠枬 所有, 如有侵权,请联系我们删除。

“Spring IoC和DI”的评论:

还没有评论