亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.430618.com 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

做JAVA的基本上都用過Spring,而IoC是Spring最核心的模塊之一。那IoC具體有什么用,Spring又是如何做到IoC的呢?這是本文要探索的話題。

關于IoC

什么是依賴?

首先我們要明確“依賴”的概念。所謂依賴,說直白點就是:A用了B,那A就依賴B。換成程序世界的說法,如果A類里面出現了B類有關的代碼(刪除B類,編譯A類會報錯),那A就依賴B。

打個比方,如果員工小明上班需要乘坐公交車,從家里到公司,那小明就依賴了公交車。抽象成代碼大概是這樣:

public class Worker() {
    private String name;
    private String home;
    private String office;
    // 這里依賴了Bus類
    private Bus bus = new Bus();
    
    public void goToWork() {
        bus.take(name, home, office);
    }
}
復制代碼

我們知道,依賴是一種耦合,而過多的耦合對程序是有害的,代碼架構的本質,就是盡量去降低耦合。試想一下,如果有一天員工小明升職加薪了,自己買了一輛小轎車代步,那凡是用到公交車的地方(比如上班、下班、接孩子、去商場、回家等等)豈不是需要修改代碼,把Bus換成Car?如果某一天又想步行或者騎自行車呢?

關于Spring IoC的那些事

 

依賴倒置原則

有了上面這個耦合的問題,于是業界的大佬們就想辦法來解決這個問題。設計模式六大原則里面有一個依賴倒置原則(Dependence Inversion Principle)。

所謂依賴倒置原則,就是把原本耦合的A和B分開,中間加一個“抽象層”。這樣A只需要依賴抽象層,并不需要關心具體實現,只要它能完成自己需要的功能就行了。而B也只依賴抽象層,實現這個功能就行了。

如果A依賴B,我們稱A為“上層”,B為“下層”,依賴倒置原則強調上層模塊不應該依賴下層模塊,兩者應依賴其抽象

關于Spring IoC的那些事

 

仍然是上面員工小明的例子,其實他上班需要的并不是一個公交車,而是一個“交通工具”,這個交通工具可以是自行車、電動車、汽車等等,只要它能夠把小明從家里帶到公司就可以了。我們改一下代碼,變成了這樣:

// 定義抽象類
public interface Vehicle {
    void take(String name, String home, String office)
}

// 下層模塊的細節,依賴抽象
public class Bus implements Vehicle {
    
    @Override
    public void take(String name, String home, String office) {
        // 實現細節
    }
}

public class Worker() {
    private String name;
    private String home;
    private String office;
    // 上層依賴了抽象類Vehicle
    private Vehicle vehicle = new Bus();
    
    public void goToWork() {
        vehicle.take(name, home, office);
    }
} 

復制代碼

看到這段代碼也許你會問:那這里Worker類里面不是還是要new一個Bus嗎?那還是依賴了呀,以后如果換成其它交通工具仍然需要改代碼。

別急,這就是我們下面會講到的控制反轉要解決的問題。

控制反轉

控制反轉(Inversion of Control)也就是我們說的IoC了。要理解IoC,需要弄清楚到底什么被反轉了?如何反轉的?

上面的示例代碼我們可以看到,即使我們引入了一個抽象層,但當一個Worker對象實際要使用Vehicle的時候,它還是必須得創建一個具體的對象,它可能是一個Bus,也可能是一個Car等。但這樣造成的問題是,依賴沒有被徹底分離,兩者還是存在耦合。

那如何把它們徹底分離呢?答案就是把創建具體的Vehicle對象交給第三方去做。這樣Worker不用管如何創建的交通工具,而Bus也不用管自己是如何被創建的。

想想我們生活中就有這樣的例子,員工小明要坐公交車,他不用每次都自己去造一輛公交車吧,只需要去公交車站,等公交車公司的調度就行了。而公交車工廠也跟小明沒有任何關系,它的職責就是生產好公交車,交付給公交車公司。通過引入了“公交車公司”這個第三方,小明和公交車工廠就完全解耦了。

反轉的是什么?對象如何獲取它的依賴對象這件事情上,控制權反轉了。從自己創建,反轉成了第三方管理。

控制反轉的進一步含義,不僅僅是獲取,還有整個要依賴對象的生命周期(包括創建、維護、銷毀等),控制權都被反轉了。

從代碼設計來看,一個簡單的解決方式是,把具體的對象通過方法參數傳進來,這樣就不強依賴了:

