一、Spring的依賴注入和控制反轉

        所謂依賴注入就是通過Spring的IOC容器來管理物件的建立、銷燬以及物件之間的依賴關係。在程式設計中,我們經常會遇到A類依賴B類的情況,這時我們就需要在A類中宣告一個B類的引用,然後在程式中new一個B類的物件,讓B類引用指向B類物件的記憶體地址,而依賴注入的出現簡化了這種維護依賴關係的頻繁操作,改由Spring的IOC容器來進行維護,在IOC容器中註冊過的Bean例項會在Spring容器初始化時就注入到某個類的依賴中,而這種依賴注入的操作是使用Java的反射機制來實現的。

        所謂控制反轉就是IOC容器的依賴注入讓介面主動獲取實現變成了容器主動將實現注入到介面中,實現了控制關係的反轉。我們在開發中,經常會遇到當前模組呼叫其他模組的介面並new一個具體實現類的情況,這樣就很容易讓當前模組與具體的實現產生了耦合,不符合面向介面程式設計和麵向抽象程式設計的思想。而且,如果我們需要更換介面的實現時,可能需要在系統中修改很多地方,這增大了出錯的風險,也不利於後期的系統維護。而依賴注入的出現則剛好解決了這些問題,它實現了系統模組之間的鬆耦合,滿足了面向介面程式設計和麵向抽象程式設計的思想。

1、簡單的介面

public interface HelloWorld {
    void say();
}

2、簡單的實現類

@Service
public class HelloWorldEnglishImpl implements HelloWorld {
    @Override
    public void say() {
        System.out.println("Hello World");
    }
}

3、applicationContext-test.xml配置檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置掃描器,指定要掃描哪個包下面的註解類 -->
    <context:component-scan base-package="com.mengfei.test"/>

    <bean id="helloWorldEnglish" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>
    <bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest">
        <property name="helloWorld" ref="helloWorldEnglish"/>
    </bean>
</beans>

4、測試類,載入配置檔案

@Component
public class HelloWorldTest {

    private HelloWorld helloWorld;

    public void setHelloWorld(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

    public void doSay(){
        helloWorld.say();
    }
}

class Test{
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/applicationContext-test.xml");
        HelloWorldTest test = (HelloWorldTest) context.getBean("helloWorldTest");
        test.doSay();
        //列印結果:Hello World
    }
}

附:@Component、@Repository、@Service、@Controller的基本含義

@Repository:用於註解持久層

@Service:用於註解業務層

@Controller:用於註解控制層

@Component:如果不屬於上面三層的,則可以使用這個註解

二、byName和@Resource註解的使用

1、byName的自動裝配

        首先,更改配置檔案中的測試bean使用byName進行自動裝配,其他不變,如下:

<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byName"></bean>

        此時,執行main方法報錯,這時我們將測試類中的setHelloWorld方法名更改為setHelloWorldEnglish,再執行main方法成功,如下:

public void setHelloWorldEnglish(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

        很多人以為,byName的自動裝配是跟類中的屬性有關,其實不是,使用byName進行自動裝配時,是利用Java的反射機制獲取自動裝配類中的set方法名,去掉set後將其首字母小寫再到IOC容器中查詢是否有對應的beanId,如果有則檢視該bean的型別與set方法的引數型別是否匹配,匹配上了則呼叫set方法進行依賴注入。因此,byName的自動裝配跟set方法名和引數型別有關,跟屬性名無關。可以嘗試將setHelloWorldEnglish中的H小寫,如:sethelloWorldEnglish,其他不變,依然可以注入成功。也可以任意更改set方法名和beanId標識進行自動裝配測試。

        現在我們將配置檔案中beanId為helloWorldEnglish的bean註釋掉,再執行發現報錯,但我們將測試類中的set方法名更改為setHelloWorldEnglishImpl之後再執行,發現執行成功,如下:

<!--<bean id="helloWorldEnglish" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>-->
    <bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byName"></bean>
public void setHelloWorldEnglishImpl(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

        在上面的配置中,我們雖然沒有顯式的宣告HelloWorldEnglishImpl的bean例項,但我們配置了掃描test包,IOC容器在載入時會將此包中的所有類都掃描到IOC容器中,當有bean例項使用byName進行自動裝配時,它將執行byName的裝配邏輯。不同的是由於沒有顯式的宣告beanId,因此所有註冊到IOC容器中的bean例項的Id都將使用類名的首字母小寫,即此時實現類HelloWorldEnglishImpl的預設beanId為helloWorldEnglishImpl,所以當我們將set方法的方法名更改為setHelloWorldEnglishImpl時就可以注入成功了。

2、從byName到@Resource註解

        現在我們將測試類的bean在配置檔案中也註釋掉,然後在測試類的setHelloWorldEnglishImpl方法上新增@Resource註解,這代表為此set方法執行byName自動裝配策略,如下:

<!--<bean id="helloWorldEnglish" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>-->
<!--<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byName"></bean>-->
    @Resource
    public void setHelloWorldEnglishImpl(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }

        但是如果我們將 setHelloWorldEnglishImpl方法更改為setHelloWorldEnglish之後發現依然可以注入成功,這是由於@Resource同時也支援byType自動裝配策略,當使用byName策略無法進行自動裝配時,它將使用byType策略。現在我們可以將@Resource註解直接註解到屬性上,如下:

    @Resource
    private HelloWorld helloWorld;

    /*@Resource
    public void setHelloWorldEnglish(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }*/

        執行main方法,發現依然可以注入成功 。我們雖然註釋掉了set方法,但程式在執行時,Java的反射機制會自動的幫我們生成一個名為setHelloWorld的方法,但HelloWorldEnglishImpl的預設beanId為helloWorldEnglishImpl與helloWorld不匹配,這時,@Resource將使用byType進行自動裝配。我們這時可以將註釋掉的beanId為的helloWorldEnglish的註釋去掉,然後執行main方法,如下:

<bean id="helloWorldEnglish" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>
<!--<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byName"></bean>-->

         這時執行報錯,因為此時@Resource既無法使用byName進行自動裝配,也無法使用byType進行自動裝配,至於為何,看完byType的裝配邏輯你就明白了。

        這時我們將beanId修改為helloWorld,其他不變,發現注入成功,此時@Resource使用的是byName注入,如下:

<bean id="helloWorld" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>

         然後,我們再將beanId修改為helloWorldEnglishImpl,其他不變,此時@Resource使用的是byType注入,如下:

<bean id="helloWorldEnglishImpl" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>

三、byType的使用

1、byType的自動裝配

        更改配置檔案中的測試bean使用byType進行自動裝配,其他不變,執行main方法注入成功,如下:

<bean id="helloWorldEnglishImpl" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>
<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byType"></bean>

        使用byType進行自動裝配時,是利用Java的反射機制獲取測試類中的set方法的引數型別,尋找此介面型別的實現類或子類,找到後將其首字母小寫與IOC容器中的beanId進行匹配,匹配成功後則呼叫set方法進行依賴注入。因此,byType自動裝配與方法名無關,與屬性名無關,與set方法的引數型別有關。如果將beanId修改為helloWorldEnglish或者其它則注入失敗。

 2、從byType到@Autowired註解

        現在我們將配置檔案中的bean全部註釋掉,還有set方法也註釋掉,然後在屬性上直接新增Autowired註解,執行main方法發現注入成功,如下:

<!--<bean id="helloWorldEnglishImpl" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>
<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byType"></bean>-->
    @Autowired
    private HelloWorld helloWorld;
    
    /*public void setHelloWorldEnglish(HelloWorld helloWorld) {
        this.helloWorld = helloWorld;
    }*/

         此時,介面引數的實現類的首字母小寫與IOC容器中預設beanId為helloWorldEnglishImpl的bean相匹配,因此注入成功。那麼@Autowired註解是否支援byName裝配呢?現在我們將配置檔案中的beanId為helloWorldEnglishImpl的註釋放開,然後將beanId修改為helloWorld,執行main方法發現注入成功,如下:

<bean id="helloWorld" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>
<!--<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byType"></bean>-->

        這時@Autowired註解使用的就是byName裝配,由於HelloWorld介面的實現類 HelloWorldEnglishImpl的類名首字母小寫在IOC容器中找不到對應的bean,它就會使用byName策略,Java的反射機制生成的set方法名小寫為helloWorld,在IOC容器中可以找到對應的bean例項,則byName裝配成功。如果我們將beanId修改為helloWorldEnglish,執行main方法,則注入失敗,因為此時@Autowired註解無論使用byType還是byName都無法找到對應的bean例項。

        如果介面有多個實現類怎麼辦,答案肯定是報錯,因為Spring不知道你要給介面注入哪個實現類。這時我們可以在set方法上新增@Qualifier註解來解決這個問題。(先把配置檔案中的配置註釋掉,然後再新增一個介面實現類,最後在測試類的屬性上新增@Qualifier註解)如下:

<!--<bean id="helloWorld" class="com.mengfei.test.HelloWorldEnglishImpl"></bean>-->
<!--<bean id="helloWorldTest" class="com.mengfei.test.HelloWorldTest" autowire="byType"></bean>-->
@Service
public class HelloWorldChineseImpl implements HelloWorld{
    @Override
    public void say() {
        System.out.println("你好世界");
    }
}
    @Qualifier("helloWorldEnglishImpl")
    @Autowired
    private HelloWorld helloWorld;

         @Qualifier註解是為了告訴Spring容器在自動裝配時如果有多個實現類應該使用哪個實現類進行依賴注入。

3、@Resource和@Autowired的區別

 @Resource是jdk提供的註解,它預設使用byName進行裝配,byName無法裝配則使用byType;

 @Autowired是spring提供的註解,@Autowired預設使用byType進行裝配,byType無法裝配則使用byName,如果介面有多個實現類,需要配合@Qualifier註解使用。

 

參考連結:

1、Spring Ioc、依賴注入原理

2、Spring自動裝配之byName和byType【Spring入門】

3、Spring原始碼學習[email protected]註解和啟動自動掃描的三種方式

4、為什麼使用IOC容器