依賴注入
什麼是依賴注入
使用一個會建立和查詢依賴物件的容器,讓它負責供給物件。
當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為介面型別時,構造實現類返回
進行測試
要交給容器的實體類
@FBean public class StudentDao { public void query(){ System.out.println("StudentDao:query()"); } }
@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; } }
測試程式碼
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,兩個類就互相引用了
@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()"); } }
在原本構造物件的方法裡面:
如果a類引用了容器的b類,a類在構造時,會讓容器去構造b類,等b類構造完畢,a類才構造完畢。
當兩個類互相引用時,a讓容器構造b,b讓容器構造a,最終造成死迴圈,可以使用上面的程式碼測試。
解決方案
原本只使用了一個Object_Map儲存物件,現在再加上一個Complete_Map。
- Object_Map是第一層,儲存的是剛剛例項化的物件。
- Complete_Map是第二層,儲存屬性填充完畢(引用的b、c、d、e全部構造好)的物件。
新的構造物件步驟
- a在例項化後,讓容器去構造b,b例項化後,將b存入Object_Map中,繼續a的構造流程。
- a從Object_Map拿到b,繼續構造,最後存入Complete_Map。
- 輪到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()); } } }