反向控制(Ioc)以及裝配JavaBean方法的變革

妖刀Coder發表於2020-12-13

反向控制(Ioc)以及裝配JavaBean方法的變革

關於學習,為什麼很多時候總是感覺付出了時間而回過頭來又覺得什麼都沒有留下。我想這是無論是我寫本文前還是各位看本文前都應該思考的問題。而解決這個問題的答案要從另一個問題的答案中去尋找——怎樣才算是把學習過程中的某個問題徹底搞懂了?我是這麼看待的,如果試著去探究而不是逃避學習過程中的問題的時候,那麼這個問題已經解決了50%了,剩下的50%只是時間問題;而當經過一段時間的學習,問題的答案差不多能在心裡形成個大概輪廓的時候了,那麼這個問題的解決程度也才完成了百分之70%;如果問題的答案能口頭表達出來或者書面語言寫出來的時候,問題的解決程度差不多就有了90%了;這時候你想象一下,假設回到幾天前或者你剛遇到這個問題的時候,你是否能給當時的你用最系統、快速、準確且簡單的方法講述出來,最重要的是對方能聽懂且信服,如果是是的話,那麼這個問題的完成度可以是99%了;最後的1%又在哪裡了,我想對於程式設計師來說,最好的品德就是不斷學習與開源的分享,對於問題,始終保持謙虛的態度,在計算機的世界中“唯一不變的就是變化”,問題亦是如此。

我想以上便是我寫此文最大的動力,本文也儘量從最基本的概念開始,通過生動想象的例子來闡述問題的答案。希望與大家共同學習進度,如有有失偏頗之處還望不吝賜教。接下來就讓我們帶著問題來一探究竟吧!

何為反向控制

初學Spring的小夥伴,一定會有如下經歷:常常會在一些文章或者書籍上與控制反轉、依賴注入等的字眼不期而遇。也肯定會有這種疑惑到底何為控制反轉(即本文所稱的反向控制,該詞摘自《Java Web程式設計實戰寶典一書》)。或清晰或迷惑或一知半解,不求甚解又或刨根問底。無論從前如何,希望再看下文時能對這個概念加深印象或者有新的感悟。

控制

什麼叫控制?大千時間控制無處不在,人創造了機器使用機器來生產這叫控制、父母不讓自己的孩子玩遊戲、好好睡覺也是一種控制,控制有好也有壞,程度有強有弱。但一旦建立了控制的關係,一定是一個物件充當控制者,另一方充當被控制者的角色。

在Java的世界中,解釋控制實在太簡單不過,因為Java是一種物件導向的語言,很多事物都能抽象成Java的類。比如現在我們就有一個Java類Worker,它是工廠工人這一客體的抽象模擬,那麼相應的,工廠中受工人操作的機器就可以抽象成Machine類。當考慮的生產這個過程時,工人有控制機器的動作,機器有生產產品的動作,這兩個動作就分別抽象成了兩個類中的方法。

考慮到單一職責原則,這兩個類分別在兩個Java檔案下。那麼這個兩個類的程式碼應該如下:

public class Worker {
    private String name;

    public void work(){
        System.out.println("開啟機器!");
        Machine machine = new Machine();
        machine.produce();
        System.out.println("機器完成生產任務!");
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.work();
    }
}
public class Machine {
    public void produce(){
        System.out.println("機器正在生產!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("機器生產完成!");
    }
}

現在開始做生產任務,執行Worker下的main方法,結果如下:

image-20201206162818254

通過Worker類中的work方法中程式碼可知道,工人物件控制機器的方法就是去new一個機器的物件,然後呼叫機器中的方法,這是初學Java中最常用的方法。這是最簡單的控制的實現方法。

稍作改進

在講反向控制之前,我們可以試著把上面的程式碼優化一下。

  • Worker中的main方法實現的功能應該抽離出來,正常來說工人開啟生產工作的指令是由老闆發起來的,不能自己例項化自己。
  • 一個工廠中的機器是多種多樣的,但他們都有一個相同的生產動作。

解決上面兩個問題,那麼就建立新的老闆類、並將機器類抽象成介面。並對機器介面新增兩個實現類。程式碼如下:

Boss.java:

public class Boss {
    public static void main(String[] args) {
        Worker worker = new Worker();
        System.out.println("boss:快去幹活吧worker!");
        worker.work();
    }
}

Worker.java:

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:開啟機器!");
        Machine machine = new ComputerMachine();
        //生產計算機
        machine.produce();
        System.out.println("worker:機器完成生產任務!");
    }
}

