Spring IOC 和 DI详解

发布于:2024-04-23 ⋅ 阅读:(138) ⋅ 点赞:(0)

目录

一、IOC介绍

1、什么是IOC

2、通过案例来了解IoC

 2.1 传统程序开发

2.2 问题分析

2.3 解决方案

 2.4 IoC程序开发

2.5 IoC 优势

二、DI介绍

三、IOC 详解

3.1 Bean的存储

3.1.1  @Controller(控制器存储)

3.1.2 @Service(服务存储)

 3.1.3 @Repository(仓库存储)

 3.1.4 @Component(组件存储)

 3.1.5 @Configuration(配置存储)

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

3.3 方法注解 @Bean

3.3.1 方法注解要配合类注解使用

 3.3.2 定义多个对象

3.3.3 重命名 Bean

四、DI 详解

4.1 属性注入

4.2 构造方法注入

4.3 Setter 注入

4.4 @Autowired存在问题


一、IOC介绍

1、什么是IOC

IOC 是Spring的核心思想。
其实IOC在前面 Spring MVC 部分我们就已经用到了。代码部分, 我们在类上添加@RestController 和 @Controller 注解, 就是把这个对象交给Spring管理Spring 框架启动时就会加载该类。 把对象交给Spring管理, 就是IoC思想。
IOC: Inversion of Control (控制反转), 也就是说 Spring 是一个"控制反转"的容器.
什么是控制反转呢? 也就是控制权反转。 什么的控制权发生了反转? 获得依赖对象的过程被反转了。
也就是说, 当需要某个对象时, 传统开发模式中需要自己通过 new 创建对象, 现在不需要再进行创 建, 把创建对象的任务交给容器, 程序中只需要依赖注入 (Dependency Injection,DI)就可以了.
这个容器称为:IoC容器. Spring是一个IoC容器, 所以有时Spring 也称为Spring 容器。

2、通过案例来了解IoC

 案例:造一辆车。

实现思路:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最 后根据车身设计好整个汽车(Car)。
这里就出现了⼀个"依赖"关系:汽车依赖车身,车身依赖底盘,底 盘依赖轮子。

 2.1 传统程序开发

最终程序的实现代码如下:
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() {
            this.tire = new Tire();
            System.out.println("Bottom init...");
        }
    }
    /**
     * 轮胎类
     */
    static class Tire {
        // 尺⼨
        private int size;
        public Tire(){
            this.size = 17;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}

2.2 问题分析

这样的设计看起来没问题,但是可维护性却很低.
如果接下来需求有了变更: 随着对的车的需求量越来越大, 个性化需求也会越来越多,我们需要加工多种尺寸 的轮胎.
那这个时候就要对上面的程序进行修改了,修改后的代码如下所示:
 /**
     * 轮胎类
     */
    static class Tire {
        // 尺⼨
        private int size;
        public Tire(int size){
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }

 修改之后, 底盘类就会报错:

那么我们将底盘类的构造方法也传一个参数后,底盘类方法不再报错。但其他调用程序又会出现错误,所以我们都要进行修改。

修改后完整代码:

public class NewCarExample {
    public static void main(String[] args) {
        Car car = new Car(20);
        car.run();
    }
    /**
     * 汽⻋对象
     */
    static class Car {
        private Framework framework;
        public Car(int size) {
            framework = new Framework(size);
            System.out.println("Car init....");
        }
        public void run(){
            System.out.println("Car run...");
        }
    }
    /**
     * ⻋⾝类
     */
    static class Framework {
        private Bottom bottom;
        public Framework(int size) {
            bottom = new Bottom(size);
            System.out.println("Framework init...");
        }
    }
    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;
        public Bottom(int size) {
            this.tire = new Tire(size);
            System.out.println("Bottom init...");
        }
    }
    /**
     * 轮胎类
     */
    static class Tire {
        // 尺⼨
        private int size;
        public Tire(int size){
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}
从以上代码可以看出,以上程序的问题是:当最底层代码改动之后,整个调用链上的所有代码都需要 修改.
程序的耦合度非常高(修改一处代码, 影响其他处的代码修改)

2.3 解决方案

在上面的程序中, 我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改. 同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改, 也就是整个设计几乎都得改。
我们尝试换⼀种思路, 我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计 底盘,最后根据底盘来设计轮子. 这时候,依赖关系就倒置过来了。
如何来实现呢:
我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作, 自己也要跟着修改.
此时,我们只需要将原来由自己创建的下级类, 改为传递的方式(也就是注入的方式) ,因为我们不 需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修 改任何代码,这样就完成了程序的解耦。
我们可以这样理解:
当我们造一辆汽车时,如果我们不自己制作轮胎,而是把轮胎外包出去,那 么即使当用户的需求发生改变,需要改变车轮尺寸大小时,我们只需要向代理工厂下订单就行了,我们自身是不需要出力的。

 2.4 IoC程序开发

基于以上思路,我们把造汽车的程序示例改造⼀下,把创建子类的方式,改为注入传递的方式.
具体实现代码如下:
public class IocCarExample {
    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);
        }
    }
}
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间 的解耦,从而实现了更加灵活、通用的程序设计了。

2.5 IoC 优势

在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
我们发现了⼀个规律,改进之后程序的实现代码,类的创建顺序是反的。
传统代码是 Car 控制并创建了 Framework,Framework 控制并创建了 Bottom,依次往下,而改进之后的控制权发生了反转,不再 是使用方对象创建并控制依赖对象了,而是把依赖对象注入将当前对象中,依赖对象的控制权不再由当前类控制了.
这样的话, 即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的 实现思想。
那什么是控制反转容器呢, 也就是IoC容器:
这部分代码, 就是IoC容器做的工作.
IOC容器具备以下优点:
资源不由使用资源的双方管理,而由不使用资源的第三方管理 ,这可以带来很多好处。
第一,资源集 中管理,实现资源的可配置和易管理。
第⼆,降低了使用资源双方的依赖程度,也就是我们说的耦合 度。
  1. 资源集中管理: IoC容器会帮我们管理一些资源(对象等), 我们需要使用时, 只需要从IoC容器中去取就可以了
  2. 我们在创建实例的时候不需要了解其中的细节, 降低了使用资源双方的依赖程度, 也就是耦合度.

Spring 就是一种IoC容器, 帮助我们来做了这些资源管理。

二、DI介绍

DI: Dependency Injection(依赖注入)
容器在运行期间, 动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。
上述代码中, 是通过构造函数的方式, 把依赖对象注入到需要使用的对象中的。
IoC 是一种思想,也是"目标",而思想只是⼀种指导原则,最终还是要有可行的落地方案,而 DI 就属于 具体的实现。所以也可以说, DI 是IoC的一种实现

三、IOC 详解

既然 Spring 是一个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能:
Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由 Spring来负责对象的创建和销毁。
我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中取出 对象。
IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对 象。 也就是bean的存储

3.1 Bean的存储

Spring框架为了更好地服务web应用程序, 提供了丰富的注解.
共有两类注解类型可以实现:
  • 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
  • 法注解:@Bean.

3.1.1  @Controller(控制器存储)

 使用 @Controller 存储 bean 的代码如下:

@Controller // 将对象存储到 Spring 中
public class UserController {
    public void sayHi(){
        System.out.println("hi,UserController...");
    }
}

然后我们来验证一下Spring容器中是否已经有了该对象:

从Spring 容器中获取对象(bean):

@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
		//从Spring上下⽂中获取对象
		UserController userController = context.getBean(UserController.class);
		//使⽤对象
		userController.sayHi();
	}
}
观察运行结果, 发现成功从Spring中获取到Controller对象, 并执行Controller的sayHi方法:
如果把@Controller删掉, 再观察运行结果,会发现报错,报错信息显示:找不到类型是: com.example.demo.controller.UserController 的 bean。
获取bean对象的其他方式:
上述代码是根据类型来查找对象, 如果Spring容器中,同一个类型存在多个bean的话, 怎么来获取呢?
ApplicationContext 也提供了其他获取bean的方式, ApplicationContext 获取bean对象的功能, 是父 类BeanFactory提供的功能.
我们来看一下其父类代码:
public interface BeanFactory {

	//以上省略...

	// 1. 根据bean名称获取bean
	Object getBean(String var1) throws BeansException;
	// 2. 根据bean名称和类型获取bean
	<T> T getBean(String var1, Class<T> var2) throws BeansException;
	// 3. 按bean名称和构造函数参数动态创建bean,只适⽤于具有原型(prototype)作⽤域的bean
	Object getBean(String var1, Object... var2) throws BeansException;
	// 4. 根据类型获取bean
	<T> T getBean(Class<T> var1) throws BeansException;
	// 5. 按bean类型和构造函数参数动态创建bean, 只适⽤于具有原型(prototype)作⽤域的
	bean
	<T> T getBean(Class<T> var1, Object... var2) throws BeansException;

	//以下省略...
}

其中第一、二、四种方式,是常见的。其中有通过 bean 的名称来获取 bean。

Spring bean是Spring框架在运行时管理的对象, Spring会给管理的对象起一个名字. 给每个对象起一个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.

 Bean 命名约定

程序开发人员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该 bean 生成唯一的名称.
命名约定使用Java标准约定作为实例字段名。也就是说, bean名称以小写字母开头,然后使用驼峰式大小写。

比如:

类名: UserController, Bean的名称为: userController
类名: AccountManager, Bean的名称为: accountManager
类名: AccountService, Bean的名称为: accountService

也有一些特殊情况, 当有多个字符并且第一个和第二个字符都是大写时, 将保留原始的大小写。

比如:
类名: UController, Bean的名称为: UController
类名: AManager, Bean的名称为: AManager

 在知道 bean 的名称约定之后,我们来看看以多种方式获取 bean 对象是如何实现的。

示例代码:

@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        //从Spring上下⽂中获取对象

        //根据bean类型, 从Spring上下⽂中获取对象
        UserController userController1 = context.getBean(UserController.class);

        //根据bean名称, 从Spring上下⽂中获取对象
        UserController userController2 = (UserController)context.getBean("userController");

        //根据bean类型+名称, 从Spring上下⽂中获取对象
        UserController userController3 =
                context.getBean("userController",UserController.class);

        System.out.println(userController1);
        System.out.println(userController2);
        System.out.println(userController3);
    }
}

运行结果:

 

 我们可以看到,地址一样,说明对象是同一个。

3.1.2 @Service(服务存储)

使用 @Service 存储 bean 的代码如下所示:

@Service
public class UserService {
    public void sayHi(String name) {
        System.out.println("Hi," + name);
    }
}

 获取 bean 的代码同 @Controller 部分的代码,我们可以获取 bean ,让 bean 对象执行 sayHi 方法,看打印结果,来判断是否获取到了bean。

此处,若把 @Service 注解去掉,会出现 与去掉 @Controller 注解 一样的错误。

 3.1.3 @Repository(仓库存储)

 使用@Repository 存储 bean 的代码如下所示:

@Repository
public class UserRepository {
    public void sayHi() {
        System.out.println("Hi, UserRepository~");
    }
}

 3.1.4 @Component(组件存储)

 使用@Component 存储 bean 的代码如下所示:

@Component
public class UserComponent {
    public void sayHi() {
        System.out.println("Hi, UserComponent~");
    }
}

 3.1.5 @Configuration(配置存储)

使用 @Configuration 存储 bean 的代码如下所示:
@Configuration
public class UserConfiguration {
    public void sayHi() {
        System.out.println("Hi,UserConfiguration~");
    }
}

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

这与我们前面文章所介绍的应用分层是呼应的。可以让我们看到类注解之后,就能直接了解当前类的用途。
  • @Controller:控制层, 接收请求, 对请求进行处理, 并进行响应.
  • @Servie:业务逻辑层, 处理具体的业务逻辑.
  • @Repository:数据访问层,也称为持久层. 负责数据访问操作
  • @Configuration:配置层. 处理项目中的一些配置信息.

3.3 方法注解 @Bean

类注解是添加到某个类上的, 但是存在两个问题:
  1. 使用外部包里的类, 没办法添加类注解
  2. 一个类, 需要多个对象, 比如多个数据源
这种场景, 我们就需要使用方法注解 @Bean。

3.3.1 方法注解要配合类注解使用

在 Spring 框架的设计中, 方法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中 如下代码所示:
@Component
public class BeanConfig {
    @Bean
    public User user(){
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}

 来获取 bean 对象:

@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context =
                SpringApplication.run(SpringIocDemoApplication.class, args);
        //从Spring上下⽂中获取对象
        User user = context.getBean(User.class);
        //使⽤对象
        System.out.println(user);
    }
}

执行代码,可以看到正确结果:

 3.3.2 定义多个对象

对于同一个类, 如何定义多个对象呢?

示例代码:
@Component
public class BeanConfig {
    @Bean
    public User user1(){
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
    @Bean
    public User user2(){
        User user = new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}

在上述代码,我们一个类型(User),定义了多个对象。

那我们根据类型获取对象, 获取的是哪个对象呢?我们可以通过代码来看一下:
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context =
                SpringApplication.run(SpringIocDemoApplication.class, args);
        //从Spring上下⽂中获取对象
        User user = context.getBean(User.class);
        //使⽤对象
        System.out.println(user);
    }
}
执行代码,我们可以发现报错了。
报错信息显示: 期望只有一个匹配, 结果发现了两个, user1, user2
从报错信息中, 也可以看出来, @Bean 注解的bean, bean的名称就是它的方法名。
接下来我们根据名称来获取bean对象:
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context =
                SpringApplication.run(SpringIocDemoApplication.class, args);
        //根据bean名称, 从Spring上下⽂中获取对象
        User user1 = (User) context.getBean("user1");
        User user2 = (User) context.getBean("user2");
        System.out.println(user1);
        System.out.println(user2);
    }
}

运行结果:

可以看到, @Bean 可以针对同一个类, 定义多个对象.

3.3.3 重命名 Bean

 我们可以通过设置 name 属性给 Bean 对象进行重命名操作,如下代码所示:

@Bean(name = {"u1","user1"})
public User user1(){
    User user = new User();
    user.setName("zhangsan");
    user.setAge(18);
    return user;
}

代码中,name={} 可以省略,直接写为:@Bean({"u1","user1"})

 此时我们使用名字 u1 就可以获取到 User 对象了。

注意:

使用五大注解声明的bean,不一定生效,要想生效,就必须让Spring扫描到这些注解。

启动类默认扫描的范围是 SpringBoot启动类所在包及其子包。
若想让 Spring 扫描到默认范围以外的地方,就需要通过 @ComponentScan 来配置扫描路径。

四、DI 详解

依赖注入是一个过程,是指IoC容器在创建Bean时, 去提供运行时所依赖的资源,而资源指的就是对象.
关于依赖注入, Spring也给我们提供了三种方式:
  1. 属性注入
  2. 构造方法注入
  3. Setter 注入

4.1 属性注入

属性注入是使用 @Autowired 实现的

我们以 Service 类注入到 Controller 类中 为例:

Service 类的实现代码如下:
@Service
public class UserService {
    public void sayHi() {
        System.out.println("Hi,UserService");
    }
}

Controller 类的实现代码如下:

@Controller
public class UserController {
    //注⼊⽅法1: 属性注⼊
    @Autowired
    private UserService userService;

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

如果不加  @Autowired 注解,也就是userService对象没有注入进来,当执行到 userService.sayHi() 时,会报空指针异常。

4.2 构造方法注入

构造方法注入是在类的构造方法中实现注入,示例代码:
@Controller
public class UserController2 {
    //注⼊⽅法2: 构造⽅法
    private UserService userService;

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

    public void sayHi(){
        System.out.println("hi,UserController2...");
        userService.sayHi();
    }
}
注意事项: 如果类只有一个构造方法,那么 @Autowired 注解可以省略;如果类中有多个构造方法, 那么需要添加上 @Autowired 来明确指定到底使用哪个构造方法。

4.3 Setter 注入

Setter 注入和属性的 Setter 方法实现类似,只不过 在设置 set 方法的时候需要加上 @Autowired 注解 ,如下代码所示:
@Controller
public class UserController3 {
    //注⼊⽅法3: Setter⽅法注⼊
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public void sayHi(){
        System.out.println("hi,UserController3...");
        userService.sayHi();
    }
}

4.4 @Autowired存在问题

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

示例代码:

@Component
public class BeanConfig {
    @Bean("u1") //bean 重命名为u1
    public User user1(){
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
    @Bean
    public User user2() {
        User user = new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}
@Controller
public class UserController {
    //注⼊user
    @Autowired
    private User user;

    public void sayHi(){
        System.out.println("hi,UserController...");
        System.out.println(user);
    }
}

运行代码发现会报错,报错的原因是,非唯一的 Bean 对象。

为了解决上述问题,Spring提供了以下几种解决方案:
  1. @Primary
  2. @Qualifier
  3. @Resource

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

@Component
public class BeanConfig {

    @Primary //指定该bean为默认bean的实现
    @Bean("u1") //bean 重命名为u1
    public User user1(){
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
    @Bean
    public User user2() {
        User user = new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}
使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean 的名称。
@Qualifier注解不能单独使用,必须配合@Autowired使用:
@Controller
public class UserController {
    @Qualifier("user2") //指定bean名称
    @Autowired
    private User user;
    public void sayHi(){
        System.out.println("hi,UserController...");
        System.out.println(user);
    }
}
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
@Controller
public class UserController {
    @Resource(name = "user2")
    private User user;
    public void sayHi(){
        System.out.println("hi,UserController...");
        System.out.println(user);
    }
}
@Autowird 与 @Resource的区别:
  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解。
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入. 相比于 @Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。

关于 Spring IOC(控制权反转)、DI(依赖注入)就先介绍到这里了,希望可以给你带来帮助呀!


网站公告

今日签到

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