public class Worker() {
    private String name;
    private String home;
    private String office;
    
    // 通過方法傳進來
    public void goToWork(Vehicle vehicle) {
        vehicle.take(name, home, office);
    }
} 
復制代碼

但這樣會帶來一個問題,就是給調用端帶來了麻煩,相當于把對Bus的依賴,從Worker類轉移到了它的調用端,那它的調用端也會強依賴Bus,這本不屬于調用端的職責,所以沒有從根本上解決問題。而且每次調用都要傳一個Vehicle對象進來,很不合理,管理對象也比較麻煩。

那你可能會想,我搞個第三方容器不就行了嗎,這樣每次去第三方容器里面拿:

public class Worker() {
    private String name;
    private String home;
    private String office;
    // 第三方容器
    private VehicleContainer container;
    
    // 通過容器取
    public void goToWork() {
        container.getTodayVehicle().take(name, home, office);
    }
} 
復制代碼
關于Spring IoC的那些事

 

這樣當然也能解決,但不是最優雅的解決方案,因為你每個Bean都需要依賴Container。那我們能不能Worker類不依賴任何東西,包括Container,實現上班這個功能呢?當然可以,且聽下文分析。

依賴注入

更優雅的方案就是使用依賴注入(Dependency Injection)。我不想使用Container,每次還要主動去拿。我想在自己被創建的時候(或者創建后),我所依賴的對象就自動被設置好了。

關于Spring IoC的那些事

 

同時,這還是一種“無侵入”的方式,我們的業務代碼里面可以不用寫任何關于IoC的代碼。這樣即使我們某一天換了IoC框架,我們的代碼也不需要做任何修改。

實現依賴注入大概有三種方式:構造器注入,方法注入和屬性注入。

構造器注入

顧名思義,就是通過構造器的方式,把依賴的對象注入進來。這樣在new一個對象的時候,就完成了它依賴的對象的裝配。

public class Worker() {
    private String name;
    private String home;
    private String office;
    private Vehicle vehicle;
    
    // 通過構造器把要依賴的對象傳進來
    public Worker(Vehicle vehicle) {
        this.vehicle = vehicle;
    }
    
    // 直接用
    public void goToWork() {
        vehicle.take(name, home, office);
    }
}
復制代碼

setter方法注入

另一種方式是使用方法注入,一般是使用要依賴的對象對應的屬性的setter方法來注入。比如:

public class Worker() {
    private String name;
    private String home;
    private String office;
    private Vehicle vehicle;
    
    // 通過setter方法注入
    public void setVehicle(Vehicle vehicle) {
        this.vehicle = vehicle;
    }
    
    // 直接用
    public void goToWork() {
        vehicle.take(name, home, office);
    }
}
復制代碼

屬性注入

構造器和setter方法都有些麻煩,需要寫額外的代碼。要是容器可以通過反射直接注入進來就好了,這樣代碼看起來比較干凈。比如:

public class Worker() {
    private String name;
    private String home;
    private String office;
    // 容器直接通過反射把相應的對象注入進來
    private Vehicle vehicle;
    
    // 直接用
    public void goToWork() {
        vehicle.take(name, home, office);
    }
}
復制代碼

控制反轉容器

前面反復提到的一個詞,叫“第三方容器”,其實就是IoC容器。所謂IoC容器,就是可以生產和管理要依賴的對象,然后通過合適的時機注入進來。

IoC容器并不等于Spring。還有其它IoC容器框架,比如google開發的Guice等,甚至我們可以自己開發一個輕量級的IoC容器。其實IoC容器實現起來并不難。

只是我們平常用Spring比較多,它又提供了非常好用的IoC功能,所以大多數項目,我們都是用Spring的IoC了。Spring作為IoC容器還是非常成熟和穩定的。

依賴注入和控制反轉是什么關系?

2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那么到底是“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證后,他得出了答案:“獲得依賴對象的過程被反轉了”。控制被反轉之后,獲得依賴對象的過程由自身管理變為了由IOC容器主動注入。于是,他給“控制反轉”取了一個更合適的名字叫做“依賴注入(Dependency Injection)”。他的這個答案,實際上給出了實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在運行期間,動態地將某種依賴關系注入到對象之中。

所以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關系注入的方式,實現對象之間的解耦。控制反轉是解決問題的一種思路和方法論,依賴注入是它的具體實現方式

在Spring中使用IoC