Machine.java:

public interface Machine {
    public void produce();
}

PhoneMachine.java:生產手機機器的類

public class PhoneMachine implements Machine{
    @Override
    public void produce(){
        System.out.println("機器正在生產手機機!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("機器生產手機完成!");
    }
}

ComputerMachine.java:生產計算機的機器類

public class ComputerMachine implements Machine{
    @Override
    public void produce(){
        System.out.println("機器正在生產計算機!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("機器生產計算機完成!");
    }
}

執行Boss中的main方法:

image-20201206184443997

好像一切都很完美了,滿足介面程式設計原則、單一職責原則。

但這時候老闆說,你接下來生產手機吧——業務發生改變了!Worker類中的work方法要發生改變了——這不滿足開閉原則了,因為Worker類要發生改變了。

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:開啟機器!");
        Machine machine = new PhoneMachine();
 		//生產手機
        machine.produce();
        System.out.println("worker:機器完成生產任務!");
    }
}

難道老闆每次改命令,Worker都要改程式碼麼?顯然這是不可能的,這嚴重違背了開閉原則(對新增開放,對修改關閉)。

進一步思考程式碼缺陷

很明顯上面的程式碼是由Worker類來控制Machine類來實現一些功能的,由此引發了一些不希望看到的結果。如果要認真去分析的話,主要存在以下幾點缺陷:

  • Worker類和Machine類的耦合性極高。Worker需要使用另外一個機器的時候,必須通過修改自身程式碼實現。如果在一個系統中存在大量這種情況,就會牽一髮而動全身,耦合度很高的情況下修改程式碼後出現BUG的機率也會大大增加。

  • Worker類的使用過於侷限,換個機器必須要更改程式碼,不具有動態性。

那是不是這樣就好了,其他地方呼叫worker物件中的work方法時,加一個引數,在work中通過if判斷看到底應該例項化哪個機器類。

public class Worker {
    private String name;

    public void work(String machineType){
        if (machineType.equals("phone")){
            System.out.println("worker:開啟機器!");
            Machine machine = new PhoneMachine();
            machine.produce();
            System.out.println("worker:機器完成生產任務!");
        }
        else if(machineType.equals("computer")){
            System.out.println("worker:開啟機器!");
            Machine machine = new ComputerMachine();
            machine.produce();
            System.out.println("worker:機器完成生產任務!");
        }
        else {
            System.out.println("沒有該型別的機器");
        }
    }
}

好像還真的滿足了動態性了。最起碼試著去解決問題了。但但但是當新增機器時,又要去Worker新增if語句麼?

  • 這樣做還是不滿足開閉原則,我希望的是,即使新增了Machine類,也不要來改Worker了。
  • 太多的if語句並且其中的程式碼都是重複的,一是不夠優雅,二是通過來實現不同邏輯,應該是由策略模式來實現的,而不是用過多的if。
  • 傳引數用於If判斷,Boss和Worker的耦合性還是太高了,因為傳引數必須有嚴格的對應才行,Worker的行為必須對Boss完全透明,否則Boss根本不知道該怎麼用Worker啊。

所以這種方式還是不太行。

控制反轉

某些事物從本質上就帶有的缺陷,是想破腦袋都無法解決的。就像是資產階級一生下來就帶有剝削性,這是無法改變的事情,偽裝改進的再好這種本質上的特點也無法消弭。

同樣的,這樣通過一個類來建立另一個類並使用它的這種主動的工作方式帶來的弊端也是無法解決的。這時候控制反轉就浮出水面了——將建立物件的工作交給外部的協調者(比如Spring容器)來完成,就能解決上面的問題了,下面介紹的方式就是通過IOC的思想來降低耦合度。

我們在Worker類的外部來建立Machine類。既然是老闆決定工人來使用什麼機器,那就在Boss中實現Machine類,通過setter方法向Worker物件傳遞Machine物件。

Boss.java

public class Boss {
    public static void main(String[] args) {
        Machine machine = new PhoneMachine();
        Worker worker = new Worker();
        worker.setMachine(machine);
        System.out.println("boss:快去幹活吧worker!");
        worker.work();
    }
}

