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