前言
最近接触了挺多低代码相关的系统,发现了许多对我这个菜鸡来说新奇的架构,抽空研究了一些,决定开一篇我的第一个系列——走进低代码平台后端。
这个系列会长期更新(指编辑已发布的)和连载(因为本人目前也只是刚看出一点门道,之后会利用业余时间慢慢研究总结),预计会是一个较大的工程,希望能在半年或一年内完整落幕。
文章中用到的代码都是本人一点一点敲出来的,或用gpt生成,绝不是拿现有项目直接拷贝而得。目前暂时考虑到2.x版本,1.x版本是串行执行,2.x会升级到并行执行。等日后项目完善的时候可能会考虑发布源码压缩包,也可能不会。
由于创建项目的时候spring官网(https://start.spring.io/)已经不支持Java8和springboot2.x了,虽然可以用其他办法下载,但想着正好与时俱进一波,于是这个项目的jdk版本用的是Java21和springboot3.1.7,后续可能有一些不重要的代码会和java版本有关,比如http相关的代码是用到了11之后的版本。
版本1.0——项目雏形(简单版)
首先想清楚我们这个低代码平台要做什么?和大多数低代码平台一样,通过用户在前端选择不同的组件,填写对应的参数,得到一个组件流,可以是按钮填写表单的形式,也可以是用“拖拉拽”方式生成的,后者需要前端的参与度会多一些,以及前后端的一些参数约定也会更复杂。点击运行或调用接口运行这个组件流,就可以执行相应的逻辑,而这个组件的运行原理则是由低代码平台控制。
如果用户需要平台能有更多的组件供选择,或需要组件能迭代升级一些个性化功能,就在后端项目里新加一个类即可,所以这篇1.0版本就是先简单搭建一个可以运行和实现上述功能的系统。
系统分析
(由于懒就不画图写分析了,以后可能会补上但可能性不大)
- 这个系统需要用很低的代价去新增或修改组件,因此组件需要是一个通用的概念、定义;
- 为了方便管理(约定传参、统一执行逻辑),组件需要有统一的出参和入参约束;
- 因为用户可以随意选择使用哪些组件、修改组件运行的顺序,因此组件的运行接口也需要是一个统一的接口。
考虑上述3点,第一点的结论是组件就是这个系统的一个抽象对象,可以划分的实体表先不总结(我还没理清楚需要有哪些表,以后补充);第二点得到自定义注解可以很好的完成这个需求;第三点可以得出每个组件类需要继承一个公共的接口或抽象类,由接口去定义执行的方法和出入参。
搭建系统
经过上述简单分析,可以开始创建项目了,这里我创建了一个名为low-code的项目,层级结构是这样的:
core
在core包下面存放项目的核心代码,其中暂时创建了4个包。
entity
entity包里面存放实体类,相关代码如下:
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.Map;
/**
* @author llxzdmd
* @version ComponentInfo.java, v 0.1 2024年01月02日 19:02 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComponentInfo implements Serializable {
private Long id;
private Long flowId;
private Long startTime;
private Long endTime;
private Map<String, Object> inputs;
private Map<String, Object> outputs;
}
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.List;
/**
* @author llxzdmd
* @version ComponentMetaInfo.java, v 0.1 2024年01月03日 19:49 llxzdmd
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ComponentMetaInfo implements Serializable {
String name;
String desc;
List<Object> inputParam;
List<Object> outputParam;
String className;
Long addTime;
Long updateTime;
}
framework
framework包下面放构成这个架构的重要代码,目前只有3个类。
package com.example.lowcode.core.framework;
import com.example.lowcode.core.entity.ComponentInfo;
/**
* @author llxzdmd
* @version IComponent.java, v 0.1 2024年01月02日 19:00 llxzdmd
*/
public interface ComponentInterface {
Object execute (ComponentInfo componentInfo) throws Exception;
}
package com.example.lowcode.core.framework;
import com.example.lowcode.core.entity.ComponentInfo;
import java.util.Map;
/**
* @author llxzdmd
* @version AbstractComponent.java, v 0.1 2024年01月02日 19:34 llxzdmd
*/
public class AbstractComponent implements ComponentInterface{
@Override
public Map<String, Object> execute(ComponentInfo componentInfo) throws Exception {
return null;
}
}
这里创建抽象类的原因是,之后可能会扩展或重载多个执行方法,让组件继承抽象类而不是实现接口,可以避免重写没用的执行方法。
package com.example.lowcode.core.framework;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author llxzdmd
* @version BeanUtil.java, v 0.1 2024年01月03日 19:45 llxzdmd
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
public static AbstractComponent getComponent(String beanName) {
return (AbstractComponent) applicationContext.getBean(beanName);
}
@Override
public void setApplicationContext (ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}
}
model
model下面放一些模型类
package com.example.lowcode.core.model;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* 组件定义
*
* @author llxzdmd
* @version ComponentDefinition.java, v 0.1 2024年01月02日 17:24 llxzdmd
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ComponentDefinition {
String name();
ComponentTypeEnum type();
String desc();
}
package com.example.lowcode.core.model;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
/**
* 组件类型枚举
*
* @author llxzdmd
* @version ModelEnum.java, v 0.1 2024年01月02日 17:05 llxzdmd
*/
@Getter
public enum ComponentTypeEnum {
NONE(0),
INPUT(1),
OUTPUT(2),
DATA_QUERY(3),
SERVICE_CALL(4),
FILTER(5),
;
private final int type;
ComponentTypeEnum(int type){
this.type = type;
}
private static Map<Integer, ComponentTypeEnum> componentTypeMap = null;
static {
initComponentTypeMap();
}
private static void initComponentTypeMap() {
componentTypeMap = new HashMap<>();
for(ComponentTypeEnum typeEnum: ComponentTypeEnum.values()) {
componentTypeMap.put(typeEnum.getType(), typeEnum);
}
}
public static ComponentTypeEnum transToEnum(int componentType) {
if(componentTypeMap.get(componentType) != null) {
return componentTypeMap.get(componentType);
}
return NONE;
}
}
package com.example.lowcode.core.model;
import java.lang.annotation.*;
/**
* 组件入参定义
*
* @author llxzdmd
* @version InputParamDefinition.java, v 0.1 2024年01月02日 17:32 llxzdmd
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InputParamDefinition {
Param[] value () default @Param();
}
package com.example.lowcode.core.model;
import java.lang.annotation.*;
/**
* 组件出参定义
*
* @author llxzdmd
* @version OutputParamDefinition.java, v 0.1 2024年01月02日 17:47 llxzdmd
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface OutputParamDefinition {
Param[] value () default @Param();
}
package com.example.lowcode.core.model;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 组件出入参详细参数
*
* @author llxzdmd
* @version Param.java, v 0.1 2024年01月02日 17:33 llxzdmd
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String name () default "";
String desc () default "";
ParamTypeEnum type () default ParamTypeEnum.STRING;
boolean required () default false;
}
package com.example.lowcode.core.model;
import lombok.Getter;
import java.util.List;
import java.util.Map;
/**
* 组件可以处理的参数的类型
*
* @author llxzdmd
* @version ParamTypeEnum.java, v 0.1 2024年01月02日 17:45 llxzdmd
*/
@Getter
public enum ParamTypeEnum {
STRING("STRING", String.class),
JSON("JSON", String.class),
INT("INT", Integer.class),
BOOLEAN("BOOLEAN", Boolean.class),
DOUBLE("DOUBLE", Double.class),
LIST("LIST", List.class),
MAP("MAP", Map.class),
;
private final String type;
private final Class<?> typeClass;
ParamTypeEnum (String type, Class<?> typeClass) {
this.type = type;
this.typeClass = typeClass;
}
public static ParamTypeEnum fromString (final String type) {
for (ParamTypeEnum value : ParamTypeEnum.values()) {
if (value.getType().equalsIgnoreCase(type)) {
return value;
}
}
return STRING;
}
}
service
service类中放一些运行组件的接口方法
package com.example.lowcode.core.service;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.entity.ComponentMetaInfo;
import java.util.List;
import java.util.Map;
/**
* @author llxzdmd
* @version RunFlowService.java, v 0.1 2024年01月03日 11:19 llxzdmd
*/
public interface RunService {
Map<String, Object> runFlowV1(List<ComponentMetaInfo> metaInfoList, Map<String, ComponentInfo> inputParams);
}
package com.example.lowcode.core.service;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.entity.ComponentMetaInfo;
import com.example.lowcode.core.framework.AbstractComponent;
import com.example.lowcode.core.framework.SpringUtil;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author llxzdmd
* @version RunServiceImpl.java, v 0.1 2024年01月03日 11:20 llxzdmd
*/
@Service
public class RunServiceImpl implements RunService {
@Override
public Map<String, Object> runFlowV1(List<ComponentMetaInfo> metaInfoList, Map<String, ComponentInfo> inputParams) {
List<Map<String, Object>> resultList = new ArrayList<>();
metaInfoList.forEach(componentInfo -> {
final Class<?> aClass;
try {
aClass = Class.forName(componentInfo.getClassName());
} catch (ClassNotFoundException e) {
System.out.println("组件+" + componentInfo.getClassName() + "不存在");
throw new RuntimeException(e);
}
AbstractComponent abstractComponent = (AbstractComponent) SpringUtil.getBean(aClass);
try {
Map<String, Object> result = abstractComponent.execute(inputParams.get(componentInfo.getName()));
resultList.add(result);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return resultList.get(resultList.size() - 1);
}
}
runFlowV1方法的大致逻辑是:根据入参的组件信息列表,顺序遍历得到每个组件的类,然后获取到抽象类(或者接口)的对象,执行运行的方法。最后返回最后一个组件的运行结果。
dao
mock
因为时间关系暂时没有创建MySQL表,因此很多实体数据可以用mock的形式创建,仿照是从MySQL中查到的数据。
package com.example.lowcode.dao.mock;
import com.example.lowcode.component.DistinctFilter;
import com.example.lowcode.component.PageFilter;
import com.example.lowcode.core.entity.ComponentMetaInfo;
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 {
/**
* 假设这是ComponentMetaInfo表中存放的全部数据
*
* @return
*/
public static List<ComponentMetaInfo> mockData() {
List<ComponentMetaInfo> metaInfoList = new ArrayList<>();
ComponentMetaInfo componentMetaInfo1 = new ComponentMetaInfo();
componentMetaInfo1.setName("DistinctFilter");
componentMetaInfo1.setClassName(DistinctFilter.class.getName());
ComponentMetaInfo componentMetaInfo2 = new ComponentMetaInfo();
componentMetaInfo2.setName("PageFilter");
componentMetaInfo2.setClassName(PageFilter.class.getName());
metaInfoList.add(componentMetaInfo1);
metaInfoList.add(componentMetaInfo2);
return metaInfoList;
}
/**
* 假设这个是查找的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());
}
}
util
存放一些工具类
package com.example.lowcode.util;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @author llxzdmd
* @version HttpUtils.java, v 0.1 2024年01月02日 19:45 llxzdmd
*/
public class HttpUtils {
/**
* 发送get请求,gpt生成
*
* @param url
* @param headers
* @return
* @throws Exception
*/
public static String sendGetRequest(String url, Map<String, String> headers) throws Exception {
// 初始化URL对象
HttpClient client = HttpClient.newHttpClient();
// 创建HttpRequest
HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.uri(URI.create(url));
headers.forEach(builder::header);
HttpRequest request = builder.build();
// 发送请求并获取响应
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 返回响应内容
return response.body();
}
/**
* 发送post请求,gpt生成
*
* @param url
* @param json
* @param headers
* @return
* @throws Exception
*/
public static String sendPostRequest(String url, String json, Map<String, String> headers) throws Exception {
// 创建HttpClient实例
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
// 创建HttpRequest,并设置URL、headers和POST请求的body
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8));
// 添加headers到HttpRequest中
headers.forEach(builder::header);
// 构建HttpRequest对象
HttpRequest request = builder.build();
// 发送POST请求,并获取响应
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 返回响应内容
return response.body();
}
}
component
在这个包下面创建各种组件。
暂时能想到的组件有:MySQL组件、Redis组件、es组件、Dubbo组件、http组件、去重组件、分页组件等。由于组件是可以随意扩展的,不是这个系统的重点,第一篇就先只创建了3个组件。
package com.example.lowcode.component;
import com.example.lowcode.core.model.*;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.framework.*;
import com.google.common.collect.Sets;
import java.util.*;
/**
* @author llxzdmd
* @version DistinctFilter.java, v 0.1 2024年01月02日 19:39 llxzdmd
*/
@ComponentDefinition(name = "DistinctFilter", type = ComponentTypeEnum.FILTER, desc = "去重过滤器")
@InputParamDefinition({
@Param(name = "list", desc = "需要去重的集合", type = ParamTypeEnum.LIST, required = true),
@Param(name = "params", desc = "对象集合的去重字段", type = ParamTypeEnum.LIST, required = false),
@Param(name = "paramTypes", desc = "去重字段的类型", type = ParamTypeEnum.LIST, required = false)
})
@OutputParamDefinition({@Param(name = "result", desc = "去重过滤器返回结果", required = true)})
public class DistinctFilter extends AbstractComponent {
@Override
public Map<String, Object> execute(ComponentInfo componentInfo) throws Exception {
// 先随便写,只考虑简单类型
List list = (List) componentInfo.getInputs().get("list");
Set set = Sets.newHashSet(list);
List difference = findListDifference(list, set);
System.out.println("被去重的元素有:"+difference);
HashMap<String, Object> result = new HashMap<>();
result.put("result", set);
return result;
}
public static List findListDifference(List list, Set set) {
// 创建List的一个副本
List listCopy = new ArrayList<>(list);
// 遍历Set,尝试从List副本中移除元素
for (Object element : set) {
listCopy.remove(element);
}
// 返回剩余的元素,即为多出的元素
return listCopy;
}
}
package com.example.lowcode.component;
import com.alibaba.fastjson.JSON;
import com.example.lowcode.core.model.*;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.framework.*;
import com.example.lowcode.util.HttpUtils;
import com.google.common.collect.Maps;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author llxzdmd
* @version HttpClient.java, v 0.1 2024年01月02日 18:54 llxzdmd
*/
@ComponentDefinition(name = "HttpClient", type = ComponentTypeEnum.SERVICE_CALL, desc = "http组件")
@InputParamDefinition({
@Param(name = "url", desc = "http请求地址", type = ParamTypeEnum.STRING, required = true),
@Param(name = "method", desc = "http请求方法", type = ParamTypeEnum.STRING, required = true),
@Param(name = "params", desc = "接口入参", type = ParamTypeEnum.MAP, required = false),
@Param(name = "headers", desc = "http请求headers", type = ParamTypeEnum.MAP, required = false)
})
@OutputParamDefinition({@Param(name = "result", desc = "http接口返回结果", required = true)})
public class HttpClient extends AbstractComponent {
@Override
public Map<String, Object> execute(ComponentInfo componentInfo) throws Exception {
// 暂时忽略判空
String url = (String) componentInfo.getInputs().get("url");
String method = (String) componentInfo.getInputs().get("method");
Map<String, Object> paramMap = (Map<String, Object>) componentInfo.getInputs().get("params");
Map<String, String> headerMap = (Map<String, String>) componentInfo.getInputs().get("headers");
String httpResult;
if (Objects.equals("post", method)) {
httpResult = HttpUtils.sendPostRequest(url, JSON.toJSONString(paramMap), headerMap);
} else {
httpResult = HttpUtils.sendGetRequest(url, headerMap);
}
HashMap<String, Object> result = Maps.newHashMap();
result.put("result", httpResult);
return result;
}
}
package com.example.lowcode.component;
import com.example.lowcode.core.model.*;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.framework.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author llxzdmd
* @version PageFilter.java, v 0.1 2024年01月02日 19:39 llxzdmd
*/
@ComponentDefinition(name = "PageFilter", type = ComponentTypeEnum.FILTER, desc = "分页过滤器")
@InputParamDefinition({
@Param(name = "list", desc = "需要分页的集合", type = ParamTypeEnum.LIST, required = true),
@Param(name = "pageNum", desc = "起始在第几页,默认1", type = ParamTypeEnum.INT, required = false),
@Param(name = "pageSize", desc = "分页大小,默认10", type = ParamTypeEnum.INT, required = false)
})
@OutputParamDefinition({@Param(name = "result", desc = "分页过滤器返回结果", required = true)})
public class PageFilter extends AbstractComponent {
@Override
public Map<String, Object> execute(ComponentInfo componentInfo) throws Exception {
List list = (List) componentInfo.getInputs().get("list");
int pageNum = componentInfo.getInputs().get("pageNum") == null ?
1 : (int) componentInfo.getInputs().get("pageNum");
int pageSize = componentInfo.getInputs().get("pageSize") == null ?
10 : (int) componentInfo.getInputs().get("pageSize");
HashMap<String, Object> result = new HashMap<>();
if (list == null || pageNum <= 0 || pageSize <= 0) {
result.put("result", Collections.emptyList());
return result;
}
int fromIndex = (pageNum - 1) * pageSize;
if (fromIndex >= list.size()) {
// 请求的页码超出了列表的范围,返回空列表
result.put("result", Collections.emptyList());
return result;
}
int toIndex = Math.min(fromIndex + pageSize, list.size());
result.put("result", list.subList(fromIndex, toIndex));
return result;
}
}
test
service
package com.example.lowcode.service;
import com.example.lowcode.core.entity.ComponentInfo;
import com.example.lowcode.core.entity.ComponentMetaInfo;
import com.example.lowcode.core.service.RunService;
import com.example.lowcode.dao.mock.ComponentMetaInfoDO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author llxzdmd
* @version RunServiceTest.java, v 0.1 2024年01月03日 11:29 llxzdmd
*/
@SpringBootTest
public class RunServiceTest {
@Autowired
private RunService runService;
@Test
public void runFlowV1Test() {
Map<String, ComponentInfo> inputParams = new HashMap<>();
// 去重过滤器填参数
ComponentInfo componentInfo1 = new ComponentInfo();
componentInfo1.setId(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, Object> inputMap1 = new HashMap<>();
inputMap1.put("list", stringList);
componentInfo1.setInputs(inputMap1);
// 分页过滤器填参数
ComponentInfo componentInfo2 = new ComponentInfo();
componentInfo2.setId(2L);
componentInfo2.setFlowId(1L);
Map<String, Object> inputMap2 = new HashMap<>();
inputMap2.put("list", stringList);
inputMap2.put("pageNum", 2);
inputMap2.put("pageSize", 3);
componentInfo2.setInputs(inputMap2);
// 整个流填参数
inputParams.put("DistinctFilter", componentInfo1);
inputParams.put("PageFilter", componentInfo2);
// 根据入参
List<ComponentMetaInfo> metaInfoList = ComponentMetaInfoDO.batchQueryByName(inputParams.keySet());
Map<String, Object> result = runService.runFlowV1(metaInfoList, inputParams);
System.out.println(result);
}
}
在这个测试类中,我们依次创建了两个组件:去重组件和分页组件。运行结果如下:
总结
在这个系统1.0中,我们实现了可以任意扩展组件,让用户随意选择组件和执行顺序的核心逻辑。现在有一个小问题是运行组件流的入参不应该是组件元数据信息list,应当有一个实体表存放每个组件流的信息,比如有哪些组件、组件的顺序等。这个会在有空的时候改掉,然后开始准备看v1.1需要优化些什么。