首先要明確Bean的概念,Spring把需要納入IoC容器觀察的對象稱為Bean。

一些對象是不用交給Spring管理的,比如POJO對象,類似DO、DTO等對象(包括DDD中的領域模型),它們都是可以在程序里面通過new或者builder來創建的,因為創建的時候要給它們的一些屬性賦值,而且在使用這些類時,沒法使用“依賴倒置原則”。

聲明Bean

首先第一步要聲明Bean,這樣Bean才能被Spring的IoC容器管理。聲明Bean有很多種方式,在一開始,Spring是使用XML的方式來聲明一個Bean:

<bean id="myVehicle" class="test.spring.bean.Bus" />
<bean id="worker" class="test.spring.bean.Worker">
    <property name="vehicle">
        <ref bean="myVehicle" />
    </property>
</bean>
復制代碼

這樣以后如果要依賴的Bean變了,只需要修改XML文件就行了。

后來由于XML文件難以閱讀和維護,Spring開始支持用注解的方式定義Bean。我們在定義具體實現類的時候,可以在class上面加上@Component注解,然后配置好Spring的自動掃描路徑,這樣Spring就能夠自己去掃描相應的類,納入IoC容器中進行管理了。

@Component
public class A {}
復制代碼

@Component的語義其實不是很明確,因為“萬物皆可為組件”。它其實是一個元注解,也就是說,可以注解其它注解。Spring提供了@Controller、@Service、@Repository、@Aspect等注解來供特定功能的Bean上面使用。

我們自己也可以聲明一些類似的注解,如果我們使用DDD,也可以用@Component聲明一些諸如@ApplicationService、@DomainService之類的注解。

SpringBoot默認的掃描路徑是啟動類當前的包和子包。我們可以通過@ComponentScan和@ComponentScans來配置包的掃描路徑。

另一種方式是通過在方法上聲明@Bean注解來聲明一個Bean。這個時候一般是會與@Configuration一起來配合使用的。

@Configuration
public class MyConfig {
    @Bean
    public B getB() {
        return new B();
    }
}
復制代碼

一般只有在對框架提供的Bean有一些特殊配置的時候,才會使用@Bean注解。比如數據庫配置等。

使用Bean

使用Bean也有很多種方式。XML就不說了,上面例子也展示了如何在XML里配置Bean的注入。

Spring比較推薦的是使用構造器注入,因為構造器注入能夠在啟動的時候就檢查要依賴的對象是否存在,如果不存在,會啟動失敗并且拋出以下異常:

Parameter 0 of constructor in com.example.springbase.bean.A required a bean of type 'com.example.springbase.config.B' that could not be found.

The following candidates were found but could not be injected:
    - User-defined bean method 'getB' in 'MyConfig' ignored as the bean value is null


Action:

Consider revisiting the entries above or defining a bean of type 'com.example.springbase.config.B' in your configuration.
復制代碼

這樣我們就可以更早地發現依賴問題,而不用在運行時才發現要依賴的對象沒有被注入進來,發生一些空指針異常。

另一種方式是注解注入,注解注入的好處是代碼簡潔,不用專門寫構造器。Spring支持三個注解:

  • Resource - JSR250定義
  • Inject - JSR330定義
  • Autowired - Spring提供

其中@Resource和@Inject都是在JSR中定義的規范,主流的IoC框架都已經支持了這兩個規范。這兩個規范的區別在于,查找Bean的方式不同。

@Resource是先通過名稱匹配,找不到再通過類型匹配,找不到再通過結合@Qualifier來匹配。

而@Inject是先通過類型匹配,找不到再通過Qualifier來匹配,找不到再通過名稱匹配。如果要使用@Inject,需要引入額外的包:

<dependency>  
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency> 
復制代碼

@Autowired和@Inject的用法一致,唯一區別就是@Autowired屬于Spring框架提供的注解。

其實最推薦的是使用JSR-330的規范,這樣可以做到與框架無關。但是筆者發現大多數項目還是使用@Autowired居多,而且很難真正做到與Spring框架無關,因為@Component就是Spring提供的注解。我們平時經常使用的@Controller、@Service、@Repository、@Aspect等注解也都是Spring提供的。

所以如果要說推薦一個注解的話,筆者更推薦Spring的@Autowired。

還有一種方式,可以從Spring的上下文中直接拿Bean。這種方式一般用于:從一個不受Spring管理的對象中獲取一個Bean。比如說二方包里面的代碼,就有可能會有這種情況。

