Spring IoC&DI

文章目录

前言

前面我们大概知道了什么是 Spring,以及 Spring 家族中 Spring Boot 和 Spring MVC的开发,但是 Spring 到底是什么呢?

什么是Spring

前面我为大家简单介绍了什么是 Spring 【Spring】什么是Spring,不过前面的介绍较为简单,要想知道Spring 的原理,这些知识不不足以帮助我们了解 Spring 的,所以这篇文章我将详细为大家介绍什么是 Spring。

通过前面的学习,我们知道了 Spring 是一个开源的框架,它让我们的开发变得更加简单,它支持广泛的应用场景,有着活跃而庞大的社区,这也是 Spring 能够经久不衰的原因。

但是这个概念对于我们来说,还是太抽象了,用一句话概括:Spring 是包含了众多工具的 IoC 容器。那么什么是 IoC 容器呢?

1. 什么是 IoC 容器

1.1 什么是容器

容器是指能够容纳某种物品的装置。在生活中,储物箱、垃圾桶、冰箱等这些都属于容器,而在计算机中,我们前面学习的List/map就是数据存储的容器,Tomcat就是Web容器。

1.2 什么是 IoC

IoC 是 Spring 的核心思想。

IoC是Inversion of Control的缩写,多数书籍翻译成"控制反转"。1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IoC这个概念。对于面向对象设计及编程的基本思想,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。

在传统的程序设计中,对象的创建和管理都是由代码直接完成的。而在IoC中,对象的创建和管理权交给了IoC Service Provider(IoC思想的具体实现),我们只需要告诉它需要什么对象,它就会为我们准备好。这种机制的引入,使得应用程序的各个部分之间的依赖关系变得非常清晰,并且可以将各个部分解耦,提高代码的可重用性和可维护性。

给大家举个例子,传统的汽车开发过程是这样的:

用代码体现就是这样的:

java 复制代码
public class NewCarExample {
    public static void main(String[] args) {
        Car car = new Car();
        car.run();
    }

    /**
     * 汽车对象
     */
    static class Car {
        private Framework framework;
        public Car() {
            framework = new Framework();
            System.out.println("Car init...");
        }

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

    /**
     * 车身类
     */
    static class Framework {
        private Bottom bottom;

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

    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;

        public Bottom() {
            tire = new Tire();
            System.out.println("Bottom init...");
        }
    }

    /**
     * 轮胎类
     */
    static class Tire {
        private int size;

        public Tire() {
            this.size = 17;
            System.out.println("轮胎尺寸:" + size);
        }
    }
}

如果我们在造车的时候,需要造车的一方指定轮胎大小的话,那么这个生产车的代码进行较大的改动。





可以看到,当需要造车方指定轮胎的大小的时候,基本上所有的零件的代码都需要做出更改,这就叫做 高耦合

什么叫做高内聚、低耦合呢?

相比大家经常会听到高内聚、低耦合这句话吧,那么它们到底代表的什么意思呢?

"高内聚、低耦合"是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。

  • 高内聚:内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。一个模块内各个元素彼此结合的紧密程度高,则内聚性高。所谓高内聚就是一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。
  • 低耦合:耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。一个程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。

上面我们设计的代码的耦合程度就比较高,那么应该如何降低耦合度呢?

我们可以将各个零件之间的依赖关系给改变一下。

我们先根据需要,创造出指定大小的轮胎,然后将造好的轮胎给底盘创造厂,然后再造好底盘,将造好的底盘交给车身制造厂,制造出车身,最后将造好的车身交给汽车制造厂,最终制造出来一个汽车。

java 复制代码
public class NewCarExample {
    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);
        car.run();
    }

    /**
     * 汽车对象
     */
    static 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...");
        }
    }

    /**
     * 车身类
     */
    static class Framework {
        private Bottom bottom;

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

    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;

        public Bottom(Tire tire) {
            this.tire = tire;
            System.out.println("Bottom init...");
        }
    }

    /**
     * 轮胎类
     */
    static class Tire {
        private int size;

        public Tire(int size) {
            this.size = size;
            System.out.println("轮胎尺寸:" + size);
        }
    }
}

