IOC+DI:Spring 框架的高效引擎

发布于:2025-02-10 ⋅ 阅读:(54) ⋅ 点赞:(0)

什么是Spring?

  Spring 是一个包含众多工具方法的框架,它大大开发了项目开发的效率。从本质上来讲,Spring 本质上是一个 IoC 容器,它负责创建、管理和销毁对象。

  传统的应用程序中,对象的创建和生命周期管理由应用程序自身负责,这使得代码耦合度高,难以维护。Spring 通过 IOC 将这些职责从应用程序中分离出来,由容器统一管理。

  我们可以联想一下生活中的容器,都是用来存放东西的,比如水杯,是用来装水的,Spring容器亦是如此,它里面存放了众多对象,当程序员需要使用时,只需要从里面去取出来就可以。这样就无需手动去创建对象,大大提高了开发效率。

一、什么是IoC?

  IoC是  Inversion of Control 的单词缩写,中文意思是:控制反转。什么意思呢?在传统的程序开发中,对象的创建和销毁由应用程序负责,而将对象存放在Sping IoC容器之后,对象的创建和销毁的控制权就从应用程序交到了Spring IoC容器手中,这个就是控制反转的意思:控制权得到了反转。 

  我们前面的文章中,其实就已经涉及到了IoC的使用,只是我们还不知道,比如,我们在类上添加了@RestController或者@Controller注解,意思就是将该类交给IoC容器管理,此时这个类的实例化对象就存放在了容器之中。

  不知你是否会有这样一个疑问,在下面这个代码中,我们明明没有实例化一个Controller对象,为什么却可以在使用URL路径访问:user/v1 时调用到这个对象的v1方法?在一般的情况下,我们要想调用一个类的成员方法,前提都是建立在创建出这个类的实例化对象,然后调用,为什么这里却没有实例化Controller对象,却可以调用里面的成员方法?

  其实是因为,当你使用@RestController 注解Controller类时,此时Spring就会自动创建该类的一个实例对象,存放在IoC容器中。

  在通过URL路径访问这个路径时,服务器会从IoC容器中寻找Controller类的实例对象,然后调用里面的v1方法。 

二、什么的DI?

  DI 是英文 Dependency Injection 的缩写,中文意思是:依赖注入。DI 是实现 IoC 的一种具体方式 ,是指在运行时,将对象所依赖的其他对象通过某种方式(如构造函数、方法参数、属性等)注入到该对象中,从而使对象能够正常工作。 

  比如,我们现在需要造一辆车,一辆车依赖一个合适的车身,因此我们需要注入一个车身;而一个车身又依赖一个合适的底盘,因此我们需要注入一个底盘;而一个底盘又依赖四个合适尺寸的轮胎,因此我们需要注入四个轮胎,这个注入的过程,就是DI。

class Tire{//轮胎
    int size;
    public Tire (int size){
        this.size=size;
        System.out.println("Tire init……");
    }
}

class Bottom{//底盘
    Tire tire;
    public Bottom(Tire tire){
        this.tire=tire;
        System.out.println("Bottom init……");
    }
}

class Framework{//车身
    Bottom bottom;
    public Framework(Bottom bottom){
        this.bottom=bottom;
        System.out.println("Framework init……");
    }
}
public class Car {//汽车
    Framework framework;
    public Car (Framework framework){
        this.framework=framework;
        System.out.println("Car init……");
    }
}


 class Main{
     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);//创建汽车时,注入车身
     }
 }

 

三、IoC详解

(一) 什么是Bean?

  Spring IoC既然是一个容器,那么它就包含容器的两大基本功能:存和取

  Spring IoC 容器管理的主要是对象,这些对象,我们统称为" Bean " 。我们把这些对象存放在容器中,由Spring 负责对象的创建和销毁,应用程序只需要负责告诉Spring,这些对象哪些需要存,哪里需要使用容器中的对象即可。下面我举一个Spring IoC 存取对象的一个例子。

  

  代码解析:

  (1)当启动类启动时,Spring会自动扫描启动类所在目录及其子目录下面的所有注解,当它扫描到UseInfo类被@Component 注解时,会将这个类创建一个实例化对象,存放在IoC容器中,等待被使用

  (2)我们将启动了中的 SpringApplication.run 的返回值用 ApplicationContext 类型的变量contex来接收,我们可以把context想象成管理容器的管家,通过它我们可以获取到存放在容器中的对象。

