【Spring】详解Spring IOC&DI

目录

前言

IoC和DI的使用

IoC详解

Bean存储

@Controller(控制器存储)

获取Bean

根据Bean类型获取Bean

根据Bean名称获取Bean

根据Bean的名称和Bean类型获取Bean

@Service(业务逻辑)

@Repository(仓库存储)

@Component(组件存储)

@Configuration(配置存储)

额外小话题

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

方法注解

同一个类创建多个对象

扫描路径

DI详解

属性注入

构造方法注入

Setter方法注入

三种注入方式的优缺点

Autowired存在的问题

@Primary

@Qualifier

@Resource

@Autowired自动装配流程图

@Autowired和@Resouce的区别

总结

[Spring、Spring Boot和Spring MVC的关系及差别](#Spring、Spring Boot和Spring MVC的关系及差别)

区别

关系

Bean的命名

五大注解存储Bean

@Bean注解存储Bean

Spring中常见注解


前言

在前面中,我们讲解了Spring MVC是什么以及如何使用。那么本篇我们就来讲Spirng Framework,我们一般讲的Spring就指的是Spring Framework。

在前面中,我们已经讲解了什么是Spring,IOC和AOP是Spring框架的核心思想

本篇我们主要讲IOC容器的使用。

想了解什么是Spirng可以看这篇【Spring】什么是Spring?-CSDN博客

回顾一下IOC和DI。

IOC(控制反转)是Spring框架中的核心思想

控制反转(Inversion of Control,IoC)是一种设计思想 ,其核心是将对象的创建和管理的控制权从使用方(客户端)转移到一个专门的容器(如Spring容器)

在软件开发中,传统设计都是将对象的创建和管理交给使用方来进行创建;而Ioc则是将对象的创建和管理交给IOC,在需要使用的时候,我们只需要告诉Ioc容器所需的对象,那么它就可以为我们创建好。

Ioc很好的让每个模块进行了解耦,实现了高内聚低耦合。
DI(Dependency Injection)依赖注入,是Ioc的一种具体方式,将对象的创建和管理权交给外部容器。

依赖注入将对象的依赖关系通过外部容器注入的方式来传递给对象, 而不是让对象自己去创建或查找依赖。

IoC和DI的使用

通过前面的学习,相信对IoC和DI有初步的认识,那么接下来我们就来具体学习IoC和DI的代码实现。

既然Spring是一个IoC(控制反转)容器,作为容器,那么就具备两个功能:存和取

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

那么在Spring中如何存取Bean?

  1. 将类存储进Spring Ioc容器我们需要通过 @Component注解(不止这个)。
  2. 取出对象需要使用注解 @Autowired

示例:

java 复制代码
package com.example.demo.model;

import org.springframework.stereotype.Component;

@Component
public class UserInfo {
    private String userName;
    public void hi(){
        System.out.println("hi");
    }
}
java 复制代码
package com.example.demo.component;

import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserComponent {

    @Autowired
    private UserInfo userInfo;

    @RequestMapping("/hi")
    public String hi(){
        userInfo.hi();
        return "调用成功";
    }
}

可以看到,确实能显现出我们想要的结果,说明这两个注解能够实现堆Bean的存取。

注意:我们在使用 @Autowired 注解时,需要使用该注解的类有 @Controller 注解修饰 ,要使用Spring的IoC容器,就要保证这个类是由Spring管理的

以上展示的就是Ioc和DI的基本使用,接下来,我们来深入学习IoC和DI。

IoC详解

Bean存储

在前面的案例中,我们要把某个对象交给 IoC 容器管理,那么就需要在类上添加一个注解:@Component。而Spring框架为了更好的服务Wen应用程序,提供了丰富的注解。

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

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

我们先来讲下类注解。

@Controller(控制器存储)

使用 @Controller 存储Bean:

java 复制代码
package com.example.demo.controller;

import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    
    public void say(){
        System.out.println("UserController ...");
    }
}

既然我们已经将Bean存储到IoC容器中了,那么如何获取Bean?

获取Bean

先上代码:

java 复制代码
@SpringBootApplication
public class BeantextApplication {
	
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

	    // 从应用上下文中获取UserController bean的实例
	    UserController bean = context.getBean(UserController.class);

	    // 调用UserController实例的say方法,执行相关逻辑
	    bean.say();
	}

}

代码是由 @SpringBootApplication 注解修饰的类中写的,并且我们用 ApplicationContext接口来接收run()方法的返回值。

当我们运行一个带有 @SpringBootApplication 注解修饰的类,会启动Spring项目,并且SpringApplication.run() 是有返回值的,这个返回值可以接收,也可以不接收。

那为什么能用 ApplicationContext 类来接收?为什么要用这个类?

