手寫IOC實現過程

張天賜的部落格發表於2020-08-08
一.手寫ioc前基礎知識

1.什麼是IOC(Inversion of Control 控制反轉)?

IoC不是一種技術,只是一種思想,一個重要的物件導向程式設計的法則,它能指導我們如何設計出鬆耦合、更優良的程式。傳統應用程式都是由我們在類內部主動建立依賴物件,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把建立和查詢依賴物件的控制權交給了容器由容器進行注入組合物件,所以物件與物件之間是鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程式的整個體系結構變得非常靈活。

其實IoC對程式設計帶來的最大改變不是從程式碼上,而是從思想上,發生了“主從換位”的變化。應用程式原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程式就變成被動的了,被動的等待IoC容器來建立並注入它所需要的資源了。

IoC很好的體現了物件導向設計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫物件找相應的依賴物件並注入,而不是由物件主動去找。

2.什麼是DI(Dependency Injection 依賴注入)?

DI—Dependency Injection,即“依賴注入”:是元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中。依賴注入的目的並非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,併為系統搭建一個靈活、可擴充套件的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何程式碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。

3.IOC和DI什麼關係?

IoC和DI由什麼關係呢?其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解為容器控制物件這一個層面,很難讓人想到誰來維護物件關係),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,依賴注入”明確描述了“被注入物件依賴IoC容器配置依賴物件”。

4.什麼是依賴?

傳統應用程式設計中所說的依賴一般指“類之間的關係”,那先讓我們複習一下類之間的關係:

泛化:表示類與類之間的繼承關係、介面與介面之間的繼承關係;

實現:表示類對介面的實現;

依賴:當類與類之間有使用關係時就屬於依賴關係,不同於關聯關係,依賴不具有“擁有關係”,而是一種“相識關係”,只在某個特定地方(比如某個方法體內)才有關係。

關聯:表示類與類或類與介面之間的依賴關係,表現為“擁有關係”;具體到程式碼可以用例項變數來表示;

聚合:屬於是關聯的特殊情況,體現部分-整體關係,是一種弱擁有關係;整體和部分可以有不一樣的生命週期;是一種弱關聯;

組合:屬於是關聯的特殊情況,也體現了體現部分-整體關係,是一種強“擁有關係”;整體與部分有相同的生命週期,是一種強關聯;

Spring IoC容器的依賴有兩層含義:Bean依賴容器和容器注入Bean的依賴資源:

Bean依賴容器:也就是說Bean要依賴於容器,這裡的依賴是指容器負責建立Bean並管理Bean的生命週期,正是由於由容器來控制建立Bean並注入依賴,也就是控制權被反轉了,這也正是IoC名字的由來,此處的有依賴是指Bean和容器之間的依賴關係。

容器注入Bean的依賴資源:容器負責注入Bean的依賴資源,依賴資源可以是Bean、外部檔案、常量資料等,在Java中都反映為物件,並且由容器負責組裝Bean之間的依賴關係,此處的依賴是指Bean之間的依賴關係,可以認為是傳統類與類之間的“關聯”、“聚合”、“組合”關係。

5.依賴注入的好處?

動態替換Bean依賴物件,程式更靈活:替換Bean依賴物件,無需修改原始檔:應用依賴注入後,由於可以採用配置檔案方式實現,從而能隨時動態的替換Bean的依賴物件,無需修改java原始檔;

更好實踐面向介面程式設計,程式碼更清晰:在Bean中只需指定依賴物件的介面,介面定義依賴物件完成的功能,通過容器注入依賴實現;

更好實踐優先使用物件組合,而不是類繼承:因為IoC容器採用注入依賴,也就是組合物件,從而更好的實踐物件組合。

  • 採用物件組合,Bean的功能可能由幾個依賴Bean的功能組合而成,其Bean本身可能只提供少許功能或根本無任何功能,全部委託給依賴Bean,物件組合具有動態性,能更方便的替換掉依賴Bean,從而改變Bean功能;

  • 而如果採用類繼承,Bean沒有依賴Bean,而是採用繼承方式新增新功能,,而且功能是在編譯時就確定了,不具有動態性,而且採用類繼承導致Bean與子Bean之間高度耦合,難以複用。

    增加Bean可複用性:依賴於物件組合,Bean更可複用且複用更簡單;

    降低Bean之間耦合:由於我們完全採用面向介面程式設計,在程式碼中沒有直接引用Bean依賴實現,全部引用介面,而且不會出現顯示的建立依賴物件程式碼,而且這些依賴是由容器來注入,很容易替換依賴實現類,從而降低Bean與依賴之間耦合;

    程式碼結構更清晰:要應用依賴注入,程式碼結構要按照規約方式進行書寫,從而更好的應用一些最佳實踐,因此程式碼結構更清晰。

