曹工說Tomcat4:利用 Digester 手擼一個輕量的 Spring IOC容器

三國夢迴發表於2019-07-08

一、前言

一共8個類,擼一個IOC容器。當然,我們是很輕量級的,但能夠滿足基本需求。想想典型的 Spring 專案,是不是就是各種Service/DAO/Controller,大家互相注入,就組裝成了我們的業務bean,然後再加上 Spring MVC,再往容器裡一放,基本齊活。

我們這篇文章,就是要照著 spring 來擼一個 相當簡單的 IOC 容器,這個容器可以完成以下功能:

1、在 xml 配置檔案裡配置 bean 的掃描路徑,語法目前只支援 component-scan,但基本夠用了;

2、Bean 用 Component 註解,bean 中屬性可以用 Autowired 來進行自動注入。

3、可以解決迴圈依賴問題。

 

bean的長相,基本就是下面這樣:

@Data
@Component
public class Girl {
    private String name = "catalina";
    private String height;
    private String breast;
    private String legLength;

    private Boolean isPregnant;

    @Autowired
    private com.ckl.littlespring.Coder coder;
}

  

xml,長下面這樣:

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <component-scan base-package="com.ckl.littlespring"/>
</beans>

 

二、思路

1、要解析xml,這個可以用Tomcat Digester 實現,這個是神器,用這個基本解決了讀配置檔案的問題;

2、讀取xml配置的包名下的所有class,這個可以參考Spring,我是在網上找的一個工具類,反正就是利用類載入器獲取classpath下的jar、class等,然後根據包名來過濾;

3、從第二步獲取的class集合中,過濾出來註解了 @Component 的類,並利用反射讀取其name、type、field集合等,其中field集合需要把帶有 @Autowired 的過濾出來,用所有這些資訊,構造一個 BeanDefinition 物件,放到 BeanDefinition 集合;

4、遍歷第三步的BeanDefinition集合,根據 BeanDefinition 生成 Bean,如果該 BeanDefinition 中的field依賴了其他bean,則遞迴處理,獲取到 field 後,反射設定到 bean中。

 

三、實現

1、程式碼結構、效果展示

強烈建議大家直接把程式碼拉下來跑,跑一跑,打個斷點,幾乎都不用看我寫的了。原始碼路徑:

https://github.com/cctvckl/tomcat-saxtest/blob/master/src/main/java/com/ckl/littlespring/TestLittleSpring.java

程式碼結構如下圖:

 

 大家看上圖,測試類中,主要是 new了 BeanDefinitionRegistry,這個就是我們的 bean 容器,可理解為 Spring 裡面的 org.springframework.beans.factory.support.DefaultListableBeanFactory,當然,我們的是玩具而已。該類的建構函式,接收一個引數,就是配置檔案的位置,預設會去classpath下查詢該檔案。bean 容器生成後,我們手動呼叫 refresh 來初始化容器,並生成 bean。 最後,我們既可以通過  getBeanByType(Class clazz) 來獲取想要的 bean了。

 

在測試程式碼中,Girl 和 Coder 迴圈依賴,我們們可以看看實際執行效果:

 1 package com.ckl.littlespring;
 2 
 3 import com.ckl.littlespring.annotation.Autowired;
 4 import com.ckl.littlespring.annotation.Component;
 5 import lombok.Getter;
 6 import lombok.Setter;
 7 
 8 @Getter
 9 @Setter
10 @Component
11 public class Coder {
12     private String name = "xiaoming";
13 
14     private String sex;
15 
16     private String love;
17     /**
18      * 女朋友
19      */
20     @Autowired
21     private com.ckl.littlespring.Girl girl;
22 
23 
24 }

 

 1 package com.ckl.littlespring;
 2 
 3 import com.ckl.littlespring.annotation.Autowired;
 4 import com.ckl.littlespring.annotation.Component;
 5 import com.coder.SaxTest;
 6 import lombok.Data;
 7 
 8 
 9 @Data
10 @Component
11 public class Girl {
12     private String name = "catalina";
13     private String height;
14     private String breast;
15     private String legLength;
16 
17     private Boolean isPregnant;
18 
19     @Autowired
20     private com.ckl.littlespring.Coder coder;
21 
22 
23 
24 }

 

 

可以看到,沒什麼問題,好了,接下來,看實現,我們按初始化--》使用的步驟來。

 

2、BeanDefinitionRegistry 初始化

    /**
     * bean定義解析器
     */
    private BeanDefinitionParser parser;
    
    public BeanDefinitionRegistry(String configFileLocation) {
        parser = new BeanDefinitionParser(configFileLocation);
    }

 