我们可以先查看一下run()方法的返回值是什么:

我们可以看到,run()的返回值类型是一个 ConfigurableApplicationContext接口,那这个接口内部是什么样?

可以看到, ConfigurableApplicationContext 接口ApplicationContext 接口 的一个子接口,根据java的多态性,子接口的实例可以被父接口类型接收,因此这里可以用 ApplicationContext来接收。

至于为什么要使用该接口?

ApplicationContext 是Spring框架中管理Bean的核心接口,提供了访问Spring容器中所有Bean的基本功能,几乎所有与Spring 容器的交互都可以通过 ApplicationContext 完成。

而在此处,我们是要获取Bean,那就需要使用 ApplicationContext 。

在BeanFactory在,我们可以看到getBean() 方法,顾名思义这个方法就是用来获取Bean的。

在这里,我们只介绍1、2、4种。

根据Bean类型获取Bean

在上面中,我们就是根据Bean的类型来获取到Bean。

这种方式是Sprinig利用反射机制实现的。Spring能通过类型找到对应的Bean,是因为在容器初始化的时候,Spring会通过反射机制扫描并注册所有Bean的定义信息

反射是Spring实现依赖注入和Bean管理的核心技术,允许Spring在运行时动态地创建对象、访问和修改对象的字段、调用对象的方法。

java 复制代码
@SpringBootApplication
public class BeantextApplication {
	
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

	    // 从应用上下文中获取UserController bean的实例
	    UserController bean = context.getBean(UserController.class);

	    // 调用UserController实例的say方法,执行相关逻辑
	    bean.say();
	}

}

根据Bean名称获取Bean

Bean名称应该如何写?

我们可以看下官方回答:

简单来说,就是如果类名前两个字母中大写字母个数小于2(也就是只有首字母大写),那么该类在交给Ioc后,就会以第一个字母小写的小驼峰形式命名,当类名前两个字母都大写,那么该类型就作Bean名称

java 复制代码
public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

	    // 从应用上下文中获取UserController bean的实例
		UserController bean = (UserController) context.getBean("userController");

	    // 调用UserController实例的say方法,执行相关逻辑
	    bean.say();
	}

根据Bean的名称和Bean类型获取Bean

java 复制代码
public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

	    // 从应用上下文中获取UserController bean的实例
		UserController bean = (UserController) context.getBean("userController", UserController.class);

	    // 调用UserController实例的say方法,执行相关逻辑
	    bean.say();
	}

@Service(业务逻辑)

这个注解也可以存储Bean。

java 复制代码
package com.example.demo.service;

import org.springframework.stereotype.Service;

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

读取:

java 复制代码
public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

		UserService userService = (UserService) context.getBean("userService");
		userService.say();

	}

@Repository(仓库存储)

java 复制代码
package com.example.demo.controller;

import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public void say() {
        System.out.println("UserRepository say");
    }
}
java 复制代码
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);

		UserRepository userRepository = (UserRepository)context.getBean("userRepository");
		userRepository.say();
    }

@Component(组件存储)

java 复制代码
package com.example.demo.component;

import org.springframework.stereotype.Component;

@Component
public class UserComponents {
    public void say() {
        System.out.println("UserComponents say");
    }
}
java 复制代码
public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);
		UserComponents bean = context.getBean(UserComponents.class);
		bean.say();
    }

@Configuration(配置存储)

java 复制代码
package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfiguration {
    public void say() {
        System.out.println("UserConfiguration");
    }
}
java 复制代码
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);
		UserConfiguration bean = context.getBean(UserConfiguration.class);
		bean.say();
    }

额外小话题

下面的代码在最后判断对象是否相等?那会不会相等呢?

java 复制代码
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);
		UserConfiguration bean = context.getBean(UserConfiguration.class);
		bean.say();

		UserConfiguration bean1 = (UserConfiguration) context.getBean("userConfiguration");
		bean1.say();

		UserConfiguration bean2 = context.getBean("userConfiguration", UserConfiguration.class);
		bean2.say();

		System.out.println(bean);
		System.out.println(bean1);
		System.out.println(bean2);
		System.out.println(bean == bean1);
		System.out.println(bean == bean2);
}

我们来看下结果:

我们可以看到,利用三种方法获取Bean,但最后返回的都是同一个实例,这是因为Spring中,默认情况下,Bean的作用域是单例

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

这个和前面讲的应用分层相呼应的,当开发者看到类注解的时候,就能直接了解当前类的用途。

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

我们可以看到,@Component都有在其他四个注解中,说明@Component 是其他注解的"父类",即其他注解是@Component注解的衍生注解。

不过其他四个注解用于更具体的用例,如三层结构,@Controller对应控制层、@Service对于业务逻辑层、@Repository对应持久层。

方法注解