Worker.java

public class Worker {
    private String name;

    private Machine machine;

    public void setMachine(Machine machine){
        this.machine = machine;
    }

    public void work(){
        System.out.println("worker:開啟機器!");
        machine.produce();
        System.out.println("worker:機器完成生產任務!");
    }
}

通過setMachine方法來完成machine物件的注入。下次老闆再想讓工人操作不同機器(或者加新機器了)完成不同生產任務的時候,只需要修改Boss中的一行程式碼就能完成。Worker程式碼不用動了。

Machine machine = new AnyMachine();

這次好像無懈可擊了!至少目前來說已經是最好的方法了。這就是通過依賴注入的方式來實現了控制反轉。原本Machine的建立權從Worker手上轉移到了Boss手上,使得Machine與Worker的耦合性大大降低。

改變前後,上述程式碼完成生產功能時有什麼不同。如下圖所示:

image-20201206195846273

疑惑

我們最後使用IOC的時候,說不用對Worker程式碼進行更改了,把建立機器的權力交給Boss了,但要使用不同機器的時候還是要更改Boss中的程式碼。這不是還是要更改麼。其實在JavaWeb的專案中,Boss、Worker、Machine所代表的層級是不同的。Boss是使用者層(可以當作是前端頁面),Worker是Service層,Machine是持久層。

它們之間的關係應該是這樣的:

image-20201206200316802

服務層的程式碼應該是極具動態性的,用一套程式碼就能為不同的使用者服務。所以該層的程式碼是不能輕易更改的,否則將會出現很多問題。而Boss層的內容隨著使用者意願而更改是很正常的現象。

至此便解釋清楚了何為反向控制了,不知道我講的大家是否能明白,又或者有什麼新的認識。

JavaBean與Ioc的使用

都說裝配JavaBean,大家有去想過JavaBean是什麼,為什麼需要裝配JavaBean嗎?

JavaBean是什麼

我自己對JavaBean的定義是:Java工程中能在業務上提供具體作用且能被其他類使用的Java類。

如果非要有一些詳細的定義:

  • 用作JavaBean的類必須有一個公共的、無引數的構造方法。
  • 這個類中的屬性必須能被其他類訪問和修改,並且使用getter、setter方法實現。——比如上文中的Worker類中的Machine屬性就能由Boss類通過setter方法注入。

JSP時代使用JavaBean

Web 技術的歷史

要談JSP與JavaBean就不得不扯點歷史。瀏覽器在幾十年前絕對在Web技術應用上佔絕對地位,HTML語言的發明,使得人們在瀏覽器上可以做很多事情。最初的動態Web技術(實現客戶端與服務端的動態互動)是由CGI(通用閘道器介面)實現的,互動的內容是靜態的HTML檔案,其動態性主要體現在可以與資料庫互動了。1998年,Servlet出現了——一種由Java語言實現的技術。為了更好的使用Servlet,Sun公司發明了JSP語言,並未JSP的使用提供了兩種模型分別是Model1和Model2,其中Model2就是基於MVC的,也是目前很多Web MVC框架的前身了。

JSP

JSP技術可以將靜態內容(如HTML,CSS,JavaScript)和動態內容(Java)程式碼混合在一個檔案(.jsp)中。

其中Java程式碼放在<%= %>或<% %>中,在此之外的就是靜態內容了。jsp檔案會由JSP引擎去解析,將解析出來的Java程式碼交由Servlet引擎處理,從而使得Java程式碼能夠執行。在一個Web專案中,一定也有Java檔案,jsp檔案中的Java程式碼想使用這些.java檔案中的內容,就得去例項化這個類——建立Java類物件。

方法

使用<jsp:useBean>標籤,該標籤主要用於在不使用Java程式碼的前提下建立類的例項物件。好好體會這句話,是不是有一點前文中提到的“在外部建立類的例項物件”的意味了。原來控制反轉早就在遠古的JSP時代運用了(這句話可能並不是很正確,但也絕不會跑很偏)。

<jsp:useBean id="MyDate" class="className" scope="application">

這句話就相當於

<%
	java.util.Date myDate = new java.util.Date();
%>

當然相當於的意思是指,是對於這個jsp檔案而言的。

