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


一、前言

一共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  其中有可以優化的空間,不過用着還是不錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM