Spring學習:簡單實現一個依賴注入和迴圈依賴的解決

划水的魚dm發表於2022-01-11

依賴注入

什麼是依賴注入

使用一個會建立和查詢依賴物件的容器,讓它負責供給物件。

當a物件需要b物件時,不再是使用new建立,而是從容器中獲取,物件與物件之間是鬆散耦合的關係,有利於功能複用。

依賴:應用程式依賴容器,需要的物件都從容器獲取

注入:容器將物件注入到應用程式中

設計思路

  • 我們必須告訴容器:哪些類是由容器來建立的;哪些類是要從容器中獲取的
    • 使用兩個註解對類進行標記
  • 容器必須對所有類進行掃描,將標記過的類建立和注入
    • 掃描src資料夾下所有java為字尾的檔案
    • 使用反射的方式檢視類定義,構造物件
  • 一個能建立、獲取物件的容器
    • 使用Map作為這個容器:Class型別為key,Object型別為value

程式碼實現

 註解定義

 

/**
 * 被標記的類需要由容器建立
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface FBean {

}

 

/**
 * 標記需要從容器獲取的物件
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.FIELD)
public @interface FAutowired {

}

 

對所有類進行掃描

通過遍歷當前專案的所有java檔案,由類名(包名 + java檔名)獲取class,使用一個List存放用註解標記過的class

 static List<Class> classList;

    public static void scanClass() throws ClassNotFoundException {
        File currentFile = new File("");
        // 當前專案的絕對路徑
        String path = currentFile.getAbsolutePath();

        classList = new ArrayList<>();
        // 專案下src下的java檔案
        File javaFile = new File(path + "/src/main/java");
        // 類所在的包
        File[] packageFiles = javaFile.listFiles();
        for (int i = 0; i < packageFiles.length; i++) {
            findClass(packageFiles[i], packageFiles[i].getName());
        }
    }

    /**
     * 遞迴開啟資料夾,尋找java檔案,沒有資料夾時結束遞迴
     *
     * @param file      當前找的檔案
     * @param className 類名稱
     * @throws ClassNotFoundException
     */
    private static void findClass(File file, String className) throws ClassNotFoundException {

        if (file.isFile()) {
            // 將className最後的.java去掉
            int endIndex = className.lastIndexOf(".");
            String[] fileNames = file.getName().split("\\.");
            // 判斷是否為java檔案
            if ("java".equals(fileNames[1])) {
                // 反射獲取類放入list中
                Class clazz = Class.forName(className.substring(0, endIndex));
                if (clazz.isAnnotationPresent(FBean.class)){
                    classList.add(clazz);
                }
            }
            return;
        }

        File[] files = file.listFiles();
        for (int i = 0; i < files.length; i++) {
            // 如果是package(資料夾),將檔名加入到類名稱中,繼續檢視包裡面的檔案
            findClass(files[i], className + "." + files[i].getName());
        }
    }

 

使用反射構造容器裡的物件

使用一個Map作為儲存物件的容器,有註解標記的引用通過class屬性獲取容器裡的物件

和上面掃描類的程式碼寫在同一個工具類(IocUtils)中

    static final Map<Class, Object> objectMap = new HashMap<>();
    
    static {
        try {
            // 先掃描類獲取class
            scanClass();

            for (int i = 0; i < classList.size(); i++) {
                // 對一個個class進行初始化
                constructClass(classList.get(i));
            }
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }

    public static Object constructClass(Class clazz) throws IllegalAccessException, InstantiationException {

        if (clazz.isInterface() || clazz.isAnnotation()) {
            return null;
        }

        if (objectMap.containsKey(clazz)) {
            return objectMap.get(clazz);
        }
        // 反射構造物件
        Object obj = clazz.newInstance();

        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 需要容器注入其他物件,並且容器中沒有,讓容器繼續構造物件
            if (field.isAnnotationPresent(FAutowired.class)) {
                if (!objectMap.containsKey(field.getType())) {
                    // 遞迴構造
                    constructClass(field.getType());
                }
                // 構造結束進行賦值
                field.setAccessible(true);
                field.set(obj, objectMap.get(field.getType()));
            }
        }
        // 每個物件構造完放進容器中
        objectMap.put(clazz, obj);

        return objectMap.get(clazz);
    }

 

  • 這裡沒有考慮使用介面的情況(因為太難了)
    • 可以使用一個介面跟實現類對應的Map集合,field為介面型別時,構造實現類返回

進行測試

要交給容器的實體類

Spring學習:簡單實現一個依賴注入和迴圈依賴的解決
@FBean
public class StudentDao {
    public void query(){
        System.out.println("StudentDao:query()");
    }
}
View Code
Spring學習:簡單實現一個依賴注入和迴圈依賴的解決
@FBean
public class TeacherDao {

    @FAutowired
    private StudentDao studentDao;

    public void query(){
        System.out.println("teacherDao:query()");
    }

    public StudentDao getStudentDao() {
        return studentDao;
    }

    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }
}
View Code

 

 

測試程式碼

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        StudentDao studentDao = (StudentDao) IocUtils.objectMap.get(StudentDao.class);

        TeacherDao teacherDao = (TeacherDao) IocUtils.objectMap.get(TeacherDao.class);

        if (teacherDao.getStudentDao() == studentDao) {
            System.out.println("物件複用,依賴注入成功");
        }
    }

測試結果

 

迴圈依賴

問題的產生

修改一下先前的StudentDao,讓它引用TeacherDao,兩個類就互相引用了

Spring學習:簡單實現一個依賴注入和迴圈依賴的解決
@FBean
public class StudentDao {
    @FAutowired
    private TeacherDao teacherDao;

    public TeacherDao getTeacherDao() {
        return teacherDao;
    }

    public void setTeacherDao(TeacherDao teacherDao) {
        this.teacherDao = teacherDao;
    }
    
    public void query(){
        System.out.println("StudentDao:query()");
    }
}
View Code

 

在原本構造物件的方法裡面:

如果a類引用了容器的b類,a類在構造時,會讓容器去構造b類,等b類構造完畢,a類才構造完畢。

當兩個類互相引用時,a讓容器構造b,b讓容器構造a,最終造成死迴圈,可以使用上面的程式碼測試。

解決方案

原本只使用了一個Object_Map儲存物件,現在再加上一個Complete_Map。

  • Object_Map是第一層,儲存的是剛剛例項化的物件。
  • Complete_Map是第二層,儲存屬性填充完畢(引用的b、c、d、e全部構造好)的物件。

新的構造物件步驟

  1. a在例項化後,讓容器去構造b,b例項化後,將b存入Object_Map中,繼續a的構造流程。
  2. a從Object_Map拿到b,繼續構造,最後存入Complete_Map。
  3. 輪到b構造時,使用Object_Map裡面的b(也就是a已經引用的b),屬性填充時,將Complete_Map裡的a拿來用,構造完畢,存入Complete_Map

更新後的完整程式碼

Spring解決迴圈依賴的問題,為了AOP的實現使用了第三個Map。沒有AOP的話,兩個Map解決迴圈依賴的思路應該跟這差不太多。

主要修改constructClass這個方法,並且多了一個方法引數,所以呼叫方法的地方要改一下

package com.david.spring.ioc;

import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class IocUtils {
    // 用註解標記的class集合
    static final List<Class> classList = new ArrayList<>();
    // 構造好物件的map
    static Map<Class, Object> completeMap = new HashMap<>();
    // 為了解決迴圈依賴問題建立的map
    static final Map<Class, Object> objectMap = new HashMap<>();

    static {
        try {
            // 先掃描類獲取class
            scanClass();
            for (int i = 0; i < classList.size(); i++) {
                // 對一個個class進行初始化,是需要完整構造
                constructClass(classList.get(i), true);
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     *
     * @param clazz 要構造的類
     * @param isInitial true表示類自己需要完整構造,false表示其他物件要求構造
     */
    public static void constructClass(Class clazz, boolean isInitial) throws InstantiationException, IllegalAccessException {

        if (clazz.isInterface() || clazz.isAnnotation() || completeMap.containsKey(clazz)) {
            return;
        }

        Object obj;
        if (!objectMap.containsKey(clazz)) {
            // 反射構造物件
            obj = clazz.newInstance();
            objectMap.put(clazz, obj);
        } else {

            obj = objectMap.get(clazz);
        }
        if (!isInitial) {
            return;
        }

        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 需要容器注入其他物件,並且容器中沒有,讓容器繼續構造物件
            if (field.isAnnotationPresent(FAutowired.class)) {

                field.setAccessible(true);

                if (completeMap.containsKey(field.getType())) {
                    field.set(obj, completeMap.get(field.getType()));
                    continue;

                } else if (!objectMap.containsKey(field.getType())) {
                    // 遞迴構造
                    constructClass(field.getType(), false);
                }
                // 構造結束進行賦值
                field.set(obj, objectMap.get(field.getType()));
            }
        }

        completeMap.put(clazz,obj);
    }

    /**
     * 掃描src資料夾下所有的以java為字尾的檔案
     */
    public static void scanClass() throws ClassNotFoundException {
        File currentFile = new File("");
        // 當前專案的絕對路徑
        String path = currentFile.getAbsolutePath();

        // 專案下src下的java檔案
        File javaFile = new File(path + "/src/main/java");
        // 類所在的包
        File[] packageFiles = javaFile.listFiles();
        for (int i = 0; i < packageFiles.length; i++) {
            findClass(packageFiles[i], packageFiles[i].getName());
        }
    }

    /**
     * 遞迴開啟資料夾,尋找java檔案,沒有資料夾時結束遞迴
     *
     * @param file      當前找的檔案
     * @param className 類名稱
     */
    private static void findClass(File file, String className) throws ClassNotFoundException {

        if (file.isFile()) {
            // 將className最後的.java去掉
            int endIndex = className.lastIndexOf(".");
            String[] fileNames = file.getName().split("\\.");
            // 判斷是否為java檔案
            if ("java".equals(fileNames[1])) {
                // 反射獲取類放入list中
                Class clazz = Class.forName(className.substring(0, endIndex));
                if (clazz.isAnnotationPresent(FBean.class)) {
                    classList.add(clazz);
                }
            }
            return;
        }

        File[] files = file.listFiles();
        for (int i = 0; i < files.length; i++) {
            // 如果是package(資料夾),將檔名加入到類名稱中,繼續檢視包裡面的檔案
            findClass(files[i], className + "." + files[i].getName());
        }
    }
}

 

相關文章