type
status
date
slug
summary
tags
category
icon
password
😱
糟了糟了写到这儿,发现要开学了。后面的东西写不完了!怎么办啊!可以咕咕咕嘛?
昨天晚上睡不着,在想Spring在IO Resource的架构和我们架构的不同之处。我们实现的Resource类是一个实体类,而Spring是一个接口,接口下的各种继承关系面面俱到,具体而微。
notion image
我其实今天早上花了2个小时的时间,按照Spring的架构仔细重构了io.Resource,但最后一句 git reset --hard 回到了解放前,因为我发现去实现像Spring一样复杂的架构根本没啥必要——为了简单,我们在IoC中获取配置的方法是默认传递ClassPath的,比如com.cheparity。而Spring考虑更加周全,可以传更多东西,这必然要有更复杂的封装。练习嘛,功能实现了就行。
所以今天依然一切从简,实现IoC类。可惜IoC初探那里的代码可能要重构了(悲)

📝 IoC容器,启动!

Bean的定义:BeanDefinition

在进行注解注入时,可能会有很多属性,比如一个Bean会有其名称、全类名等属性。仍然对比我们最开始实现的简单Bean注入,会发现除了类名→实例的映射之外,什么信息也没有保留。
这好吗?这不好。我们应该要对Bean进行一个封装,至少应该包含以下一些字段:
  • val clazz: Class<*>。强制性的属性,用来创建Bean。
  • val name: String。Bean的唯一标识符,也就是 <bean id="xxx"> 的id(或者name)。
  • var instance: Any? Bean实例,一般是由clazz创建的。
  • var factoryMethod: Method? 。这是纳尼玩意儿?先存疑,往下看。
BeanDefinition在Spring中本身也是一个接口。来看看Spring错综复杂的BeanDefinition。
notion image
那么Bean又能大致分为两类:
  1. 第一种是标注在普通类上的,开袋即食的Bean:
我们大致梳理一下这种Bean的初始化过程:
扫描到 @Component 或其他Bean注解,读取name和clazz → 根据clazz获取到constructor → 扫描注解,读取初始化方法和销毁方法和其他
  1. 另一种是配置类,比如@Configuration
配置类就麻烦了,这个Bean里面又会创建其他大大小小的Bean。
这种配置类本质上是一个工厂,大致流程可以这样:
扫描到 @Configuration ,读取name和clazz → 在configuration类下扫描 @Bean → 把读取到的方法作为该配置类工厂的工厂方法 → 收集标注了@Bean注解的Bean的其他信息,回到1中的流程
所以我们上面的BeanFactory字段里还要再添加一个 var factoryMethod: Method?
其余的一些附加属性,没有那么重要,廖老师的教程里写了,我也把他们写上吧。
简要讲解一下。
  1. primary:如果一个clazz对象对应了多个Bean,比如(这里直接使用了廖老师的例子):
那么我们 getBean(Number.class) 就无法知道是myBean1还是myBean2。但如果我们给myBean1加上 @primary 注解,就可以在冲突时将其作为主Bean返回。
  1. order:定义类内排序顺序。在初始化的时候,如果有的Bean是另一个Bean的依赖,那么他们初始化的顺序就不能随意,而需要我们人为规定:

实现Annotation

实现以下几个注解:
以Bean注解为例:
然后我们需要搞一个AnnotationUitls工具文件,用以扫描注解。
注意,我们应该递归扫描,比如若有一个类标了@Configuration注解(而@Configuration里含有@Component注解),我们也应该把这个类放到Bean中。
所以我们在ClassUtil里搞一个 digAnnotation 方法,在这里用到了kotlin的拓展函数。(不得不说Kotlin写得好简洁啊)。
还要注意一点,kotlin.reflect.full 这个包中自带了 findAnnotation 的方法,不要用混了。
鬼知道我为了用现成的轮子试了多久。由于inline函数无法递归,以及kotlin暂不支持包反射(Packages and file facades are not yet supported in Kotlin reflection.),所以最终放弃了。真的值得开个新博客好好说一下原理

实现AnnotationApplicationContext的bean扫描

