手寫一個HTTP框架:兩個類實現基本的IoC功能

JavaGuide發表於2020-10-14

jsoncat: 仿 Spring Boot 但不同於 Spring Boot 的一個輕量級的 HTTP 框架

國慶節的時候,我就已經把 jsoncat 的 IoC 功能給寫了,具體可以看這篇文章《手寫“SpringBoot”近況:IoC模組已經完成》 。

今天這篇文章就來簡單分享一下自己寫 IoC 的思路與具體的程式碼實現。

在這裡插入圖片描述

IoC (Inverse of Control:控制反轉)AOP(Aspect-Oriented Programming:面向切面程式設計) 可以說是 Spring 框架提供的最核心的兩個功能。但凡是瞭解過 Spring 的小夥伴,那肯定對這個兩個概念非常非常瞭解。不瞭解的小夥伴,可以檢視《面試被問了幾百遍的 IoC 和 AOP ,還在傻傻搞不清楚?》這篇通俗易懂的文章。

考慮到這篇文章要手寫 Spring 框架的 IoC 功能,所以,我這裡還是簡單介紹一下 IoC 。如果你不太清楚 IoC 這個概念,一定要搞懂之後再看後面具體的程式碼實現環節。

IoC 介紹

IoC(Inverse of Control:控制反轉)是一種設計思想,也就是 將原本在程式中手動建立物件的控制權交由Spring框架來管理。 IoC 在其他語言中也有應用,並非 Spring 特有。

IoC 容器

IoC 容器是用來實現 IoC 的載體,被管理的物件就被存放在IoC容器中。IoC 容器在 Spring 中實際上就是個Map(key,value),Map 中存放了各種被管理的物件。

IoC 解決了什麼問題

將物件之間的相互依賴關係交給 IoC 容器來管理,並由 IoC 容器完成物件的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 IoC 容器就像是一個工廠一樣,當我們需要建立一個物件的時候,只需要配置好配置檔案/註解即可,完全不用考慮物件是如何被建立出來的。 在實際專案中一個 Service 類可能有幾百甚至上千個類作為它的底層,假如我們需要例項化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的建構函式,這可能會把人逼瘋。如果利用 IoC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了專案的可維護性且降低了開發難度。

IoC 和 DI 別再傻傻分不清楚

IoC(Inverse of Control:控制反轉)是一種設計思想 或者說是某種模式。這個設計思想就是 將原本在程式中手動建立物件的控制權,交由 Spring 框架來管理。 IoC 在其他語言中也有應用,並非 Spring 特有。IoC 容器是 Spring 用來實現 IoC 的載體, IoC 容器實際上就是個 Map(key,value),Map 中存放的是各種被管理的物件。

IoC 最常見以及最合理的實現方式叫做依賴注入(Dependency Injection,簡稱 DI)。

並且,老馬(Martin Fowler)在一篇文章中提到將 IoC 改名為 DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html

IoC實現思路

?注意 :以下思路未涉及解決迴圈依賴的問題!

開始程式碼實現之前,我們先簡單聊聊實現 IoC 的思路,搞清楚了思路之後,實現起來就非常簡單了。

  1. 掃描指定包下的特定註解比如@Component標記的類,並將這些類儲存起來。
  2. 遍歷所有被特定註解比如@Component標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件。
  3. 再一次遍歷所有被特定註解比如@Component標記的類,並獲取類中所有的欄位,如果類被 @Autowired 註解標記的話,就進行第 4 步。
  4. 通過欄位名 key,從bean容器中獲取對應的物件 value。
  5. 判斷獲取到的物件是否為介面。如果是介面的話,需要獲取介面對應的實現類,然後再將指定的實現類的例項化物件通過反射賦值給指定物件。如果不是介面的話,就直接將獲取到的物件通過反射賦值給指定物件。

IoC 實現核心程式碼

核心註解

@Autowired :註解物件

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

}

@Component :宣告物件被IoC容器管理


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
    String name() default "";
}

@Qualifier: 指定注入的bean(當介面有多個實現類的時候需要使用)

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
    String value() default "";
}