該bean容器中,建構函式中,將配置檔案直接傳給瞭解析器,解析器 BeanDefinitionParser 會真正負責從 xml 檔案內讀取 BeanDefinition。

 

3、 BeanDefinitionParser 初始化

 1 @Data
 2 public class BeanDefinitionParser {
 3     /**
 4      * xml 解析器
 5      */
 6     private Digester digester;
 7   
 8     private String configFileLocation;
 9 
10     
11     private List<MyBeanDefiniton> myBeanDefinitonList = new ArrayList<>();
12 
13 
14 
15     public BeanDefinitionParser(String configFileLocation) {
16         this.configFileLocation = configFileLocation;
17         digester = new Digester();
18     }
19 }

 

BeanDefinitionParser 中一共三個field,一個為配置檔案位置,一個為Tomcat Digester,一個用於儲存解析到的 BeanDefinition。Tomcat Digester用於解析 xml,這個一會實際的解析過程我們再說它。建構函式中,主要是給配置檔案賦值,以及生成 Digester例項。

 

4、refresh 方法解析

初始化完成後,呼叫BeanDefinitionRegistry 的 refresh 解析:

 1     public void refresh() {
 2         /**
 3          * 判斷是否已經解析完成bean定義。如果沒有完成,則先進行解析
 4          */
 5         if (!hasBeanDefinitionParseOver) {
 6             parser.parse();
 7             hasBeanDefinitionParseOver = true;
 8         }
 9 
10         /**
11          * 初始化所有的bean,完成自動注入
12          */
13         for (MyBeanDefiniton beanDefiniton : getBeanDefinitions()) {
14             getBean(beanDefiniton);
15         }
16     }

 

這裡,關注第6行,因為是首次解析,所以要進入BeanDefinitionParser .parse方法。

 1 /**
 2      * 根據指定規則,解析xml
 3      */
 4     public void parse() {
 5         digester.setValidating(false);
 6         digester.setUseContextClassLoader(true);
 7 
 8         // Configure the actions we will be using
 9         digester.addRule("beans/component-scan",
10                 new ComponentScanRule(this));
11 
12         InputSource inputSource = null;
13         InputStream inputStream = null;
14         try {
15             inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(configFileLocation);
16             inputSource = new InputSource(inputStream);
17             inputSource.setByteStream(inputStream);
18             Object o = digester.parse(inputSource);
19             System.out.println(o);
20         } catch (Exception e) {
21             e.printStackTrace();
22         }
23     }

 

先關注第9、10行,配置解析規則,在解析xml時,遇到 beans元素下的component-scan時,則回撥 ComponentScanRule 的規則。第18行,真正開始解析xml。

我們看看 ComponentScanRule 的實現:

 1 package com.ckl.littlespring.parser;
 2 
 3 import org.apache.commons.digester3.Rule;
 4 import org.xml.sax.Attributes;
 5 
13 public class ComponentScanRule extends Rule {
14 
15     private String basePackage;
16 
17     private BeanDefinitionParser beanDefinitionParser;
18 
19     public ComponentScanRule(BeanDefinitionParser beanDefinitionParser) {
20         this.beanDefinitionParser = beanDefinitionParser;
21     }
22 
23     @Override
24     public void begin(String namespace, String name, Attributes attributes) throws Exception {
25         basePackage = attributes.getValue("base-package");
26         beanDefinitionParser.doScanBasePackage(basePackage);
27     }
28 
29     @Override
30     public void end(String namespace, String name) throws Exception {
31 
32     }
33 }

 

關注25/26行,這裡從xml中獲取屬性 base-package,然後再呼叫 com.ckl.littlespring.parser.BeanDefinitionParser#doScanBasePackage 來進行處理。

 1 /**
 2      * 當遇到component-scan元素時,該函式被回撥,解析指定包下面的bean 定義,並加入bean 定義集合
 3      * @param basePackage
 4      */
 5     public void doScanBasePackage(String basePackage) {
 6         Set<Class<?>> classSet = ClassUtil.getClasses(basePackage);
 7 
 8         if (classSet == null) {
 9             return;
10         }
11 
12         //過濾出帶有Component註解的類,並將其轉換為beanDefinition
13         List<Class<?>> list = classSet.stream().filter(clazz -> clazz.getAnnotation(Component.class) != null).collect(Collectors.toList());
14 
15         for (Class<?> clazz : list) {
16             MyBeanDefiniton myBeanDefiniton = BeanDefinitionUtil.convert2BeanDefinition(clazz);
17             myBeanDefinitonList.add(myBeanDefiniton);
18         }
19 
20     }

 