类注解是加上类上的,但是存在两个问题:

  1. 使用外部包里的类,没有办法添加类注解;
  2. 一个类,需要多个对象,比如多个数据源。

对于这种场景,我们就需要用方法注解@Bean。

我们假设User类是一个外包中的类,不能加类注解,所以这就需要使用方法注解。

java 复制代码
package com.example.demo.model;

import lombok.Data;

@Data
public class User {
    private String name;
    private int age;
}
java 复制代码
package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.context.annotation.Bean;


public class UserController {

    @Bean
    public User user(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}

测试一下代码:

可以看到报了异常说没有找到Bean,这是因为UserController没有加类注解,Spring没有扫描到。所以要加上类注解:

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Bean
    public User user(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}

同一个类创建多个对象

在前面中,我们已经知道了在Spring容器中,Bean默认是单例,那么如果想要获取多个Bean,应该如何做?

使用@Bean时指定Bean名称,这里的名称是方法名称

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Bean
    public User user(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        return user;
    }

    @Bean("user1")
    public User user1(){
        User user = new User();
        user.setName("李四");
        user.setAge(20);
        return user;
    }
}

此外,我们观察@Bean注解的属性,可以看到:

name字段是一个字符串数组,说明一个Bean可以有多个名称

例如:

java 复制代码
    @Bean(name={"user1","lisi"})

需要注意:有多个名称的情况下,需要用大括号括起来,并指定字段

当然,类注解也可以指定名称,但只能指定一个

扫描路径

在项目中,不是所有的文件都会被扫描的,只有在 @SpringBootApplication注解所在的包中才会被扫描。

假如我们把 @SpringBootApplication的类移动一下。

运行一下:

该错误是由于Spring容器无法找到类型为com.example.demo.model.UserInfo的Bean,导致依赖注入失败。具体来说,UserComponent类中的userInfo字段需要一个UserInfo类型的Bean,但Spring上下文中没有定义该Bean。

这是因为UserInfo类没有被扫描的,因为UserInfo类不在SpringBoot启动类所在的包中。所以无法注入。

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

推荐:把启动类放在我们希望扫描的包的路径下,这样我们定义的Bean就都能被扫描到。

DI详解

依赖注入是一个过程,指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。在前面IoC学习中,我们使用了 @Autowired 注解来完成注入。

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

  • 属性注入(Field Injection)
  • 构造方法注入(Constructor Injection)
  • Setter 注入(Setter Injection)

属性注入

属性注入是用 @Autowired 实现的,直接在属性上加上该注解。

java 复制代码
package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void say(){
        System.out.println("UserService say");
    }
}
java 复制代码
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class DiController {
    //属性注入
    @Autowired
    private UserService userService;

    public void say(){
        System.out.println("DiController say");
        userService.say();
    }
}

测试一下;

java 复制代码
	public static void main(String[] args) {
	    // 启动Spring Boot应用,并将应用上下文注入到变量context中
	    ApplicationContext context=SpringApplication.run(BeantextApplication.class, args);
		DiController bean = context.getBean(DiController.class);
		bean.say();
    }

构造方法注入

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class DiController {
    //属性注入
//    @Autowired
    private UserService userService;
    
    //构造方法注入
    public DiController(UserService userService){
        this.userService = userService;
    } 
    public void say(){
        System.out.println("DiController say");
        userService.say();
    }
}

注意:如果类中只有一个构造方法,那么 @Autowired 注解就可以省略,但如果类中有多个构造方法,那么就需要用 @Autowired 来明确要使用哪个构造方法。

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class DiController {
    //属性注入
//    @Autowired
    private UserService userService;

    public DiController(){
        System.out.println("DiController 构造函数");
    }

    @Autowired
    public DiController(UserService userService){
        this.userService = userService;
    }
    public void say(){
        System.out.println("DiController say");
        userService.say();
    }
}

Setter方法注入

java 复制代码
@Controller
public class DiController {
    //属性注入
//    @Autowired
    private UserService userService;

    //set注入
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public void say(){
        System.out.println("DiController say");
        userService.say();
    }
}

这里报错的原因是由于set方法没有被调用到,所以我们需要用 @Autowired注解修饰。

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class DiController {
    //属性注入
//    @Autowired
    private UserService userService;

    //set注入
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public void say(){
        System.out.println("DiController say");
        userService.say();
    }
}

