低代码平台技术架构:动态表单、流程引擎、可视化配置的Java实现
低代码平台技术架构:动态表单、流程引擎、可视化配置的Java实现
适读人群:Java架构师、平台开发工程师 | 阅读时长:约19分钟 | 技术栈:Spring Boot 3.x、Flowable、EasyExcel、Redis
开篇故事
做了这么多年Java开发,我发现一类需求会周期性地出现:业务方要"灵活配置",要"不改代码就能调整业务逻辑",要"自己配表单,自己定流程"。
最开始我对这类需求有点反感,因为我见过太多"低代码"项目最后变成了高代码——为了实现"灵活",底层构建了一个极其复杂的元数据系统,业务方配不明白,开发方维护困难,两头都不讨好。
但换个角度想,这类需求存在有其合理性。企业里确实有大量重复性的表单流程类需求,如果每个都要写代码,开发成本很高,需求积压严重。低代码平台的核心价值是把通用能力标准化,让简单需求不需要开发介入。
今天这篇文章,聊聊低代码平台后端核心能力的Java实现思路,以及真正踩过的坑。
一、核心问题:低代码平台的三层能力模型
后端工程师主要关注数据层和流程层,展示层大部分在前端实现。
二、原理深度解析
2.1 动态表单的数据存储策略
动态表单最难的问题是:不同表单有不同字段,如何在关系型数据库里存储?
有几种主流方案:
我的实践选择:JSON列存储(MySQL/PostgreSQL的JSON类型)+ 索引字段冗余。对于需要查询的字段,单独建普通列并加索引;不常查询的字段,统一存JSON列。
2.2 工作流引擎选型
三、完整代码实现
3.1 动态表单定义与存储
// 表单定义
@Entity
@Table(name = "form_definition")
public class FormDefinition {
@Id
private String formId;
private String name;
private String description;
private String version;
@Column(columnDefinition = "JSON")
private String schema; // 表单Schema,JSON格式
@Enumerated(EnumType.STRING)
private FormStatus status; // DRAFT, PUBLISHED, DEPRECATED
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// 表单Schema示例(JSON格式)
/*
{
"fields": [
{
"fieldKey": "applicantName",
"fieldType": "TEXT",
"label": "申请人姓名",
"required": true,
"maxLength": 50
},
{
"fieldKey": "department",
"fieldType": "SELECT",
"label": "部门",
"required": true,
"options": [
{"value": "tech", "label": "技术部"},
{"value": "product", "label": "产品部"}
]
},
{
"fieldKey": "applyDate",
"fieldType": "DATE",
"label": "申请日期",
"required": true,
"defaultValue": "today"
},
{
"fieldKey": "amount",
"fieldType": "NUMBER",
"label": "申请金额",
"required": true,
"min": 0,
"max": 100000
}
]
}
*/
// 表单数据存储
@Entity
@Table(name = "form_data")
public class FormData {
@Id
private String dataId;
private String formId;
private String businessKey; // 关联业务对象ID(如订单ID)
@Column(columnDefinition = "JSON")
private String data; // 表单填写的数据,JSON格式
// 常用查询字段单独冗余
private String applicantId;
private String department;
private LocalDate applyDate;
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private DataStatus status; // DRAFT, SUBMITTED, APPROVED, REJECTED
private LocalDateTime createdAt;
}// 表单服务
@Service
public class FormService {
@Autowired
private FormDefinitionRepository formDefRepo;
@Autowired
private FormDataRepository formDataRepo;
@Autowired
private ObjectMapper objectMapper;
/**
* 提交表单数据(带校验)
*/
public FormData submit(String formId, String applicantId, Map<String, Object> formValues) {
FormDefinition formDef = formDefRepo.findById(formId)
.orElseThrow(() -> new FormNotFoundException(formId));
// 根据Schema校验数据
validateFormData(formDef, formValues);
// 保存数据
FormData formData = new FormData();
formData.setDataId(UUID.randomUUID().toString());
formData.setFormId(formId);
formData.setApplicantId(applicantId);
// JSON存储所有字段
formData.setData(serializeData(formValues));
// 冗余常用查询字段
formData.setDepartment((String) formValues.get("department"));
formData.setApplyDate(parseDate(formValues.get("applyDate")));
formData.setAmount(parseBigDecimal(formValues.get("amount")));
formData.setStatus(DataStatus.SUBMITTED);
return formDataRepo.save(formData);
}
/**
* 表单数据校验
*/
private void validateFormData(FormDefinition formDef, Map<String, Object> data) {
FormSchema schema = parseSchema(formDef.getSchema());
List<String> errors = new ArrayList<>();
for (FormField field : schema.getFields()) {
Object value = data.get(field.getFieldKey());
if (field.isRequired() && (value == null || "".equals(value))) {
errors.add(field.getLabel() + "不能为空");
continue;
}
if (value != null) {
switch (field.getFieldType()) {
case TEXT -> {
String strValue = value.toString();
if (field.getMaxLength() > 0 && strValue.length() > field.getMaxLength()) {
errors.add(field.getLabel() + "超过最大长度" + field.getMaxLength());
}
}
case NUMBER -> {
try {
BigDecimal numValue = new BigDecimal(value.toString());
if (field.getMin() != null && numValue.compareTo(field.getMin()) < 0) {
errors.add(field.getLabel() + "不能小于" + field.getMin());
}
if (field.getMax() != null && numValue.compareTo(field.getMax()) > 0) {
errors.add(field.getLabel() + "不能大于" + field.getMax());
}
} catch (NumberFormatException e) {
errors.add(field.getLabel() + "必须是数字");
}
}
case SELECT -> {
boolean validOption = field.getOptions().stream()
.anyMatch(opt -> opt.getValue().equals(value.toString()));
if (!validOption) {
errors.add(field.getLabel() + "的值不在允许范围内");
}
}
}
}
}
if (!errors.isEmpty()) {
throw new FormValidationException(errors);
}
}
}3.2 Flowable流程引擎集成
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.8.0</version>
</dependency>@Service
public class ProcessService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private HistoryService historyService;
/**
* 启动流程实例
*/
public String startProcess(String processKey, String businessKey,
Map<String, Object> variables) {
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
processKey,
businessKey, // 关联业务键(如表单数据ID)
variables // 流程变量(审批人、金额等)
);
log.info("流程启动成功: processInstanceId={}, businessKey={}",
instance.getId(), businessKey);
return instance.getId();
}
/**
* 查询待办任务
*/
public List<TaskVO> getMyTasks(String userId, int page, int size) {
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(userId) // 直接指派给我的
.or()
.taskCandidateUser(userId) // 或者候选人包含我的
.orderByTaskCreateTime().desc()
.listPage(page * size, size);
return tasks.stream().map(task -> {
// 获取流程变量
Map<String, Object> variables = runtimeService.getVariables(task.getExecutionId());
return new TaskVO(task, variables);
}).collect(Collectors.toList());
}
/**
* 审批操作:同意/拒绝
*/
@Transactional
public void completeTask(String taskId, String userId, boolean approved, String comment) {
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
if (task == null) {
throw new TaskNotFoundException(taskId);
}
// 权限检查
if (!userId.equals(task.getAssignee()) && !isCandidateUser(task, userId)) {
throw new UnauthorizedException("无权审批此任务");
}
// 添加审批意见
taskService.addComment(taskId, task.getProcessInstanceId(), comment);
// 设置审批结果变量
Map<String, Object> variables = new HashMap<>();
variables.put("approved", approved);
variables.put("approver", userId);
variables.put("approveComment", comment);
variables.put("approveTime", LocalDateTime.now().toString());
// 完成任务
taskService.complete(taskId, variables);
log.info("任务{}审批完成: approved={}, operator={}", taskId, approved, userId);
}
/**
* 动态部署流程定义(低代码的关键:流程可配置)
*/
public String deployProcess(String processName, String bpmnXml) {
Deployment deployment = repositoryService.createDeployment()
.name(processName)
.addString(processName + ".bpmn20.xml", bpmnXml)
.deploy();
log.info("流程部署成功: deploymentId={}", deployment.getId());
return deployment.getId();
}
}3.3 BPMN流程动态生成
/**
* 根据配置动态生成BPMN XML
* 将可视化配置转换为Flowable可执行的流程定义
*/
@Service
public class BpmnGenerator {
public String generateBpmn(ProcessConfig config) {
StringBuilder bpmn = new StringBuilder();
bpmn.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
bpmn.append("<definitions xmlns=\"http://www.omg.org/spec/BPMN/20100524/MODEL\" ");
bpmn.append("xmlns:flowable=\"http://flowable.org/bpmn\" ");
bpmn.append("targetNamespace=\"http://www.flowable.org/processdef\">\n");
bpmn.append("<process id=\"").append(config.getProcessKey()).append("\" ");
bpmn.append("name=\"").append(config.getProcessName()).append("\">\n");
// 开始事件
bpmn.append("<startEvent id=\"start\"/>\n");
bpmn.append("<sequenceFlow id=\"flow-start\" sourceRef=\"start\" targetRef=\"");
bpmn.append(config.getNodes().get(0).getNodeId()).append("\"/>\n");
// 审批节点
for (int i = 0; i < config.getNodes().size(); i++) {
ProcessNode node = config.getNodes().get(i);
generateApprovalNode(bpmn, node);
// 连接下一个节点
String nextNodeId = (i < config.getNodes().size() - 1)
? config.getNodes().get(i + 1).getNodeId() : "end";
bpmn.append("<sequenceFlow id=\"flow-").append(i).append("\" ");
bpmn.append("sourceRef=\"").append(node.getNodeId()).append("\" ");
bpmn.append("targetRef=\"").append(nextNodeId).append("\">\n");
// 如果有条件分支
if (node.getCondition() != null) {
bpmn.append("<conditionExpression>${").append(node.getCondition()).append("}");
bpmn.append("</conditionExpression>\n");
}
bpmn.append("</sequenceFlow>\n");
}
// 结束事件
bpmn.append("<endEvent id=\"end\"/>\n");
bpmn.append("</process></definitions>");
return bpmn.toString();
}
private void generateApprovalNode(StringBuilder bpmn, ProcessNode node) {
bpmn.append("<userTask id=\"").append(node.getNodeId()).append("\" ");
bpmn.append("name=\"").append(node.getNodeName()).append("\">\n");
bpmn.append("<extensionElements>\n");
// 审批人配置
if ("ROLE".equals(node.getAssigneeType())) {
bpmn.append("<flowable:taskListener event=\"create\" ");
bpmn.append("class=\"com.example.listener.RoleAssignListener\">");
bpmn.append("<flowable:field name=\"role\" stringValue=\"").append(node.getAssigneeValue()).append("\"/>");
bpmn.append("</flowable:taskListener>\n");
} else if ("EXPRESSION".equals(node.getAssigneeType())) {
bpmn.append("</extensionElements>\n");
bpmn.append("<potentialOwner><resourceAssignmentExpression>");
bpmn.append("<formalExpression>${").append(node.getAssigneeValue()).append("}");
bpmn.append("</formalExpression></resourceAssignmentExpression></potentialOwner>\n");
}
bpmn.append("</extensionElements></userTask>\n");
}
}3.4 规则引擎:动态条件配置
/**
* 简单规则引擎:替代硬编码的条件判断
*/
@Service
public class RuleEngine {
/**
* 根据配置的规则计算审批人
* 规则示例:amount > 10000 → 总监审批; amount > 1000 → 经理审批
*/
public String calculateApprover(String ruleGroup, Map<String, Object> context) {
List<Rule> rules = ruleRepository.findByGroup(ruleGroup)
.stream()
.sorted(Comparator.comparingInt(Rule::getPriority))
.collect(Collectors.toList());
for (Rule rule : rules) {
if (evaluateCondition(rule.getCondition(), context)) {
return rule.getAction(); // 返回审批人或角色
}
}
return null; // 没有匹配的规则
}
/**
* 条件表达式求值
* 支持简单的比较运算:>, <, >=, <=, ==, !=, contains
*/
private boolean evaluateCondition(String condition, Map<String, Object> context) {
// 简单解析器,真实场景可以用AviatorEvaluator或Groovy
// 这里用Spring Expression Language
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext evalContext = new StandardEvaluationContext();
evalContext.setVariables(context);
try {
return Boolean.TRUE.equals(parser.parseExpression(condition).getValue(evalContext, Boolean.class));
} catch (Exception e) {
log.error("规则表达式求值失败: {}", condition, e);
return false;
}
}
}四、工程实践
4.1 低代码平台的设计边界
这是最重要的工程判断:低代码平台不能什么都做,必须有清晰的边界。
4.2 性能优化
表单和流程配置通常变化不频繁,但查询频繁,需要缓存:
@Service
public class CachedFormService {
@Autowired
private FormDefinitionRepository repo;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Cacheable(value = "formDef", key = "#formId", unless = "#result == null")
public FormDefinition getFormDefinition(String formId) {
return repo.findById(formId).orElse(null);
}
@CacheEvict(value = "formDef", key = "#formDef.formId")
public FormDefinition updateFormDefinition(FormDefinition formDef) {
return repo.save(formDef);
}
}五、踩坑实录
坑一:动态表单的版本管理
表单定义修改后,历史数据用的是旧Schema,新数据用新Schema。如果不做版本管理,查询历史数据时会出现字段对不上的问题。
解决方案:表单数据存储时记录当时的Schema版本号,展示时按版本号获取对应的Schema来渲染。
坑二:Flowable流程升级问题
Flowable不支持正在运行的流程实例迁移到新版本流程。如果修改了流程定义,已经在运行的实例还是跑旧版本,只有新发起的才用新版本。
这在实际运营中会造成混乱,需要设计流程版本管理策略,以及旧版本实例的退出机制。
坑三:复杂条件的规则引擎性能
用SpEL做规则表达式,每次计算都要解析表达式,高并发下性能不好。
解决方案:编译缓存SpEL表达式:
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
private boolean evaluateCondition(String condition, Map<String, Object> context) {
Expression expression = expressionCache.computeIfAbsent(condition,
k -> new SpelExpressionParser().parseExpression(k));
// 后续用缓存的表达式,不再重新解析
return Boolean.TRUE.equals(expression.getValue(evalContext, Boolean.class));
}坑四:流程和表单的耦合问题
表单字段变更后,流程中的条件表达式(基于字段名)可能失效。我们曾经修改了一个字段名,导致大量进行中的审批流程条件判断错误。
解决方案:表单字段重命名时,同步检查并更新所有引用了该字段的流程规则。
六、总结与个人判断
低代码平台是一个"看着简单做着难"的领域。表面上是"配置替代代码",但背后需要一套扎实的数据模型、工作流引擎、规则引擎、版本管理等基础设施。
我的经验是:不要试图做"万能"的低代码平台,专注于解决特定领域(如审批流程、数据收集)的低代码需求,比做一个"什么都能配"的平台要成功率高得多。
对于普通业务系统,在规则引擎和流程引擎层面做"低代码化",就已经能覆盖大多数"灵活配置"的需求,不需要从零构建一套完整的低代码平台。
