1. Spring IoC 容器 | Spring 学习笔记

在上次的笔记中,为大家简单的介绍了 Spring点击查看

Spring Framework 中,最重要的莫过于 控制反转容器 了,英文名称 Inversion of Control (IoC) container。相信每个程序员都听过这个词,那么这玩意儿到底是啥?

控制反转 Inversion of Control

在学习 IoC 容器 之前,我们有必要先了解一下,什么是 控制反转

控制反转 (Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,可以减少代码之间的耦合度,耦合度这东西是程序员最大的敌人,如果程序的耦合度太高,那么维护起来(Bug修复,新功能开发)的成本就会越来越高,这就好比一体机和组装电脑,如果主板坏了,那么组装电脑只用把主机箱抱去维修就行了,但是一体机就要把所有东西拿去维修。

控制反转最常见的实现叫做依赖注入(Dependency Injection,简称 DI),大家不要把控制反转依赖注入搞混了,前者是一种设计原则,而后者是一种具体的实现。比如控制反转的实现还有依赖查找

那么控制反转到底反转了什么呢?

我们知道在面向对象编程中,最重要的当然是一个一个的对象,程序的运行靠的就是这些对象之间的彼此合作来完成具体的业务,那么这个对象是如何创建和获取的呢?在没有控制反转之前,这个对象就是我们自己创建并获取的,比如:

SmsService service = new AliyunSmsService();
service.send(user, "测试短信");

这里我有一个短信发送服务,它负责向一个用户发送一条短信,那么我们如果获取我们想要的短信服务呢?很直接,我们在这里直接创建一个短信服务,是基于阿里云的短信发送服务,这样,我们就自己手动创建并且获取到了我们的业务对象。
上面的代码看似没有问题,但是这样的代码就有很高的耦合性了,比如有一天我们需要将短信服务从阿里云换到腾讯云,那么我们就只能回来修改代码了:

SmsService service = new QcloudService();
service.send(user, "测试短信");

这样是会提高很多维护的成本,如果,我们能把代码写成下面的样子就好了

SmsService service = 给我一个短信服务();
service.send(user, "测试短信");

这个业务的对象的获取不是我自己来做,而且别人来做的,那这样的话,我不是就可以随意的切换我想要的短信服务了吗。
控制反转 就是帮我们干这个事情的,它反转的也正是把我们主动控制对象的创建和获取反转为别人来控制对象的创建和获取

上面说的别人也就是我们所说的控制反转容器(IoC Container)。

控制反转的实现方式有两种:

依赖注入依赖查找,依赖注入是被动地接受对象,依赖查找是主动的查询依赖的对象。

依赖注入的实现方式有:

  • 基于接口:对象实现了特定的接口来让容器注入所依赖的对象
  • 基于set方法:实现特定字段的 public set 方法,让容器传入所依赖类型的对象
  • 基于构造函数:实现特定参数的构造函数,在创建对象时传入所依赖的对象
  • 基于注解:基于特定的注解,告诉容器需要注入什么样的依赖对象

Spring IoC 容器

上面我讲到了什么是控制反转,我们可以看到,控制反转以后,最核心的一定就是上面所谓的容器了,创建对象,传入依赖这些工作都是容器来做的,那么接下我们就介绍在 Spring Framework 中的 IoC 容器

Spring 框架中,IoC 容器 可以通过对象 构造函数参数工厂方法参数属性set方法 来注入依赖。

容器会在创建 Bean 的时候进行依赖注入, Bean 就是容器管理的对象的统称。

org.springframework.beansorg.springframework.contextSpring Framework IoC 容器的基础。接口 BeanFactory 代表了 IoC 容器,可以管理任何类型的对象,我们看看该接口提供的方法有哪些:

BeanFactory

但是使用的更广泛的是该接口的子类 ApplicationContext,该接口提供了以下额外的功能:

  • Spring AOP 支持,点此查看Spring AOP教程
  • 国际化(Internationalization),也叫i18n
  • 事件发布
  • 应用层的上下文,比如 web 应用的 WebApplicationContext

简单来说 BeanFactory 提供了容器相关的配置框架,而 ApplicationContext 增加更多的企业级功能。

Spring 中,由 IoC 容器管理,用来组成应用骨架的这些对象,叫做 Bean。一个 Bean 就是一个对象,这个对象通过容器来控制它的创建和组装(注入依赖)。BeanBeanBean之间的依赖关系,通过配置来体现,这些配置是通过 Java 注解或 Java 代码来实现的。(在以前的应用中,大量使用的 XML 的配置方法,这种方式已经使用的越来越少了,在这里就不做介绍了)

容器概览

接口 org.springframework.context.ApplicationContext 代表了 Spring IoC 容器,该容器负责初始化、配置和组装各种 Bean

下图展现了 IoC 容器是如何工作的:

The Spring IoC Container

应用通过各种配置组成(Java 代码或Java 注解),然后在 ApplicationContext 被创建和初始化之后,就得到了一个已经配置好可以使用的系统。

配置元数据 (Configuration Metadata)

通过上面的图片,我们知道 Spring IoC 容器 通过 配置元数据 来进行配置。通过配置元数据来告诉容器应该如何初始化、配置和组装应用中的各种对象。

我们通过一个例子来演示如何使用 注解 或 代码 来进行配置,例子使用了 Spring Boot 来简化配置。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>aaric</groupId>
    <artifactId>spring-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 将上面的XML内容保存为 pom.xml 并使用 IntelliJ 以项目的方式打开
  • 创建启动类 Application
package aaric;

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

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(Application.class);
        SmsService smsService = applicationContext.getBean(SmsService.class);
        smsService.send("Aaric", "hello world");
    }
}
  • 可以看到在启动类创建了 IoC 容器
  • 在IoC容器中根据类型取得了一个SmsService
  • 进行短信的发送

我们再看看接口SmsService

package aaric;

public interface SmsService {
    void send(String user, String content);
}

该接口有两个实现,一个是阿里云的短信发送,一个是腾讯云的短信发送

package aaric.impl;

import aaric.SmsService;

public class AliyunSmsService implements SmsService {
    @Override
    public void send(String user, String content) {
        System.out.println("Aliyun Send sms to user " + user + " with content " + content);
    }
}
package aaric.impl;

import aaric.SmsService;

public class QcloudSmsService implements SmsService {
    @Override
    public void send(String user, String content) {
        System.out.println("Qcloud Send sms to user " + user + " with content " + content);
    }
}
  • 我们知道,要在容器中使用 Bean 必须先将 Bean 放在容器中,那么如何放在容器中呢?
  • 接下来我们将通过 Java 代码的方式来进行 Bean的配置
  • 创建如下的配置类
package aaric;

import aaric.impl.AliyunSmsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {

    @Bean
    public SmsService getService() {
        return new AliyunSmsService();
    }
}

该类有几个特点

  • @Configuration 注解表明该类是一个配置类,同时也是一个 Bean,配置类一般就是我们用来定义 Bean 的地方

  • @Bean 注解表明该方法返回的返回值会作为一个 Bean 放在容器中,这里我们将阿里云的实现放入了容器中

  • 运行程序以后得到如下结果


  • 可以看到我们从容器中获取的 Bean 确实为阿里云的短信服务

  • 如果现在需要将阿里云的短信服务改为腾讯云的短信服务,我们只需要修改创建 Bean 的配置就可以了

package aaric;

import aaric.impl.QcloudSmsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {

    @Bean
    public SmsService getService() {
        return new QcloudSmsService();
    }
}
  • 运行程序后得到如下结果


  • 可以看到,我们在没有修改代码的情况下,就切换了短信服务的实现,这就是控制反转的力量

  • 除了通过 Java 代码,即在 @Configuration 类里面通过 @Bean 来定义 Bean, 还可以使用注解的方式来定义 Bean,下面我们用同样的例子来试试通过注解的方式来定义 Bean

  • 删掉ApplicationConfiguration

  • 在我们要用的实现类上使用 @Service 注解,比如阿里云服务

package aaric.impl;

import aaric.SmsService;
import org.springframework.stereotype.Service;

@Service
public class AliyunSmsService implements SmsService {
    @Override
    public void send(String user, String content) {
        System.out.println("Aliyun Send sms to user " + user + " with content " + content);
    }
}
  • 运行结果如下


  • 我们发现 Bean 同样放入到了容器中

  • 如果要切换服务的实现,只需要将 @Service 注解从 阿里云服务中删掉,然后写在腾讯云服务的实现类上就可以了

我们可以看到通过这两种方式都可以达到目的,那么应该在什么情况下使用什么方式呢,推荐的有,如果是想要创建第三方的业务对象,放入容器中,那么只能使用代码的方式,即通过 @Bean 注解来创建,如果是自己写的业务对象,那么推荐使用 注解的方式来定义 Bean, 包括 @Service 注解在内,Spring Framework 一共提供了四种注解来定义 Bean

  • @Component:一般情况下使用该注解
  • @Service:作为服务的 Bean 一般使用它
  • @Repository: 作为数据持久化的 Bean 一般使用它
  • @Controller: 在 MVC 中的 C 使用它

在上面的例子中,我们对容器有了一个直观的感受,Spring Boot也放我们完成了容器的启动、初始化等相关工作,这也是推荐的方式,这样不需要我们自己写代码来完成这些工作了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容