從以上我們可以看出,其實依賴注入只是一種裝配物件的手段,設計的類結構才是基礎,如果設計的類結構不支援依賴注入,Spring IoC容器也注入不了任何東西,從而從根本上說如何設計好類結構才是關鍵,依賴注入只是一種裝配物件手段”。

二. 手寫IOC

標記配置分為集中式管理和分散式管理,即xml檔案方式和註解方式配置後設資料資訊,手寫ioc我採用註解配置後設資料方式來實現ioc的大致執行過程。

1.定義後設資料配置資訊

@Component/@Controller/@Repository@Service

掃描什麼樣的class裝載到ioc容器中進行管理。

@Autowired

什麼樣的物件進行依賴注入。

/**
 * @Classname Component
 * @Description
 * @Date 2020/8/6 14:54
 * @Created by zhangtianci
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
/**
 * @Classname Controller
 * @Description TODO
 * @Date 2020/8/6 14:56
 * @Created by zhangtianci
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
/**
 * @Classname Repository
 * @Description TODO
 * @Date 2020/8/6 14:58
 * @Created by zhangtianci
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
}
/**
 * @Classname Service
 * @Description TODO
 * @Date 2020/8/6 14:57
 * @Created by zhangtianci
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
/**
 * @Classname Autowired
 * @Description 自動注入註解
 * @Date 2020/8/6 14:28
 * @Created by zhangtianci
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
    String value() default "";
}

2.實現ioc容器

核心功能:載入被配置標記的class檔案,交給ioc容器管理。

package org.simplespring.core;

import lombok.extern.slf4j.Slf4j;
import org.simplespring.core.annotation.Component;
import org.simplespring.core.annotation.Controller;
import org.simplespring.core.annotation.Repository;
import org.simplespring.core.annotation.Service;
import org.simplespring.util.ClassUtil;
import org.simplespring.util.ValidationUtil;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Classname BeanContainer
 * @Description bean容器
 * <p>
 * BeanContainer應該是單例的 採用內部列舉的方式實現。
 *
 * 應該擁有的例項方法:
 * 1.boolean isLoad() 是否載入
 * 2.int getSize()    獲取bean個數
 * 3.loadBeans(String packageName) 根據包名載入所有被配置標記的class檔案/
 * 4.獲取/刪除/增加bean及提供一些便利的方法
 *
 * @Date 2020/8/6 14:41
 * @Created by zhangtianci
 */
@Slf4j
public class BeanContainer {
    /**
     * 存放所有被配置標記(xml/註解)的class,並new出一個例項物件bean
     * 存放在一個map中
     */
    private final Map<Class<?>, Object> beanMap = new ConcurrentHashMap<>();

    /**
     * 載入bean的註解列表
     */
    private static final List<Class<? extends Annotation>> BEAN_ANNOTATION
            = Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);

    /**
     * 是否已經載入過bean
     */
    private boolean loaded = false;

    /**
     * 獲取bean容器
     *
     * @return
     */
    public static BeanContainer getInstance() {
        return ContainerHolder.HOLDER.instance;
    }

    private enum ContainerHolder {
        HOLDER;

        private BeanContainer instance;

        ContainerHolder() {
            instance = new BeanContainer();
        }
    }

    /**
     * 是否已經載入過bean
     */
    public boolean isLoad(){
        return loaded;
    }

    /**
     * 獲取容器中bean的個數
     */

    public int getSize(){
        return beanMap.size();
    }

    /**
     * 載入指定路徑下的所有class檔案
     * 並將所有被配置標記(xml/註解)的class物件和new出一個例項物件放入容器
     */
    public synchronized void loadBeans(String packageName){
        //判斷容器是否已經載入過
        if (loaded){
            log.warn("Container has loaded!");
            return;
        }
        // 匯出所有的class物件
        Set<Class<?>> classSet = ClassUtil.extractPackageClass(packageName);
        //為空直接return
        if (ValidationUtil.isEmpty(classSet)) {
            log.warn("extract nothing from packageName" + packageName);
            return;
        }

        classSet.stream().forEach(clazz -> {
            for (Class<? extends Annotation> annotationClazz : BEAN_ANNOTATION) {
                if (clazz.isAnnotationPresent(annotationClazz)){
                    //將目標類本身作為鍵,目標類的例項作為值,放入到beanMap中
                    beanMap.put(clazz, ClassUtil.newInstance(clazz, true));
                }
            }

        });

        loaded = true;
    }


    /**
     * 新增一個class物件及其Bean例項
     *
     * @param clazz Class物件
     * @param bean  Bean例項
     * @return 原有的Bean例項, 沒有則返回null
     */
    public Object addBean(Class<?> clazz, Object bean) {
        return beanMap.put(clazz, bean);
    }

    /**
     * 移除一個IOC容器管理的物件
     *
     * @param clazz Class物件
     * @return 刪除的Bean例項, 沒有則返回null
     */
    public Object removeBean(Class<?> clazz) {
        return beanMap.remove(clazz);
    }

    /**
     * 根據Class物件獲取Bean例項
     *
     * @param clazz Class物件
     * @return Bean例項
     */
    public Object getBean(Class<?> clazz) {
        return beanMap.get(clazz);
    }
    /**
     * 獲取容器管理的所有Class物件集合
     *
     * @return Class集合
     */
    public Set<Class<?>> getClasses(){
        return beanMap.keySet();
    }
    /**
     * 獲取所有Bean集合
     *
     * @return Bean集合
     */
    public Set<Object> getBeans(){
        return new HashSet<>( beanMap.values());
    }
    /**
     * 根據註解篩選出Bean的Class集合
     *
     * @param annotation 註解
     * @return Class集合
     */
    public Set<Class<?>> getClassesByAnnotation(Class<? extends Annotation> annotation){
        //1.獲取beanMap的所有class物件
        Set<Class<?>> keySet = getClasses();
        if(ValidationUtil.isEmpty(keySet)){
            log.warn("nothing in beanMap");
            return null;
        }
        //2.通過註解篩選被註解標記的class物件,並新增到classSet裡
        Set<Class<?>> classSet = new HashSet<>();
        for(Class<?> clazz : keySet){
            //類是否有相關的註解標記
            if(clazz.isAnnotationPresent(annotation)){
                classSet.add(clazz);
            }
        }
        return classSet.size() > 0? classSet: null;
    }
    /**
     * 通過介面或者父類獲取實現類或者子類的Class集合,不包括其本身
     *
     * @param interfaceOrClass 介面Class或者父類Class
     * @return Class集合
     */
    public Set<Class<?>> getClassesBySuper(Class<?> interfaceOrClass){
        //1.獲取beanMap的所有class物件
        Set<Class<?>> keySet = getClasses();
        if(ValidationUtil.isEmpty(keySet)){
            log.warn("nothing in beanMap");
            return null;
        }
        //2.判斷keySet裡的元素是否是傳入的介面或者類的子類,如果是,就將其新增到classSet裡
        Set<Class<?>> classSet = new HashSet<>();
        for(Class<?> clazz : keySet){
            //判斷keySet裡的元素是否是傳入的介面或者類的子類
            if(interfaceOrClass.isAssignableFrom(clazz) && !clazz.equals(interfaceOrClass)){
                classSet.add(clazz);
            }
        }
        return classSet.size() > 0? classSet: null;
    }


}

3.定義依賴注入器

核心功能:掃描被管理的bean物件,進行依賴注入。

package org.simplespring.inject;

import lombok.extern.slf4j.Slf4j;
import org.simplespring.core.BeanContainer;
import org.simplespring.inject.annotation.Autowired;
import org.simplespring.util.ClassUtil;
import org.simplespring.util.ValidationUtil;
import java.lang.reflect.Field;
import java.util.Set;

/**
 * @Classname DependencyInjector
 * @Description 依賴注入器
 * @Date 2020/8/6 14:38
 * @Created by zhangtianci
 */
@Slf4j
public class DependencyInjector {
    /**
     * 擁有一個Bean容器
     */
    private BeanContainer beanContainer;
    public DependencyInjector(){
        beanContainer = BeanContainer.getInstance();
    }


