SpringIOC的简化实现(源码总结向)

背景

我们都知道,Spring拥有两大特性: 控制反转(IOC)面向切面编程(AOP)
在我博客之前的文章里有提到过如何去使用AOP,今天这篇文章则主要是简单实现一个通过注解实现的IOC容器

主要的实现思想为反射,如果不懂反射的话可以先去本站看一下反射的文章。

基础:通过方法获取Bean实例

如上图所示,获取一个Bean的实例首先需要被注册到容器,否则是不能被获取到的,注册到容器的意思就是将这个类的信息存到容器管理处,等到需要使用这个类的时候不再去new一个实例,而是直接向容器索取,容器如果查询到对应的类已经注册了,那么会根据注册信息获取类返回,具体获取方法后文详细说明,具体代码如下:

/**
     * 通过名称获取对应Bean实例
     * @param beanName Bean的名称
     * @return 对应实例
     */
    @SuppressWarnings("unchecked")
    public static<T> T getBean(String beanName) {

        // 先查有没有注册
        BeanDefinition beanDefinition = BeanDefinitionRegisterFactory.getBeanDefinition(beanName);
        // 没有注册,直接返回
        if (Objects.isNull(beanDefinition)) {
            return null;
        }
        // 调用实例工厂去找一个 返回
        return (T) SingletonBeanRegisterFactory.getBean(beanName);
    }
}

注册与注册工厂

上文已经说到,获取实例需要注册,注册的方式是通过工厂模式构建一个新的BeanDefinition并缓存到工厂内部的一个线程安全的HashMap中,即为完成注册,整个注册流程使用了设计模式中的工厂模式,因此被称为注册工厂。

private static final Map<String, BeanDefinition> beanDefinitionMap =
        new ConcurrentHashMap<>();
/**
 * 注册一个Bean信息
 * @param beanName Bean名称
 * @param beanClass Bean字节码
 * @param lazyLoad 是否懒加载
 * @param singleton 是否单例
 */
public static void registerBeanDefinition(String beanName, Class<?> beanClass, boolean lazyLoad, boolean singleton) {
    BeanDefinition beanDefinition = new BeanDefinition(beanClass, beanName);
    beanDefinition.setLazyLoad(lazyLoad);
    beanDefinition.setSingleton(singleton);

    beanDefinitionMap.put(beanName,beanDefinition);
}

这样完成了注册的流程。

通过特定注解实现对象注册

我们实际开发过程中每一个单例类都需要手动调用工厂去注册一遍,显得非常笨拙,因此可以使用注解注册,只需要在需要注册的类上加上@Component注解,在程序运行开始的时候,调用扫描注册方法,扫描主包下的所有带有该注解的类,通过反射去获取类字节码信息,通过注解去获取参数信息,再对这些类执行上面代码的注册方法,即可完成所有类的注册,这样就可以更优雅了。

于是,我们的流程就变成了:

看看实现代码:

/**
 * 注册Bean 扫描指定包及其子包下的所有类,将带有Component注解的类进行注册
 * @param packageName 报名
 */
public static void registerBeansWithComponentScan(String packageName) {
    // 先进行扫描获得带有Component注解的类字节码
    List<Class<?>> classes = scanAnnotation(packageName, Component.class);
    for (var clazz : classes) {
        // 获得注解内信息
        Component clazzAnnotation = clazz.getAnnotation(Component.class);
        String beanName = clazzAnnotation.beanName();
        // 对没有注明名字的Bean,取其字节码名称为Bean名称
        if (Objects.isNull(beanName) || beanName.isEmpty()) {
            beanName = clazz.getName();
        }
        // 执行注册
        BeanDefinitionRegisterFactory.registerBeanDefinition(beanName, clazz,
                clazzAnnotation.lazyLoad(), clazzAnnotation.singleton());
    }
}

/**
 * 扫描指定包下的带有指定注解的类
 * @param packageName 包名
 * @param annotation 要扫描的注解名
 * @return 带有对应注解的类字节码列表
 */
private static List<Class<?>> scanAnnotation(String packageName, Class<? extends Annotation> annotation) {

    // 使用反射获取对应字节码类
    Reflections reflections = new Reflections(packageName);
    Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(annotation);
    return typesAnnotatedWith.stream().toList();
}

这样实现后,我们只需要main函数里调用一下扫描方法,指定包扫描路径,即可完成类的注册。

实例化工厂

相对于单例注册,实例化的操作就比较简单了,只需要根据BeanDefinition里提供的信息(字节码对象)通过反射获取相应构造方法进行调用即可获得对应实例,检测是否注册时是否单例,如果是单例,那么将其放进缓存池,下次使用直接从缓存池获取即可(缓存池也为一个线程安全的HashMap)。

如果注册注解时使用的信息不是单例的 那么就直接new一个对象返回就好啦。
看看实现代码:

/**
 * 单例注册工厂(实例化Bean注册信息)
 * @author Polister
 */