不同在於<jsp:useBean>中還有一個很重要的引數 scope。scope表示物件例項的有效範圍。可選的範圍有4個,從小到大依次是page、request、session和application。預設值為page,如將scope設為session,就意味者所有屬於同一個session的JSP頁面和Servlet都可以訪問這個物件例項了。通過scope就能控制例項物件的作用域,這一種由外部建立例項物件的感覺是不是已經愈發強烈了。

JSP中的useBean標籤還有其他引數,在此不是重點,就不介紹了。這一小節主要是為了讓大家明確,反轉控制與JavaBean的注入絕不是Spring的專屬,早就從早期的JSP中就有了。本節內容很多是參考了《JavaWeb 程式設計實戰寶典》一書,2014年出版的。大學就買了這本書,但翻開還是在幾年後的今天(2020年12月初),書中的Web開發方式早已過時,但卻可以用來回顧歷史——真是諷刺。

更諷刺的是,大學課程中除了JSP外沒有學習任何Web開發框架,等工作了要學習Spring,需要再回顧JSP和Servlet的時候還全忘了。

Struts2部署JavaBean

Struts2使用xml檔案,並用標籤將元件(比如JavaBean)來部署到Struts的Ioc容器中。下面是一個示例:

<struts>
	<bean class="com.yaodao.Deomo" scope="default" option="true"/>
</struts>

本人也對Struts2框架也只是淺嘗輒止的狀態,在此不過多解釋一些用法了,僅僅是為了展示Ioc容器在web框架中歷史的傳承。

Spring與Bean

裝配Bean

說明:Bean其實就是容器中放置的一個個工程中可複用的元件,下文中的元件即為Bean。

時間如果倒退到好幾年前(本文寫於2020-12),Spring裝配JavaBean的含義可能是這樣的:

裝配Bean實際上就是在XML中配置檔案JavaBean的相關資訊,然後由Spring框架讀取該配置檔案,並建立相應的JavaBean物件例項的過程。

但時代變了,歷史上,指導Spring應用上下文將bean裝配在一起的方式是使用一個或者多個XML檔案(用於描述各個元件以及它們與其他元件的關聯關係)。但是u現在的Spring更推薦基於Java的配置——通過構造一個配置類並用註解的方式來配置Bean。而隨著Spring帶來了自動配置的功能——由自動裝配與元件掃描技術所支援,XML與Java配置的手動配置又顯得雞肋了,在下一節中將會講解SpringBoot是如何進行自動裝配的。

XML方式裝配Bean

這種方式的裝配與JSP時代、Struts2框架沒有多大的區別(僅指使用方式,而非深層實現方式)。在xml檔案中配置了Bean只是第一步,回到上文中那段話:“裝配Bean實際上就是在XML中配置檔案JavaBean的相關資訊,然後由Spring框架讀取該配置檔案,並建立相應的JavaBean物件例項的過程。”,後面還有兩步即讀取配置檔案以及建立物件例項。

在Spring中,提供了兩種方式來讀取配置檔案並建立JavaBean的物件例項。

  • Bean工廠(BeanFactory)
  • 應用上下文(ApplicationContext)

下面我們分別來介紹兩種方法的使用方式。在此之前,我們先在IDEA中建立一個簡單的Spring的專案用於程式碼展示。先準備一個類作為JavaBean。在此就用一個上文中寫過的Worker類,但先不在此類中使用機器類(刪除了部分程式碼)。

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:開啟機器!");
        System.out.println("worker:機器完成生產任務!");
    }
}

並建立XML檔案配置該JavaBean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id = "worker" class = "Worker"/>
</beans>

使用BeanFactory介面

被拋棄的XmlBeanFactory

在很久(Spring3.1)之前,使用BeanFactory是這樣使用的:

public class Test {
    public static void main(String[] args) {

        BeanFactory factory = new XmlBeanFactory(new FileSystemResource("src\\SpringBean.xml"));
        Worker worker = (Worker)factory.getBean("worker");
        worker.work();
    }
}

將配置檔案通過FileSystemResource物件傳入XmlBeanFactory類(是BeanFactory類的實現類)的構造方法。

image-20201213130329707

我使用的Spring是5.0的,在Idea中使用XmlBeanFactory時會有刪除的橫線,提醒該實現類已被拋棄,執行的時候會報錯的。

