Spring学习:简单实现一个依赖注入和循环依赖的解决
- 2022 年 1 月 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为接口类型时,构造实现类返回
进行测试
要交给容器的实体类


@FBean public class StudentDao { public void query(){ System.out.println("StudentDao:query()"); } }
View Code


@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,两个类就互相引用了


@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全部构造好)的对象。
新的构造对象步骤
- 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()); } } }