跟我动手搭框架一之IOC容器实现

举报
西魏陶渊明 发表于 2022/09/25 03:53:32 2022/09/25
【摘要】 本篇文章面对的是有开发经验的Java developer 因为我们将要实现的Spring的IOC容器, 前些天由于工作中要开发公司的Callback系统,一直在研究Netty及IO模型,对于Ne...

本篇文章面对的是有开发经验的Java developer 因为我们将要实现的Spring的IOC容器,

前些天由于工作中要开发公司的Callback系统,一直在研究Netty及IO模型,对于Netty这种非阻塞异步框架,非常崇拜,于是萌发一个想法,用Netty作为web容器,替换Tomcat研究性能.出于这种初衷,就开始为SmileBoot项目开始慢慢积累开发知识.本篇属于小编SmileBoot中的一个模块,为什么要起名Smile呢?因为小编始终认为,我们要带着好的心态,才能学更多的东西,其实小编也是一个菜鸟,之所以要写下来,就是为了记忆和理解更深.因为如果把自己理解的东西,能清楚的讲给其他人,那么才算是真正的理解.

目录

  • 1.原理分析及设计
  • 2.实现方案
    • 2.1 拿到扫描范围
    • 2.2 更具扫描范围加载范围内所有字节码文件
    • 2.3 定义自己的上下文对象接口及实现类
  • 3.测试可用性
  • 4.扩展性
  • 5.下篇预告

1.原理分析及设计

Spring的源码,这里不跟着阅读,直接去实现,然后刚兴趣的童鞋,可以自己在看看,原理是一样的.

1.加载项目中所有的Class文件到Set集合

2.遍历Set将标记有IOC的组件的Class,获取到,注册到IOC容器,这个里面的重点是如何将Class里面的组件,注入进来.

@SmileComponent

在这里@SmileComponent注解是用来标记,需要加入到IOC容器的类

@SmileBean

@SmileBean是用来标记方法中返回值作为Bean,是将要被注册到IOC容器的对象

@InsertBean

@InserBean是标记,该字段是一个Bean,需要从IOC容器中获取,然后注入到该对象中

2.实现方案

1.获取所有的Class字节码,在这中间我们有一个困难那就是如果知道,开发者的所有字节码呢?这个时候我们就可以用注解的形式,在启动类上做一个标记,那么我们就能获取到启动类的字节码,从而获取到将要扫描的跟目录.

我们看下Spring是如何实现的吧

@SpringBootApplication
public class OtoSaasApplication  {
    public static void main(String[] args) {
      SpringApplication.run(OtoSaasApplication.class, args);
    }
}

在这段代码中,有一个注解@SpringBootApplication ,了解Spring的开发同事,都是知道这个注解其实包括了多个注解的,其中一个就是@ComponentScan

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {}
}

那么我们就可以知道,其实也main方法所包含的注解,拿到根目录的.这个有一个Spring的特性,那就是如果启动类在最外层的,那么默认就是扫描,其子目录中的Class,如果不是在根目录,那么要指定扫描的范围.

2.1拿到扫描范围

那么我们想,如果用户不指定,我们怎么拿到根目录呢?

好,如果有疑惑的话,那么久带着疑惑,看下面这段代码吧!

我们定一个注解@SmileBootApplication 目录就是获取到用户的根目录,这里关于注解不在解释,如果有不了解实现注解的可以看小编SpringBoot实践中的自定义注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SmileBootApplication {
    String[] basePackages() default {};
}
@SmileBootApplication
public class SmileApplication {
    public static void main(String[] args) {
       SmileApplication.run(SmileApplication.class, args);
    }
}

我们定义一个方法也就是在run方法中,根据class,文件,获取到注解的根目录

 public static String getBaseRootPackage(Class<?> cls) {
        SmileBootApplication declaredAnnotation = null;
        try {
            declaredAnnotation = cls.getDeclaredAnnotation(SmileBootApplication.class);
        } catch (Exception e) {
            throw new IllegalArgumentException("请添加@SmileBootApplication");
        }
        /**
         * 获取注解上的扫描目录
         * 如果没有指定,就从当前目录获取
         */
        String[] strings = declaredAnnotation.basePackages();
        String baseRootPackage = "";
        if (strings.length == 0) {
            baseRootPackage = cls.getPackage().getName();
        }
        return baseRootPackage;
    }

看到这里,我们已经拿到了项目的根目录,或者说是将要扫描的范围了

2.2 获取指定目录下的所有字节码文件

这个时候我们要知道一个基础的方法,那就是

/**
 * @param className     完整类路径
 * @param isInitialized 是否初始化 第2个boolean参数表示类是否需要初始化Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化
 * @param classLoader   类加载器
 * @return
 */
Class.forName(className, isInitialized, classLoader);

我们要用到一个工具类ClassUtils,该类中可以将根目录中所有字节码(.java,.jar文件)加载到Set

2.3 定义自己的上下文对象接口及实现类

/**
 * @Package: pig.boot.ioc.context
 * @Description: 上下文
 * @author: liuxin
 * @date: 2017/11/17 下午11:52
 */