通过更改各个类之间的依赖关系,那么就算底层轮胎如何变化,也不会影响整个产业链,这样就实现了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。

通过上面的优化,我们发现:类的创建顺序是相反的,之前是 Car 控制并创建了 Framework,Framework 创建并控制创建了 Bottom,Bottom 创建并控制创建了 Tire,改进之后的控制权发生了反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了。

这样,即使依赖对象发生任何变化,当前类都是不受影响的,这就是典型的控制反转,也就是是 IoC 的实现思想。

知道了什么是容器以及什么是 IoC 之后我们就知道了什么叫做 IoC 容器了。

IoC 容器的优点:

通过上面的案例我们可以看出来,使用 IoC 容器,资源不再由使用资源的双方管理,而是由不使用资源的第三方进行管理,这样可以带来以下好处:1. 实现资源的集中统一管理;2. 降低了使用资源的双方的依赖程度,也就是耦合程度。

  1. 资源集中管理:IoC容器会帮我们管理一些资源(对象)等,我们在使用的时候只需要从IoC中去取就可以了。
  2. 我们在创建实例的时候不需要了解其中的具体细节,降低了使用资源的双方的依赖程度(耦合程度)。

2. 什么是DI

DI(Dependency Injection)即依赖注入,是面向对象编程中的一种设计模式,用来减少代码之间的耦合度。

具体来说,依赖注入将对象的创建和管理权从代码中转移到了外部容器,通过外部容器来创建对象并注入需要的依赖。这种方式可以降低代码的耦合度,提高代码的可重用性和可维护性。

在Java中,Spring框架是使用依赖注入最广泛的开源框架之一。通过使用依赖注入,Spring可以将应用程序中的各个组件解耦,使得它们之间的依赖关系变得更加清晰和易于管理。

容器在运行期间,动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。

从这点来看,依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦。

在造汽车的过程中,将 Tire 这个依赖注入到 Bottom 中造出 Bottom,然后将造好的 Bottom 依赖注入到 Framework 中造出 Framework,最后将造好的 Framework 依赖注入到 Car 中,最终创建出 Car。

IoC 是⼀种思想,也是"⽬标",⽽思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI 就属于具体的实现。所以也可以说, DI 是 IoC 的⼀种实现。

IoC & DI 的使用

Spring 既然是一个 IoC 容器,那么他肯定具有两个基本的功能:存和取。

Spring 容器管理的主要是对象,这些对象我们称之为"Bean",这个跟我们前面学习的 Bean 不一样。我们把这些 Bean 交给 Spring 进行管理,由 Spring 来负责对象的创建和销毁,我们在写 Spring 代码的时候只需要告诉 Spring,哪些对象是我们要交给 Spring 管理,我们又要取出哪些对象进行使用。

那么在 Spring 中,如何存储和取出 Bean 呢?

  1. 将类存储进 Spring IoC 容器中需要使用 @Component 注解,其实还有很多注解,这里我们先为大家介绍这个注解,本文后面再为大家介绍另外几种存储 Bean 的注解
  2. 取出依赖对象使用注解 @Autowired
java 复制代码
package com.example.springiocdi20231209;

import org.springframework.stereotype.Component;

@Component
public class UserComponent {
    public void sayHi() {
        System.out.println("hello spring");
    }
}
java 复制代码
package com.example.springiocdi20231209;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/component")
public class GetMessage {
    @Autowired
    private UserComponent userComponent;
    
    @RequestMapping("/get")
    public void get() {
        userComponent.sayHi();
    }
}


这里显示出了我们想要的结果,就说明我们使用 @Componnet 注解和 @Autowired 注解对 Bean 实现了存储和取出。