三种注入方式的优缺点

  1. 属性注入
    1. 优点:简单,使用方便;
    2. 缺点:
      1. 只能用于IoC容器,如果是非IoC容器不可用,并且只有在使用的时候才会出现NPE(空指针异常);
      2. 不能注入一个Final修饰的属性
  2. 构造方法注入(Spring 4.X推荐):
    1. 优点:
      1. 可以注入final修饰的属性
      2. 注入的对象不会被修改
      3. 依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法。
      4. 通用性好,构造方法是JDK支持的,所以更换任何框架,都适用
    2. 缺点:注入多个对象时,代码会比较繁琐
  3. Setter注入(Spring 3.X推荐):
    1. 优点:方便在类实例后,重新对该对象进行配置或者注入
    2. 缺点:
      1. 不能注入一个Final修饰的属性;
      2. 注入对象可能会被改变,因为setter方法可能会被多次调用,就有被修改的风险

示例:

为什么有final修饰的属性不能直接注入?

如果一个属性被final修饰,那么这个属性就变成了一个常量,不可被修改,而依赖注入的属性需要动态地修改属性的值,所以不能对final修饰的属性进行依赖注入。

但在构造方法中,final修饰的属性是可以被赋值的,这是因为构造方法是在对象创建时执行,此时final属性还没有被赋值,所以在构造方法中可以对final属性进行赋值。

总的来说**,final修饰的属性不能直接通过依赖注入赋值,是因为其不可变性和依赖注入的动态赋值机制相冲突**;但构造方法注入是一种合法且符合final语义的方案。

Autowired存在的问题

当同一个类类型存在多个Bean,使用 @Autowired 会存在问题。

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Bean
    public User user1(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        return user;
    }

    @Bean("u2")
    public User user2(){
        User user = new User();
        user.setName("李四");
        user.setAge(20);
        return user;
    }
}

那么应该如何解决上面这种情况呢?

在下面有给出了建议:

在Spring中,提供了以下几种方法:

  • @Primary
  • @Qualifier
  • @Resource

@Primary

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

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Bean
    public User user1(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        return user;
    }

    @Bean("u2")
    @Primary
    public User user2(){
        User user = new User();
        user.setName("李四");
        user.setAge(20);
        return user;
    }
}

@Qualifier

@Qualifier注解:指定当前要注入的Bean对象,在@Qualifier的value属性中,指定要注入Bean的名称

注意@Qualifier注解不能单独使用,必须配合@Autowired使用

@Resource

@Resource:按照Bean的名称来进行注入。通过name属性来指定要注入的bean名称

java 复制代码
    @Resource(name = "user1")
    private User user;

@Autowired自动装配流程图

@Autowired和@Resouce的区别

  • @Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊。相⽐于 @Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。

总结

Spring、Spring Boot和Spring MVC的关系及差别

区别

SpringSpring是一个轻量级、一站式、模块化的开发应用框架,其目的是用于简化企业应用程序开发。

Spring MVCSpring MVC是Spring的一个子框架,Spring诞生之后,那要如何使用?所以就需要提供接口,于是按照MVC设计模式设计了一个MVC框架(一些用Spring解耦的组件),主要用于Web开发和网络接口,简单来说,Spring MVC是一个Web框架。

Spring BootSpring Boot是对Spring的一个封装,为了简化Spring应用开发而出现的,使用Spring Boot能更加快速的搭建框架,降低成本,让开发者更加专注Spring应用开发,而无需过多关注XML的配置和一些底层实现。

关系

Spring的主要功能是管理对象,以及对象之间的依赖关系,AOP、数据库事务管理、数据访问、Wen框架等。

而Spring MVC是基于Spring开发的,与Spring框架集成,可以让我们更加简洁的进行Wen开发。

而Spring相当于一个脚手架,插拔式搭建项目,可以快速集成其他框架进来。

比如我们想要使用Spring Boot开发Web项目,那么只需要引入 Spring MVC即可,Wen开发的工作是Spring MVC完成的,而不是Spring Boot,想要完成数据访问,就只需要引入MyBatis框架。

用一句话总结:Spring MVC和Spring Boot都属于SprIng,Spring MVC是基于Spring的一个Web MVC框架,而Spring Boot是基于Spring的一套快速开发整合包

Bean的命名

五大注解存储Bean

  • 前两位字母均为大写,则Bean名称为类型

  • 其他的为类名首字母小写

  • 通过value属性设置,如@Controller(value="user")。

@Bean注解存储Bean

  • bean名称为方法名
  • 通过name属性设置

Spring中常见注解

  • web url映射:@RequestMapping
  • 参数接收和接口响应:@RequestParam、@RequestBody、@ResponseBody
  • bean的存储:@Controller、@Service、@Repository、@Component、@Configuration、@Bean
  • bean的获取:@Autowired、@Qualifier、@Resource

以上就是有关IoC和DI的相关知识。

若有不足,欢迎指正~

相关推荐
葫芦和十三4 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp4 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑5 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯6 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan8 小时前
多Agent之间的区别
后端
青石路9 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充10 小时前
1.面向对象设计思想
后端
IT_陈寒10 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro11 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗11 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端