public interface ApplicationContext {
    Object getBean(String var1);
    <T> T getBean(String name, Class<T> requiredType);
    <T> T getBean(Class<T> name);
    boolean containsBean(String var1);
    void scan(String basePackRoot);
}
public class SmileApplicationContext implements ApplicationContext {
  /**
     * 扫描所有的类,并装载
     *
     * @param basePackRoot
     */
    @Override
    public void scan(String basePackRoot) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Set<Class<?>> classesByPackage = null;
        try {
            /**
             * recursively 是否从根目录,向下查找
             */
            classesByPackage = ClassUtils.getClassesByPackageName(classLoader, basePackRoot, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         * 加载到所有的bean
         */
        allBeans.addAll(classesByPackage);
       /**
        *扫描所有的标记,加入到容器
        */
        classesByPackage.forEach(this::scanComponent);
        /**
         * 将没有注册的bean检查,然后注入
         */
        processEarlyBeans();
    }
}

这个类的重点方法就是scan扫描所有的标记,并从set中拿到每个字节码,传给scanComponent 方法去解析注册

这个时候我们可能遇到一种情况,就是BeanA中需要注册BeanB但是可能BeanB此时并没有解析到,那么这个时候,就要考虑,把暂时实例化不了的,放入到delayBeans,等所有能解析的解析之后,在回过头加载,我们来看这个方法,在看之前我们定义这样一个类 BeanDefinition 用处就是讲Bean的class和实例化对象都保存起来

/**
 * @Package: pig.boot.ioc.context
 * @Description: bean描述
 * @author: liuxin
 * @date: 2017/11/17 下午11:53
 */
public class BeanDefinition {
    Class<?> clazz;
    Object instance;
    public BeanDefinition(Class<?> clazz, Object instance) {
        this.clazz = clazz;
        this.instance = instance;
    }
}
 /**
     * 扫描所有被标记的组件
     */
    public void scanComponent(Class<?> nextCls) {
        SmileComponent declaredAnnotation = nextCls.getDeclaredAnnotation(SmileComponent.class);
        Object beanInstance = null;
        if (declaredAnnotation != null) {
            try {
                beanInstance = nextCls.newInstance();
                String beanName = declaredAnnotation.vlaue();
                if (beanName.isEmpty()) {
                    beanName = nextCls.getSimpleName();
                }
                /**
                 * 保证bean名称的唯一性
                 */
                Long beanId = beanIds.get();
                //将类名,首字母小写,并检查是否存在,如果存在就后面添加id,这个id是原子操作,保证唯一.
                beanName= getUniqueBeanNameByClassAndBeanId(nextCls,beanId);
                /**
                 * 实例化里面的需要注入的字段都获取到
                 * 如果返回true就可以直接添加到IOC容器
                 * lastChance=true 如果注入失败就报错,false不报错,因为第一次,可能所有类没有初始化,所以等待延迟加载方法去,加载
                 */
                if (autowireFields(beanInstance, nextCls, false)) {
                    registeredBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
                } else {
                    /**
                     * 上面那种情况,可能会出现,当要注入,但是被注入的未加载到IOC容器中的情况,所以对于这种,就添加到earlyBeans中,后期注入
                     */
                    delayBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
                }

                /**
                 * 获取方法上的bean
                 * 因为方法肯定是有返回值,的返回值就是实例化对象,所以可以直接,加入到IOC容器
                 */
                createBeansByMethodsOfClass(beanInstance, nextCls);
            } catch (Exception e) {

            }

        }
    }

3.测试可用性

@SmileBootApplication
public class SmileApplication {
    public static void main(String[] args) {
       SmileApplicationContext run = SmileApplication.run(SmileApplication.class, args);
        System.out.println(run.getBean(BeanB.class).toString());
        System.out.println(run.getBean(BeanA.class).beanB().toString());
      //BeanB{content='hi. iam is beanB'}
      //BeanB{content='hi. iam is beanB'}

    }
}
/**
 * @Package: pig.boot.ioc.context
 * @Description: 获取参数
 * @author: liuxin
 * @date: 2017/11/17 下午11:55
 */
@SmileComponent
public class BeanA {
    private String content;
    @InsertBean
    private BeanB beanb;
    public BeanA() {
    }
    public BeanA(String content) {
        this.content = content;
    }
    @SmileBean
    public BeanB beanB() {
        return new BeanB("hi. iam is beanB");
    }

}

4.可扩展性

  • 定义上下文对象接口类,developer,可以定义自己的上下文类

    一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

  • ApplicationContextInitialezer 初始化类执行,获取初始化条件和初始化方法,指定合适的上下文实现类

不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责。

  • 小编最喜欢的方法是抽象,即具有共同特征的方法,用抽象类去实现,具体的方法有继承类去实现
  • 接口隔离,即依赖最小的接口,eg.A接口有五个方法 B此时要用3个,C要用2个,但是他们不得不全部实现.此时我们可以把A接口拆分为2个. 当D5个方法的时候,就继承2个接口,就可以

5.下篇预告

定义@SmileGetMapping,@SmilePostMapping,注解,绑定处理逻辑handler. 放入SmileNettyTaskHandler中,交给Netty处理异步处理

附录

好的代码就想一本书,读的书越多,思路就越广,想法就越多

每个开发人员要把自己当做一个工程师,而不是一个coding 的码农,工程师考虑问题要从顶层设计考虑,而不是为了单纯解决一个问题而code.

文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。

原文链接:springlearn.blog.csdn.net/article/details/78858075

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。