    /**
     * 執行依賴注入
     *
     * 1.遍歷Bean容器中所有的Class物件
     * 2.遍歷Class物件的所有成員變數
     * 3.找出被Autowired標記的成員變數
     * 4.獲取這些成員變數的型別
     * 5.獲取這些成員變數的型別在容器裡對應的例項
     * 6.通過反射將對應的成員變數例項注入到成員變數所在類的例項裡
     */
    public void doIoc(){
        if(ValidationUtil.isEmpty(beanContainer.getClasses())){
            log.warn("empty classset in BeanContainer");
            return;
        }
        //1.遍歷Bean容器中所有的Class物件
        for(Class<?> clazz : beanContainer.getClasses()){
            //2.遍歷Class物件的所有成員變數
            Field[] fields = clazz.getDeclaredFields();
            if (ValidationUtil.isEmpty(fields)){
                continue;
            }
            for(Field field : fields){
                //3.找出被Autowired標記的成員變數
                if(field.isAnnotationPresent(Autowired.class)){
                    Autowired autowired = field.getAnnotation(Autowired.class);
                    String autowiredValue = autowired.value();
                    //4.獲取這些成員變數的型別
                    Class<?> fieldClass = field.getType();
                    //5.獲取這些成員變數的型別在容器裡對應的例項
                    Object fieldValue = getFieldInstance(fieldClass, autowiredValue);
                    if(fieldValue == null){
                        throw new RuntimeException("unable to inject relevant type,target fieldClass is:" + fieldClass.getName() + " autowiredValue is : " + autowiredValue);
                    } else {
                        //6.通過反射將對應的成員變數例項注入到成員變數所在類的例項裡
                        Object targetBean =  beanContainer.getBean(clazz);
                        ClassUtil.setField(field, targetBean, fieldValue, true);
                    }
                }
            }
        }


    }
    /**
     * 根據Class在beanContainer裡獲取其例項或者實現類
     */
    private Object getFieldInstance(Class<?> fieldClass, String autowiredValue) {
        Object fieldValue = beanContainer.getBean(fieldClass);
        if (fieldValue != null){
            return fieldValue;
        } else {
            Class<?> implementedClass = getImplementedClass(fieldClass, autowiredValue);
            if(implementedClass != null){
                return beanContainer.getBean(implementedClass);
            } else {
                return null;
            }
        }
    }
    /**
     * 獲取介面的實現類
     */
    private Class<?> getImplementedClass(Class<?> fieldClass, String autowiredValue) {
        Set<Class<?>> classSet =  beanContainer.getClassesBySuper(fieldClass);
        if(!ValidationUtil.isEmpty(classSet)){
            if(ValidationUtil.isEmpty(autowiredValue)){
                if(classSet.size() == 1){
                    return classSet.iterator().next();
                } else {
                    //如果多於兩個實現類且使用者未指定其中一個實現類,則丟擲異常
                    throw new RuntimeException("multiple implemented classes for " + fieldClass.getName() + " please set @Autowired's value to pick one");
                }
            } else {
                for(Class<?> clazz : classSet){//別名採用clazz.getSimpleName()簡單實現
                    if(autowiredValue.equals(clazz.getSimpleName())){
                        return clazz;
                    }
                }
            }
        }
        return null;
    }


}

4.工具包

作為框架 類載入/反射/校驗等功能。

package org.simplespring.util;

import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * @Classname ClassUtil
 * @Description
 * @Date 2020/8/8 15:20
 * @Created by zhangtianci
 */
@Slf4j
public class ClassUtil {

    public static final String FILE_PROTOCOL = "file";

    /**
     * 獲取類載入器
     * 因為專案部署在tomcat等一些web容器中
     * 這些web容器載入部署在裡面的apps 所以需要自定義classLoader去載入class檔案
     * 當我們寫框架需要去通過類載入器去載入class檔案時 就需要通過Thread.currentThread().getContextClassLoader()
     * 拿到tomcat的自定義的classLoader去載入專案裡面的class檔案
     * 詳情 參考我寫的classLoader載入相關文章
     * @return
     */
    public static ClassLoader getClassLoader(){
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 通過包名載入class
     *
     * 點進去Class.forName()方法進去看看
     *
     * @CallerSensitive
     *     public static Class<?> forName(String className)
     *                 throws ClassNotFoundException {
     *         Class<?> caller = Reflection.getCallerClass();
     *         return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
     *     }
     *
     *     獲取到呼叫這個方法的class物件  然後用這個class物件的classLoader去載入這個class檔案
     *     所以歸根結底是 拿到tomcat的自定義的classLoader去載入的class檔案
     *     所以沒問題
     * @param className class全名=package + 類名
     * @return
     */
    public static Class<?> loadClass(String className){
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            log.error("loadClass failed!",e);
            throw new RuntimeException(e);
        }

    }