// 定義一個aware,持有一個static的context對象
@Component
public class MySpringContextAware implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        MySpringContextAware.applicationContext = applicationContext;
    }
}

// A是不受Spring管理的
public class A {
    // B是受Spring管理的
    private B b;

    public A() {
        System.out.println("a init");
        // 這樣就可以在不受Spring管理的對象里面,獲取到Bean了
        this.b = (B) MySpringContextAware.applicationContext.getBean(B.class);;
        System.out.println(b.hashCode());
    }
}
復制代碼

常見問題

單例和多例

Spring默認Bean是單例的。因為絕大多數Bean其實是“無狀態的”,比如Controller、Service、Repository。所以多個線程去使用同一個Bean不會造成什么問題。本著節約成本的理念,使用單例Bean比較好。

但是有時候我們可能會需要一個“有狀態”的類,它內部又依賴其它Bean。比如一個Context或者一個Processor之類的。對于這種有狀態有依賴其它Bean的類,有兩種設計思路:

  • 不給Spring管理,如果要用到其它Bean,使用上面的applicationContext來直接獲取Bean。
  • 給Spring管理,做成多例Bean,每次都新建一個

第二種使用起來會更優雅一些,也比較好測試一點。這里有一個小問題,我們來考慮以下這種情況:如果我們使用了一個多例Bean,它可能會依賴一些單例Bean,這個很好解決,在多例Bean中正常地注入單例Bean就行了。但是,如果我們要在一個單例Bean中使用一個多例Bean,我們知道無論是構造器注入,還是方法注入,還是屬性注入,都只會在Bean初始化的時候注入一次,那怎么能保證多個線程得到的是不同的多例Bean呢?

所以要在單例Bean中使用多例Bean,不能使用一般的自動注入。Spring提供了@Lookup注解來幫我們做這個事。它是方法級別的注解。

// 定義一個多例Bean
@Component
@Scope("prototype")
public class PrototypeBean {
    public void say() {
        System.out.println("say something...");
    }
}


@Component
public class SingletonBean {
 
    public void print() {
        // 單例Bean中用多例Bean
        PrototypeBean bean = methodInject();
        System.out.println("Bean SingletonBean's HashCode " + bean.hashCode());
    }
 
    @Lookup
    public PrototypeBean methodInject() {
        return null;
    }
}
復制代碼

需要注意的是,用@Lookup修飾的方法,不能是private的。可以是包訪問權限、protected或public的。這里推薦寫成public的,這樣在單元測試的時候比較方便mock。

循環依賴

循環依賴其實很好理解,就是A依賴B,而B又依賴A。這樣就形成了循環依賴。那Spring是如何解決循環依賴的呢?

聰明你的肯定能夠馬上想到,如果兩個Bean都是使用構造器注入,那是不能解決循環依賴的,一旦有循環依賴只能報錯。而如果是屬性注入或者方法注入,那可以先初始化兩個Bean,然后分別延遲注入進去。這樣就可以解決循環依賴的問題。

這也是為什么我們推薦使用構造器注入。循環依賴不是一個好設計,構造器注入可以提早發現這種循環依賴。

Spring使用了一個叫做三級緩存的東西來解決循環依賴,具體的實現細節本文不做討論,感興趣的讀者可以自己去找找相關的文章。

給不給Spring管理?

又回到上面那個單例和多例的問題。如果一個類是多例的,那它一般是有狀態的,我們有必要把它交給Spring管理嗎?或者說,有必要交給IoC容器管理嗎?

在回答這個問題之前,我們先假設一下,如果不給IoC容器管理,會怎樣?我們從三個角度來考慮:

  • 這個類依不依賴其它Bean
  • 這個類是不是單例的

如果這個類不依賴其它Bean,那其實不太需要交給IoC容器管理,POJO類就是一個很典型的例子。但如果這個類是一個單例的,那其實推薦交給IoC容器管理,因為要自己保證單例是比較麻煩的,而且不優雅。不信去看看單例模式的各種實現。

如果這個類依賴其它Bean,那推薦交給IoC容器管理,不然還得使用上面的那種applicationContext的getBean方法來獲取依賴的Bean,這就與IoC框架耦合了,不太劃算。

關于作者

我是Yasin,一個有顏有料又有趣的程序員。

微信公眾號:編了個程

個人網站:yasinshaw.com

分享到:
標簽:Spring IoC
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定