需要注意的是:当我们在使用 @Autowired 注解的时候,需要保证这个类有 Controller 或者 RestController 注解,因为我们既然要想使用 Spring 的 IoC 容器肯定要保证这个类是被 Spring 管理的。

IoC详解

上面为大家展示了 IoC 和 DI 的基本使用,接下来将为大家详细的讲解一下 IoC。

Bean的存储

上面我们存储 Bean 使用的是 @Component 注解,而 Spring 框架为了更好的服务 Web 应用程序,提供了更丰富的注解。

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

@Controller注解

java 复制代码
package com.example.springiocdi20231209.Controller;

import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

使用 @Controller 就将这个 Bean 给存储到 IoC 容器中了,那么我们如何获取这个 Bean 呢?

如何获取Bean

获取 Bean 的方法有很多种,我们只要介绍下面的第1、2、4种。

1. 根据Bean的名称获取Bean

我们可以根据 Bean 的名字来获取到指定的 Bean,但是某个 Bean 的名称是什么,我们该怎么知道呢?我们来看看官方的解释。



简单来讲就是当类名中前两个字母中大写字母小于2的时候,那么该类交给 IoC 后就会以第一个字母小写的小驼峰形式命名,当类名的前两个字母都为大写字母的时候,那么该 Bean 名就是原类名。

所以要以 Bean 名获取到 UserController 这个 Bean 的话,就需要将 userController 作为参数。

java 复制代码
package com.example.springiocdi20231209;

import com.example.springiocdi20231209.Controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

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

我们这个代码是在 项目名称+Application 这个类中写的,准确来说是在有 @SpringBootApplication 这个注解的类中写的,并且 SpringApplication.run() 方法是可以有返回值也可以没有返回值的,我们可以根据需要使用变量来接收这个方法的返回值。要想获取到 IoC 容器中的 Bean,需要依靠 ApplicationContext 这个类,所以我们就用这个类的变量来接收 run 方法的返回值。

启动项目的时候,就会发现我们预想中的结果出现在了控制台中,并且这个不需要我们发送什么 Http 请求,而是启动项目就会自动执行这个类当中的代码。并且通过 Bean 名获取到的 Bean 名返回的是一个 Object 类型,所以在拿变量进行接收的时候就需要进行类型的转换。

2. 根据Bean类型获取Bean

可以通过 Bean 类型来获取到 Bean。

java 复制代码
package com.example.springiocdi20231209;

import com.example.springiocdi20231209.Controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class SpringIoCDi20231209Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args);
        //1. 根据Bean名字来获取Bean
        //UserController userController = (UserController) context.getBean("userController");
        //2. 根据Bean类型来获取Bean
        UserController userController = context.getBean(UserController.class);
        userController.sayHi();
    }
}
3. 根据Bean名和Bean类型获取Bean

通过 Bean 名获取 Bean 需要进行类型的转换,可以在传递参数的时候就指定返回值的类型。

java 复制代码
package com.example.springiocdi20231209;

import com.example.springiocdi20231209.Controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class SpringIoCDi20231209Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args);
        //1. 根据Bean名字来获取Bean
        //UserController userController = (UserController) context.getBean("userController");
        //2. 根据Bean类型来获取Bean
        //UserController userController = context.getBean(UserController.class);
        //3. 根据Bean名和Bean类型获取Bean
        UserController userController = context.getBean("userController", UserController.class);
        userController.sayHi();
    }
}

@Service注解

通过这个注解,也可以将类交给 Spring 进行管理。

java 复制代码
package com.example.springiocdi20231209.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;

@Service
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

@Repository注解

java 复制代码
package com.example.springiocdi20231209.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Repository
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

@Component注解

java 复制代码
package com.example.springiocdi20231209.Controller;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Component
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

@Configuration注解

java 复制代码
package com.example.springiocdi20231209.Controller;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Configuration
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

为什么会有这么多类注解

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

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

并且通过观察这五个注解的源码我们可以发现一些问题。





查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:

其实这些注解⾥⾯都有⼀个注解 @Component ,说明它们本⾝就是属于@Component 的"⼦类".@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等.这些注解被称为 @Component 的衍⽣注解.

@Controller @Service 和 @Repository ⽤于更具体的⽤例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果你要在业务逻辑层使⽤ @Component 或@Service,显然@Service是更好的选择。

方法注解

类注解是写在我们项目代码中的类上的,但是存在两个问题:

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

上面两个问题是无法使用类注解来解决的。所以也就出现了方法注解 @Bean

假设我们这里的 User 类是一个外部包里的类,那么我们就无法在这个类中添加类注解,这是就需要使用到方法注解。

java 复制代码
package com.example.springiocdi20231209;

import org.springframework.context.annotation.Bean;

public class BeanConfig {
    @Bean
    public User user() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}

运行 @SpringApplication 注解的代码,看看什么效果。

这里报错说这个 Bean 没有被定义。其实使用方法注解 @Bean 的时候,需要保证该方法所在的类也有被类注解注释。

java 复制代码
package com.example.springiocdi20231209;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class BeanConfig {
    @Bean
    public User user() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}
java 复制代码
package com.example.springiocdi20231209;

import com.example.springiocdi20231209.Controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

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

同一个类定义多个对象。

java 复制代码
package com.example.springiocdi20231209;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class BeanConfig {
    @Bean
    public User user() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

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

当我们使用方法注解,并且一个类有多个相同类型的 Bean 类型的时候,并且我们通过 Bean 类型获取 Bean 的话就会出错。

所以这里获取 Bean 的话就需要指定 Bean 名称。

java 复制代码
User user = context.getBean("user2", User.class);

@Bean 注解的 Bean,Bean 的,名称就是方法名。

重命名Bean

前面我们呢说了 Bean 的默认名称,但其实我们可以指定 Bean 的名称。那么如何重命名 Bean 呢?

java 复制代码
@Bean("beanName")
java 复制代码
package com.example.springiocdi20231209;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class BeanConfig {
    @Bean("u1")
    public User user() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }

    @Bean("u2")
    public User user2() {
        User user = new User();
        user.setName("liis");
        user.setAge(20);
        return user;
    }
}
java 复制代码
ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args);
User user = context.getBean("u2", User.class);
System.out.println(user);

可以看到我们通过重命名的名字u2获取到了Bean。

不仅如此,通过观察 @Bean 的源码我们可以发现,这里的name参数是一个字符串数组,也就是说一个 Bean 可以有多个名字。

java 复制代码
@Bean({"u2", "s2"})

同样的类注解也可以重命名,但是类注解只支持一个名字。

java 复制代码
@Configuration("c1")
public class UserController {
    public void sayHi() {
        System.out.println("hi, spring");
    }
}

扫描路径

其实并不是项目下的所有文件中的加了注解的类都会被 Spring 进行管理,而是需要看扫描路径在哪。假设我们将 @SpringBootApplication 注解所在的类给换个路径。

java 复制代码
@SpringBootApplication
public class SpringIoCDi20231209Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args);
        User user = context.getBean("u1", User.class);
        System.out.println(user);
    }
}

这里就报错说找不到 u1 这个 Bean,说明这个注解没有被扫描到,那么为什么呢?

这其实跟 @ComponentScan 注解配置的扫描路径有关,但是我们 SpringBootApplication 注解的类当中不是没有这个注解吗?其实这个注解继承了 @ComponentScan 注解。

而如果 @ComponnetScan 没有配置的话,就默认的是当前 @ComponentScan 注解的文件所在的路径。

这里 @SpringBootApplication 注解的类所在的路径是这个 package com.example.springiocdi20231209.springiocdi20231209.Controller;

而我们的 u1 Bean 所在的路径是 package com.example.springiocdi20231209.springiocdi20231209;,所以这个 Bean 是 Component 无法扫描到的。

要想扫描到这个路径,我们可以对 @ComponentScan 注解进行配置。

