從零開始實現一個簡易的Java MVC框架(三)--實現IOC

zzzzbw發表於2018-07-10

Spring中的IOC

IoC全稱是Inversion of Control,就是控制反轉,他其實不是spring獨有的特性或者說也不是java的特性,他是一種設計思想。而DI(Dependency Injection),即依賴注入就是Ioc的一種實現方式。關於Ioc和DI的具體定義和優缺點等大家可以自行查詢資料瞭解一下,這裡就不詳細贅述,總之spring的IoC功能很大程度上便捷了我們的開發工作。

在實現我們的Ioc之前,我們先了解一下spring的依賴注入,在spring中依賴注入有三種方式,分別是:

  1. 介面注入(Interface Injection)
  2. 設值方法注入(Setter Injection)
  3. 構造注入(Constructor Injection)
@Component
public class ComponentA {
    @Autowired // 1.介面注入
    private ComponentB componentB;
    
    @Autowired // 2.設值方法注入
    public void setComponentB(ComponentB componentB) {
        this.componentB = componentB;
    }

    @Autowired // 3.構造注入
    public ComponentA(ComponentB componentB) {
        this.componentB = componentB;
    }
}
複製程式碼

迴圈依賴注入

如果只是實現依賴注入的話實際上很簡單,只要利用java的反射原理將對應的屬性‘注入’進去就可以了。但是必須要注意一個問題,那就是迴圈依賴問題。迴圈依賴就是類之間相互依賴形成了一個迴圈,比如A依賴於B,同時B又依賴於A,這就形成了相互迴圈。

// ComponentA
@Component
public class ComponentA {
    @Autowired
    private ComponentB componentB;
}

// ComponentB
@Component
public class ComponentB {
    @Autowired
    private ComponentA componentA;
}
複製程式碼

那麼在spring中又是如何解決迴圈依賴問題的呢,我們大致說一下原理。

如果要建立一個類,先把這個類放進'正在建立池'中,通過反射等建立例項,建立成功的話就把這個例項放入建立池中,並移除'正在建立池'中的這個類。每當例項中有依賴需要注入的話,就從建立池中找對應的例項注入進去,如果沒有找到例項,則先建立這個依賴。

利用了這個正在建立的中間狀態快取,讓Bean的建立的時候即使有依賴還沒有例項化,可以先把Bean放進這個中間狀態,然後跑去建立那個依賴,假如那個依賴的類又依賴與這個Bean,那麼只要在'正在建立池'中再把這個Bean拿出來,注入到這個依賴中,就可以保證Bean的依賴能夠例項化完成。再回頭來把這個依賴注入到Bean中,那麼這個Bean也例項化完成了,就把這個Bean從'正在建立池'移到'建立完成池'中,就解決了迴圈依賴問題。

雖然spring巧妙的避免了迴圈依賴問題,但是事實上構造注入是無法避免迴圈依賴問題的。因為在例項化ComponentA的建構函式的時候必須得到ComponentB的例項,但是例項化ComponentB的建構函式的時候又必須有ComponentA的例項。這兩個Bean都不能通過反射例項化然後放到'正在建立池',所以無法解決迴圈依賴問題,這時候spring就會主動丟擲BeanCurrentlyInCreationException異常避免死迴圈。

* 注意,前面講的這些都是基於spring的單例模式下的,如果是多例模式會有所不同,大家有興趣可以自行了解。

實現IOC

現在可以開始實現IOC功能了

增加註解

先在zbw.ioc包下建立一個annotation包,然後再建立一個Autowired的註解。這個註解的Target只有一個ElementType.FIELD,就是隻能註解在屬性上。意味著我們目前只實現介面注入的功能。這樣可以避免構造注入造成的迴圈依賴問題無法解決,而且介面注入也是用的最多的方式了。如果想要實現設值方式注入大家可以自己去實現,實現原理幾乎都一樣。

package com.zbw.ioc.annotation;

import ...

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

複製程式碼

實現IOC類

package com.zbw.ioc;

import ...

@Slf4j
public class Ioc {

    /**
     * Bean容器
     */
    private BeanContainer beanContainer;