public class SingletonBeanRegisterFactory {
    // 实例化的缓存池
    private static final Map<String, Object> beanList = new ConcurrentHashMap<>();

    /**
     * 创建Bean操作
     * @param beanDefinition Bean注册信息
     * @return 创建的实例
     */
    public static Object createBean(BeanDefinition beanDefinition) {

        // 执行创建
        Object bean = doCreate(beanDefinition);
        // 如果是单例Bean,放入单例池
        if (beanDefinition.isSingleton())
            beanList.put(beanDefinition.getBeanName(), bean);
        // 进行类内注解注入
        BeanInjectFactory.injectWithAutoWired(bean);
        return bean;
    }

    /**
     * 实例化Bean的创建周期
     * @param beanDefinition Bean的注册信息
     * @return Bean实例
     */
    private static Object doCreate(BeanDefinition beanDefinition) {

        // 没有对应信息,无法执行创建
        if (Objects.isNull(beanDefinition) ||
                Objects.isNull(beanDefinition.getBeanClass())) {
            throw new CreateBeanException("找不到定义的Bean信息");
        }
        Object instance;
        try {
            // 通过反射获取对应构造方法进行实例创建
            instance = beanDefinition.getBeanClass()
                        .getConstructor().newInstance();

        } catch (Exception e) {
            throw new CreateBeanException("创建实体Bean失败" + beanDefinition);
        }

        return instance;
    }

注入工厂

单例对象实例化之后,就会被加入到缓存池里,那么如果这些类里面所定义的字段里有已经交由容器管理的类,那么在构造的时候需要再使用getBean方法获取,这样子不便于开发,因此在代码编写的时候,只需要将已经交由容器管理的类写入字段内,并加上@AutoWired注解,容器创建该类实例之后,会对该类的字段进行一次扫描,如果字段中带有@AutoWired注解,那么会对该字段所属类名称(默认是字节码名称)进行一次getBean操作,如果时已经注册的单例类,那么会直接进行注入,使用的时候无需进行getBean操作,直接使用即可,这就是类的自动注入。

/**
 * 对加了注解的类进行注入
 * @param obj 实例
 */
protected static void injectWithAutoWired(Object obj) {
    Class<?> clazz = obj.getClass();
    Field[] fields = clazz.getDeclaredFields();
    // 遍历参数 查询是否有注入注解
    for (Field field : fields) {
        AutoWired annotation = field.getAnnotation(AutoWired.class);
        if (Objects.nonNull(annotation)) {
            try {
                // 对其进行注入
                field.setAccessible(true);
                // 获取注解名称
                String beanName = annotation.beanName();
                // 注解没有加名称的话,用字节码当名称
                if (Objects.isNull(beanName) || beanName.isEmpty()) {
                    beanName = field.getType().getName();
                }
                // 获取是否有这个Bean
                Object bean = BeanUtils.getBean(beanName);
                // 没有 不注入了
                if (Objects.isNull(bean)) {
                    continue;
                }
                // 判断获取到的Bean是否是目标的子类或者实现
                if (!bean.getClass().isAssignableFrom(field.getType())) {
                    throw new InjectBeanException("注入类与注册实例类型不同!");
                }
                // 注入操作
                field.set(obj, bean);
            } catch (IllegalAccessException e) {
                throw new InjectBeanException("注入Bean失败");
            }
        }
    }
}

懒汉模式与饿汉模式

完成了基本操作后,一旦程序启动,我们可以通过包扫描向容器注册所有带有Component注解的类,但是他们什么时候实例化呢,我们可以在程序启动的时候就对他们进行实例化,运行的时候就无需再进行实例化了,这便是饿汉模式,优点是所有实例初始化之后程序运行相对稳定,缺点也非常明显,那就是启动程序的时候要对所有注册类进行实例化,导致启动时间稍长,这种模式下,可以通过注册注解的lazyLoad标签对其进行懒加载,使其不在程序启动的时候实例化。

而另外一种方式是启动的时候只会向容器注册相应的类,不会将其实例化,等到getBean操作发生后,即需要使用对应的实例后,才会触发实例化,这种方式称为懒汉模式

至此,一个简单的IOC管理容器就已经完成实现,同时也解决了循环依赖的问题。

总结

这篇文章其实是从我的课程设计报告中摘录出来的,因为课程设计有简单的需求,那么干脆自己动手去试试,也算是对反射知识的一点应用,同时也加深了对IOC的理解(这也是为啥这篇文章语言比较正式
至于为啥不用三级缓存,那肯定是没有多线程生命周期的需求啊,单线程的循环依赖解决起来也比较简单(其实是懒,逃

多读读底层代码,领悟到的东西确实不太一样~
不知道下一次更新又是啥时候了(捂脸

噢,完整代码稍后传到我的Github上,大家感兴趣可以来看看!~

LICENSED UNDER CC BY-NC-SA 4.0
Comment