java 复制代码
@ComponentScan({"com.example.springiocdi20231209.springiocdi20231209"})
@SpringBootApplication
public class SpringIoCDi20231209Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIoCDi20231209Application.class, args);
        User user = context.getBean("u1", User.class);
        System.out.println(user);
    }
}


@ComponentScan 也是可以配置多个扫描路径的。

DI 详解

依赖注⼊是⼀个过程,是指IoC容器在创建Bean时,去提供运⾏时所依赖的资源,⽽资源指的就是对象。在上⾯程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作。简单来说,就是把对象取出来放到某个类的属性中。

在⼀些⽂章中,依赖注⼊也被称之为"对象注⼊","属性装配",具体含义需要结合⽂章的上下⽂来理解。

关于依赖注入,Spring 为我们提供了三种方法:

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

1. 属性注入

属性注⼊是使⽤ @Autowired 实现的。

java 复制代码
package com.example.springiocdi2;

import org.springframework.stereotype.Service;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
	//属性注入
    @Autowired
    private UserService userService;
    public void sayHi() {
        System.out.println("Hi UserController...");
        userService.sayHi();
    }
}
java 复制代码
package com.example.springiocdi2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class SpringIoCDi2Application {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringIoCDi2Application.class, args);
        UserController userController = context.getBean("userController", UserController.class);
        userController.sayHi();
    }

}

运行结果:

构造方法注入

构造⽅法注⼊是在类的构造⽅法中实现注⼊。

java 复制代码
package com.example.springiocdi2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

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


注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法,
那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法。


Setter 注入

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

解。

java 复制代码
package com.example.springiocdi2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController3 {
    //setter方法注入
    private UserService userService;
    
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
    public void sayHi() {
        System.out.println("Hi UserController3");
        userService.sayHi();
    }
}

如果没加@Autowired 注解,就会报错。

三种注入的优缺点

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

属性注入,注入一个final修饰的属性:

构造方法注入,注入一个final修饰的属性:

Setter注入,注入一个final修饰的属性:

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

如果一个属性被final关键字修饰,那么这个属性就成为了一个常量,它的值就不能被改变。而依赖注入的属性注入需要动态地修改属性的值,所以不能对被final关键字修饰的属性进行依赖注入。但是,在构造方法中,final属性可以被赋值。这是因为构造方法是在对象创建时执行的,此时final属性还没有被赋值。因此,在构造方法中可以对final属性进行赋值操作。

Autowired 存在的问题

当同一个类类型存在多个 Bean 时,就会出现问题。

java 复制代码
package com.example.springiocdi2;

import lombok.Data;

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

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class BeanConfig {
    @Bean("u1")
    public User user1() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(17);
        return user;
    }

    @Bean("u2")
    public User user2() {
        User user = new User();
        user.setName("lisi");
        user.setAge(18);
        return user;
    }
}
java 复制代码
package com.example.springiocdi2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserController4 {
    @Autowired
    private User user;

    public void sayHi() {
        System.out.println("Hi UserController4");
        System.out.println(user);
    }
}

如何解决这个一个类型有多个 bean 的问题呢?Spring 提供了以下的几种方案:

  • @Primary
  • @Qualifier
  • Resource

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

java 复制代码
	@Bean("u1")
    @Primary  //指定该bean为默认实现
    public User user1() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(17);
        return user;
    }

使⽤@Qualifier注解:指定当前要注⼊的bean对象。在@Qualifier的value属性中,指定注⼊的bean

的名称。

  • @Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
java 复制代码
	@Qualifier("u2")
    @Autowired
    private User user;

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

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

常见面试题:

@Autowird 与 @Resource的区别:

  • @Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注⼊,⽽@Resource是按照名称注⼊。相⽐于 @Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean
相关推荐
向前看-36 分钟前
验证码机制
前端·后端
xlsw_37 分钟前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹1 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫2 小时前
泛型(2)
java
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
南宫生2 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石2 小时前
12/21java基础
java
李小白663 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp3 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea