Spring IoC有什么好处呢?

发布时间 2023-06-26 15:55:51作者: wodepingzi

 

 

来源    https://www.zhihu.com/question/23277575?utm_id=0     Spring IoC有什么好处呢?

 

作者:Mingqi
链接:https://www.zhihu.com/question/23277575/answer/169698662
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

要了解控制反转( Inversion of Control ), 我觉得有必要先了解软件设计的一个重要思想:依赖倒置原则(Dependency Inversion Principle )


什么是依赖倒置原则?假设我们设计一辆汽车:先设计轮子,然后根据轮子的大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!

我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。

这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。

这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。


控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:

为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:

这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:

由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:

由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:

这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:

看到没?这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写Car类的单元测试,就只需要Mock一下Framework类传入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再来构造Car。

这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转


看到这里你应该能理解什么控制反转和依赖注入了。那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。

显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。

IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:

这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):

这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:

我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。

实际项目中,有的Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写的一个API需要实例化这个Service,我们总不可能回头去搞清楚这几百个类的构造函数吧?IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。


这里只是很粗略的讲了一下我自己对IoC和DI的理解。主要的目的是在于最大限度避免晦涩难懂的专业词汇,用尽量简洁,通俗,直观的例子来解释这些概念。如果让大家能有一个类似“哦!原来就是这么个玩意嘛!”的印象,我觉得就OK了。想要深入了解的话,可以上网查阅一些更权威的资料。这里推荐一下 Dependency injection Inversion of Control Containers and the Dependency Injection pattern 这两篇文章,讲的很好很详细。

 
 
 

私以为以上各位都没有对spring ioc的精髓讲解到位。大多都在很模糊的说是什么,抽象化的表述或者含糊其辞的说概念。

ioc的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。

也就是说,甲方要达成某种目的不需要直接依赖乙方,它只需要达到的目的告诉第三方机构就可以了,比如甲方需要一双袜子,而乙方它卖一双袜子,它要把袜子卖出去,并不需要自己去直接找到一个卖家来完成袜子的卖出。它也只需要找第三方,告诉别人我要卖一双袜子。这下好了,甲乙双方进行交易活动,都不需要自己直接去找卖家,相当于程序内部开放接口,卖家由第三方作为参数传入。甲乙互相不依赖,而且只有在进行交易活动的时候,甲才和乙产生联系。反之亦然。这样做什么好处么呢,甲乙可以在对方不真实存在的情况下独立存在,而且保证不交易时候无联系,想交易的时候可以很容易的产生联系。甲乙交易活动不需要双方见面,避免了双方的互不信任造成交易失败的问题。因为交易由第三方来负责联系,而且甲乙都认为第三方可靠。那么交易就能很可靠很灵活的产生和进行了。

这就是ioc的核心思想。生活中这种例子比比皆是,支付宝在整个淘宝体系里就是庞大的ioc容器,交易双方之外的第三方,提供可靠性可依赖可灵活变更交易方的资源管理中心。另外人事代理也是,雇佣机构和个人之外的第三方。嗯,就这样,希望对题主有帮助。

==========================update===========================

在以上的描述中,诞生了两个专业词汇,依赖注入和控制反转

所谓的依赖注入,则是,甲方开放接口,在它需要的时候,能够讲乙方传递进来(注入)

所谓的控制反转,甲乙双方不相互依赖,交易活动的进行不依赖于甲乙任何一方,整个活动的进行由第三方负责管理。

这就是spring IOC的思想所在,不要只谈DI IOC这些概念。

人之所恶在好为人师,不实知,谨慎言。

 
 

废话1:有的朋友可能把依赖倒置(DIP)和依赖注入(DI)弄混了。

敏捷软件开发》第11章:

依赖倒置原则
a.高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
b.抽象不应该依赖于细节,细节应该依赖于抽象。

可见,依赖倒置的本质是依赖抽象,这与依赖注入的本质依赖容器,是两回事。

换句话说,如果Java没有接口、多态,依赖倒置就无从谈起。

而依赖注入依然可以存在,只要有一个注册表(《企业应用架构模式》第18章第5节)定义bean,利用反射来实例化并装配bean,有个容器容纳它们即可。




废话2:还有的朋友可能把控制反转(IoC)和依赖注入(DI)混为一谈了。

EXPERT ONE ON ONE J2EE DEVELOPMENT WITHOUT EJB》第6章:

IoC主要的实现方式有两种:依赖查找,依赖注入。(128页)
依赖注入是一种更可取的方式。(130页)

流行的「Martin Fowler将IoC改名为DI」的说法,Martin Fowler的原文在这里:

Inversion of Control Containers and the Dependency Injection pattern
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

大意是

已经存在某种模式,该模式被称为IoC,但IoC太普遍,任何框架都IoC,为了让表意更明确,决定用DI来精确指称那个模式。

意思大概就是

IoC ioc = the_pattern;
DI di = (DI)ioc;

显然,说the_pattern是IoC或DI都行,多态。但严格说IoC.class == DI.class肯定不为真,两者还是有区别,是 is-a 的关系。

不过正如不少人把Service和ServiceImpl分开,但Service事实上永远只有一个ServiceImpl实现一样,IoC虽然理论上还有其他实现,但DI过于主流,以至于混用了。




废话说完了,正面答题:

题主虽然问的是IoC,但可以确定在问DI。依赖注入的好处,各位说得差不多了,不赘。

Spring实战》第1章:

在项目中应用DI,你会发现你的代码会变得异常简单并且更容易理解测试

另外说说不限于DI的IoC,你的代码不再被直接调用,而是被框架代码调用,所以说框架与类库的区别在于控制反转( 好莱坞原则,don't call me, i'll call you )。

往大了说,http请求不由你的Servlet/Filter直接处理,而是由Struts/Spring MVC的Servlet/Filter处理,再分配给你的组件,这也算IoC。

往小了说,操作数据库不再由你直接写jdbc的一大堆try嵌套,而是把固定的部分抽到JdbcTemplate中,你只负责写局部代码,然后由框架调你的局部代码,这也算IoC。

前一种,也就是前端控制器模式(《企业应用架构模式》第14章第3节),后一种,也就是模板方法模式(《设计模式》第5章第10节)。当然,除此之外还有很多。

所以说,IoC的好处,也就是框架的好处。

Struts2技术内幕》第2章:

框架是一组程序的集合,包含了一系列的最佳实践,作用是解决某个领域的问题

这说得很对,但把框架换成类库也适用。

关于框架的好处,在stackoverflow上一个问题下的答案我很喜欢:

What is the difference between a framework and a library?

大意是:

框架提供了骨架,你只要提供肉就可以了,你的肉在骨架中被调用

我认为,这就是控制反转的意义。

框架提供了骨架(通用代码),你提供肉(业务逻辑)就行了。

你的肉在骨架中被调用(如web容器先调用框架,框架再调用你的代码,而不是你的代码直接被web容器调用)。

这让你专心于业务。

 
logo
crmeb
广告​
知乎广告介绍

Java程序员:用这个免费的商城系统做二开,省时、省力!

CRMEB Java版商城系统可免费商用、代码全开源无加密,功能强大、文档齐全、二开方便,基于Java + Uni-app 开发。查看详情
 

IoC(Inversion of Control​,控制反转)也称为依赖注入(Dependency Injection),作为Spring的一个核心思想,是一种设计对象之间依赖关系的原则及其相关技术。


IoC是什么?

高内聚低耦合可以说是软件技术形态的终极目标。用学术界的话来说,软件的两个本质特性就是构造性和演化性,高内聚低耦合的设计能够让构造和演化都更加高效,比如:

  • 开发更方便组织分工
  • 代码更容易进行复用
  • 更容易进行测试
  • 软件演化有更好的灵活性,能快速响应需求变化,维护代价更小

软件设计各种技术的出现,无一不是朝着这个终极目标的努力。面向对象、基于组件(学术界称为构件)的软件开发、面向切面编程(AOP)、Java近些年流行的模块化方法(比如OSGi技术)等等,这些方法和技术的出现,无外乎都是为了让软件更加高内聚低耦合。与此同时,各路大神还提出各种软件设计原则和模式,来规范我们的软件形态。我们今天谈的IoC也是其中的一个大招。


IoC(Inversion of Control​,控制反转)也称为依赖注入(Dependency Injection),作为Spring的一个核心思想,是一种设计对象之间依赖关系的原则及其相关技术。


先来看看字面上怎么来解释:当一个对象创建时,它所依赖的对象由外部传递给它,而非自己去创建所依赖的对象(比如通过new操作)。因此,也可以说在对象如何获取它的依赖对象这件事情上,控制权反转了。这便不难理解控制反转和依赖注入这两个名字的由来了。


一个场景

上面的解释听起来还是有点晦涩,让我们来看看具体的例子吧!

有个土豪老板,我们经常要出差,因此经常要订机票。定机票呢,可以通过去哪儿网订票,也可以通过携程订票。

我们马上可以想到可以通过三个类来表达这个场景,Boss,QunarBookingService,CtripBookingService。当然了,我们还应该提供一个BookingService接口,作为QunarBookingService,CtripBookingService的公共抽象。面向接口编程是面向对象设计的基本原则,如果这都不了解,赶紧先回去看GoF的《设计模式》第一章!

BookingService.java

package com.tianmaying.iocdemo;

public interface BookingService {
    void bookFlight();
}

QunarBookingService.java

package com.tianmaying.iocdemo;

public class QunarBookingService implements BookingService {
    public void bookFlight() {
        System.out.println("book fight by Qunar!");

    }
}

CtripBookingService.java

package com.tianmaying.iocdemo;

public class CtripBookingService implements BookingService {
    public void bookFlight() {
        System.out.println("book fight by Ctrip!");
    }
}

好了,土豪出门谈生意,得订机票了,Boss就琢磨着怎么订票呢,Boss比较了一下价格,这一次决定用去哪儿,对应的Boss的代码:

Boss.java

package com.tianmaying.iocdemo;

public class Boss {

    private BookingService bookingService;

    public Boss() {
        this.bookingService = new QunarBookingService();
    }

    public BookingService getBookingService() {
        return bookingService;
    }

    public void setBookingService(BookingService bookingService) {
        this.bookingService = bookingService;
    }

    public void goSomewhere() {
        bookingService.bookFlight();
    }
}

在Boss的构造函数中,将其bookingService成员变量实例化为​QunarBookingService,goSomewhere()函数中就可以调用bookingService的bookFlight方法了!

为了把这个场景Run起来,我们还需要一个main函数

package com.tianmaying.iocdemo;

public class App {
    public static void main(String[] args) {
        bossGoSomewhere();
    }

    static void bossGoSomewhere() {
        Boss boss = new Boss();
        boss.goSomewhere();
    }
}

运行之后可以看到控制中可以打印出"book fight by Qunar!"了。


使用IoC的场景

在这个例子中,我们看到Boss需要使用BookingService,于是Boss自己实例化了一个QunarBookingService对象。同志们想想,身为土豪Boss,思考的都是公司战略的事儿,定个票还要自己选择通过什么方式来完成,这个Boss是不是当得实在太苦逼。


所以土豪赶紧给自己找了个美女秘书(别想歪!),Boss要出差时,只需要说一声他需要订票服务,至于是哪个服务,让美女秘书选好后告诉他即可(注入啊!注入!)。(别跟我较真说美女秘书直接把票送上就行!)


这样的话,Boss是不是一身轻松了? 而这个美女秘书还是免费包邮的,这正是Spring扮演的角色!来看看使用Spring之后的代码。


我们在pom.xml文件中加入依赖(项目使用Maven作为构建工具):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.2.0.RELEASE</version>
</dependency>

QunarBookingService.java

package com.tianmaying.iocdemo;
import org.springframework.stereotype.Component;

@Component
public class QunarBookingService implements BookingService {
    public void bookFlight() {
        System.out.println("book fight by Qunar!");

    }
}

这里我们使用Spring的@Component标注将QunarBookingService注册进Spring的Context,这样它就可以被注入到需要它的地方!相应地,创建QunarBookingService实例的责任也交给了Spring。我们说了,美女秘书帮你搞定嘛!


新建一个SmartBoss类,聪明的老板知道把选择订机票服务这样的杂事交给秘书来做。

SmartBoss.java

package com.tianmaying.iocdemo;

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

@Component
public class SmartBoss {
    private BookingService bookingService;

    @Autowired
    public void setBookingService(BookingService bookingService) {
        this.bookingService = bookingService;
    }

    public BookingService getBookingService() {
        return bookingService;
    }

    public void goSomewhere() {
        bookingService.bookFlight();
    }
}

在上面的代码中,SmartBoss不再自己创建BookingService的实例,只是通过@Autowired标注告诉Spring小秘我需要一个BookingService!


调用代码因此也要做一些小修改,需要创建Spring的Context:

static void smartBossGoSomewhere() {
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(
            App.class);
    try {
        SmartBoss boss = context.getBean(SmartBoss.class);
        boss.goSomewhere();
    } finally {
        context.close();
    }
}



IoC的好处

回到正题,通过上面的例子,我们来看看IoC到底带来了哪些好处?


Boss没有和某个具体的BookingService类耦合到一起了,这样Boss的维护和演化就更加方便。想象一下,如果Boss需要改用CtripBookingService,这时也不需要修改Boss.java的代码,更换接口的实现非常方便,给Boss注入新的实现即可,轻松惬意。(当然,要做到热插拔还需要进一步的工作,要么得玩转类加载器这玩意,或者借助OSGi这样的神器)。这也是典型的开放-封闭原则的例子,即对现有模块,功能扩展应该是开放的,而对其代码修改应该是封闭的,即能够做到不需要修改已有代码来扩展新的功能。


想象一下,如果Boss自己直接去实例化QunarBookingService,而QunarBookingService在另外一个Package中甚至另外一个Jar包中,你可得import进来才能使用,紧耦合啊!现在好了,Boss只依赖于抽象接口,测试更方便了吧,Mock一下就轻松搞定!Boss和QunarBookingService彼此不知道对方,Spring帮两者粘合在一起。


为什么IoC是个大招,因为它会自然而然得促进你应用一些好的设计原则,会帮助你开发出更加“高内聚低耦合”的软件。


IoC如何实现

最后我们简单说说IoC是如何实现的。想象一下如果我们自己来实现这个依赖注入的功能,我们怎么来做? 无外乎:

  1. 读取标注或者配置文件,看看Boss依赖的是哪个BookingService,拿到类名
  2. 使用反射的API,基于类名实例化对应的对象实例
  3. 将对象实例,通过构造函数或者setter,传递给Boss

我们发现其实自己来实现也不是很难,Spring实际也就是这么做的。这么看的话其实IoC就是一个工厂模式的升级版!当然要做一个成熟的IoC框架,还是非常多细致的工作要做,Spring不仅提供了一个已经成为业界标准的Java IoC框架,还提供了更多强大的功能,所以大家就别去造轮子啦!希望了解IoC更多实现细节不妨通过学习Spring的源码来加深理解!


例子中的源码戳 Spring的IoC原理


针对评论的更新(2016-09-08):



Xeric的评论:DI不等于IoC,混为一谈会干扰理解。IoC是方法论,DI是实现形式。Inversion of Control Containers and the Dependency Injection pattern

难得在知乎上有一个注重概念的,不错!那就来说说。



什么是方法论,最接近的英文是Methodology,简言之是概念体系+符号表示+过程指导,IoC是不是呢?显然不是。关于IoC,其实你该引的文章是这篇InversionOfControl,是一种框架的现象(phenomenon),在你引用的文章里称之为框架的特征(characteristic)。

Flowler大叔最早对IoC感到困惑(那时Rod Johnson已经一战成名),为什么呢?如果对框架(软件工程中的framework)有研究的人,都知道有个微内核的概念,更通俗一点地理解是GoF设计模式中也提到过的,你不要调用我,我来调用你,反转嘛!这都多少年前的概念了,所以你一个容器说自己支持IoC,TM的不是类似说我的汽车有轮子码? 这是一个再通用不过的框架特征罢了,任何一种开发平台(iOS,Windows,Android),任何一个开发框架,不都控制反转嘛。

注意你引的文章:

As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

关键来了:所以在当时Spring和PicoContainer等轻量级容器的这个Context下,IoC这个概念真正表达的含义其实应该用一个更特化的概念来表达,这就是依赖注入了,这不叫“混为一谈”。但是IoC大家还是这么着被大家用下来了,表达了一个相比它原来更加狭义的概念(软件工程史上很多概念都是这样,即使学术界也必然会被所谓“事实上的工业标准”所影响),因为那正是工业界框架横飞迅猛发展的年代啊,参考Web 建站技术中,HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、ASP.NET、Web Services 是什么? - David 的回答

至于什么“方法论”-“实现”这个层次,一些中文文章(比如百度百科)有见类似说法,大抵因为Flower说的:

I'll point out now that that's not the only way of removing the dependency from the application class to the plugin implementation.

注意啊注意,是有其他方式(比你你可能会说依赖查找),这个时候IoC的概念指的是removing the dependency from the application class to the plugin implementation,这又回到泛化的概念了。不是这些流行Container语境下的IoC。但是人云亦云,大家就都这么说了。

所以题主问的是Spring的IoC,文中也是介绍Spring,没有任何不妥,说个Spring IoC的好处犯不着说方法论,就这个问题而言反而可能让题主困惑。

欢迎探讨。