1
0

完成spring jpa的存储适配

This commit is contained in:
2025-01-02 16:05:15 +08:00
parent 13b9366346
commit 422e024b7b
30 changed files with 1271 additions and 14 deletions

46
flowable-core/pom.xml Normal file
View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-core</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.NoArgsConstructor;
/**
* List工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class ListHelper {
@SafeVarargs
public static <T> List<T> of(T... entries) {
return new ArrayList<>(Arrays.asList(entries));
}
public static <T> List<T> empty() {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,30 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import java.util.HashMap;
import java.util.Map;
import lombok.NoArgsConstructor;
/**
* Map工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class MapHelper {
@SuppressWarnings("unchecked")
public static <K, V> Map<K, V> of(Object... entries) {
if (entries.length % 2 != 0) {
throw new IllegalArgumentException("参数必须为偶数个");
}
Map<K, V> map = new HashMap<>(entries.length / 2 + 1);
for (int index = 0; index < entries.length; index += 2) {
map.put((K) entries[index], (V) entries[index + 1]);
}
return map;
}
public static Map<String, Object> empty() {
return new HashMap<>(0);
}
}

View File

@@ -0,0 +1,20 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import lombok.NoArgsConstructor;
/**
* String工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class StringHelper {
public static Boolean isBlank(String text) {
return text == null || text.trim().isEmpty();
}
public static Boolean equals(String text1, String text2) {
return text1 != null && text1.equals(text2);
}
}

View File

@@ -0,0 +1,156 @@
package com.lanyuanxiaoyao.flowable.core.manager;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import com.lanyuanxiaoyao.flowable.core.helper.StringHelper;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableListener;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
* 流程服务
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Slf4j
public abstract class FlowableManager {
private final FlowableRepository flowableRepository;
private final IdGenerator idGenerator;
public FlowableManager(FlowableRepository flowableRepository, IdGenerator idGenerator) {
this.flowableRepository = flowableRepository;
this.idGenerator = idGenerator;
}
public List<FlowableNode> listNodes() {
return flowableRepository.listNodes();
}
public FlowableNode getNode(String nodeId) {
return flowableRepository.getNode(nodeId);
}
public List<FlowableInstance> listInstances() {
return flowableRepository.listInstances();
}
public FlowableInstance getInstance(String instantId) {
return flowableRepository.getInstance(instantId);
}
public List<FlowableHistory> listHistories(String instanceId) {
return flowableRepository.listHistories(instanceId);
}
public FlowableHistory getHistory(String historyId) {
return flowableRepository.getHistory(historyId);
}
public void create(FlowableNode... node) {
flowableRepository.saveNode(ListHelper.of(node));
}
public String start(String nodeId) {
return start(nodeId, MapHelper.empty());
}
public String start(String nodeId, Map<String, Object> metadata) {
FlowableNode node = flowableRepository.getNode(nodeId);
FlowableInstance instance = new FlowableInstance(idGenerator.createId(), node.getNodeId(), metadata);
flowableRepository.saveInstance(instance);
if (FlowableNode.RunType.AUTOMATIC.equals(node.getRunType())) {
action(instance, node, FlowableAction.APPROVE, "系统审批通过", MapHelper.empty());
}
return instance.getInstanceId();
}
public void approve(String instanceId) {
approve(instanceId, "审批通过");
}
public void approve(String instanceId, String comment) {
action(instanceId, FlowableAction.APPROVE, comment);
}
public void reject(String instanceId) {
reject(instanceId, "审批不通过");
}
public void reject(String instanceId, String comment) {
action(instanceId, FlowableAction.REJECT, comment);
}
public void terminal(String instanceId) {
terminal(instanceId, "流程被终止");
}
public void terminal(String instanceId, String comment) {
action(instanceId, FlowableAction.TERMINAL, comment);
}
public void action(String instanceId, FlowableAction action, String comment) {
action(instanceId, action, comment, MapHelper.empty());
}
private void action(String instanceId, FlowableAction action, String comment, Map<String, Object> metadata) {
FlowableInstance instance = flowableRepository.getInstance(instanceId);
FlowableNode node = flowableRepository.getNode(instance.getCurrentNodeId());
action(instance, node, action, comment, metadata);
}
private void action(FlowableInstance instance, FlowableNode node, FlowableAction action, String comment, Map<String, Object> metadata) {
if (FlowableInstance.Status.COMPLETED.equals(instance.getStatus()) || FlowableInstance.Status.ERROR.equals(instance.getStatus())) {
throw new IllegalArgumentException("ID为" + instance.getInstanceId() + "的流程已结束,无法操作");
}
if (FlowableAction.TERMINAL.equals(action)) {
saveInstance(instance, FlowableInstance.Status.ERROR, metadata, action, comment);
return;
}
if (Objects.isNull(node.getNextNodes())
|| !node.getNextNodes().containsKey(action)
|| StringHelper.isBlank(node.getNextNodes().get(action))) {
saveInstance(instance, FlowableInstance.Status.COMPLETED, metadata, action, comment);
return;
}
String nextNodeId = node.getNextNodes().get(action);
FlowableNode nextNode = flowableRepository.getNode(nextNodeId);
instance.setCurrentNodeId(nextNode.getNodeId());
saveInstance(instance, FlowableInstance.Status.RUNNING, metadata, action, comment);
if (FlowableNode.RunType.AUTOMATIC.equals(nextNode.getRunType())) {
action(instance, node, FlowableAction.APPROVE, "系统审批通过", metadata);
}
}
private void saveInstance(FlowableInstance instance, FlowableInstance.Status status, Map<String, Object> metadata, FlowableAction action, String comment) {
instance.setStatus(status);
if (Objects.nonNull(metadata)) {
instance.addMetadata(metadata);
}
instance.setUpdatedTime(LocalDateTime.now());
FlowableHistory history = new FlowableHistory(idGenerator.createId(), instance.getInstanceId(), action, comment);
flowableRepository.saveInstanceAndHistory(instance, history);
}
private void callListeners(List<FlowableListener> listeners, Consumer<FlowableListener> handler) {
for (FlowableListener listener : listeners) {
handler.accept(listener);
}
}
public interface IdGenerator {
String createId();
}
}

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 权限校验
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableAccessor {
void access(String accessor);
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 节点操作
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public enum FlowableAction {
APPROVE,
REJECT,
TERMINAL,
}

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.flowable.core.model;
import java.time.LocalDateTime;
import lombok.Data;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableHistory {
private final String historyId;
private final String instanceId;
private final FlowableAction action;
private final String comment;
private final LocalDateTime createdTime;
public FlowableHistory(String historyId, String instanceId, FlowableAction action, String comment) {
this(historyId, instanceId, action, comment, LocalDateTime.now());
}
public FlowableHistory(String historyId, String instanceId, FlowableAction action, String comment, LocalDateTime createdTime) {
this.historyId = historyId;
this.instanceId = instanceId;
this.action = action;
this.comment = comment;
this.createdTime = createdTime;
}
}

View File

@@ -0,0 +1,48 @@
package com.lanyuanxiaoyao.flowable.core.model;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Data;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableInstance {
private final String instanceId;
private final Map<String, Object> metadata;
private final LocalDateTime createdTime;
private String currentNodeId;
private Status status;
private LocalDateTime updatedTime = LocalDateTime.now();
public FlowableInstance(String instanceId, String currentNodeId) {
this(instanceId, currentNodeId, MapHelper.empty(), Status.RUNNING, LocalDateTime.now(), LocalDateTime.now());
}
public FlowableInstance(String instanceId, String currentNodeId, Map<String, Object> metadata) {
this(instanceId, currentNodeId, metadata, Status.RUNNING, LocalDateTime.now(), LocalDateTime.now());
}
public FlowableInstance(String instanceId, String currentNodeId, Map<String, Object> metadata, Status status, LocalDateTime createdTime, LocalDateTime updatedTime) {
this.instanceId = instanceId;
this.metadata = metadata;
this.createdTime = createdTime;
this.currentNodeId = currentNodeId;
this.status = status;
this.updatedTime = updatedTime;
}
public void addMetadata(Map<String, Object> metadata) {
this.metadata.putAll(metadata);
}
public enum Status {
RUNNING,
COMPLETED,
ERROR,
}
}

View File

@@ -0,0 +1,19 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 节点监听
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableListener {
void onStart(FlowableInstance instance);
void onError(FlowableInstance instance, Throwable throwable);
void onComplete(FlowableInstance instance);
void onApprove(FlowableInstance instance);
void onReject(FlowableInstance instance);
}

View File

@@ -0,0 +1,45 @@
package com.lanyuanxiaoyao.flowable.core.model;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* 流程节点
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableNode {
private final String nodeId;
private final String name;
private final String description;
private final RunType runType;
private final Map<FlowableAction, String> nextNodes;
private final List<Class<? extends FlowableListener>> listeners;
private final LocalDateTime createdTime;
private LocalDateTime updatedTime = LocalDateTime.now();
public FlowableNode(String nodeId, String name, String description, RunType runType, Map<FlowableAction, String> nextNodes) {
this(nodeId, name, description, runType, nextNodes, ListHelper.empty(), LocalDateTime.now());
}
public FlowableNode(String nodeId, String name, String description, RunType runType, Map<FlowableAction, String> nextNodes, List<Class<? extends FlowableListener>> listeners, LocalDateTime createdTime) {
this.nodeId = nodeId;
this.name = name;
this.description = description;
this.runType = runType;
this.nextNodes = nextNodes;
this.listeners = listeners;
this.createdTime = createdTime;
}
public enum RunType {
AUTOMATIC,
MANUAL,
}
}

View File

@@ -0,0 +1,39 @@
package com.lanyuanxiaoyao.flowable.core.repository;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.List;
/**
* 存储统一管理
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableRepository {
void saveNode(FlowableNode node);
void saveNode(List<FlowableNode> nodes);
FlowableNode getNode(String nodeId);
List<FlowableNode> listNodes();
void saveInstance(FlowableInstance instance);
FlowableInstance getInstance(String instantId);
List<FlowableInstance> listInstances();
void saveHistory(FlowableHistory history);
FlowableHistory getHistory(String historyId);
List<FlowableHistory> listHistories(String instanceId);
/**
* 同时保存实例和历史记录,单独设立这里一个方法是方便数据库使用事务重写
*/
void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history);
}

