徒手擼框架--實現IoC

犀利豆發表於2018-01-12

原文地址:www.xilidou.com/2018/01/08/…

Spring 作為 J2ee 開發事實上的標準,是每個Java開發人員都需要了解的框架。但是Spring 的 IoC 和 Aop 的特性,對於初級的Java開發人員來說還是比較難於理解的。所以我就想寫一系列的文章給大家講解這些特性。從而能夠進一步深入瞭解 Spring 框架。

讀完這篇文章,你將會了解:

  • 什麼是依賴注入和控制反轉
  • Ioc有什麼用
  • Spring的 Ioc 是怎麼實現的
  • 按照Spring的思路開發一個簡單的Ioc框架

IoC 是什麼?

wiki百科的解釋是:

控制反轉(Inversion of Control,縮寫為IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體,將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。

Ioc 有什麼用?

看完上面的解釋你一定沒有理解什麼是 IoC,因為是第一次看見上面的話也覺得雲裡霧裡。

不過通過上面的描述我們可以大概的瞭解到,使用IoC的目的是為了解耦。也就是說IoC 是解耦的一種方法。

我們知道Java 是一門物件導向的語言,在 Java 中 Everything is Object,我們的程式就是由若干物件組成的。當我們的專案越來越大,合作的開發者越來越多的時候,我們的類就會越來越多,類與類之間的引用就會成指數級的增長。如下圖所示:

徒手擼框架--實現IoC

這樣的工程簡直就是災難,如果我們引入IoC框架。由框架來維護類的生命週期和類之間的引用。我們的系統就會變成這樣:

徒手擼框架--實現IoC

這個時候我們發現,我們類之間的關係都由 IoC 框架負責維,同時將類注入到需要的類中。也就是說類的使用者只負責使用,而不負責維護。把專業的事情交給專業的框架來完成。大大的減少開發的複雜度。

用一個類比來理解這個問題。Ioc框架就是我們生活中的房屋中介,首先中介會收集市場上的房源,分別和各個房源的房東建立聯絡。當我們需要租房的時候,並不需要我們四處尋找各類租房資訊。我們直接找房屋中介,中介就會根據你的需求提供相應的房屋資訊。大大提升了租房的效率,減少了你與各類房東之間的溝通次數。

Spring 的 IoC 是怎麼實現的

瞭解Spring框架最直接的方法就閱讀Spring的原始碼。但是Spring的程式碼抽象的層次很高,且處理的細節很高。對於大多數人來說不是太容易理解。我讀了Spirng的原始碼以後以我的理解做一個總結,Spirng IoC 主要是以下幾個步驟。

1. 初始化 IoC 容器。
2. 讀取配置檔案。
3. 將配置檔案轉換為容器識別對的資料結構(這個資料結構在Spring中叫做 BeanDefinition) 
4. 利用資料結構依次例項化相應的物件
5. 注入物件之間的依賴關係
複製程式碼

自己實現一個IoC框架

為了方便,我們參考 Spirng 的 IoC 實現,去除所有與核心原理無關的邏輯。極簡的實現 IoC 的框架。 專案使用 json 作為配置檔案。使用 maven 管理 jar 包的依賴。

在這個框架中我們的物件都是單例的,並不支援Spirng的多種作用域。框架的實現使用了cglib 和 Java 的反射。專案中我還使用了 lombok 用來簡化程式碼。

下面我們就來編寫 IoC 框架吧。

首先我們看看這個框架的基本結構:

徒手擼框架--實現IoC

從巨集觀上觀察一下這個框架,包含了3個package、在包 bean 中定義了我們框架的資料結構。core 是我們框架的核心邏輯所在。utils 是一些通用工具類。接下來我們就逐一講解一下:

1. bean 定義了框架的資料結構

BeanDefinition 是我們專案的核心資料結構。用於描述我們需要 IoC 框架管理的物件。

@Data
@ToString
public class BeanDefinition {

    private String name;

    private String className;

    private String interfaceName;

    private List<ConstructorArg> constructorArgs;

    private List<PropertyArg> propertyArgs;

}
複製程式碼

包含了物件的 name,class的名稱。如果是介面的實現,還有該物件實現的介面。以及建構函式的傳參的列表 constructorArgs 和需要注入的引數列表 propertyArgs

2. 再看看我們的工具類包裡面的物件:

ClassUtils 負責處理 Java 類的載入,程式碼如下:

public class ClassUtils {
    public static ClassLoader getDefultClassLoader(){
        return Thread.currentThread().getContextClassLoader();
    }
    public static Class loadClass(String className){
        try {
            return getDefultClassLoader().loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
複製程式碼

我們只寫了一個方法,就是通過 className 這個引數獲取物件的 Class。

BeanUtils 負責處理物件的例項化,這裡我們使用了 cglib 這個工具包,程式碼如下:

public class BeanUtils {
    public static <T> T instanceByCglib(Class<T> clz,Constructor ctr,Object[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clz);
        enhancer.setCallback(NoOp.INSTANCE);
        if(ctr == null){
            return (T) enhancer.create();
        }else {
            return (T) enhancer.create(ctr.getParameterTypes(),args);
        }
    }
}
複製程式碼

ReflectionUtils 主要通過 Java 的反射原理來完成物件的依賴注入:

public class ReflectionUtils {

    public static void injectField(Field field,Object obj,Object value) throws IllegalAccessException {
        if(field != null) {
            field.setAccessible(true);
            field.set(obj, value);
        }
    }
}
複製程式碼

injectField(Field field,Object obj,Object value) 這個方法的作用就是,設定 obj 的 field 為 value。

JsonUtils 的作用就是為了解析我們的json配置檔案。程式碼比較長,與我們的 IoC 原理關係不大,感興趣的同學可以自行從github上下載程式碼看看。

有了這幾個趁手的工具,我們就可以開始完成 Ioc 框架的核心程式碼了。

3. 核心邏輯

我的 IoC 框架,目前只支援一種 ByName 的注入。所以我們的 BeanFactory 就只有一個方法:

public interface BeanFactory {
    Object getBean(String name) throws Exception;
}
複製程式碼

然後我們實現了這個方法:

public class BeanFactoryImpl implements BeanFactory{

    private static final ConcurrentHashMap<String,Object> beanMap = new ConcurrentHashMap<>();

    private static final ConcurrentHashMap<String,BeanDefinition> beanDefineMap= new ConcurrentHashMap<>();

    private static final Set<String> beanNameSet = Collections.synchronizedSet(new HashSet<>());

    @Override
    public Object getBean(String name) throws Exception {
        //查詢物件是否已經例項化過
        Object bean = beanMap.get(name);
        if(bean != null){
            return bean;
        }
        //如果沒有例項化,那就需要呼叫createBean來建立物件
        bean =  createBean(beanDefineMap.get(name));
        
        if(bean != null) {

            //物件建立成功以後,注入物件需要的引數
            populatebean(bean);
            
            //再把物件存入Map中方便下次使用。
            beanMap.put(name,bean;
        }

        //結束返回
        return bean;
    }

    protected void registerBean(String name, BeanDefinition bd){
        beanDefineMap.put(name,bd);
        beanNameSet.add(name);
    }

    private Object createBean(BeanDefinition beanDefinition) throws Exception {
        String beanName = beanDefinition.getClassName();
        Class clz = ClassUtils.loadClass(beanName);
        if(clz == null) {
            throw new Exception("can not find bean by beanName");
        }
        List<ConstructorArg> constructorArgs = beanDefinition.getConstructorArgs();
        if(constructorArgs != null && !constructorArgs.isEmpty()){
            List<Object> objects = new ArrayList<>();
            for (ConstructorArg constructorArg : constructorArgs) {
                objects.add(getBean(constructorArg.getRef()));
            }
            return BeanUtils.instanceByCglib(clz,clz.getConstructor(),objects.toArray());
        }else {
            return BeanUtils.instanceByCglib(clz,null,null);
        }
    }

    private void populatebean(Object bean) throws Exception {
        Field[] fields = bean.getClass().getSuperclass().getDeclaredFields();
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                String beanName = field.getName();
                beanName = StringUtils.uncapitalize(beanName);
                if (beanNameSet.contains(field.getName())) {
                    Object fieldBean = getBean(beanName);
                    if (fieldBean != null) {
                        ReflectionUtils.injectField(field,bean,fieldBean);
                    }
                }
            }
        }
    }
}
複製程式碼

首先我們看到在 BeanFactory 的實現中。我們有兩 HashMap,beanMap 和 beanDefineMap。 beanDefineMap 儲存的是物件的名稱和物件對應的資料結構的對映。beanMap 用於儲存 beanName和例項化之後的物件。

容器初始化的時候,會呼叫 BeanFactoryImpl.registerBean 方法。把物件的 BeanDefination 資料結構,儲存起來。

當我們呼叫 getBean() 的方法的時候。會先到 beanMap 裡面查詢,有沒有例項化好的物件。如果沒有,就會去beanDefineMap查詢這個物件對應的 BeanDefination。再利用DeanDefination去例項化一個物件。

物件例項化成功以後,我們還需要注入相應的引數,呼叫 populatebean()這個方法。在 populateBean 這個方法中,會掃描物件裡面的Field,如果物件中的 Field 是我們IoC容器管理的物件,那就會呼叫 我們上文實現的 ReflectionUtils.injectField來注入物件。

一切準備妥當之後,我們物件就完成了整個 IoC 流程。最後這個物件放入 beanMap 中,方便下一次使用。

所以我們可以知道 BeanFactory 是管理和生成物件的地方。

4. 容器

我們所謂的容器,就是對BeanFactory的擴充套件,負責管理 BeanFactory。我們的這個IoC 框架使用 Json 作為配置檔案,所以我們容器就命名為 JsonApplicationContext。當然之後你願意實現 XML 作為配置檔案的容器你就可以自己寫一個 XmlApplicationContext,如果基於註解的容器就可以叫AnnotationApplcationContext。這些實現留個大家去完成。

我們看看 ApplicationContext 的程式碼:

public class JsonApplicationContext extends BeanFactoryImpl{
    private String fileName;
    public JsonApplicationContext(String fileName) {
        this.fileName = fileName;
    }
    public void init(){
        loadFile();
    }
    private void loadFile(){
        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
        List<BeanDefinition> beanDefinitions = JsonUtils.readValue(is,new TypeReference<List<BeanDefinition>>(){});
        if(beanDefinitions != null && !beanDefinitions.isEmpty()) {
            for (BeanDefinition beanDefinition : beanDefinitions) {
                registerBean(beanDefinition.getName(), beanDefinition);
            }
        }
    }
}
複製程式碼

這個容器的作用就是 讀取配置檔案。將配置檔案轉換為容器能夠理解的 BeanDefination。然後使用 registerBean 方法。註冊這個物件。

至此,一個簡單版的 IoC 框架就完成。

5. 框架的使用

我們寫一個測試類來看看我們這個框架怎麼使用:

首先我們有三個物件

public class Hand {
    public void waveHand(){
        System.out.println("揮一揮手");
    }
}

public class Mouth {
    public void speak(){
        System.out.println("say hello world");
    }
}

public class Robot {
    //需要注入 hand 和 mouth 
    private Hand hand;
    private Mouth mouth;

    public void show(){
        hand.waveHand();
        mouth.speak();
    }
}
複製程式碼

我們需要為我們的 Robot 機器人注入 hand 和 mouth。

配置檔案:

[
  {
    "name":"robot",
    "className":"com.xilidou.framework.ioc.entity.Robot"
  },
  {
    "name":"hand",
    "className":"com.xilidou.framework.ioc.entity.Hand"
  },
  {
    "name":"mouth",
    "className":"com.xilidou.framework.ioc.entity.Mouth"
  }
]
複製程式碼

這個時候寫一個測試類:

public class Test {
    public static void main(String[] args) throws Exception {
        JsonApplicationContext applicationContext = new JsonApplicationContext("application.json");
        applicationContext.init();
        Robot aiRobot = (Robot) applicationContext.getBean("robot");
        aiRobot.show();
    }
}
複製程式碼

執行以後輸出:


揮一揮手
say hello world

Process finished with exit code 0
複製程式碼

可以看到我們成功的給我的 aiRobot 注入了 hand 和 mouth。

至此我們 Ioc 框架開發完成。

總結

這篇文章讀完以後相信你一定也實現了一個簡單的 IoC 框架。

雖然說閱讀原始碼是瞭解框架的最終手段。但是 Spring 框架作為一個生產框架,為了保證通用和穩定,原始碼必定是高度抽象,且處理大量細節。所以 Spring 的原始碼閱讀起來還是相當困難。希望這篇文章能夠幫助理解 Spring Ioc 的實現。

下一篇文章 應該會是 《徒手擼框架--實現AOP》。

github 地址:https://github.com/diaozxin007/xilidou-framework

相關文章