(3)最后,我们通过 context.getBean(UserInfo.class) 获取到UserInfo类型的对象,调用UserInfo对象的say()方法,这就是IoC容器的的存取的整个过程。

  通过getBean()获取对象的方式有点过于繁琐,那么这里我们介绍另外一种获取对象的方式,使用@Autowired注解

@Component//实例化一个UserInfo对象存放中容器中
public class UserInfo {
    private String name;
    private Integer age;
    public void say(){
        System.out.println("UserInfo say……");
    }
}

@RestController
@RequestMapping("/user")
class User{
    @Autowired//从容器中取出UserInfo对象,赋值给userInfo
    private UserInfo userInfo;

    @RequestMapping("/say")
    public void say(){
        System.out.println("调用UserInfo的say()方法");
        userInfo.say();
    }
}

接下来我们通过Postman访问, 然后看看IDE控制台的效果:

  可以看到,当我们使用 Postman 访问 http:127.0.0.1:8080/user/say 时,会调用User类中的say方法。然后say方法再去调用UserInfo对象的say方法。

  可以看到,整个过程,我们都没有给UserInfo实例化对象,但是却可以调用其方法,这是因为,@Autowired注解 private UserInfo userInfo时,也就是告诉容器:帮我在容器中找出一个UserInfo对象,赋值给userInfo。

(二) Bean的命名规则

  我们都知道,在我们实例化对象的时候,都会给这个对象取一个名称,那么IoC容器里面既然存放的是对象,那么每个对象理所当然也都有自己的名称,那么它究竟是如何来命名的呢?

  默认情况下,如果在使用@Component注解时,创建对象的默认的名称是:将类名的名称首字符改为小写,比如UserInfo默认的名称是userInfo。

UserInfo --> userInfo

StudentMarjar --> studentMarjar

MyName --> myName

  特殊情况:如果类名的前两个字符都是大写,那么对象名和类名保持一致,假如存放USerInfo类,它默认的对象名称是USerInfo,和类名保持一致。

MClass --> MClass

YName --> YName

MFather --> MFather

我们可以通过类型或者名称获取Bean,因此,获取Bean的方式分为了三种:

(三)Bean的存储

  在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解: 而Spring框架为了更好的服务web应用程序,提供了更丰富的注解。

共有两类注解类型可以实现:

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

接下来让我们分别来看看它们是如何存储对象的。

1、类注解:

@Controller(控制器存储)

存放对象代码: 

@Controller//将对象交给Spring IoC容器管理
public class UserController {
    public void say(){
        System.out.println("UserController say……");
    }
}

@Service(服务存储)

存放对象代码: 

@Service//将对象交给Spring IoC容器管理
public class UserService {
    public void say(){
        System.out.println("UserService say……");
    }

}

 

@Repository(仓库存储)

存放对象代码:

@Repository//将对象交给Spring IoC容器管理
public class UserRepository {
    public void say(){
        System.out.println("UserRepository say……");
    }
}

 

@Component(组件存储)

存放对象代码:

@Component//将对象交给Spring IoC容器管理
public class UserComponent {
    public void say(){
        System.out.println("UserComponent say……");
    }
}

 

@Configuration(配置存储)

存放对象代码:

@Configuration//将对象交给Spring IoC容器管理
public class UserConfiguration {
    public void say(){
        System.out.println("UserConfiguration say……");
    }
}

 

接下来,我们在启动类中统一获取刚才存放在Spring IoC容器中的对象:

 取出对象代码:

程序运行结果: 

 

为什么要有这门多类注解?

通过上面注解的学习,我们发现,上述注解的功能好像都是一致的,即将类的实例对象交给IoC容器管理。那么还有必要将它们划分成为不同的名称吗?这背后其实和应用分层有关!

  什么是应用分层呢?一个公司,需要有不同的部门,分别去负责抓专门的工作;一个学校,需要有不同的老师,去负责不同的学科,一个项目也是如此,它也需要根据某种规范,将里面的文件划分为不同的层次,比如控制层、业务逻辑层等……

