造个轮子-任务调度执行小框架-IOC容器实现

前言

忙里偷闲,今天终于是把概率论这块骨头干下来了。所以的话,留了点时间,把整个项目的结构和基本的功能给实现以下。通过昨天的一个功能的一个设计,我想应该可以明白我想干啥吧。这里的话,重复一下,那就是俺们要搞一个任务执行框架。

使用场景

这个框架到底有啥用?举个简单的例子那就是,假设你要去超市买菜:为了方便买菜,你可以写一个购物清单,然后按照购物清单上面的每一个项,去购买东西。每一次写购物清单的时候,都要写上物品的名字,每次这样写实在是太麻烦了。不过,好在超市的商品有限,于是我们把这些商品都编上序号。于是下一次在书写购物清单的时候,只需要写上这个商品对应的编号就可以了,如果有特殊需求,只需要在商品序号上写上注释即可。同时有些常用的注释也有对应的序号,如果不是很特别的需求的话,直接写上这些注释序号就可以了。

那么在这个例子当中,指代的是啥呢,当你的一个任务,一个业务,需要执行多个方法的时候,你需要编写一个业务类这就相当于刚刚的例子当中去写清单一样。这个业务类里面写了很多调用那些方法的代码,你可能会写出这样的代码

功能方法代码
class A{
	A1(){};
	A2(){};
}



class B{
	B1(){};
	B2(){};
}

class C{
	C1(){};
	C2(){};
}





执行业务的方法(){
	B.B1();
	A.A2();
	C.C1();
}

当你的业务发生变更的时候你可能需要做出新的编排。当然这都不是最麻烦的,最麻烦的是,这些方法的执行可能有着不同的条件,于是接下来又进入了if-else时间。同时,有时候,我们还期望这些方法都能够执行成功,如果有一个方法执行失败,我们希望同时失败,并且恢复原来的状态,例如,你去超市买排骨炖玉米,清单上面有一堆佐料和排骨,如果排骨没有卖了,那么其他的也就没有必要购买了,这个时候买了的东西就要去退款,不要了。因此这样做代码就将显得臃肿,并且修改的代价较大。 那么为了解决这个问题,HTodoScheduling 小框架诞生啦~。

这里为啥是小框架呢,因为实现比较简陋,赶工嘛。所以实现会简单的,但是思想都是一样的,适合拿出来作为学习使用。

特性

这里的话,主要有如下特性:

  1. 清单异步执行
  2. 清单项同步执行
  3. 状态保存
  4. 状态恢复
  5. 定时失败任务重试

这里其实,比较核心的功能就这五个,接下来的小功能围绕这个展开即可。稍微敏感一点儿,你可能还发现了,这个状态保存和状态恢复,以及定时失败任务重试作用可不行哦~

项目结构

okey,我们先来查看一下我们的项目结构:
在这里插入图片描述
这里面的话,目前基本上是把整个项目需要用到的类,数据结构都定义好了。然后重点实现了一下这个IOC容器。因为接下来还需要解析到方法上面,所以要先把类解析出来。

那么在这里的话,和IOC容器相关的其实就这几个地方:

首先是我们的注解,这个的话,这几个注解啥意思,我估计应该不用我多说了,就是Spring的。当然在这个项目里面其实,依赖注入是不需要的,只是看起来不爽,加上去的。
在这里插入图片描述

然后,干活的类就这几个:有啥用我们一一道来。
在这里插入图片描述

初始化执行流程

okey,我们先来看看这个项目初始化执行的流程,当然这里的初始化流程是指当前的执行流程。因为后面的代码还没动,只是架子搭好了。

在这里插入图片描述

可替换核心组件

okey,我们先来看到我们的核心组件部分。目前你能够看到的其实就是这个:
在这里插入图片描述
这个是我们的配置解析,后面还有我们的日志,恢复等等,这里的话我们直接使用适配器设计模式。然后设计一个顶级接口:

package com.huterox.common;







public interface ConfigurationParse {