    public Ioc() {
        beanContainer = BeanContainer.getInstance();
    }

    /**
     * 執行Ioc
     */
    public void doIoc() {
        for (Class<?> clz : beanContainer.getClasses()) { //遍歷Bean容器中所有的Bean
            final Object targetBean = beanContainer.getBean(clz);
            Field[] fields = clz.getDeclaredFields();
            for (Field field : fields) { //遍歷Bean中的所有屬性
                if (field.isAnnotationPresent(Autowired.class)) {// 如果該屬性被Autowired註解,則對其注入
                    final Class<?> fieldClass = field.getType();
                    Object fieldValue = getClassInstance(fieldClass);
                    if (null != fieldValue) {
                        ClassUtil.setField(field, targetBean, fieldValue);
                    } else {
                        throw new RuntimeException("無法注入對應的類,目標型別:" + fieldClass.getName());
                    }
                }
            }
        }
    }

    /**
     * 根據Class獲取其例項或者實現類
     */
    private Object getClassInstance(final Class<?> clz) {
        return Optional
                .ofNullable(beanContainer.getBean(clz))
                .orElseGet(() -> {
                    Class<?> implementClass = getImplementClass(clz);
                    if (null != implementClass) {
                        return beanContainer.getBean(implementClass);
                    }
                    return null;
                });
    }

    /**
     * 獲取介面的實現類
     */
    private Class<?> getImplementClass(final Class<?> interfaceClass) {
        return beanContainer.getClassesBySuper(interfaceClass)
                .stream()
                .findFirst()
                .orElse(null);
    }

}

複製程式碼

在知道IOC的原理之後發現其實真的是非常簡單,這裡用了幾十行的程式碼就實現了IOC的功能。

首先在Ioc類構造的時候先獲取到我們之前已經單例化的BeanContainer容器。

然後在doIoc()方法中就是正式實現IOC功能的了。

  • 遍歷在BeanContainer容器的所有Bean
  • 對每個Bean的Field屬性進行遍歷
  • 如果某個Field屬性被Autowired註解,則呼叫getClassInstance()方法對其進行注入
  • getClassInstance()會根據Field的Class嘗試從Bean容器中獲取對應的例項,如果獲取到則返回該例項,如果獲取不到,則我們認定該Field為一個介面,我們就呼叫getImplementClass()方法來獲取這個介面的實現類Class,然後再根據這個實現類Class在Bean容器中獲取對應的實現類例項。

測試用例

為了測試我們的Ioc和之前寫的BeanContainer編寫正確,我們寫一下測試用例測試一下。

先在pom.xml新增junit的依賴

<properties>
    ...
    <junit.version>4.12</junit.version>
</properties>
<dependencies>
	...
    <!-- junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
複製程式碼

然後在test包下新增DoodleControllerDoodleServiceDoodleServiceImpl三個類方便測試

// DoodleController
package com.zbw.bean;
@Controller
@Slf4j
public class DoodleController {
    @Autowired
    private DoodleService doodleService;

    public void hello() {
        log.info(doodleService.helloWord());
    }
}

// DoodleService
package com.zbw.bean;
public interface DoodleService {
    String helloWord();
}

// DoodleServiceImpl
package com.zbw.bean;
@Service
public class DoodleServiceImpl implements DoodleService{
    @Override
    public String helloWord() {
        return "hello word";
    }
}
複製程式碼

再編寫IocTest的測試用例

package com.zbw.ioc;

import ...

@Slf4j
public class IocTest {
    @Test
    public void doIoc() {
        BeanContainer beanContainer = BeanContainer.getInstance();
        beanContainer.loadBeans("com.zbw");
        new Ioc().doIoc();
        DoodleController controller = (DoodleController) beanContainer.getBean(DoodleController.class);
        controller.hello();
    }
}
複製程式碼

從零開始實現一個簡易的Java MVC框架(三)--實現IOC

看到在DoodleController中輸出了DoodleServiceImplhelloWord()方法裡的字串,說明DoodleController中的DoodleService已經成功注入了DoodleServiceImpl。那麼我們的IOC的功能也完成了。


原始碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架--實現IOC

相關文章