@Controller:控制层,接收请求,对请求进行处理,并进行响应.
@Service:业务逻辑层,处理具体的业务逻辑.
@Repository:数据访问层,也称为持久层。负责数据访问操作
@Configuration:配置层。处理项目中的一些配置信息.

  通过分层,不同的层次使用不同的注解,这样程序员在看到注解之后,也就明白了该类的用途。比如,当看到一个类被@Controller注解时,我们就知道该类是用来接收请求,并对请求进行处理做出响应的。

类注解之间的关系:

  当我们按下ctrl然后点击上述注解时,我们会发现,@Controller、@Service、@Repository和@Configuration注解里面,都包含有@Component注解,因此,@Component是其他四个注解的元注解,其他四个注解是@Component的衍生注解,它们都共同用于@Component注解的功能。

 

2、方法注解:@Bean 

@Bean注解的使用

  虽然类注解可以帮助我们将实例对象存放在IoC容器中管理,但是如果我们要存放多个实例对象在容器中,使用类注解好像就无法实现了,因为类注解只能定义在类上,但是一个类只能定义一次,不能重复定义。

  因此,我们使用方法注解,帮助我们在容器中存放多个相同类型的对象!接下来,让我们看看@Bean注解的简单使用。

首先,我们先定义一个Student类:

public class Student {
    private String name;
    private Integer age;


    //生成Getter和Setter方法和toString方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 使用@Bean注解代码:

  注意:当使用@Bean注解方法时,创建的对象名称默认是方法名,意思就是,此时@Bean注解的作用是创建一个Student类的对象交给容器管理,对象名称是student1。

  获取Student对象代码:

程序运行结果:

 

@Bean定义多个对象

  当使用@Bean注解多个方法,存放多个相同类型的Student对象,此时我们在获取对象的时候,该如何获取呢?

(1)存放两个Student对象在Spring容器中,根据@Bean注解的对象命名规则:两个对象的名称应该是方法名:student1和student2

 

(2)从容器中获取Student对象

  可以看到,程序报错,它的意思是: 

  没有符合条件的类型为 “com.example.test2.student.Student” 的 Bean:期望找到单个匹配的 Bean,但找到了 2 个:student1,student2 。

 

(3)  因为我们前面在容器中存放了两个Student对象,此时如果按照类型取出Student对象,Spring不知道该取出来哪一个,这种情况,我们可以根据对象名称获取对象。

 

 

3、@Bean重命名 

如果我们想自己指定对象的名称,就可以使用@Bean重命名。

 

四、DI详解 

  上面我们讲解了控制反转IoC的细节,接下来呢,我们学习依赖注入 DI 的细节。依赖注入是⼀个过程,是指 IoC 容器在创建 Bean 时,去提供运行时所依赖的资源,而资源指的就是对象。 在上面程序案例中,我们使用了 @Autowired 这个注解,完成了依赖注入的操作。简单来说,就是把对象取出来放到某个类的属性中。

关于依赖注入,Spring 也给我们提供了三种方式:

1、属性注入 (Field Injection)
2、构造方法注入 (Constructor Injection)
3、Setter 注入 (Setter Injection)

(一)属性注入

 属性注入就是通过@Autowired注解实现的,可以从IoC容器中获取对象,注入到属性中。

属性注入代码: (注入UserService对象,调用其say()方法)

启动类代码: 

程序运行结果: 

 

(二)构造方法注入

 构造方法注入,顾名思义,就是在类的构造方法中实现依赖注入。

构造方法注入代码:

启动类代码: 

 程序运行结果:

 

  在使用构造方法注入时,Spring在创建UserComponent对象时,也要通过调用其构造方法创建对象,那么它在调用带有参数的构造方法时,Spring是如何给构造方法传递参数的呢?

  其实,Spirng在调用带有参数的构造方法时,会从IoC容器中查询相同类型的对象,查询到之后,会将该对象作为构造方法的参数。

  比如,在这个例子中,Spring在创建UserComponent对象时,会调用其构造方法(需要UserService类型的参数),此时,Spring就从容器中去查询UserService类型的对象,查到之后,作为构造方法的参数。

