前言
忙里偷闲,今天终于是把概率论这块骨头干下来了。所以的话,留了点时间,把整个项目的结构和基本的功能给实现以下。通过昨天的一个功能的一个设计,我想应该可以明白我想干啥吧。这里的话,重复一下,那就是俺们要搞一个任务执行框架。
使用场景
这个框架到底有啥用?举个简单的例子那就是,假设你要去超市买菜:为了方便买菜,你可以写一个购物清单,然后按照购物清单上面的每一个项,去购买东西。每一次写购物清单的时候,都要写上物品的名字,每次这样写实在是太麻烦了。不过,好在超市的商品有限,于是我们把这些商品都编上序号。于是下一次在书写购物清单的时候,只需要写上这个商品对应的编号就可以了,如果有特殊需求,只需要在商品序号上写上注释即可。同时有些常用的注释也有对应的序号,如果不是很特别的需求的话,直接写上这些注释序号就可以了。
那么在这个例子当中,指代的是啥呢,当你的一个任务,一个业务,需要执行多个方法的时候,你需要编写一个业务类这就相当于刚刚的例子当中去写清单一样。这个业务类里面写了很多调用那些方法的代码,你可能会写出这样的代码
功能方法代码
class A{
A1(){};
A2(){};
}
class B{
B1(){};
B2(){};
}
class C{
C1(){};
C2(){};
}
执行业务的方法(){
B.B1();
A.A2();
C.C1();
}
当你的业务发生变更的时候你可能需要做出新的编排。当然这都不是最麻烦的,最麻烦的是,这些方法的执行可能有着不同的条件,于是接下来又进入了if-else时间。同时,有时候,我们还期望这些方法都能够执行成功,如果有一个方法执行失败,我们希望同时失败,并且恢复原来的状态,例如,你去超市买排骨炖玉米,清单上面有一堆佐料和排骨,如果排骨没有卖了,那么其他的也就没有必要购买了,这个时候买了的东西就要去退款,不要了。因此这样做代码就将显得臃肿,并且修改的代价较大。 那么为了解决这个问题,HTodoScheduling
小框架诞生啦~。
这里为啥是小框架呢,因为实现比较简陋,赶工嘛。所以实现会简单的,但是思想都是一样的,适合拿出来作为学习使用。
特性
这里的话,主要有如下特性:
- 清单异步执行
- 清单项同步执行
- 状态保存
- 状态恢复
- 定时失败任务重试
这里其实,比较核心的功能就这五个,接下来的小功能围绕这个展开即可。稍微敏感一点儿,你可能还发现了,这个状态保存和状态恢复,以及定时失败任务重试作用可不行哦~
项目结构
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,接下来创建我们的容器。
这里的话,主要有这几个步骤:
- 把我们刚刚扫描到的包存放到Map当中。注意,我们这里的实现,和Spring ScanComponent这个注解实现初始话容器有点像,但是和
老八股的三级缓存,循环依赖的流程是不同的,在这里解决循环依赖用到是标记链(这个你待会就知道了,我自己取的名字)
- 初始化容器
- 注入依赖
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容器实例化处理。之后的话,在我们这边拿到这个容器,然后的话,后面就是解析它的方法,完成清单创建,等等工作就好了。