1. 前言
实习已经快两周了,刚刚来的时候,师兄向我介绍了部门的主要工作,然后,让我看了一下他写的框架及分享了思路。并且,让我在上面做两个需求。小需求做完了,也了解了业务相关内容。刚好最近也有学习设计模式和思想,突然想自己能不能实现个低配版的小框架。
2. 设计目标
2.1 场景
- 有一系列的管道,每个管道中,分别有自己的策略,配置信息分别存在一张pipeline_rule表中。
- 针对每个策略本身,为了可以实现动态配置,配置信息分别存到一张strategy_rule。
2.2 期待目标
- 希望用户传入一个管道,这个管道配置着不同的策略
- 希望用户可以修改策略信息就可以实现目标结果的不同
- 希望日志功能够基于每个管道有一个独立的id,便于查找问题
2.3 技术点
- bean 生命周期
- 设计模式
- ql表达式
- ThreadLocal
3. 代码实现开始
3.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 '管道表';
复制代码
- 再建立一张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 分析
项目整体结构分析
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 数据库信息
- pipeline_rule 信息如下:
他代表的意思就是,testPipeline需要执行OnloadStrategy JudgeStrategy MessageStrategy这三个策略类
- strategy_rule
这是3个策略类各自的规则
- OnloadStrategy 我没有配置规则(使用者自己可以配)
- JudgeStrategy 我配置了ql表达式,框架中也是使用该表达式来进行跑的
- MessageStrategy 我配置了当成绩差的学生,才需要发短信给父母
4.2 项目启动
- 写一个对应的controller提供接口
- 查看控制台日志输出:
日志输出也满足预期:
- 经过测试各部分都满足预期。可以按照流程一步一步的走下来;