View File

@@ -0,0 +1,123 @@
package com.lanyuanxiaoyao.flowable.core.test;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.Map;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* 集成测试
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Slf4j
public abstract class TestFlowableManager {
protected abstract FlowableManager flowableManager();
private FlowableNode createManualNode() {
return createManualNode(UUID.randomUUID().toString(), null);
}
private FlowableNode createManualNode(String nodeId) {
return createManualNode(nodeId, null);
}
private FlowableNode createManualNode(String nodeId, Map<FlowableAction, String> nextNodes) {
return new FlowableNode(
nodeId,
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
FlowableNode.RunType.MANUAL,
nextNodes
);
}
/**
* 单节点审批
*/
@Test
public void testSingleNode() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode();
manager.create(node1);
Assertions.assertNotNull(manager.getNode(node1.getNodeId()));
String instanceId = manager.start(node1.getNodeId());
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertTrue(manager.listHistories(instanceId).isEmpty());
manager.approve(instanceId, "3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f");
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(1, manager.listHistories(instanceId).size());
Assertions.assertEquals(FlowableAction.APPROVE, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f", manager.listHistories(instanceId).get(0).getComment());
instanceId = manager.start(node1.getNodeId());
manager.reject(instanceId, "3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f");
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(FlowableAction.REJECT, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f", manager.listHistories(instanceId).get(0).getComment());
}
@Test
public void testMultiNode() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode(
"02779cbe-0d82-4e09-9bf8-60885400d100",
MapHelper.of(
FlowableAction.APPROVE, "1e126640-34ae-40f9-b55f-9cb8099d638f",
FlowableAction.REJECT, "02779cbe-0d82-4e09-9bf8-60885400d100"
)
);
FlowableNode node2 = createManualNode(
"1e126640-34ae-40f9-b55f-9cb8099d638f",
MapHelper.of(FlowableAction.REJECT, "02779cbe-0d82-4e09-9bf8-60885400d100")
);
manager.create(node1, node2);
String instanceId = manager.start(node1.getNodeId());
manager.reject(instanceId);
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node1.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
manager.approve(instanceId);
manager.reject(instanceId, "我觉得不行");
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node1.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
manager.approve(instanceId);
manager.approve(instanceId);
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node2.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
Assertions.assertEquals(FlowableAction.APPROVE, manager.listHistories(instanceId).get(4).getAction());
Assertions.assertEquals(5, manager.listHistories(instanceId).size());
}
@Test
public void testTerminal() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode();
manager.create(node1);
String instanceId = manager.start(node1.getNodeId());
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
manager.terminal(instanceId, "d896b642-a1d8-499c-92e7-bed63581f2f8");
Assertions.assertEquals(FlowableInstance.Status.ERROR, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(1, manager.listHistories(instanceId).size());
Assertions.assertEquals(FlowableAction.TERMINAL, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("d896b642-a1d8-499c-92e7-bed63581f2f8", manager.listHistories(instanceId).get(0).getComment());
Assertions.assertThrows(IllegalArgumentException.class, () -> manager.approve(instanceId));
}
}

View File

@@ -0,0 +1,90 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import com.lanyuanxiaoyao.flowable.core.helper.StringHelper;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 内存存储
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public class InMemoryFlowableRepository implements FlowableRepository {
private static final Map<String, FlowableNode> nodes = new HashMap<>();
private static final Map<String, FlowableInstance> instances = new HashMap<>();
private static final Map<String, List<FlowableHistory>> histories = new HashMap<>();
@Override
public void saveNode(FlowableNode node) {
nodes.put(node.getNodeId(), node);
}
@Override
public void saveNode(List<FlowableNode> nodes) {
for (FlowableNode node : nodes) {
saveNode(node);
}
}
@Override
public FlowableNode getNode(String nodeId) {
return nodes.get(nodeId);
}
@Override
public List<FlowableNode> listNodes() {
return new ArrayList<>(nodes.values());
}
@Override
public void saveInstance(FlowableInstance instance) {
instances.put(instance.getInstanceId(), instance);
}
@Override
public FlowableInstance getInstance(String instantId) {
return instances.getOrDefault(instantId, null);
}
@Override
public List<FlowableInstance> listInstances() {
return new ArrayList<>(instances.values());
}
@Override
public void saveHistory(FlowableHistory history) {
String instanceId = history.getInstanceId();
if (!histories.containsKey(instanceId)) {
histories.put(instanceId, new ArrayList<>());
}
histories.get(instanceId).add(history);
}
@Override
public FlowableHistory getHistory(String historyId) {
return histories.values()
.stream()
.flatMap(List::stream)
.filter(history -> StringHelper.equals(history.getHistoryId(), historyId))
.findFirst()
.orElse(null);
}
@Override
public List<FlowableHistory> listHistories(String instanceId) {
return histories.getOrDefault(instanceId, ListHelper.empty());
}
@Override
public void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history) {
saveInstance(instance);
saveHistory(history);
}
}

View File

@@ -0,0 +1,14 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import java.util.UUID;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
public class SimpleFlowableManager extends FlowableManager {
public SimpleFlowableManager() {
super(new InMemoryFlowableRepository(), () -> UUID.randomUUID().toString());
}
}

View File

@@ -0,0 +1,15 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.test.TestFlowableManager;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
public class TestSimpleFlowableManager extends TestFlowableManager {
@Override
protected FlowableManager flowableManager() {
return new SimpleFlowableManager();
}
}