回顾
接上篇v1.0的总结,在上一篇搭建的最简单系统中,有一个很大的问题,就是运行组件流的接口定义部分:
这里为了拿到类对象采用了 List<ComponentMetaInfo> 类型的入参,并且花了很大的代价构造inputParams。
但实际项目中我们不可能每次运行组件流之前都在前端填好所有的参数,再点击运行;而是应该把固定的参数格式存起来,用户只需要在前端填写变量再点击保存,以后每次调用接口执行这个配置好的组件流即可。因此runFlow接口的入参不能是List<ComponentMetaInfo>,应该是传一个当前组件流配置的id,接着从数据库中查询这个组件流的配置信息:有哪些组件、组件的运行顺序、组件的参数位置用户填了什么。
因此本篇的第一部分需要先实现把组件配置的快照保存起来。
一、持久保存配置
首先为了和持久层对象区分开,我新建了一个dto包,并把componentInfo类转移到了这里:
里面的代码也发生了一点变化,这个过一会再说。
新建了两个实例对象:FlowInstance、FlowSnapshot。这两个实例一个代表在首页列表看到的基本信息,一个代表点进详情后若干个组件的详细配置信息。比如一个是csdn的用户首页的文章列表,一个是点进博客后的章节目录、目录下面有哪些文字等信息。
entity
FlowInstance
这个类暂时不会用到,因为主要是做前端列表展示用的,现在创建是为了和Snapshot区分开。
package com.example.lowcode.core.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* @author llxzdmd
* @version FlowInstance.java, 2024年01月08日 10:19 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FlowInstance implements Serializable {
private Long id;
private String name;
private String desc;
private String createName;
private String modifiedName;
private Date createTime;
private Date modifiedTime;
}
FlowSnapshot
package com.example.lowcode.core.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* @author llxzdmd
* @version FlowSnapshot.java, 2024年01月08日 16:43 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FlowSnapshot implements Serializable {
// 主键id
private Long id;
// FlowInstance表的主键id
private Long flowId;
// 组件详情的json信息
private String jsonParam;
private String createName;
private String modifiedName;
private Date createTime;
private Date modifiedTime;
// 版本号,在组件配置页点保存时记录一个版本
private Integer version;
}
dao
老样子还是没有接入MySQL,直接mock数据。现在感觉这样好像挺好的,仅供学习的话代码也不会很重。
mock
同样的也创建了两个构造数据的类。
FlowInstanceDO
package com.example.lowcode.dao.mock;
import com.example.lowcode.core.entity.FlowInstance;
import java.util.ArrayList;
import java.util.List;
/**
* @author llxzdmd
* @version FlowInstanceDO.java, 2024年01月08日 17:32 llxzdmd
*/
public class FlowInstanceDO {
private static final List<FlowInstance> FLOW_INSTANCE_List = new ArrayList<>();
public static List<FlowInstance> mockData() {
return FLOW_INSTANCE_List;
}
public static void insertData(String name, String desc) {
FlowInstance flowInstance = new FlowInstance();
flowInstance.setId((long) FLOW_INSTANCE_List.size() + 1);
flowInstance.setName(name);
flowInstance.setDesc(desc);
FLOW_INSTANCE_List.add(flowInstance);
}
public static FlowInstance selectById(long flowId) {
return FLOW_INSTANCE_List.stream().filter(flow -> flow.getId().equals(flowId)).toList().get(0);
}
}
FlowSnapshotDO
因为这个类更多是用flowId去查数据,因此我把数据放到了Map中。(当然这一切都是因为mock)
package com.example.lowcode.dao.mock;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.example.lowcode.core.entity.FlowSnapshot;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
/**
* @author llxzdmd
* @version FlowSnapshot.java, 2024年01月08日 17:32 llxzdmd
*/
public class FlowSnapshotDO {
private static final Map<Long, List<FlowSnapshot>> FLOW_SNAPSHOT_MAP = new ConcurrentHashMap<>();
public static List<FlowSnapshot> mockData() {
// 返回所有的FlowDetail,根据Long升序
return FLOW_SNAPSHOT_MAP.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toList());
}
public static void insertData(long flowId, String jsonParams) {
FlowSnapshot snapshot = new FlowSnapshot();
snapshot.setId((long) FLOW_SNAPSHOT_MAP.size() + 1);
snapshot.setFlowId(flowId);
snapshot.setJsonParam(jsonParams);
int version;
// 判断这个组件流下有没有历史版本
if (FLOW_SNAPSHOT_MAP.get(flowId) == null) {
version = 1;
snapshot.setVersion(version);
FLOW_SNAPSHOT_MAP.put(flowId, Lists.newArrayList(snapshot));
} else {
OptionalInt maxVersion =
FLOW_SNAPSHOT_MAP.get(flowId).stream().mapToInt(FlowSnapshot::getVersion).max();
version = maxVersion.isPresent() ? maxVersion.getAsInt() + 1 : 1;
snapshot.setVersion(version);
FLOW_SNAPSHOT_MAP.get(flowId).add(snapshot);
}
}
public static FlowSnapshot selectByFlowId(long flowId) {
List<FlowSnapshot> flowSnapshots = FLOW_SNAPSHOT_MAP.get(flowId);
return CollectionUtils.isEmpty(flowSnapshots) ? null : flowSnapshots.get(flowSnapshots.size() - 1);
}
}
一口气建好4个类后,下一步是FlowSnapshot表的jsonParam字段怎么填呢?想一下解析组件的运行规则需要什么:
- 组件的参数信息——参数名、参数值、参数类型等
- 组件元数据信息——全路径类名等
- 组件执行的顺序
- 其他个性化信息,比如自定义组件名、组件类型方便差异化显示
所以可以创建一个类包含这些信息,作为和前端交互的结构,以及以json形式存到FlowSnapshot表中。
dto
FlowNode
package com.example.lowcode.core.dto;
import com.example.lowcode.core.entity.ComponentMetaInfo;
import com.example.lowcode.core.model.ComponentTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author llxzdmd
* @version FlowNode.java, 2024年01月08日 21:03 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FlowNode {
// 自定义名
private String nodeName;
// 组件id
private Long componentId;
// 组件名
private String componentName;
// 组件信息
private ComponentInfo componentInfo;
// 组件元数据信息
private ComponentMetaInfo componentMetaInfo;
// 顺序
private Integer order;
// 组件类型
private ComponentTypeEnum type;
}
其中ComponentInfo和ComponentMetaInfo我做了一些修改:
ComponentInfo
package com.example.lowcode.core.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* @author llxzdmd
* @version ComponentInfo.java, v 0.1 2024年01月02日 19:02 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComponentInfo {
private Long componentId;
private Long flowId;
// 参数名->此参数的详细配置
private Map<String, ComponentParam> inputs;
private Map<String, ComponentParam> outputs;
}
里面新加了一个结构,用于存储某个参数的详细信息:
ComponentParam
package com.example.lowcode.core.dto;
import com.example.lowcode.core.model.ParamTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用于把组件流快照存数据库,参数值比组件的入参定义多一个value——用户在组件上面对应参数填写的值
*
* @author llxzdmd
* @version ComponentParam.java, v 0.1 2024年01月02日 19:04 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComponentParam {
// 参数名称,自定义
private String name;
// 参数值
private Object value;
private String desc;
private ParamTypeEnum type;
private boolean required;
}
至于 ComponentMetaInfo 则是一点无关紧要的小改动:
entity.ComponentMetaInfo
package com.example.lowcode.core.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* @author llxzdmd
* @version ComponentMetaInfo.java, v 0.1 2024年01月03日 19:49 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComponentMetaInfo implements Serializable {
private Long id;
// 类名
private String name;
private String desc;
// 类型
private Integer type;
// 全路径类名
private String className;
private Date createTime;
private Date modifiedTime;
}
二、逻辑层改动
接下来就可以改写我们的runFLow接口了:
@Override
public Map<String, Object> runFlowV2(long flowId) {
FlowSnapshot flowSnapshot = FlowSnapshotDO.selectByFlowId(flowId);
assert flowSnapshot != null;
List<FlowNode> flowNodeList = JSON.parseObject(flowSnapshot.getJsonParam(), new TypeReference<List<FlowNode>>() {
}).stream().sorted(Comparator.comparingInt(FlowNode::getOrder)).toList();
List<Map<String, Object>> resultList = new ArrayList<>();
flowNodeList.forEach(flowNode -> {
final Class<?> aClass;
try {
aClass = Class.forName(flowNode.getComponentMetaInfo().getClassName());
} catch (ClassNotFoundException e) {
System.out.println("组件+" + flowNode.getComponentMetaInfo().getClassName() + "不存在");
throw new RuntimeException(e);
}
AbstractComponent abstractComponent = (AbstractComponent) SpringUtil.getBean(aClass);
try {
Map<String, Object> result = abstractComponent.execute(flowNode.getComponentInfo());
resultList.add(result);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return resultList.get(resultList.size() - 1);
}
在上面的方法中,第一步先根据flowId查找组件流的以后一个版本的快照详情,然后解析jsonParam字段为List<FlowNode>对象,获取组件的详细信息并根据order排序。接着用老方法通过反射获取到类对象,分别执行组件。
三、测试
首先需要构造测试数据,也就是mock用户配置组件点击保存的步骤,可以参考我这里的配置:
@Test
public void saveFlowSnapshotTest() {
// 组装FlowNode(即组件节点,正常应该是前端把若干个FlowNode放到Map中传到后端)
// 至于为什么要新创建一个FlowNode,而不是用已有的ComponentInfo或ComponentMetaInfo,
FlowNode flowNode1 = new FlowNode();
flowNode1.setNodeName("去重组件(前端展示的组件名)");
flowNode1.setOrder(1);
flowNode1.setComponentId(1L);
flowNode1.setComponentName("DistinctFilter");
flowNode1.setType(ComponentTypeEnum.FILTER);
// 去重过滤器填参数
ComponentInfo componentInfo1 = new ComponentInfo();
componentInfo1.setComponentId(1L);
componentInfo1.setFlowId(1L);
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> inputMap1 = new HashMap<>();
inputMap1.put("list", buildComponentParam(
ParamTypeEnum.LIST, "list123", "需要去重的集合", stringList, true
));
componentInfo1.setInputs(inputMap1);
flowNode1.setComponentInfo(componentInfo1);
flowNode1.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(0));
FlowNode flowNode2 = new FlowNode();
flowNode2.setNodeName("分页组件(前端展示的组件名)");
flowNode2.setOrder(2);
flowNode2.setComponentId(2L);
flowNode2.setComponentName("PageFilter");
flowNode2.setType(ComponentTypeEnum.FILTER);
// 分页过滤器填参数
ComponentInfo componentInfo2 = new ComponentInfo();
componentInfo2.setComponentId(2L);
componentInfo2.setFlowId(1L);
Map<String, ComponentParam> inputMap2 = new HashMap<>();
// 这里是否就已经需要上下文了?放list
inputMap2.put("list", buildComponentParam(
ParamTypeEnum.LIST, "list456", "需要分页的集合", stringList, true
));
inputMap2.put("pageNum", buildComponentParam(
ParamTypeEnum.INT, "pageNum111", "起始在第几页,默认1", 2, false
));
inputMap2.put("pageSize", buildComponentParam(
ParamTypeEnum.INT, "pageSize111", "分页大小,默认10", 3, false
));
componentInfo2.setInputs(inputMap2);
flowNode2.setComponentInfo(componentInfo2);
flowNode2.setComponentMetaInfo(ComponentMetaInfoDO.mockData().get(1));
List<FlowNode> flowNodeList = new ArrayList<>();
flowNodeList.add(flowNode1);
flowNodeList.add(flowNode2);
// 调用保存组件流快照的接口,模拟入库
FlowSnapshotDO.insertData(1L, JSON.toJSONString(flowNodeList));
}
private ComponentParam buildComponentParam(
ParamTypeEnum typeEnum, String name, String desc, Object value, boolean required) {
ComponentParam componentParam = new ComponentParam();
componentParam.setType(typeEnum);
componentParam.setName(name);
componentParam.setDesc(desc);
componentParam.setValue(value);
componentParam.setRequired(required);
return componentParam;
}
然后打通构造数据和运行组件流:
@Test
public void runFlowV2Test() {
saveFlowSnapshotTest();
Map<String, Object> result = runService.runFlowV2(1L);
System.out.println(result);
}
得到结果:
总结
但是到了这里如果有比较细的小伙伴就会眉头一皱,想喷博主写的什么破bug。没错这个v1.1版本因为升级了runFLow接口的入参,但测试方法的构造方式还是按照v1.0的思路,就会出现问题: 如果在第二个组件处打断点就会发现,第二个组件用到的list是没有去重过的。
大家可以回顾一下v1.0的时候是取巧了,在测试方法中两个组件用到了同一个list对象,也是修改的同一个对象;但这次是类似在前端的入参写了两个list,之后解析为json存到库中了,因此两个组件编辑的是两个不同的list。
那么如何解决这个问题呢?留到下一篇再解决吧,写太多了看的人会烦躁,而且也晚了我先睡觉了告辞。。。