回顾
上一篇结束后终于算是搭建了一个没有大毛病的可扩展的低代码系统,在结尾处也提到之后会增加此系统在生产中所需的一些必备能力。当时想的是能继续水几篇,然而在我舒服的给系统添枝加叶的期间,却越来越有危机感,变故总是很突然的,也许明天我的这个专栏就会由于一些原因被腰斩了。所以决定再敲这最后一篇,从下一篇开始改为2.x版本,使组件流可以并行执行。
本篇的改动点有:简单的日志处理、自动持久化组件元数据能力、增加两个组件完成演示。
日志处理
core.framework.AopLog
package com.example.lowcode.core.framework;
import com.alibaba.fastjson.JSON;
import com.example.lowcode.core.dto.ComponentInfo;
import com.google.common.collect.Maps;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author llxzdmd
* @version AopLog.java, 2024年01月16日 15:08 llxzdmd
*/
@Aspect
@Component
@Log4j2
public class AopLog {
@Pointcut("execution(* com.example.lowcode.core.framework.AbstractComponent+.execute(..))")
public void execute() {
}
@Around("execute()")
public Object execute(ProceedingJoinPoint pjd) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 放到前面执行,先解析入参
Object proceed = pjd.proceed();
log.info(buildLogMap(pjd, startTime));
return proceed;
} catch (Throwable e) {
log.error(buildLogMap(pjd, startTime), e);
return new HashMap<>();
}
}
// 异常通知,在目标方法抛出异常时执行。但感觉在环绕通知catch就可以了
// @AfterThrowing(value="execute()", throwing="ex")
// public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
// System.err.println("An error occurred during method execution." + ex.getMessage());
//
// // 这里可以添加记录异常信息到日志的逻辑
// // ...
// log.error("error");
//
// }
private Map<String, String> buildLogMap(ProceedingJoinPoint pjd, long startTime) {
Map<String, String> logMap = Maps.newLinkedHashMap();
Object[] args = pjd.getArgs();
ComponentContext context = (ComponentContext) args[0];
ComponentInfo componentInfo = (ComponentInfo) args[1];
logMap.put("flowId", JSON.toJSONString(componentInfo.getFlowId()));
logMap.put("componentName", pjd.getTarget().getClass().getCanonicalName());
logMap.put("inputParam", JSON.toJSONString(componentInfo.getInputs()));
logMap.put("outputParam", JSON.toJSONString(componentInfo.getOutputs()));
logMap.put("context", JSON.toJSONString(context));
logMap.put("startTime", String.valueOf(startTime));
long endTime = System.currentTimeMillis();
logMap.put("endTime", String.valueOf(endTime));
return logMap;
}
}
比较常规的写切面,就不多解释了,运行后效果:
但这样记录组件的入参还有一个问题,就是表达式无法显示实际的文本。因此我们需要在解析入参后增加一个把解析后的入参替换原来的表达式步骤,这也是为什么AopLog.class的第34行要先执行方法。
在这里我又优化了一下解析表达式的代码,上一篇只在分页组件中写了解析部分,这在实际中肯定是不可行的,把它们提取出来到 AbstractComponent 父类中:
protected Object parseInputParam(String paramName, ComponentContext context, ComponentInfo componentInfo) {
Object o = componentInfo.getInputs().get(paramName).getValue();
if (RegexUtils.hasEl(o.toString())) {
Object parsedParam = context.getContextMap().get(RegexUtils.dealEl(o.toString()));
// 把解析后的结果放到入参中
componentInfo.getInputs().get(paramName).setValue(parsedParam);
return parsedParam;
} else {
return componentInfo.getInputs().get(paramName).getValue();
}
}
protected String parseOutputParam(String paramName, ComponentInfo componentInfo){
return componentInfo.getOutputs().get(paramName).getValue().toString();
}
然后把去重过滤器和分页过滤器组件的解析入参部分修改一下:
pageFilter:
这里也体现了EL表达式的坏处:每个组件都需要加一步写入上下文的操作;另外还有一个坏处是表达式内的变量不能重名。不过这些都不算大问题。
修改后再次测试,日志可以记录表达式的真实值了:
持久化组件
设想一下如果有前端页面,每个组件的出参入参等样式一定会有所区别,所以需要提供一个接口让前端查询所有组件的元数据信息。在系统迭代的过程中增加或修改组件时,也需要修改数据库中相应组件的元数据信息json数据,但是修改线上的数据库内容会有很大的风险,最好还是能实现自动写入元数据信息的能力。
可以选择在系统每次初始化时,都读取一遍所有组件的元数据信息,把他们更新或新增到数据库中。这个的实现可以用到我以前学过和写过博客的一个能力——Spring的事件机制。
core.framework.SpringUtil
在这个类中新加一个方法 handleContextRefresh,使用 @EventListener 注解监听容器初始化的事件,并新增或更新组件元数据json。
@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
// 在这里编写初始化逻辑
System.out.println("ApplicationContext已经被刷新或初始化");
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(ComponentDefinition.class);
List<ComponentMetaInfo> componentMetaInfoList = new ArrayList<>();
beansWithAnnotation.keySet().forEach(componentName -> {
ComponentMetaInfo componentMetaInfo = new ComponentMetaInfo();
Class<?> clazz = AopUtils.getTargetClass(applicationContext.getBean(componentName));
ComponentDefinition componentDefinition = clazz.getAnnotation(ComponentDefinition.class);
componentMetaInfo.setName(componentDefinition.name());
componentMetaInfo.setType(componentDefinition.type().getType());
componentMetaInfo.setDesc(componentDefinition.desc());
componentMetaInfo.setClassName(clazz.getName());
InputParamDefinition inputParamDefinition = clazz.getAnnotation(InputParamDefinition.class);
// 下面那行代码运行 JSON.toJSONString(inputParam) 会报错,提升版本或换成Jackson会是空值,原因未知
// List<Param> inputParam = Arrays.asList(inputParamDefinition.value());
componentMetaInfo.setInputParam(writeParam(inputParamDefinition.value()));
OutputParamDefinition outputParamDefinition = clazz.getAnnotation(OutputParamDefinition.class);
// 同上
// List<Param> outputParam = Arrays.asList(outputParamDefinition.value());
componentMetaInfo.setOutputParam(writeParam(outputParamDefinition.value()));
componentMetaInfoList.add(componentMetaInfo);
});
ComponentMetaInfoDO.insertOrUpdate(componentMetaInfoList);
}
private String writeParam(Param[] params) {
List<String> paramList =
Arrays.stream(params).map(annotation -> {
// 获取注解的所有方法,这些方法对应于注解的字段
Method[] methods = annotation.annotationType().getDeclaredMethods();
List<Map<String, String>> list = new ArrayList<>();
// 遍历所有方法(字段),获取字段名和对应的值
for (Method method : methods) {
try {
// 调用方法获取字段值
Map<String, String> map = new HashMap<>();
Object value = method.invoke(annotation);
map.put(method.getName(), value.toString());
list.add(map);
} catch (Exception e) {
throw new RuntimeException();
}
}
return JSON.toJSONString(list);
}).toList();
return JSON.toJSONString(paramList);
}
值得一提的是被注释掉的那两行代码,是用来把组件的入参json序列化的,在有些环境下没问题,但在我这次使用的环境下,要么报错要么得到空结果,不得已只能手写一个序列化的方法(writeParam)。
然后也提供一波更新后的数据库mock代码:
dao.mock.ComponentMetaInfoDO
增加了一个全局List当做表数据;增加 insertOrUpdate 方法。
package com.example.lowcode.dao.mock;
import com.example.lowcode.core.entity.ComponentMetaInfo;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author llxzdmd
* @version ComponentMetaInfoDO.java, v 0.1 2024年01月04日 10:08 llxzdmd
*/
public class ComponentMetaInfoDO {
public static final List<ComponentMetaInfo> META_INFO_LIST = new ArrayList<>();
/**
* 假设这是ComponentMetaInfo表中存放的全部数据
*
* @return
*/
public static List<ComponentMetaInfo> mockData() {
return META_INFO_LIST;
}
public static void insertOrUpdate(List<ComponentMetaInfo> list) {
Map<String, ComponentMetaInfo> nameInfoMap =
list.stream().collect(Collectors.toMap(ComponentMetaInfo::getName, e -> e));
META_INFO_LIST.forEach(info -> {
if (nameInfoMap.get(info.getName()) != null) {
// 更新
try {
BeanUtils.copyProperties(info, nameInfoMap.get(info.getName()));
info.setModifiedTime(new Date());
list.remove(nameInfoMap.get(info.getName()));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
});
// 插入
int i = META_INFO_LIST.size() + 1;
for (ComponentMetaInfo info : list) {
info.setId((long) i);
info.setCreateTime(new Date());
info.setModifiedTime(new Date());
i++;
}
META_INFO_LIST.addAll(list);
}
/**
* 假设这个是查找的sql接口
*
* @param componentName
* @return
*/
public static List<ComponentMetaInfo> batchQueryByName(Collection<String> componentName) {
Map<String, ComponentMetaInfo> nameInfoMap =
mockData().stream().collect(Collectors.toMap(ComponentMetaInfo::getName, e -> e));
return componentName.stream().map(nameInfoMap::get).collect(Collectors.toList());
}
}
测试
既然是1.x的最后一篇,就用一个相对完整的组件流进行展示,如下:
这个组件流中创建了5个组件,分别是入口、去重组件、分页组件、http组件、出口。
@startuml
'https://plantuml.com/class-diagram
class BuildParamComponent1 {
Map map: inputList -> ['a','b','c','d','e','f','g','c','a'];
output() result: result1
}
class DistinctFilter {
List list: ${result1};
output() result: result2
}
class HttpClient {
String url: https://www.baidu.com;
String method: get;
output() result: result3
}
class PageFilter {
List list: ${result2}
Integer pageNum: 2
Integer pageSize: 3
output() result: result4
}
class BuildParamComponent2 {
Map map: pageResult -> ${result4};
Map map: httpResult -> ${result3};
output() result: result5
}
BuildParamComponent1 --|> DistinctFilter
BuildParamComponent1 --|> HttpClient
DistinctFilter --|> PageFilter
HttpClient --|> BuildParamComponent2
PageFilter --|> BuildParamComponent2
@enduml
component.BuildParamComponent
由于其他组件的返回值都是只有一个键值对的map,如果需求是希望把一些值整合到一个map中,可以创建一个BuildParamComponent组件,用于整合各种参数。
package com.example.lowcode.component;
import com.example.lowcode.core.dto.ComponentInfo;
import com.example.lowcode.core.framework.AbstractComponent;
import com.example.lowcode.core.framework.ComponentContext;
import com.example.lowcode.core.model.*;
import java.util.Map;
/**
* @author llxzdmd
* @version OutputComponent.java, 2024年01月17日 19:45 llxzdmd
*/
@ComponentDefinition(name = "BuildParamComponent", desc = "BuildParamComponent", type = ComponentTypeEnum.BUILD_PARAM)
@InputParamDefinition({
@Param(name = "params", desc = "需要整合的入参或出参Map", required = true, type = ParamTypeEnum.MAP)
})
@OutputParamDefinition({@Param(name = "result", desc = "组件流返回结果", required = true)})
public class BuildParamComponent extends AbstractComponent {
@Override
public Map<String, Object> execute(ComponentContext context, ComponentInfo componentInfo) throws Exception {
Map<String,Object> map = (Map<String,Object>) parseMapParam("params", context, componentInfo);
context.getContextMap().putAll(map);
return map;
}
}
这里会发现解析入参写了一个新方法而不是 parseInputParam,因为map类型的入参在定义时会多包一层,在这里选择了新写一个解析方法,目前没发现bug不知道以后会不会有。。。
protected Object parseMapParam(String paramName, ComponentContext context, ComponentInfo componentInfo) {
Object o = componentInfo.getInputs().get(paramName).getValue();
Map<String, Object> map = (Map<String, Object>) o;
return map.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> {
Object value = entry.getValue();
// 检查值是否匹配正则表达式
if (RegexUtils.hasEl(value.toString())) {
Object parsedParam = context.getContextMap().get(RegexUtils.dealEl(value.toString()));
// 把解析后的结果放到入参中
Map<String, Object> mapParam = (Map<String, Object>) componentInfo.getInputs().get(paramName).getValue();
mapParam.put(entry.getKey(), parsedParam);
return parsedParam;
} else {
// 如果不匹配正则表达式,保留原始值
return value;
}
}
));
}
然后就可以对着设计图编写测试代码了(测试代码中创建组件的顺序和order不一致,如果报错可以检查一下runFlow方法中有没有根据order排序,最新更新了v1.1中加了这一点):
@Test
public void runFlowV2Test() {
saveFlowSnapshotTest2();
Map<String, Object> result = runService.runFlowV2(2L);
System.out.println(result);
}
@Test
public void saveFlowSnapshotTest2() {
// 1.组件1
FlowNode flowNode1 = new FlowNode();
flowNode1.setNodeName("去重组件(前端展示的组件名)");
flowNode1.setOrder(2);
flowNode1.setComponentId(2L);
flowNode1.setComponentName("DistinctFilter");
flowNode1.setType(ComponentTypeEnum.FILTER);
// 去重过滤器填参数
ComponentInfo componentInfo1 = new ComponentInfo();
componentInfo1.setComponentId(2L);
componentInfo1.setFlowId(2L);
Map<String, ComponentParam> inputMap1 = new HashMap<>();
inputMap1.put("list", buildComponentParam(ParamTypeEnum.LIST, "list123", "需要去重的集合", "${inputList}", true));
Map<String, ComponentParam> outputMap1 = new HashMap<>();
outputMap1.put("result", buildComponentParam(ParamTypeEnum.STRING, "result", "去重组件执行结果", "result2", true));
componentInfo1.setInputs(inputMap1);
componentInfo1.setOutputs(outputMap1);
flowNode1.setComponentInfo(componentInfo1);
flowNode1.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(1));
// 2.组件2
FlowNode flowNode2 = new FlowNode();
flowNode2.setNodeName("分页组件(前端展示的组件名)");
flowNode2.setOrder(3);
flowNode2.setComponentId(4L);
flowNode2.setComponentName("PageFilter");
flowNode2.setType(ComponentTypeEnum.FILTER);
// 分页过滤器填参数
ComponentInfo componentInfo2 = new ComponentInfo();
componentInfo2.setComponentId(4L);
componentInfo2.setFlowId(2L);
Map<String, ComponentParam> inputMap2 = new HashMap<>();
inputMap2.put("list", buildComponentParam(ParamTypeEnum.LIST, "list456", "需要分页的集合", "${result2}", true));
inputMap2.put("pageNum", buildComponentParam(ParamTypeEnum.INT, "pageNum111", "起始在第几页,默认1", 2, false));
inputMap2.put("pageSize", buildComponentParam(ParamTypeEnum.INT, "pageSize111", "分页大小,默认10", 3, false));
Map<String, ComponentParam> outputMap2 = new HashMap<>();
outputMap2.put("result", buildComponentParam(ParamTypeEnum.STRING, "result", "分页组件执行结果", "result3", true));
componentInfo2.setInputs(inputMap2);
componentInfo2.setOutputs(outputMap2);
flowNode2.setComponentInfo(componentInfo2);
flowNode2.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(3));
// 3.组件3
FlowNode flowNode3 = new FlowNode();
flowNode3.setNodeName("inputParam");
flowNode3.setOrder(1);
flowNode3.setComponentId(1L);
flowNode3.setComponentName("BuildParamComponent");
flowNode3.setType(ComponentTypeEnum.FILTER);
// 填参数
ComponentInfo componentInfo3 = new ComponentInfo();
componentInfo3.setComponentId(1L);
componentInfo3.setFlowId(2L);
List<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("b");
stringList.add("c");
stringList.add("d");
stringList.add("e");
stringList.add("f");
stringList.add("g");
stringList.add("c");
stringList.add("a");
Map<String, ComponentParam> inputMap3 = new HashMap<>();
inputMap3.put("params", buildComponentParam(ParamTypeEnum.MAP, "list", "入口入参list", new HashMap<>() {{
put("inputList", stringList);
}}, true));
Map<String, ComponentParam> outputMap3 = new HashMap<>();
outputMap3.put("result", buildComponentParam(ParamTypeEnum.STRING, "result", "入参结果", "result1", true));
componentInfo3.setInputs(inputMap3);
componentInfo3.setOutputs(outputMap3);
flowNode3.setComponentInfo(componentInfo3);
flowNode3.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(0));
// 4.组件4
FlowNode flowNode4 = new FlowNode();
flowNode4.setNodeName("HttpClient-123");
flowNode4.setOrder(4);
flowNode4.setComponentId(3L);
flowNode4.setComponentName("HttpClient");
flowNode4.setType(ComponentTypeEnum.FILTER);
// 填参数
ComponentInfo componentInfo4 = new ComponentInfo();
componentInfo4.setComponentId(3L);
componentInfo4.setFlowId(2L);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("a","A");
paramMap.put("b","B");
componentInfo4.setInputs(new HashMap<>() {{
put("url", new ComponentParam().setType(ParamTypeEnum.STRING).setRequired(true).setName("url").setValue("https://www.baidu.com"));
put("method", new ComponentParam().setType(ParamTypeEnum.STRING).setRequired(true).setName("method").setValue("get"));
put("params", new ComponentParam().setType(ParamTypeEnum.STRING).setRequired(false).setName("params").setValue(paramMap));
put("headers", new ComponentParam().setType(ParamTypeEnum.STRING).setRequired(false).setName("headers").setValue(new HashMap<>()));
}});
componentInfo4.setOutputs(new HashMap<>() {{
put("result", new ComponentParam().setType(ParamTypeEnum.STRING).setRequired(true).setName("result").setValue("result4"));
}});
flowNode4.setComponentInfo(componentInfo4);
flowNode4.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(2));
// 5.组件5
FlowNode flowNode5 = new FlowNode();
flowNode5.setNodeName("inputParam");
flowNode5.setOrder(5);
flowNode5.setComponentId(1L);
flowNode5.setComponentName("BuildParamComponent");
flowNode5.setType(ComponentTypeEnum.FILTER);
// 填参数
ComponentInfo componentInfo5 = new ComponentInfo();
componentInfo5.setComponentId(1L);
componentInfo5.setFlowId(2L);
Map<String, ComponentParam> inputMap5 = new HashMap<>();
inputMap5.put("params", buildComponentParam(ParamTypeEnum.MAP, "params", "呃呃", new HashMap<>() {{
put("pageResult", "${result3}");
put("httpResult", "${result4}");
}}, true));
Map<String, ComponentParam> outputMap5 = new HashMap<>();
outputMap5.put("result", buildComponentParam(ParamTypeEnum.STRING, "result", "输出结果", "result5", true));
componentInfo5.setInputs(inputMap5);
componentInfo5.setOutputs(outputMap5);
flowNode5.setComponentInfo(componentInfo5);
flowNode5.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(0));
List<FlowNode> flowNodeList = new ArrayList<>();
flowNodeList.add(flowNode1);
flowNodeList.add(flowNode2);
flowNodeList.add(flowNode3);
flowNodeList.add(flowNode4);
flowNodeList.add(flowNode5);
// 调用保存组件流快照的接口,模拟入库
FlowSnapshotDO.insertData(2L, JSON.toJSONString(flowNodeList));
}
运行后效果如下:
顺带看一下日志:
总结
到这里低代码平台1.x暂时告一段落,下一篇没有意外的话会出2.0,组件流的执行引擎从串行改为多路时并行,进度目前还遥遥无期,对这块的知识储备还处在没学明白的阶段,未来可期!