    void parseConfig();



}


然后有具体的实现:

package com.huterox.common.impl;










import com.huterox.config.Configuration;
import com.huterox.common.ConfigurationParse;



import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * 解析配置文件,这里目前还是解析这properties的文件
 * */


public class PropertiesConfigurationParse implements ConfigurationParse {




    private final Properties properties = new Properties();

    private void doLoadConfig(String contextproperties) {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextproperties);
        try {
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(is!=null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void parseConfig() {
        //加载解析properties配置文件
        this.doLoadConfig(Configuration.configFileName);
        //解析完毕之后,获取对应的属性,然后把解析结果,放在Configuration当中
        String scanPackage = properties.getProperty("scanPackage");
        int schedulerSeconds = Integer.parseInt(properties.getProperty("schedulerSeconds"));
        Configuration.scanPackage = scanPackage;
        Configuration.schedulerSeconds = schedulerSeconds;
    }
}

在未来,你可以变成yaml,xml等等都可以,现在的话只是这个:

在这里插入图片描述
那么这个时候的话,我们在这里有一个配置类:
在这里插入图片描述
那个解析器将解析到的值给到这里,别的地方就知道了。

之后就是俺们的替换部分。
在这里插入图片描述
看到这里,我们把要用的基本核心的组件放在这里,在需要的地方被调用。为了如果外部需要实现或者升级框架的话,那么只需要实现对应的接口,然后在这里覆盖原来的组件接口。当然后面这个玩意还会封装出一个接口,用户不是直接改这个玩意,这玩意是框架开发者使用的

容器创建

okey,前面的内容说完了,我们来说说这个,我们的容器创建。首先的话,我们其实是可以直接使用Spring的,但是这样做的话,扩展性就很差了,你必须依赖Spring,然鹅我压根用不到Spring全家桶的时候就很尴尬(当然估计现在很少这种场景了,苦笑)

扫描目标包

那么创建容器的话,当前第一步是扫描我们的目标包:

package com.huterox.core.suports;






import com.huterox.common.BeanNameConvert;
import com.huterox.common.ConfigurationParse;
import com.huterox.config.ConfigEngine;
import com.huterox.config.Configuration;
import com.huterox.core.beans.BeanDefinition;
import java.io.File;;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;




/**
 * 扫描当前需要被放在容器当中的类
 * */
public class BeanDefinitionReader {

    private List<String> registryBeanClass = new ArrayList<>();
    
    public BeanDefinitionReader() {
        //初始化配置
        ConfigurationParse configurationParse = 
                (ConfigurationParse)ConfigEngine.corePart.get(ConfigurationParse.class);
        configurationParse.parseConfig();
        //初始化时,扫描目标包
        doScanner(Configuration.scanPackage);
        doLoadBeanDefinitions();
    }


    //负责读取配置文件
    public List<BeanDefinition> doLoadBeanDefinitions() {
        //这里注意此时我们存入的是类的包名com.xx.xx.xx
        List<BeanDefinition> result = new ArrayList<>();
        try {
            for (String className:registryBeanClass){
                Class<?> beanClass = Class.forName(className);
                if(beanClass.isInterface()) continue;
                result.add(doCreateBeanDefinition(BeanNameConvert.toLowerFirstCase(beanClass.getSimpleName()), beanClass.getName()));
                //同时把对应的接口也扫描进去
                for(Class<?> InfClass:beanClass.getInterfaces()){
                    result.add(doCreateBeanDefinition(InfClass.getSimpleName(), beanClass.getName()));
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return result;
    }


    private BeanDefinition doCreateBeanDefinition(String factoryBeanName, String beanClassName) {
        BeanDefinition beanDefinition = new BeanDefinition();
        beanDefinition.setFactoryBeanName(factoryBeanName);
        beanDefinition.setBeanClassName(beanClassName);
        return beanDefinition;
    }
    
    private void doScanner(String scanPackage){
        // 扫描我们想要控制的那个包下面的所有类
        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
        assert url != null;
        File classDir = new File(url.getFile());
        for(File file: Objects.requireNonNull(classDir.listFiles())){
            if(file.isDirectory()){
                this.doScanner(scanPackage+"."+file.getName());
            }else {
                if (!file.getName().endsWith(".class")) continue;
                String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
                registryBeanClass.add(clazzName);
            }
        }
    }


}

这段代码的话主要就是,将我们的这个类扫描进去。同时存储到列表当中,这里面对类的信息进行了简单封装:

package com.huterox.core.beans;










/**

 * BeanDefinition 对目标类进行初步处理
 * */

public class BeanDefinition {


    //符合标准bean的名字
    private String factoryBeanName;
    //这个类的全路径
    private String beanClassName;


    public void setFactoryBeanName(String factoryBeanName) {
        this.factoryBeanName = factoryBeanName;
    }

    public void setBeanClassName(String beanClassName) {
        this.beanClassName = beanClassName;
    }


    public String getFactoryBeanName() {
        return factoryBeanName;
    }

    public String getBeanClassName() {
        return beanClassName;
    }

}


当然这里还用到一个简单的工具类:

package com.huterox.common;











/**

 * 将Bean的名字进行转换,转化为符合Javabean标准的名字
 * */

public class BeanNameConvert {
    public static String toLowerFirstCase(String simpleName){
        //字母转换,符合javabean标准
        char[] chars = simpleName.toCharArray();
        if(67<=(int)chars[0]&&(int)chars[0]<=92)
            chars[0] +=32;
        return String.valueOf(chars);
    }
}





容器实例

okey,接下来创建我们的容器。
这里的话,主要有这几个步骤:

  1. 把我们刚刚扫描到的包存放到Map当中。注意,我们这里的实现,和Spring ScanComponent这个注解实现初始话容器有点像,但是和老八股的三级缓存,循环依赖的流程是不同的,在这里解决循环依赖用到是标记链(这个你待会就知道了,我自己取的名字)
  2. 初始化容器
  3. 注入依赖

BeanDefinitionMap 创建

在这里插入图片描述

    private void doRegistryBeanDefinition(List<BeanDefinition> beanDefinitions) throws Exception {






        for (BeanDefinition beanDefinition : beanDefinitions) {
            //双键存储便于双向查找,把bean的名字和对应的包名放在一起
            if (this.beanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) {
                throw new Exception("The " + beanDefinition.getFactoryBeanName() + " is exists!");
            }
            this.beanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition);
            this.beanDefinitionMap.put(beanDefinition.getBeanClassName(), beanDefinition);
        }
    }

这个代码没啥,就是把先前扫描到的玩意放在Map当中,其实也是做个检测,看看有没有重复的类名。

过滤并初始化创建对象

之后的话,我们去遍历这个Map,然后创建对象:
在这里插入图片描述
然后在这里面调用:
在这里插入图片描述

之后在这里进行基本过滤:

    /**
     * 把打了TodoComponent,TodoService,TodoService的类放在我们的容器里面
     * 并且这里进行无参构造,也就是初步实例化
     * 后面我们再把这些玩意的方法进行解析
    * */
    private Object instanceBean(String beanName, BeanDefinition beanDefinition) {

        String className = beanDefinition.getBeanClassName();
        Object instance = null;
        try {
            Class<?> clazz = Class.forName(className);
            if (!(clazz.isAnnotationPresent(TodoComponent.class) ||
                    clazz.isAnnotationPresent(TodoService.class)) ||
                    clazz.isAnnotationPresent(TodoService.class)
            ) {
                return null;
            }
            //先进行初步得无参构造,然后放到这个factoryBeanObjectCache当中去
            instance = clazz.newInstance();
            this.factoryBeanObjectCache.put(beanName, instance);


        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return instance;
    }

这个时候的话,我们只是初始化构建,还没有注入依赖。factoryBeanObjectCache 只是一个Cache,因为后面有一个获取全部对象的方法。

依赖注入

接下来我们进行依赖注入。这个时候的话,参与到我们真正的容器创建,目前是有这两个家伙:

    private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    private final Map<String, BeanWrapper> factoryBeanInstanceCache = new HashMap<>();




在处理循环依赖的时候,使用到这个:

    //记录一下谁依赖了谁,在创建过程当中,还没有被创建的话,表示:B被A依赖,但是此时B没有完成实例
    private final Map<String, List<String>> factoryBeanPopulateCache = new HashMap<>();

这里的话,和Spring老八股当中的循环依赖的那个解决方案是不同的,那个老八股里面的其实是懒实例,按需注入的时候才那样处理的,有历史遗留问题,当然还有就是,我们这个比较简单,一开始就是全加载,人家还有懒加载。

    //创建Bean的实例
    public Object getBean(String beanName) {
        //得到对象的全包名,当然这个信息封装在beanDefinition里面
        BeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName);
        //实例化,这个的话是先进行基本地无参构造实例化对象,此时是半成品
        Object instance = instanceBean(beanName, beanDefinition);
        if (instance == null) return null;
        //实例封装为beanWrapper当中
        BeanWrapper beanWrapper = new BeanWrapper(instance);
        //将这个wrapper对象放在IOC容器里面,允许提取暴露
        this.factoryBeanInstanceCache.put(beanName, beanWrapper);
        //完成依赖注入
        populateBean(beanWrapper);
        return this.factoryBeanInstanceCache.get(beanName).getWrapperInstance();
    }




然后我们的核心代码在这儿:

    private void populateBean(BeanWrapper beanWrapper){
        //开始做依赖注入给值
        Object instance = beanWrapper.getWrapperInstance();
        Class<?> clazz = beanWrapper.getWrapperClass();
        Field[] fields = clazz.getDeclaredFields();
        String beanName = BeanNameConvert.toLowerFirstCase(clazz.getSimpleName());
        //遍历所有的字段,看看有木有需要依赖注入的
        for (Field field : fields) {
            if (!(field.isAnnotationPresent(TodoAutowired.class))) continue;
            TodoAutowired autowired = field.getAnnotation(TodoAutowired.class);
            String autowiredBeanName = autowired.value().trim();
            if ("".equals(autowiredBeanName)) {
                autowiredBeanName = field.getType().getName();
            }
            //打开权限
            field.setAccessible(true);
            try {
                if (!this.factoryBeanInstanceCache.containsKey(autowiredBeanName)){
                    //说明现在这个容器当中,依赖的那个家伙还没有实例化
                    if(this.factoryBeanPopulateCache.containsKey(autowiredBeanName)){
                        List<String> needings = this.factoryBeanPopulateCache.get(autowiredBeanName);
                        needings.add(beanName);
                    }else {
                        List<String> needings = new ArrayList<>();
                        needings.add(beanName);
                        this.factoryBeanPopulateCache.put(autowiredBeanName,needings);
                    }
                    continue;
                }
                //这个就是为什么要按照标准来写首字母要小写
                field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance());

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        //顺便检测当前的bean有没有在创建之前被依赖的情况,有的话补偿一下
        if(this.factoryBeanPopulateCache.containsKey(beanName)){
            for (String populateBeanName:this.factoryBeanPopulateCache.get(beanName)){
                Object instanceNeed = this.factoryBeanInstanceCache.get(populateBeanName).getWrapperInstance();
                Class<?> clazzNeed = this.factoryBeanInstanceCache.get(populateBeanName).getWrapperClass();
                try {
                    Field declaredField = clazzNeed.getDeclaredField(beanName);
                    declaredField.setAccessible(true);
                    declaredField.set(instanceNeed,instance);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }


    }

这样以来的话,就完成了简单的IOC容器实例化处理。之后的话,在我们这边拿到这个容器,然后的话,后面就是解析它的方法,完成清单创建,等等工作就好了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYMF9CrW' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片