一、前言
一共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、程式碼結構、效果展示
強烈建議大家直接把程式碼拉下來跑,跑一跑,打個斷點,幾乎都不用看我寫的了。原始碼路徑:
程式碼結構如下圖:
大家看上圖,測試類中,主要是 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 其中有可以優化的空間,不過用著還是不錯。