小框架之练习

1. 前言

实习已经快两周了,刚刚来的时候,师兄向我介绍了部门的主要工作,然后,让我看了一下他写的框架及分享了思路。并且,让我在上面做两个需求。小需求做完了,也了解了业务相关内容。刚好最近也有学习设计模式和思想,突然想自己能不能实现个低配版的小框架。

2. 设计目标

2.1 场景

  • 有一系列的管道,每个管道中,分别有自己的策略,配置信息分别存在一张pipeline_rule表中。
  • 针对每个策略本身,为了可以实现动态配置,配置信息分别存到一张strategy_rule。

2.2 期待目标

  • 希望用户传入一个管道,这个管道配置着不同的策略
  • 希望用户可以修改策略信息就可以实现目标结果的不同
  • 希望日志功能够基于每个管道有一个独立的id,便于查找问题

2.3 技术点

  • bean 生命周期
  • 设计模式
  • ql表达式
  • ThreadLocal

3. 代码实现开始

3.1 建表

  1. 先建立一张pipeline_rule表
CREATE TABLE `pipeline_rule` (
	`id` BIGINT ( 18 ) NOT NULL AUTO_INCREMENT COMMENT '主键',
	`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT  '创建时间',
	`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT  '修改时间',
	`pipeline_name` VARCHAR ( 16 ) NOT NULL COMMENT  '管道名称',
	`strategy_rule` VARCHAR ( 192 ) NOT NULL COMMENT  '策略规则',
	`is_del` VARCHAR ( 8 ) NOT NULL COMMENT  '是否删除',
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB CHARSET = utf8 COMMENT  '管道表';

复制代码
  1. 再建立一张strategy_rule表
CREATE TABLE `strategy_rule` (
	`id` BIGINT ( 18 ) NOT NULL AUTO_INCREMENT COMMENT '主键',
	`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT  '创建时间',
	`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT  '修改时间',
	`strategy_name` VARCHAR ( 16 ) NOT NULL COMMENT  '策略名称',
	`rule` VARCHAR ( 48 ) NOT NULL COMMENT  '策略规则',
	`is_del` VARCHAR ( 8 ) NOT NULL COMMENT  '是否删除',
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB CHARSET = utf8 COMMENT  '策略表';
复制代码

3.2 分析

项目整体结构分析

image.png
controller:提供一个接口
strategy: 代表各自策略,如果需要增加新的策略直接继承AbstractStrategy即可。
util:

  • ExpressUTils : 这个是处理ql表达式的
  • LogUtils : 这个封装了log日志模块,使打日志更加方便
  • PipelineParmasMap: 这个是设置每个策略自定义rule的
  • StrategyKeyValueMap: 这个用来保存和提取各个策略的结果

PipelineStart: 这个类似于模板方法的中心类,封装了每个Pipelined都要执行的步骤
StrategyFactory: 策略工厂

我们基于pipeline_name查询strategy_rule时,就需要调用这些strategy来执行。但是,如何获取到strategy呢?很自然,我们会想到使用工厂模式。但是,使用工厂模式也会有一个问题,那就是当我创建一个新的strategy的时候,就需要去工厂中添加一个strategy,这就意味着我新创建一个strategy就需要在工厂类中添加这个strategy,违反开闭原则了。为了解决这个问题,我们可以创建strategy时,基于其生命周期,将其添加到工厂中。

工厂类:

public class StrategyFactory {


    private final static Map<String, AbstractStrategy> beanMap = new HashMap<>();

    /**
     * 注册bean
     */
    public static void registerBean(String key, AbstractStrategy abstractStrategy) {
        if (StringUtils.isBlank(key)) {
            return;
        }
        if (abstractStrategy == null) {
            return;
        }
        beanMap.put(key, abstractStrategy);
    }

    /**
     * 获取bean
     */
    public static AbstractStrategy getBean (String key) {
        return beanMap.get(key);
    }
}

复制代码

这时候,只要创建了工厂类,就可以实现注册到工厂map容器中。

策略抽象类:

public abstract class AbstractStrategy implements InitializingBean {

    public void startStrategy() {
        // 实现日志相关的内容
        baseProcess();
    }
    
    public abstract void baseProcess();
    
    @Override
    public void  afterPropertiesSet() {
        StrategyFactory.registerBean(this.getClass().getSimpleName(), this);
    }
}
复制代码

现在,我们需要来实现pipeline类了,这个pipeline 主要功能如下:

  • 传入pipelineName 去数据库找到对应的策略类
  • 对策略类加入到责任链中
  • 循环执行每个策略的startStrategy()方法

具体实现代码如下:

@Component
public class PipelineStart {
    @Resource
    PipelineMapper pipelineMapper;
    @Resource
    StrategyMapper strategyMapper;

    private final static DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static ThreadLocal<String> threadId = new ThreadLocal();


    List<AbstractStrategy> strategyList = new ArrayList<>();

    public void startPipeline(String pipelineName) {
        loadStrategy(pipelineName);
        handleStrategy();
    }

    private void loadStrategy(String pipelineName) {
        // 线程id先用uuid来代替,到时候,可以使用雪花算法或者redis递增id来设置
        threadId.set(UUID.randomUUID().toString());

        // 1. 获取Pipeline信息
        PipelineDO pipelineDO = pipelineMapper.queryByPipelineName(pipelineName);

        if (pipelineDO == null) {
            LogUtils.errorLog("pipelineDo is null");
            throw new RuntimeException("pipelineDo is null");
        }

        String strategyRule = pipelineDO.getStrategyRule();
        if (StringUtils.isBlank(strategyRule)) {
            LogUtils.errorLog("strategyRule is null");
            throw new RuntimeException("strategyRule is null");
        }
        // 2. 获取pipeline对应的strategy信息
        List<String> strategiesNameList = divisionStrategyName(strategyRule);
        // 3. 将strategy转换成bean
        getStrategiesByName(strategiesNameList);
    }

    private void handleStrategy() {
        LogUtils.infoLog("strategy start. strategiesList: " + strategyList + "time :" +
                timeFormatter.format(LocalDateTime.now()));
        for (AbstractStrategy strategy : strategyList) {
            setStrategiesQLRule(strategy);
            strategy.startStrategy();
        }
        LogUtils.infoLog("strategy end. time :" +
                timeFormatter.format(LocalDateTime.now()));
        threadId.remove();
    }


    private List<String> divisionStrategyName(String strategyRule) {
        String[] strategies = strategyRule.split(",");
        ArrayList<String> strategyRuleList = new ArrayList<>();
        for (String strategy : strategies) {
            if (StringUtils.isBlank(strategy)) {
                LogUtils.infoLog("one strategy is empty strategyRule:" +strategyRule);
                continue;
            }
            strategyRuleList.add(strategy);
        }
        return strategyRuleList;
    }

    private void getStrategiesByName(List<String> strategyRuleList) {
        if (CollectionUtils.isEmpty(strategyRuleList)) {
            return ;
        }

        for (String strategyName : strategyRuleList) {
            AbstractStrategy strategy = StrategyFactory.getBean(strategyName);
            if (strategy == null) {
                LogUtils.infoLog("strategy :" + strategyName + "get bean failure");
                continue;
            }
            strategyList.add(strategy);
        }
    }
    /**
     * 既然是每个Strategy都有自己的ql表达式,肯定是需要从数据库找的。
     * 选择了模板方法的话,就先提前找出来,这样子类就不需要再实现这个方法了
     * 至于传参问题,可以使用map来进存储。
     */
    private void setStrategiesQLRule(AbstractStrategy strategy) {

        String beanName = strategy.getClass().getSimpleName();
        StrategyDO strategyDO = strategyMapper.queryStrategyByName(beanName);
        if (strategyDO == null) {
            LogUtils.infoLog(beanName + "not information in mysql");
            return;
        }
        String rule = strategyDO.getRule();
        PipelineParamsMap.setStrategyRule(rule);
    }
}

复制代码

这时候,只需要调用startPipeline() 方法,就会执行责任链,将所有的策略进行执行;
下面我们分析一下日志类:

  • 一个pipeline 是一个主链路,所以需要有一个链路id
  • 每个pipeline中有不同的策略,我们需要对其进行分析,所以需要打印策略类名称
  • 日志打印的行号,如果我们知道在哪一行调用的,也能帮助我们查找问题

所以,封装了如下的日志类 (这里使用堆栈信息来判断行号和类名,需要对写死堆栈位置,不知道能不能改进一下)

package com.sainan114.pipelinestudy.util;


import com.sainan114.pipelinestudy.PipelineStart;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.annotations.Mapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.print.DocFlavor;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lgb
 * @Date: 2021/7/9
 */
public class LogUtils {
    private final static Logger logger = LoggerFactory.getLogger(LogUtils.class);
    private final static StringBuilder res = new StringBuilder();

    private final static String CLASSNAME = "ClassName";
    private final static String EQUAL = "=";
    private final static String SEPARATOR = " | ";
    private final static String LINENUMBER = "lineNumber";
    private final static String COLON = " : ";
    private final static String MSG = "msg";
    private final static String THREADID = "threadId";
    private final static Integer STACKTRACE = 6;

    public static void infoLog(String msg) {
        logger.info(buildContent(msg));
    }

    public static void infoLog(Map<String, String> contentMap) {
        logger.info(buildContent(contentMap));
    }

    public static void warnLog(String msg) {
        logger.warn(buildContent(msg));
    }

    public static void warnLog(Map<String, String> contentMap) {
        logger.warn(buildContent(contentMap));
    }

    public static void errorLog(Map<String, String> contentMap) {
        logger.info(buildContent(contentMap));
    }

    public static void errorLog(String msg) {
        logger.error(buildContent(msg));
    }


    private static String buildContent(String msg) {
        buildCommonContent();
        res.append(MSG).append(COLON).append(msg);
        return res.toString();
    }

    private static String buildContent(Map<String, String> contentMap) {
        buildCommonContent();

        res.append(MSG).append(COLON);

        for (String key : contentMap.keySet()) {
            String value = contentMap.get(key);
            if (value != null) {
                res.append(key).append(EQUAL).append(value).append(SEPARATOR);
            }
        }
        return res.toString();
    }


    private static void buildCommonContent() {
        res.setLength(0);

        res.append(THREADID).append(COLON).append(PipelineStart.threadId.get()).append(SEPARATOR);

        String className = getSimpleClassName();
        if (className != null) {
            res.append(CLASSNAME).append(COLON).append(className).append(SEPARATOR);
        }

        res.append(LINENUMBER).append(COLON).append(getLineNumber()).append(SEPARATOR);


    }


    private static StackTraceElement getStackTrace() {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        int currentStackTraceOffset = STACKTRACE;
        return stackTrace[currentStackTraceOffset];
    }

    private static Integer getLineNumber() {
        return getStackTrace().getLineNumber();
    }


    private static String getSimpleClassName() {
        String className = getStackTrace().getClassName();

        String simpleClassName = null;
        if (!StringUtils.isBlank(className)) {
            for (int i = className.length() - 1; i >= 0; i--) {
                if (className.charAt(i) == '.') {
                    simpleClassName = className.substring(i + 1);
                    break;
                }
            }
        }
        return simpleClassName;
    }

}
复制代码

下面分析一下ExpressUtils

public class ExpressUtils {
    private final static ExpressRunner runner = new ExpressRunner();

    public static String executeExpression(String ChineseScale, String mathScale, String strategyQlContent) {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("语文", ChineseScale);
        context.put("数学", mathScale);
        if (StringUtils.isBlank(strategyQlContent)) {
            LogUtils.infoLog("context is empty | judge failure ");
            return "预测失败";
        }
        Object result = null;
        try {
            result = runner.execute(strategyQlContent, context, null, true, false);
        } catch (Exception e) {
            LogUtils.errorLog("execute failure ChineseScale : " + ChineseScale + "mathScale:" + mathScale + "qlContent:" + strategyQlContent );
        }
        if (result instanceof String) {
            return String.valueOf(result);
        } else {
            return null;
        }
    }

    public static void main(String[] args) {

        LogUtils.infoLog("sss");
        HashMap<String, String> map = new HashMap<>();
        map.put("a", "a");
        map.put("b", "b");
        LogUtils.errorLog(map);
        String context = "if (语文 >= 95 && 数学 >= 95) {return '小卢考的还行'} else if (语文 >= 95 && 数学 < 95) { return '多做数学题'}" +
                "else if (语文 < 95 && 数学 >= 95) {return '多做语文题'} else {return '今晚学到2点'}";
        System.out.println(executeExpression("99", "98", context));
        System.out.println(executeExpression("95","88", context));
        System.out.println(executeExpression("90", "98", context));
        System.out.println(executeExpression("80", "88", context));

    }
}

复制代码

ql表达式还是挺有意思的。之后自定义配置时,可以进行使用

4. 测试

写好框架 下面对其进行测试:

4.1 数据库信息

  1. pipeline_rule 信息如下:

image.png
他代表的意思就是,testPipeline需要执行OnloadStrategy JudgeStrategy MessageStrategy这三个策略类

  1. strategy_rule

image.png
这是3个策略类各自的规则

  • OnloadStrategy 我没有配置规则(使用者自己可以配)
  • JudgeStrategy 我配置了ql表达式,框架中也是使用该表达式来进行跑的
  • MessageStrategy 我配置了当成绩差的学生,才需要发短信给父母

4.2 项目启动

  1. 写一个对应的controller提供接口

image.png

  1. 查看控制台日志输出:

日志输出也满足预期:

image.png

  1. 经过测试各部分都满足预期。可以按照流程一步一步的走下来;

5. 源码

gitee地址

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