工具類

簡單封裝一個反射工具類。工具類包含3個後面會用到的方法:

  1. scanAnnotatedClass() :掃描指定包下的被指定註解標記的類(使用Reflections這個反射框架一行程式碼即可解決掃描獲取指定註解的類)。
  2. newInstance() : 傳入 Class 即可返回 Class 對應的物件。
  3. setField() :為物件的指定欄位賦值。
@Slf4j
public class ReflectionUtil {
    /**
     * scan the classes marked by the specified annotation in the specified package
     *
     * @param packageName specified package name
     * @param annotation  specified annotation
     * @return the classes marked by the specified annotation in the specified package
     */
    public static Set<Class<?>> scanAnnotatedClass(String packageName, Class<? extends Annotation> annotation) {
        Reflections reflections = new Reflections(packageName, new TypeAnnotationsScanner());
        Set<Class<?>> annotatedClass = reflections.getTypesAnnotatedWith(annotation, true);
        log.info("The number of class Annotated with  @RestController :[{}]", annotatedClass.size());
        return annotatedClass;
    }

    /**
     * create object instance through class
     *
     * @param cls target class
     * @return object created by the target class
     */
    public static Object newInstance(Class<?> cls) {
        Object instance = null;
        try {
            instance = cls.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            log.error("new instance failed", e);
        }
        return instance;
    }

    /**
     * set the value of a field in the object
     *
     * @param obj   target object
     * @param field target field
     * @param value the value assigned to the field
     */
    public static void setField(Object obj, Field field, Object value) {

        field.setAccessible(true);
        try {
            field.set(obj, value);
        } catch (IllegalAccessException e) {
            log.error("set field failed", e);
            e.printStackTrace();
        }

    }
  
}

根據實現思路寫程式碼

?注意 :以下程式碼未涉及解決迴圈依賴的問題!以下是 IoC 實現的核心程式碼,完整程式碼地址:https://github.com/Snailclimb/jsoncat

1.掃描指定包下的特定註解比如@Component標記的類,並將這些類儲存起來。

掃描指定註解@RestController@Component並儲存起來:

public class ClassFactory {
    public static final Map<Class<? extends Annotation>, Set<Class<?>>> CLASSES = new ConcurrentHashMap<>();
    //1.掃描指定包下的特定註解比如`@Component`標記的類,並將這些類儲存起來
    public static void loadClass(String packageName) {
        Set<Class<?>> restControllerSets = ReflectionUtil.scanAnnotatedClass(packageName, RestController.class);
        Set<Class<?>> componentSets = ReflectionUtil.scanAnnotatedClass(packageName, Component.class);
        CLASSES.put(RestController.class, restControllerSets);
        CLASSES.put(Component.class, componentSets);
    }
}

2.遍歷所有被特定註解比如@Component標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件。

public final class BeanFactory {
    public static final Map<String, Object> BEANS = new ConcurrentHashMap<>(128);

    public static void loadBeans() {

        // 2.遍歷所有被特定註解比如 @Component 標記的類,然後將這些類通過反射例項化並通過一個 Map 儲存起來,Map 的 key 為類名,value為類物件
        ClassFactory.CLASSES.forEach((annotation, classes) -> {
            if (annotation == Component.class) {
                //將bean例項化, 並放入bean容器中
                for (Class<?> aClass : classes) {
                    Component component = aClass.getAnnotation(Component.class);
                    String beanName = "".equals(component.name()) ? aClass.getName() : component.name();
                    Object obj = ReflectionUtil.newInstance(aClass);
                    BEANS.put(beanName, obj);
                }
            }

            if (annotation == RestController.class) {
                for (Class<?> aClass : classes) {
                    Object obj = ReflectionUtil.newInstance(aClass);
                    BEANS.put(aClass.getName(), obj);
                }
            }
        });
    }
}

3.再一次遍歷所有被特定註解比如@Component標記的類,並獲取類中所有的欄位,如果類被 @Autowired 註解標記的話,就進行第 4 步。

