自研Spring IOC(一)

声明

本篇文章除部分引用外,均为原创内容,如有雷同纯属巧合,引用转载请附上原文链接与声明。

阅读条件

读本篇文章需掌握java基础知识,对Spring IOC有清楚的认识,掌握注解的使用方式,灵活运用反射

注意

本文若包含部分下载内容,本着一站式阅读的想法,本站提供其对应软件的直接下载方式,但是由于带宽原因下载缓慢是必然的,建立读者去相关官网进行下载,若某些软件禁止三方传播,请在主页上通过联系作者的方式将相关项目进行取消。

相关文章
文章大纲
  • 注解定义
  • 注解处理&实现容器
  • 测试结果
简述

项目github地址传送门点此 ,该篇文章对应的代码在ioc-1.0分支上
该篇文章将详细阐述如何实现一个简易的IOC框架,创造一个类似于Spring IOC类似的框架,提供其依赖注入能力。
该自研IOC框架具有以下特征。

  • 支持.Class文件的类资源识别和加载功能,暂时未实现jar包资源和网络资源识别
  • 提供 @Controller @Service @Repository @Component 注解的bean定义
  • 提供 @Autowired 注解实现注入能力,但仅支持通过反射并调用Set方法方式注入,且要求注入目标类的构造函数均为无参构造函数
  • 提供基础的容器管理能力
  • 不支持识别jar包内.class文件资源,不识别定义的网络class资源,即仅支持本地启动和识别和加载.class
  • 不支持构造函数注入
  • 装配时不考虑并发问题
  • 仅支持单例模式
注解定义

定义@SpringBootApplication注解,用于标记在启动类上,并获取扫描包路径。该注解中存在唯一的方法,用于指定需要扫描包的位置,如果不指定,则将默认使用该注解标记类的包路径。该注解起到的作用与Spring中的@ComponentScan注解效果类似

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SpringBootApplication {

    /**
     * 扫描的包位置
     *
     * @return
     */
    String value() default "";
}

定义@Controller注解,用于标记Controller层对象,和Spring中的@Controller注解定位相同

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

定义@Service注解,用于标记Service层对象,和Spring中的@Service注解定位相同

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}

定义@Repository注解,用于标记Repository层对象,和Spring中的@Repository注解定位相同

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
}

定义@Autowired注解,用于标记自动注入的字段,仅支持对类成员变量进行注入,和Spring中的@Autowired注解定位相同

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
注解处理&实现容器

识别@SpringBootApplication。首先创建SpringApplication类,定位与Spring中的SpringApplication相同,可以看到有抽取basePackage的逻辑代码,如果启动类上的@SpringBootApplication未设置值,则使用启动类的包位置作为扫描路径,反之使用设置的值

public class SpringApplication {

    public static void run(Class<?> appClass, String... args) {
        SpringBootApplication annotation = appClass.getAnnotation(SpringBootApplication.class);
        String basePackage = Strings.isNullOrEmpty(annotation.value()) ? appClass.getPackageName() : annotation.value();
        if (Strings.isNullOrEmpty(basePackage)) {
            basePackage = appClass.getPackageName();
        }
        BeanContainer.scanBean(basePackage);
        BeanContainer.doIoc();
    }
}

识别@Controller,@Service,@Repositiry标记的类资源。首先创建容器内BeanContainer,这里主要关注scanBean方法,extractClassPath方法,needLoad方法。主要经过了以下几个流程:

  • 首先scanBean方法接收basePackage作为根路径,并通过ClassLoader得到basePackage的Resource对象
  • 通过Resource对象可以定位到basePackage在其实际运行环境上的文件系统路径,从而定位到相关的子文件和子文件夹目录
  • 通过ClassLoader将其子文件加载为class对象,并且在needLoad方法中判断该class是否带有@Controller.@Service.@Repository注解,有的话,则将其class对象装载到容器中
  • 对上述步骤得到的class对象通过反射获取其对象实例,然后放入container容器中以供后续使用
    完成了上述流程即完成了basePackage下的满足条件的class装载为bean的过程,此时的bean并没有进行依赖注入的,仅仅是刚生成的对象。
public class BeanContainer {

    private static Map<Class<?>, Object> container = new ConcurrentHashMap<>();
    private static List<Class<? extends Annotation>> beanAnnotations = Arrays.asList(Controller.class, Service.class, Repository.class);

    private static Set<Class<?>> getClasses() {
        // 省略实现
    }

    public static <T> T getBean(Class<T> clazz) {
        // 省略实现
    }

    public static void addBean(Class<?> clazz, Object bean) {
        // 省略实现
    }

    public static Object remove(Class<?> clazz) {
        // 省略实现
    }

    public static void doIoc() {
        // 省略实现
    }

    /**
     * 扫描指定包名下的所有bean
     * @param basePackage
     */
    public static void scanBean(String basePackage) {
        URL resource = Thread.currentThread().getContextClassLoader().getResource(basePackage.replace(".", "/"));
        if (Objects.isNull(resource)) {
            throw new RuntimeException("con't locate the package :" + basePackage);
        }
        Set<Class<?>> classes = new HashSet<>();
        // 这里简化实现只处理.class结尾的类文件,可以进行扩展,支持jar包,网络加载等.
        // 通过 resource.getProtocol() 来判断
        extractClassPath(basePackage, resource.getPath(), classes);
        for (Class<?> clazz : classes) {
            try {
                container.put(clazz, clazz.getConstructor().newInstance());
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                log.error("can't invoke the constructor of [{}]", clazz.getName());
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 扫描basePackage下所有.class文件,若该类被相关注解标记,则进行装载
     * @param basePackage
     * @param path
     * @param classes
     */
    private static void extractClassPath(String basePackage, String path, Set<Class<?>> classes) {
        File file = new File(path);
        File[] files = file.listFiles();
        if (Objects.isNull(files) || files.length == 0) {
            return;
        }
        for (File f : files) {
            if (f.isDirectory()) {
                extractClassPath(basePackage, f.getAbsolutePath(), classes);
            } else if (f.getName().endsWith(".class")) {
                try {
                    String filePath = f.getAbsolutePath().replace(File.separator, ".");
                    String className = filePath.substring(filePath.indexOf(basePackage));
                    className = className.substring(0, className.indexOf(".class"));
                    Class<?> aClass = Thread.currentThread().getContextClassLoader().loadClass(className);
                    if (needLoad(aClass)) {
                        classes.add(aClass);
                    }
                } catch (ClassNotFoundException e) {
                    log.error("can't find class");
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private static boolean needLoad(Class<?> clazz) {
        for (Class<? extends Annotation> cla : beanAnnotations) {
            if (clazz.isAnnotationPresent(cla)) {
                return true;
            }
        }
        return false;
    }

}

识别@Autowired注解,该注解的作用即通过该注解达到自动注入的能力,这里主要关注BeanContainer中的doIoc方法,该方法经过了以下几个流程

  • 对容器所有受管理的bean,获取其所有字段
  • 对每一个字段,判断是否存在@Autowired注解,如果存在则通过getBean的方式获取其bean实例
  • getBean方法兼容了若声明的字段类型是接口或者抽象类,可以动态去从容器中获取其实现类或者子类的bean实例进行注入
    此时,通过上述流程,即对容器中的bean实例进行了注入相关依赖。此时容器的bean实例为完整的实例,可以对外提供服务。
public class BeanContainer {

    private static Map<Class<?>, Object> container = new ConcurrentHashMap<>();
    private static List<Class<? extends Annotation>> beanAnnotations = Arrays.asList(Controller.class, Service.class, Repository.class);

    private static Set<Class<?>> getClasses() {
        // 省略实现
    }

    /**
     * 从容器中获取受管理的bean,这里需要注意的是需要支持对于接口类型注入其实现类
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        T bean = (T) container.get(clazz);
        if (Objects.isNull(bean)) {
            for (Class<?> cla : getClasses()) {
                if (clazz.isAssignableFrom(cla) && !clazz.equals(cla)) {
                    bean = (T) getBean(cla);
                    break;
                }
            }
        }
        return bean;
    }

    public static void addBean(Class<?> clazz, Object bean) {
        // 省略实现
    }

    public static Object remove(Class<?> clazz) {
        // 省略实现
    }

    /**
     * 根据 {@link tech.chenx.core.annotation.Autowired} 标记进行set装配
     */
    public static void doIoc() {
        // 根据@Autowired进行装配
        for (Map.Entry<Class<?>, Object> entry : container.entrySet()) {
            Class<?> clazz = entry.getKey();
            Object o = entry.getValue();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(Autowired.class)) {
                    Object bean = getBean(field.getType());
                    field.setAccessible(true);
                    try {
                        field.set(o, bean);
                    } catch (IllegalAccessException e) {
                        log.error("inject by set fail.", e);
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    public static void scanBean(String basePackage) {
        // 省略实现
    }

    private static void extractClassPath(String basePackage, String path, Set<Class<?>> classes) {
        // 省略实现
    }

    private static boolean needLoad(Class<?> clazz) {
        // 省略实现
    }
}
测试结果

测试项目源码请查看项目源码中的实现,这里直接贴图展示依赖情况

以下是debug的测试结果,可以看到容器中保存了声明的bean实例,并在ExampleController中注入了依赖的service实现和repository实现,service声明的接口类,但实际上注入的service实现类,表明getBean方法正确。此时ioc容器的大部分功能已完成