我们如果想配置包扫描路径,就这样写:
这表示了我们要扫描com.cheparity下的所有class,去寻找需不需要创建Bean实例。
所以如果要在 AnnotationApplicationContext 里解析这个 MainScanConfig类,我们应该遵循以下的步骤:
  1. MainScanConfig类当参数传进AnnotationApplicationContext ,然后扫描 @ComponentScan 注解并分析。如果value值为空串,默认为MainScanConfig 所在包。最终得到了待扫描的包路径,接下来就在包路径下扫描Bean实例。注意待扫描的包路径可能不止一个,因为@ComponentScan的定义是这样的:
    1. 扫描这些包路径,得到包路径下的.class文件,比如 com.cheparity.UserDao.class com.cheparity.UserService.class 等。这些.class文件有的有@Component注解,有的有@Configuration注解并且其方法里有@Bean注解,等等诸多情况。我们需要根据注解进行BeanDefinition的创建
    思路明确之后开始写代码。
    首先我们的类应该长这样,其下存放各种各样的Bean。这是毋庸置疑的。
    我们分别完成第一步和第二步操作。
    1. Kotlin中几行代码即可完成第一步:获得 @ComponentScan("com.cheparity.kotlin") 待扫描的包,和 @Import 进来的包。
      1. 第二步就有些麻烦了。从第二步起,后面的步骤放在init中。
        1. 首先应该遍历pkgToScan和importPkgToScan。对于pkgToScan里的所有package,用ResourceResolver解析它,得到其下的所有.class文件,保留其名(比如com.cheparity.kotlin.test.UserDao,删去了.class),存在classNameSet
        2. 而对于importPkgToScan,把它下的包看作是第一步的package,对其递归调用此方法就可以做到扫描到了import的包。
      1. 根据第一步扫描到的class文件,创建BeanDefinition。分为以下几步。
        1. 依次扫描class,看有无 @Component 注解,有的话则给他创建BeanDefinition。注意要用我们之前写的递归方法 digAnnotation ,因为 @Configuration 注解也算是 @Component 。依次给Bean的属性进行初始化。
        2. 在第一步创建好了之后,还要看看有没有哪个class小可爱标注的是 @Configuration 。如果是,则还要把BeanDefinition的factoryMethod给它初始化一下。 这里有的小伙伴可能忘记了factoryMethod是个什么玩意儿。以下这个代码中,myBean方法就是factoryMethod。我们要根据此方法进行@Bean注解的实例化。
        3. 最后要注意的是我们这一步创建出来的BeanDefinition长什么样。
      1. 给BeanDefinition实例化。要先实例化ConfigBean,再实例化普通Bean。至于原因,廖老师讲得很不辍,建议看看。实例化的过程比较复杂,我们单开一个模块讲。
      1. 最后,处理字段注入和setter方法注入的Bean。我们为了简化,取消setter方法注入,因为我觉得和字段注入本质相同且写法冗余。

        给BeanDefinition实例化

        整体步骤很简单,要先实例化ConfigBean,再实例化普通Bean
        麻烦的是 createBeanAsEarlySingleton() 这个过程,也就是给Bean具体实例化的过程。建议先把已有的条件——BeanDefinition里存了那些东西——拿出来好好看看,然后理一理思路——如何通过这些条件获得一个Bean的实例。
        首先就是解决循环依赖的问题。廖老师的教程写得很清楚,如果后面循环依赖了,也就是beanA正在被创建的过程中依赖于beanB,beanB又依赖于beanA,而beanA正在被创建,这就导致了循环依赖。所以我们需要一个全局的Set集合,存放正在被实例化的Bean。如果检测到某个bean创建的过程中又需要被创建,则就是循环依赖了。
        接下来应该怎么做呢……😭还是考虑我们的老例子吧。
        1. 任何实例都得从构造方法谈起。对于@Bean注解标注的bean(也就是工厂下的Bean),它们的构造方法就是工厂方法。所以我们第一步就是获取构造方法
        1. 获得了构造方法之后,我们要获取构造方法的参数。此时会出现以下几种情况:
          1. 参数标注了@Autowired,如 @Autowired value2: myBean2。我们此时应该递归调用 createBeanAsEarlySingleton
          2. 参数标注了@Value,如 @value("abc") value1: String 。我们要用PropertyResolver解析属性,把属性值交给构造方法的参数。
          3. 参数什么也没标,比如value1 = "abc"value2 : myBean2 。这种情况很有意思,我们来分别考虑一下:
          4. 由于kotlin的特性:可以给参数传递默认值,所以可以默认第一种情况是标注了@value注解的吗?也就是说,其实这么来看的话@value注解完全可以不要吗?
            value2: myBean2为什么不能默认标注了@Autowired注解?
            默认标注@Autowired的做法,不利于用户后面的自定义操作,因为用户可能想自己写一个类然后手动导入进来,结果被你强制导入了默认类,这不好。
            所以第三中情况的写法均被pass了,合法的情况只有前两种。
        按照以上思路,我们便可以写出创造单实例Bean的方法 createBeanAsEarlySingleton 了。

        倒数第二项工作:实现字段注入

        最后要处理这种情况:
        比上个模块简单多了对不对!
        1. 先遍历BeanDefinition集合,拿到每个Bean的Clazz,比如以上例子的MyBean4。
        1. 通过Clazz,获取properties,比如上个例子的myBean1和s。
        1. 如果标注了@Value,则用PropertyResolver解析并将值注入给该属性;如果标注了@Autowired,则从BeanDefinitions中查找到合适的Bean,注入给该属性。
        需要注意的是继承关系:如果继承了一个SuperClass,同样需要在SuperClass的属性中查找有没有标注@Autowired或@Value,因为如果这样,子类也可以访问到该属性。好在Kotlin提供的拓展属性memberProperties能浑然天成地实现这一点:
        所以最后,我们这么写:

        收尾工作:创建接口

        包括给用户的ApplicationContext接口:
        系统级别的ConfigurableApplicationContext接口:
        实现一下:
        至此,kotring-context模块堂堂完结!

        🤗 写在后面

        Kotlin和Java还是很不一样的。我在项目里面大量重构,以后还会再次重构更新,所以一切以此项目的Github仓库代码为准。下一张,就要实现AOP了。
        还有就是,廖老师的教程太厉害啦!
        其实写完之后反思了一下,如果要用实现IoC容器,其实可以有更优雅的方法。Kotlin天然集成了委托模式,所以如果我们使用 Kotlin 的属性委托来实现注入等功能,就不用依赖于 @Autowired 等注解了。
        好好好!下次重构!

        📎 参考文章

         
        💡
        Kotlin~
         
        vscode插件——根据vue template生成style开发日记整点有用的:chrome和edge的隐藏功能
        Niyuta
        Niyuta
        变分无限,孤心测度有同伦
        公告
        type
        status
        date
        slug
        summary
        tags
        category
        icon
        password
        🎉热烈庆祝Niyuta拥有了个人网站!🎉
        -- 感谢支持喵:) ---
        👏网站正在建设中,有bug望不吝反馈赐教~👏
        (迟早要重构掉