public class DependencyInjection {

    public static void dependencyInjection(String packageName) {
        Map<String, Object> beans = BeanFactory.BEANS;
        if (beans.size() == 0) return;
        //3.再一次遍歷所有被特定註解比如 @Component 標記的類,並獲取類中所有的欄位,如果類被 `@Autowired` 註解標記的話,就進行第 4 步。
        // 3.1.遍歷bean容器中的所有物件
        beans.values().forEach(bean -> {
            // 3.2.獲取物件所屬的類宣告的所有欄位/屬性
            Field[] beanFields = bean.getClass().getDeclaredFields();
            if (beanFields.length == 0) return;
            //3.3.遍歷物件所屬的類宣告的所有欄位/屬性
            for (Field beanField : beanFields) {
              //3.4.判斷欄位是否被 @Autowired 註解標記
                if (beanField.isAnnotationPresent(Autowired.class)) {
                    //4.通過欄位名 key,從bean容器中獲取對應的物件 value。
                    //4.1.欄位對應的型別
                    Class<?> beanFieldClass = beanField.getType();
                    //4.2.欄位對應的類名
                    String beanName = beanFieldClass.getName();
                    if (beanFieldClass.isAnnotationPresent(Component.class)) {
                        Component component = beanFieldClass.getAnnotation(Component.class);
                        beanName = "".equals(component.name()) ? beanFieldClass.getName() : component.name();
                    }
                    //4.3.從bean容器中獲取對應的物件
                    Object beanFieldInstance = beans.get(beanName);
                    //5.判斷獲取到的物件是否為介面。如果是介面的話,需要獲取介面對應的實現類,然後再將指定的實現類的例項化物件通過反射賦值給指定物件。如果不是介面的話,就直接將獲取到的物件通過反射賦值給指定物件。
                    if (beanFieldClass.isInterface()) {
                        //如果是介面,獲取介面對應的實現類
                        Set<Class<?>> subClasses = getSubClass(packageName, beanFieldClass);
                        //沒有實現類的話就丟擲異常
                        if (subClasses.size() == 0) {
                            throw new InterfaceNotHaveImplementedClassException("interface does not have implemented class exception");
                        }
                        //實現類只有一個話,直接獲取
                        if (subClasses.size() == 1) {
                            Class<?> aClass = subClasses.iterator().next();
                            beanFieldInstance = ReflectionUtil.newInstance(aClass);
                        }
                        //實現類多與一個的話,根據 Qualifier 註解的值獲取
                        if (subClasses.size() > 1) {
                            Class<?> aClass = subClasses.iterator().next();
                            Qualifier qualifier = beanField.getDeclaredAnnotation(Qualifier.class);
                            beanName = qualifier == null ? aClass.getName() : qualifier.value();
                            beanFieldInstance = beans.get(beanName);
                        }

                    }
                    // 如果最後獲取到的欄位物件為null,就丟擲異常
                    if (beanFieldInstance == null) {
                        throw new CanNotDetermineTargetBeanException("can not determine target bean");
                    }
                    //通過反射設定指定物件中的指定欄位的值
                    ReflectionUtil.setField(bean, beanField, beanFieldInstance);
                }
            }
        });


    }

    /**
     * 獲取介面對應的實現類
     */
    @SuppressWarnings("unchecked")
    public static Set<Class<?>> getSubClass(String packageName, Class<?> interfaceClass) {
        Reflections reflections = new Reflections(packageName);
        return reflections.getSubTypesOf((Class<Object>) interfaceClass);
    }
}

我整理了一份優質原創PDF資源免費分享給大家,大部分內容都是我的原創,少部分來自朋友。

手寫一個HTTP框架:兩個類實現基本的IoC功能 image-20201012105608336

下載地址:https://cowtransfer.com/s/fbed14f0c22a4d
我是 Guide 哥,一 Java 後端開發,會一點前端,自由的少年。我們下期再見!微信搜“JavaGuide”回覆“面試突擊”領取我整理的 4 本原創PDF

相關文章