  接下来让我们通过打印的方式验证一下:

  已知Spring创建UserComponent对象时,需要调用其带有UserService类型参数的构造方法,以实现依赖注入,接下来我们分别从IoC容器中获取UserComponent和UserService对象,先打印UserService对象的值,再打印UserComponent对象中的UserService属性。

程序运行: 

  发现打印出来的userService内容一致,说明它们是同一个对象,因此证明:Spring在调用有参构造方法时,会从容器中获取合适的对象作为参数! 

 

我们还有一个疑问:当这个类中包含多个构造方法时,Spring会调用哪个构造方法呢? 

  让我们在这个类中,创建两个构造方法:一个带有参数,一个不带参数,看看Spring会默认调用哪个构造方法!为方便观察,我们在构造方法中,写入了打印的语句! 

 启动类代码:

 程序运行结果:

 我们发现,Spring默认调用了无参构造方法。那么,有没有什么方法,可以自定义Spring默认调用的构造方法呢?我们可以使用@Autowired注解!

 

(三)Setter注入

Setter 注⼊和属性的Setter⽅法实现类似,只不过在设置set⽅法的时候需要加上@Autowired 注解。

Setter方法注入代码:

 启动类代码:

程序运行:

 

(四)@Autowired注解存在的问题

当同⼀类型存在多个bean时,使用@Autowired会存在问题

 可以看到,当IoC容器中存放两个相同类型(Student)的对象时,此时用@Autowired注解实现属性注入会报错,报错的原因是:Bean类型不是唯一的。

如何解决上述问题呢?Spring 提供了以下几种解决方案:

@Primary
@Qualifier
@Resource

1、使用 @Primary 注解:当存在多个相同类型的 Bean 注入时,加上 @Primary 注解,来确定默认的实现。

 

启动类代码:为方便观察,我们打印一下Student对象,看看属性注入的是哪个对象(STUDENT1还是STUDENT2)

 

  让我们使用@Primary注解之后,再运行一下程序,此时程序报错了,显示出现了依赖循环,这是为什么呢? 

  这可能是因为,我们在启动这个类的时候,Spring开始将对象存放在容器中,或者将对象从容器中取出放在指定的位置中。

  如果我们将存放Student对象取出Student对象写在同一个类代码中,很可能出现这样一种情况,在Student对象还没有存放进容器时,就有指令要取出Student对象了,此时就出现了循环依赖。因为,为避免这种情况,我们将它们分开写在不同的类代码中。

 

   代码修改:将@Bean注解的方法挪到Student类中。

Student类:

@Component//@Bean注解要和类注解搭配使用!!!
public class Student {
    private String name;
    private Integer age;
    @Primary//指定默认选择的对象为STUDENT1
    @Bean(name="STUDENT1")//给存放在容器的对象取名称为STUDENT1
    public Student student1(){
        Student student=new Student();
        student.setName("zhangSan");
        student.setAge(18);
        return student;
    }

    @Bean(name="STUDENT2")//给存放在容器的对象取名称为STUDENT2
    public Student student2(){
        Student student=new Student();
        student.setName("wangWu");
        student.setAge(19);
        return student;
    }

    //生成Getter和Setter方法和toString方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

StudentController类:

@Component//@Bean注解要和类注解配合使用,否则会报错
public class StudentController {
    @Autowired
    public Student student;//使用public修饰

}

启动程序,看看程序运行结果:

  此时,程序运行成功,可以看到,属性注入,默认注入的是STUDENT1对象 

2、使用 @Qualifier 注解:指定当前要注入的 bean 对象。在 @Qualifier 的 value 属性中,指定注入的 bean 的名称。

注意:在使用之前,记得把前面的@Primary注解给注释掉!!! 

可以看到,当使用@Qualifier注解默认取出STUDENT2对象后,程序获取到的就是STUDENT2对象! 

3、使用@Resource注解:是按照bean的名称进行注⼊。通过name属性指定要注⼊的bean的名称

 

 可以看到,当使用@Qualifier注解默认取出STUDENT1对象后,程序获取到的就是STUDENT1对象! 


网站公告

今日签到

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