    /**
     * 通過一個class物件例項化一個例項物件(通過預設構造器)
     * @param clazz
     * @param accessible
     * @param <T>
     * @return
     */
    public static <T> T newInstance(Class<?> clazz, boolean accessible){
        try {
            //clazz.getDeclaredConstructor() 獲取指定引數類表的構造器
            //這裡獲取預設構造器
            Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(accessible);
            return (T)constructor.newInstance();
        } catch (Exception e) {
            log.error("new instance failed!");
            throw new RuntimeException(e);
        }
    }

    /**
     * 設定類的屬性值
     *
     * @param field      成員變數
     * @param target     類例項
     * @param value      成員變數的值
     * @param accessible 是否允許設定私有屬性
     */
    public static void setField(Field field, Object target, Object value, boolean accessible){
        field.setAccessible(accessible);
        try {
            field.set(target, value);
        } catch (IllegalAccessException e) {
            log.error("setField error", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 載入包路徑下的所有的class檔案
     * 將class物件放進set集合中
     *
     * 1.獲取classLoader載入器
     * 2.遞迴載入路徑下的所有class檔案
     * @param packageName
     * @return
     */
    public static Set<Class<?>> extractPackageClass(String packageName){

        //1.獲取classLoader載入器
        ClassLoader classLoader = getClassLoader();
        //2.獲取資原始檔的的url
        URL url = classLoader.getResource(packageName.replace(".","/"));
        if (url == null){
            log.warn("unable to retrieve anything from package: " + packageName);
            return  null;
        }

        //3.依據不同的資源型別,採用不同的方式獲取資源的集合
        Set<Class<?>> classSet = null;
        //過濾出檔案型別的資源
        if (url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL)){
            classSet = new HashSet<Class<?>>();
            File packageDirectory = new File(url.getPath());
            extractClassFile(classSet, packageDirectory, packageName);
        }
        //TODO 此處可以加入針對其他型別資源的處理

        return null;
    }

    /**
     * 遞迴載入包路徑下的class檔案
     * @param classSet
     * @param packageDirectory
     * @param packageName
     */
    private static void extractClassFile(Set<Class<?>> classSet, File packageDirectory, String packageName) {

        if(!packageDirectory.isDirectory()){
            return;
        }
        //如果是一個資料夾,則呼叫其listFiles方法獲取資料夾下的檔案或資料夾
        File[] files = packageDirectory.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                if(pathname.isDirectory()){
                    return true;
                } else{
                    //獲取檔案的絕對值路徑
                    String absoluteFilePath = pathname.getAbsolutePath();
                    if(absoluteFilePath.endsWith(".class")){
                        //若是class檔案,則直接載入
                        addToClassSet(absoluteFilePath);
                    }
                }
                return false;
            }


            //根據class檔案的絕對值路徑,獲取並生成class物件,並放入classSet中
            private void addToClassSet(String absoluteFilePath) {
                //1.從class檔案的絕對值路徑裡提取出包含了package的類名
                //如/Users/zhangtc/springframework/simple-spring/target/classes/com/zhangtianci/entity/dto/MainPageInfoDTO.class
                //需要弄成com.zhangtianci.entity.dto.MainPageInfoDTO
                absoluteFilePath = absoluteFilePath.replace(File.separator, ".");
                String className = absoluteFilePath.substring(absoluteFilePath.indexOf(packageName));
                className = className.substring(0, className.lastIndexOf("."));
                //2.通過反射機制獲取對應的Class物件並加入到classSet裡
                Class targetClass = loadClass(className);
                classSet.add(targetClass);
            }
        });

        if(files == null){
            return;
        }
        Arrays.stream(files).forEach( file -> {
            extractClassFile(classSet,file,packageName);
        });


    }


}
package org.simplespring.util;

import java.util.Collection;
import java.util.Map;

public class ValidationUtil {
    /**
     * String是否為null或""
     *
     * @param obj String
     * @return 是否為空
     */
    public static boolean isEmpty(String obj) {
        return (obj == null || "".equals(obj));
    }

    /**
     * Array是否為null或者size為0
     *
     * @param obj Array
     * @return 是否為空
     */
    public static boolean isEmpty(Object[] obj) {
        return obj == null || obj.length == 0;
    }
    /**
     * Collection是否為null或size為0
     *
     * @param obj Collection
     * @return 是否為空
     */
    public static boolean isEmpty(Collection<?> obj){
        return obj == null || obj.isEmpty();
    }
    /**
     * Map是否為null或size為0
     *
     * @param obj Map
     * @return 是否為空
     */
    public static boolean isEmpty(Map<?, ?> obj) {
        return obj == null || obj.isEmpty();
    }
}

相關文章