新的方法
public class Test {
    public static void main(String[] args) {

        Resource resource = new ClassPathResource("SpringBean.xml");
        BeanFactory beanFactory = new DefaultListableBeanFactory();
        BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
        beanDefinitionReader.loadBeanDefinitions(resource);

        Worker worker = (Worker)beanFactory.getBean("worker");
        worker.work();
    }
}

執行成功:

image-20201213125311326

關於為什麼要拋棄之前的實現類我還沒有搞明白,由於時間關係和自身能力限制,只能等以後搞明白啦,到時候來補充上!

使用ApplicationContext介面

使用ApplicationContext介面可以實現bean的裝配,主要有兩種實現類可以使用。

使用FileSystemXmlApplicationContext實現類

通過絕對或相對路徑指定XML配置檔案

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new FileSystemXmlApplicationContext("SpringBean.xml");
        Worker worker = (Worker)applicationContext.getBean("worker");
        worker.work();
    }
}
使用ClasspathXmlApplicationContext實現類

從類路徑中搜尋XML配置檔案

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("SpringBean.xml");
        Worker worker = (Worker)applicationContext.getBean("worker");
        worker.work();
    }
}

image-20201213140318251

總結

但從裝配Bean上看,ApplicationContext和BeanFactory類似,但ApplicationContext比BeanFactory提供了更多的功能,比如國際化,裝載檔案資源、向監聽器Bean傳送事件等。因此當用XML來配置Bean的時候,用ApplicationContext來裝配Bean是比較好的方式。

但!XML已經不是Spring推薦的配置方式了。

使用Java裝配JavaBean

“在最近的Spring版本中,基於Java的配置更為常見。”

建立如下一個配置類:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {
    @Bean
    public Worker worker(){
        return new Worker();
    }
}

它的作用和下面的XML的作用是一樣的。

<bean id = "worker" class = "Worker"/>

@Configuration註解會告知Spring這是一個配置類,會為Spring應用上下文提供Bean。這個配置類的方法使用@Bean註解進行了標註,表明這些方法所返回的物件會以bean的形式新增到Spring的應用上下文中(預設情況下,這些bean所對應的bean ID與定義它們的方法名稱是相同的)。

如何使用?如下:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test3 {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
        Worker worker = ctx.getBean(Worker.class);
        worker.work();
    }
}

使用了一個ApplicationContext(應用上下文)介面的一個實現類:AnnotationConfigApplicationContext,和XML的裝配沒有什麼不同。

但為啥Spring推薦使用Java的配置方法呢?

在《Spring實戰》一書中,這麼寫道:“相對於基於XML的配置方式,基於Java的配置會帶來更多項額外的收益,包括更強的型別安全性以及更好的重構能力。”

Spring Boot的魔法

自動裝配

Spring Boot為Spring家族帶來的最大改變之一就是自動配置(autoconfiguration)。Spring Boot能夠基於類路徑中的條目、環境變數和其他因素合理猜測需要配置的元件並將它們裝配在一起。

建立SpringBoot工程

首先建立一個SpringBoot工程,結構目錄:

image-20201213223806058

新建一個Worker類,並實現自動注入。

package com.example.demo;

import org.springframework.context.annotation.Configuration;

@Configuration
public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:開啟機器!");
        System.out.println("worker:機器完成生產任務!");
    }
}

僅僅加上一個Configuration的註解即可。

用起來也是十分的方便,在Spring Boot自動提供的測試類中編寫程式碼:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private Worker worker;
    @Test
    void contextLoads() {
        worker.work();
    }
}

只需要宣告一個私有變數,並且用Autowired註解標註,在測試程式碼中就能直接用worker的物件了。

image-20201213224448988

沒用一點點配置檔案,這就是Spring Boot自動裝配的魅力所在。而關於Spring Boot的自動配置又完全可以另起一篇文章了。【評論留言,我就更新Spring Boot自動裝配的文章】

關於Ioc與裝配Bean的文章到此就結束啦。不知道小夥伴們是否哪怕一絲絲的收穫,寫這麼多字不易,希望大家能幫忙點點贊!

關注俺的公眾號,方便我們能夠互相交流學習!

大家也可以來我的個人部落格:妖刀的個人部落格 評論留言。

相關文章