以上方法執行結束時,basePackage下的被 Component 註解的 class就收集完畢。具體怎麼實現的?

1、呼叫  ClassUtil.getClasses(basePackage); 來獲取指定包下面的全部class

2、從第一步的集合中,過濾出帶有 Component 註解的class

3、利用 工具類 BeanDefinitionUtil.convert2BeanDefinition,從class 中提取 bean 定義的各類屬性。

 

先看看 BeanDefinition 的定義:

 1 package com.ckl.littlespring.parser;
 2 
 3 import lombok.Data;
 4 
 5 import java.lang.reflect.Field;
 6 import java.util.List;
 7 
 8 
 9 @Data
10 public class MyBeanDefiniton {
11 
12     /**
13      * bean的名字,預設使用類名,將首字母變成小寫
14      */
15     private String beanName;
16 
17     /**
18      * bean的型別
19      */
20     private String beanType;
21 
22     /**
23      * bean的class
24      */
25     private Class<?> beanClazz;
26 
27     /**
28      * field依賴的bean
29      */
30     private List<Field> dependencysByField;
31 
32 
33 }

 

就幾個屬性,相當簡單,下面看 BeanDefinitionUtil.convert2BeanDefinition:

 1  public static MyBeanDefiniton convert2BeanDefinition(Class<?> clazz){
 2         MyBeanDefiniton definiton = new MyBeanDefiniton();
 3         String name = clazz.getName();
 4         definiton.setBeanName(name.substring(0,1).toLowerCase() + name.substring(1));
 5         definiton.setBeanType(clazz.getCanonicalName());
 6         definiton.setBeanClazz(clazz);
 7 
 8         Field[] fields = clazz.getDeclaredFields();
 9         if (fields == null || fields.length == 0){
10             return definiton;
11         }
12 
13         ArrayList<Field> list = new ArrayList<>();
14         list.addAll(Arrays.asList(fields));
15         List<Field> dependencysField = list.stream().filter(field -> field.getAnnotation(Autowired.class) != null).collect(Collectors.toList());
16         definiton.setDependencysByField(dependencysField);
17 
18         return definiton;
19     }

 

最重要的就是第 15/16行,從所有的 field 中獲取 帶有 autowired 註解的field。這些 field 都是需要進行自動注入的。

執行完以上這些後,com.ckl.littlespring.parser.BeanDefinitionParser#myBeanDefinitonList 就持有了所有的 BeanDefinition。 下面就開始進行自動注入了,let's go!

 

5、bean初始化,完成自動注入

我們接下來,再看一下 BeanDefinitionRegistry 中的refresh,上面我們完成了 parser.parse 方法,此時,BeanDefinitionParser#myBeanDefinitonList 已經準備就緒了。

 1 public void refresh() {
 2         /**
 3          * 判斷是否已經解析完成bean定義。如果沒有完成,則先進行解析
 4          */
 5         if (!hasBeanDefinitionParseOver) {
 6             parser.parse();
 7             hasBeanDefinitionParseOver = true;
 8         }
 9 
10         /**
11          * 初始化所有的bean,完成自動注入
12          */
13         for (MyBeanDefiniton beanDefiniton : getBeanDefinitions()) {
14             getBean(beanDefiniton);
15         }
16     }

 

我們要關注的是,第13行,getBeanDefinitions()主要是從 parser 中獲取 BeanDefinition 集合。因為是內部使用,我們定義為private。

1     private List<MyBeanDefiniton> getBeanDefinitions() {
2         return parser.getBeanDefinitions();
3     }

 

然後,我們關注第14行,getBean 會真正完成 bean 的建立,如果有依賴的field,則會進行注入。

 1 /**
 2      * 根據bean 定義獲取bean
 3      * 1、先查bean容器,查到則返回
 4      * 2、生成bean,放進容器(此時,依賴還沒注入,主要是解決迴圈依賴問題)
 5      * 3、注入依賴
 6      *
 7      * @param beanDefiniton
 8      * @return
 9      */
