跳至主要內容

Spring - 原理手写IoC

友人CoderMast大约 3 分钟

Spring - 原理手写IoC

Spring 框架的 IOC 是基于 Java 反射机制实现的,在学习手写 IoC 之前,你需要具备一定的 Java 反射相关的知识,参考本站内的 Java 教程。

Java教程

//TODO: 待更新

IoC(控制反转)和DI(依赖注入)是Spring里面核心的东西,本章节我们就来自己实现 Spring 框架中的 IoC 和 DI。

创建子模块

还是和前面的几个章节一致,为了避免冲突,这里我们创建一个新的子模块,名为 spring6-ioc-reflect。

  • 引入依赖

引入相关的依赖,即将依赖项写入到子模块中的 pom.xml 配置文件中。如果之前在父模块中引入了该依赖,则不用重复添加。

<dependencies>
    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.3.1</version>
    </dependency>
</dependencies>
  • 创建 UserDao
public interface UserDao {

    public void print();
}
  • 创建UserDaoImpl实现
public class UserDaoImpl implements UserDao {

    @Override
    public void print() {
        System.out.println("Dao层执行结束");
    }
}

  • 创建UserService接口
public interface UserService {

    public void out();
}
  • 创建UserServiceImpl实现类
@Bean
public class UserServiceImpl implements UserService {

//    private UserDao userDao;

    @Override
    public void out() {
        //userDao.print();
        System.out.println("Service层执行结束");
    }
}
  • 定义注解

    • bean注解
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Bean {
    }
    
    • 依赖注入DI注解
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Di {
    }
    

说明:上面两个注解可以随意取名

  • 定义容器接口
public interface ApplicationContext {

    Object getBean(Class clazz);
}
  • 定义容器接口实现类
AnnotationApplicationContext 实现类
public class AnnotationApplicationContext implements ApplicationContext {

    //存储bean的容器
    private HashMap<Class, Object> beanFactory = new HashMap<>();
    private static String rootPath;

    @Override
    public Object getBean(Class clazz) {
        return beanFactory.get(clazz);
    }

    /**
     * 根据包扫描加载bean
     *
     * @param basePackage
     */
    public AnnotationApplicationContext(String basePackage) {
        // 这里要注意的是如果在 linux 或 mac 平台下,文件的绝对路径使用的是 / 斜杠
        // 而 windows 平台下是 反斜杠 \
        // 如果使用的错误的符号,那么就会导致 dirs 为空
        // windows 使用这个
        // String packageDirName = basePackage.replaceAll("\\.", "\\\\");
        try {
            String packageDirName = basePackage.replaceAll("\\.", "/");
            Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
                rootPath = filePath.substring(0, filePath.length() - packageDirName.length());
                loadBean(new File(filePath));
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //依赖注入
        loadDi();
    }

    private void loadBean(File fileParent) {
        if (fileParent.isDirectory()) {
            File[] childrenFiles = fileParent.listFiles();
            if (childrenFiles == null || childrenFiles.length == 0) {
                return;
            }
            for (File child : childrenFiles) {
                if (child.isDirectory()) {
                    //如果是个文件夹就继续调用该方法,使用了递归
                    loadBean(child);
                } else {
                    //通过文件路径转变成全类名,第一步把绝对路径部分去掉
                    String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
                    //选中class文件
                    if (pathWithClass.contains(".class")) {
                        //    com.codermast.dao.UserDao
                        //去掉.class后缀,并且把 \ 替换成 .

                        // 这里和上面同理,还是 windows 和 linux Mac 平台下的文件路径分隔符不一致的问题
                        // String fullName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");

                        String fullName = pathWithClass.replaceAll("/", ".").replace(".class", "").substring(1);
                        try {
                            Class<?> aClass = Class.forName(fullName);
                            //把非接口的类实例化放在map中
                            if (!aClass.isInterface()) {
                                Bean annotation = aClass.getAnnotation(Bean.class);
                                if (annotation != null) {
                                    Object instance = aClass.newInstance();
                                    //判断一下有没有接口
                                    if (aClass.getInterfaces().length > 0) {
                                        //如果有接口把接口的class当成key,实例对象当成value
                                        System.out.println("正在加载【" + aClass.getInterfaces()[0] + "】,实例对象是:" + instance.getClass().getName());
                                        beanFactory.put(aClass.getInterfaces()[0], instance);
                                    } else {
                                        //如果有接口把自己的class当成key,实例对象当成value
                                        System.out.println("正在加载【" + aClass.getName() + "】,实例对象是:" + instance.getClass().getName());
                                        beanFactory.put(aClass, instance);
                                    }
                                }
                            }
                        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    private void loadDi() {
        for (Map.Entry<Class, Object> entry : beanFactory.entrySet()) {
            //就是咱们放在容器的对象
            Object obj = entry.getValue();
            Class<?> aClass = obj.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field : declaredFields) {
                Di annotation = field.getAnnotation(Di.class);
                if (annotation != null) {
                    field.setAccessible(true);
                    try {
                        System.out.println("正在给【" + obj.getClass().getName() + "】属性【" + field.getName() + "】注入值【" + beanFactory.get(field.getType()).getClass().getName() + "】");
                        field.set(obj, beanFactory.get(field.getType()));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

警告

AnnotationApplicationContext 的代码中,有涉及到文件的路径转换。需要注意 WindowsLinuxMac 平台下的文件路径分隔符是不相同的。

  • Windows 下是 \,反斜杠
  • LinuxMac 下是 /,斜杠
    需要注意区分,否则容易造成无法反射和注入对应的类和对象,导致空指针异常。
  • 对类进行@Bean注解标识
@Bean
public class UserServiceImpl implements UserService
@Bean
public class UserDaoImpl implements UserDao 
  • 执行测试
@Test
public void testIoc() {
    ApplicationContext applicationContext = new AnnotationApplicationContext("com.codermast.reflect");
    UserService userService = (UserService)applicationContext.getBean(UserService.class);
    userService.out();
    System.out.println("run success");
}
测试结果
测试结果