10     private Object getBean(MyBeanDefiniton beanDefiniton) {
11         Class<?> beanClazz = beanDefiniton.getBeanClazz();
12         Object bean = beanMapByClass.get(beanClazz);
13         if (bean != null) {
14             return bean;
15         }
16 
17         //沒查到的話,說明還沒有,需要去生成bean,然後放進去
18         try {
19             bean = beanClazz.newInstance();
20         } catch (InstantiationException | IllegalAccessException e) {
21             e.printStackTrace();
22             return null;
23         }
24 
25         // 先行暴露,解決迴圈依賴問題
26         beanMapByClass.put(beanClazz, bean);
27 
28         //注入依賴,如果沒有依賴的field,直接返回
29         List<Field> dependencysByField = beanDefiniton.getDependencysByField();
30         if (dependencysByField == null) {
31             return bean;
32         }
33 
34         for (Field field : dependencysByField) {
35             try {
36                 autowireField(beanClazz, bean, field);
37             } catch (Exception e) {
38                 throw new RuntimeException(beanClazz.getName() + " 建立失敗",e);
39             }
40         }
41 
42         return bean;
43     }

 

在這個方法裡,我們主要就是,建立了 bean,並且放到了 容器中(一個hashmap,key是class,value就是對應的bean例項)。我們關注第36行,這裡會進行field 的注入:

 1 private void autowireField(Class<?> beanClazz, Object bean, Field field) {
 2         Class<?> fieldType = field.getType();
 3         List<MyBeanDefiniton> beanDefinitons = getBeanDefinitions();
 4         if (beanDefinitons == null) {
 5             return;
 6         }
 7 
 8         // 根據型別去所有beanDefinition看,哪個型別是該型別的子類;把滿足的都找出來
 9         List<MyBeanDefiniton> candidates = beanDefinitons.stream().filter(myBeanDefiniton -> {
10             return fieldType.isAssignableFrom(myBeanDefiniton.getBeanClazz());
11         }).collect(Collectors.toList());
12 
13         if (candidates == null || candidates.size() == 0) {
14             throw new RuntimeException(beanClazz.getName() + "根據型別自動注入失敗。field:" + field.getName() + " 無法注入,沒有候選bean");
15         }
16         if (candidates.size() > 1) {
17             throw new RuntimeException(beanClazz.getName() + "根據型別自動注入失敗。field:" + field.getName() + " 無法注入,有多個候選bean" + candidates);
18         }
19 
20         MyBeanDefiniton candidate = candidates.get(0);
21         Object fieldBean;
22         try {
23             // 遞迴呼叫
24             fieldBean = getBean(candidate);
25             field.setAccessible(true);
26             field.set(bean, fieldBean);
27         } catch (Exception e) {
28             throw new RuntimeException("注入屬性失敗:" + beanClazz.getName() + "##" + field.getName(), e);
29         }
30 
31 
32     }

 

這裡,我們先看第10行,我們要根據field 的 型別,看看當前的bean 容器中有沒有 field 型別的bean,比如我們的 field 的型別是個介面,那我們就會去看有沒有實現類。

這裡有兩個異常可能會丟擲,如果一個都沒找到,無法注入;如果找到了多個,我們也判斷為無法注入。(基礎版本,暫沒考慮 spring 中的 qualifier 註解)

最後,我們在第24行,根據找到的 beanDefinition 查詢 bean,這裡是個遞迴呼叫。 找到之後,會設定到 對應的 field 中。

注意的是,該遞迴的終結條件就是,該 bean 沒有依賴需要注入。 完成所有這些步驟後,我們的 bean 都註冊到了 BeanDefinitionRegistry#beanMapByClass 中。

1     /**
2      * map:儲存 bean的class-》bean例項
3      */
4     private Map<Class, Object> beanMapByClass = new ConcurrentHashMap<>();

 

後續,只需要根據class來查詢對應的bean即可。

1     /**
2      * 根據型別獲取bean物件
3      *
4      * @param clazz
5      * @return
6      */
7     public Object getBeanByType(Class clazz) {
8         return beanMapByClass.get(clazz);
9     }

 

 

 四、總結

一個簡易的ioc,大概就是這樣子了。後邊有時間,再把 aop 的功能加進去。當然,加進去了依然是玩具,我們造輪子的意義在哪裡呢?大概就是讓你更懂我們現在在用的輪子,知道它的核心程式碼大概是什麼樣子的。我們雖然大部分時候都是api 呼叫者,寫點膠水,但是真正出問題的時候,當框架不滿足的時候,我們還是得有搞定問題和擴充套件框架的能力。

個人水平也很有限,大家可以批評指正,歡迎加入下發二維碼的 Java 交流群一起溝通學習。

原始碼在github,連結在上文發過了哈。

參考的工具類連結:https://www.cnblogs.com/Leechg/p/10058763.html  其中有可以優化的空間,不過用著還是不錯。

相關文章