image-WX20230926

假设要实现以上 学生信息查询流程 中的逻辑:

    🟢 流程开始后分别查询学生基础信息、敏感信息,再将两种信息进行组合装配

    🟢 如果需要展示分数列表的话,流程开始时还会查询学生学年列表,再通过学年列表分别查询班级和分数信息并组合装配

    🟢 最后将学生信息和分数信息组合装配并返回结果

# 1.1 配置引入

<dependency>
    <groupId>cn.kstry.framework</groupId>
    <artifactId>kstry-core</artifactId>
    <version>最新版本</version>
</dependency>
1
2
3
4
5

查看最新版本 (opens new window)

# 1.2 项目引入

TIP

Kstry框架与Spring容器有较为密切的关联和依赖,当前版本暂时只能在Spring环境中运行


@SpringBootApplication
@EnableKstry(bpmnPath = "./bpmn/*.bpmn,./bpmn/*.json")
public class KstryFluxDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(KstryFluxDemoApplication.class, args);
    }
}

1
2
3
4
5
6
7
8
9
10

@EnableKstry注解代表了要启动Kstry容器

    🟢 bpmnPath: 指定bpmn文件位置,多个路径可以通过,隔开。支持bpmn、json两种流程配置文件格式

# 1.3 业务实体定义

// 学生基础信息
@Data
public class StudentBasic {

    private Long id;

    private String name;

    private String address;
}

// 学生敏感信息
@Data
public class StudentPrivacy {

    private Long id;

    private String idCard;

    private String birthday;
}

// 学生全量信息
@Data
public class Student {

    private Long id;

    private String name;

    private String address;

    private String idCard;

    private String birthday;
}

// 学年信息
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StudyExperience {

    private Long studentId;

    private Long classId;

    private String studyYear;
}

// 分数信息
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ScoreInfo {

    private int score;

    private Long studentId;

    private String studyYear;

    private String course;

    private Long classId;

    private ClassInfo classInfo;
}

// 班级信息
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ClassInfo {

    private Long id;

    private String name;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

Entity Definition (opens new window)

# 1.4 定义出入参及数据传递类

// 入参
@Data
@FieldNameConstants(innerTypeName = "F")
public class QueryScoreRequest {

    private Long studentId;

    private boolean needScore;
}

// 出参
@Data
@FieldNameConstants(innerTypeName = "F")
public class QueryScoreResponse {

    private Student student;

    private List<ScoreInfo> scoreInfos;
}

// 对象形式定义var数据域
@Data
@FieldNameConstants(innerTypeName = "F")
public class QueryScoreVarScope implements ScopeData {

    private StudentBasic studentBasic;

    private StudentPrivacy studentPrivacy;

    private String student;

    private List<StudyExperience> studyExperienceList;

    private List<Long> classIds;

    private List<ClassInfo> classInfos;

    private List<ScoreInfo> scoreInfos;

    @Override
    public ScopeTypeEnum getScopeDataEnum() {
        return ScopeTypeEnum.VARIABLE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

Facade Definition (opens new window)

    🟢 QueryScoreRequestQueryScoreResponse分别为流程执行的出入参

    🟢 @FieldNameConstants(innerTypeName = "F")是lombok工具注解,可以读取类中的字段并自动生成与字段名相同的类常量。比如QueryScoreRequest中的studentId变量,因为加了此注解,QueryScoreRequest类中会多出一个名为F的类,里面会定义public static String studentId = "studentId";常量字段

    🟢 Kstry框架中有req、var、sta、res四个数据域

        🔷 req:保存请求传入的request对象,例子中便是指QueryScoreRequest

        🔷 var:保存节点执行完成后产生的变量。QueryScoreVarScope实现ScopeData接口,重写getScopeDataEnum()方法返回ScopeTypeEnum.VARIABLE。是以对象的形式定义了var数据域

        🔷 sta:和var一样也是保存节点执行完成后产生的变量。只是保存后的结果不可变。可以理解成var是HasMap,sta是ImmutableMap

        🔷 res:保存请求传入的response对象,例子中便是指QueryScoreResponse

# 1.5 编写服务组件

@TaskComponent
public class StudentScoreService {

    /**
     * 注册name为:getStudentBasic 描述信息为:查询学生基本信息 的服务节点方法
     *
     * @param id 从req域中获取变量名为studentId的值,并赋值给方法入参id
     * @return 将方法返回结果通知到var域的studentBasic变量上
     */
    @TaskService(desc = "查询学生基本信息")
    @NoticeVar(target = QueryScoreVarScope.F.studentBasic)
    public StudentBasic getStudentBasic(@ReqTaskParam(QueryScoreRequest.F.studentId) Long id) {
        // mock return result
        StudentBasic studentBasic = new StudentBasic();
        studentBasic.setId(id);
        studentBasic.setAddress("XX省XX市XX区");
        studentBasic.setName("张一");
        return studentBasic;
    }

    @TaskService(desc = "查询学生敏感信息")
    @NoticeVar(target = QueryScoreVarScope.F.studentPrivacy)
    public StudentPrivacy getStudentPrivacy(@ReqTaskParam(QueryScoreRequest.F.studentId) Long id) {
        // mock return result
        StudentPrivacy studentPrivacy = new StudentPrivacy();
        studentPrivacy.setId(id);
        studentPrivacy.setBirthday("1994-01-01");
        studentPrivacy.setIdCard("133133199401012345");
        return studentPrivacy;
    }

    @TaskService(desc = "装配学生信息")
    @NoticeVar(target = QueryScoreVarScope.F.student)
    public Student assembleStudentInfo(@VarTaskParam(QueryScoreVarScope.F.studentBasic) StudentBasic studentBasic,
                                       @VarTaskParam(QueryScoreVarScope.F.studentPrivacy) StudentPrivacy studentPrivacy) {
        Student student = new Student();
        BeanUtils.copyProperties(studentBasic, student);
        BeanUtils.copyProperties(studentPrivacy, student);
        return student;
    }

    @TaskService(desc = "获取学生学年列表")
    @NoticeVar(target = QueryScoreVarScope.F.studyExperienceList)
    public List<StudyExperience> getStudyExperienceList(@ReqTaskParam(QueryScoreRequest.F.studentId) Long id) {
        // mock return result
        return Lists.newArrayList(
                StudyExperience.builder().studentId(id).classId(1L).studyYear("2013-1-2").build(),
                StudyExperience.builder().studentId(id).classId(2L).studyYear("2014-1-2").build(),
                StudyExperience.builder().studentId(id).classId(3L).studyYear("2015-1-2").build()
        );
    }

    /**
     * 服务节点可以定义指定对数据域中的某个集合变量进行遍历执行
     *      sourceScope = ScopeTypeEnum.VARIABLE  指要对var域的变量进行遍历
     *      source = QueryScoreVarScope.F.classIds 指对var域中名为classIds的集合变量进行遍历
     *      async = true        指开启并发遍历模式
     *      strategy = IterateStrategyEnum.BEST_SUCCESS     指使用best策略遍历,best策略可以做到迭代集合中的全部项,执行失败会被忽略,将会尽量多的拿到成功项
     *
     * @param classIdItem  IterDataItem 类是框架内部类型,直接放到服务节点方法入参上,用来接收遍历项信息
     * @return @NoticeVar(target = QueryScoreVarScope.F.classInfos) 指定要将返回结果通知到var域中的classInfos变量上,classInfos变量是List类型,框架会自动将所有单个元素组合成List返回给变量
     */
    @TaskService(desc = "查询班级信息",
            iterator = @Iterator(sourceScope = ScopeTypeEnum.VARIABLE, source = QueryScoreVarScope.F.classIds, async = true, strategy = IterateStrategyEnum.BEST_SUCCESS)
    )
    @NoticeVar(target = QueryScoreVarScope.F.classInfos)
    public ClassInfo getClasInfoById(IterDataItem<Long> classIdItem) {
        // mock return result
        Optional<Long> idOptional = classIdItem.getData();
        ClassInfo classInfo = new ClassInfo();
        classInfo.setId(idOptional.orElse(null));
        classInfo.setName("班级" + idOptional.orElse(null));
        return classInfo;
    }

    @TaskService(desc = "查询学生分数列表")
    @NoticeVar(target = QueryScoreVarScope.F.scoreInfos)
    public List<ScoreInfo> getStudentScoreList(@VarTaskParam(QueryScoreVarScope.F.studyExperienceList) List<StudyExperience> studyExperienceList) {
        // mock return result
        return studyExperienceList.stream().flatMap(se -> {
            List<ScoreInfo> scoreInfos = Lists.newArrayList(
                    ScoreInfo.builder().studentId(se.getStudentId()).classId(se.getClassId()).studyYear(se.getStudyYear()).course("语文").score(99).build(),
                    ScoreInfo.builder().studentId(se.getStudentId()).classId(se.getClassId()).studyYear(se.getStudyYear()).course("数学").score(88).build()
            );
            return scoreInfos.stream();
        }).collect(Collectors.toList());
    }

    @TaskService(desc = "组装成绩及班级信息")
    public void assembleScoreClassInfo(@VarTaskParam(QueryScoreVarScope.F.scoreInfos) List<ScoreInfo> scoreInfos,
                                       @VarTaskParam(QueryScoreVarScope.F.classInfos) List<ClassInfo> classInfos) {
        Map<Long, ClassInfo> classInfoMap = classInfos.stream().collect(Collectors.toMap(ClassInfo::getId, Function.identity(), (x, y) -> y));
        scoreInfos.forEach(scoreInfo -> {
            ClassInfo classInfo = classInfoMap.get(scoreInfo.getClassId());
            if (classInfo == null) {
                return;
            }
            scoreInfo.setClassInfo(classInfo);
        });
    }

    @NoticeResult
    @TaskService(desc = "各维度信息聚合")
    public QueryScoreResponse getQueryScoreResponse(@VarTaskParam(QueryScoreVarScope.F.student) Student student,
                                                    @VarTaskParam(QueryScoreVarScope.F.scoreInfos) List<ScoreInfo> scoreInfos) {
        QueryScoreResponse response = new QueryScoreResponse();
        response.setStudent(student);
        response.setScoreInfos(scoreInfos);
        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

StudentScoreService (opens new window)

@TaskComponent作用:

    🟢 与 Spring 容器中@Component注解有着相同的作用,将被标注的类组件托管至Spring容器中

    🟢 指定该类成为了 Kstry 容器可解析的任务组件,可在其中定义服务节点(被@TaskService注解修饰的方法称为服务节点

        🔷 name:指定组件名称,与 bpmn 流程配置文件中task-component属性对应,默认为首字母小写的类名

@TaskService作用:

    🟢 指定该方法是Kstry容器中的服务节点,也是在bpmn配置文件中可编排的最小执行单元,对应于bpmn配置文件中的bpmn:serviceTask节点

    🟢 该注解只能定义在Kstry容器可解析的任务组件类中,否则将不被解析

        🔷 name:指定该服务节点的名称,与 bpmn 配置文件中的task-service属性进行匹配对应,默认为方法名

        🔷 desc:节点描述信息。流程配置中未指定节点名称时,尝试获取@TaskService注解的desc属性作为name

        🔷 iterator:指定遍历信息,可以参考上面代码中的注释,也可跳转至后面的文档了解资源遍历功能详情

@NoticeResult作用:

    🟢 指定执行结果将被通知到StoryBus中的哪些作用域中,@NoticeResult说明,该方法执行结果将被通知到result域,最终作为Story的执行结果返回给调用方

@VarTaskParam作用:

    🟢 @VarTaskParam标注在服务节点的方法参数上,用来从StoryBus的var域获取变量值,并直接赋值给被标注的参数项

@ReqTaskParam作用:

    🟢 @ReqTaskParam标注在服务节点的方法参数上,用来从StoryBus的req域获取变量值,并直接赋值给被标注的参数项

        🔷 reqSelf:只有从req域获取参数时才有这个属性,该属性代表将客户端传入的request对象直接赋值给被标注的参数项

# 1.6 定义服务流程

    框架支持BPMN、JSON、代码三种文件格式的流程定义。其中BPMN、JSON两种格式可以通过文件放入resources目录、项目启动时解析注册、程序运行时动态修改三种方式来注册使用。三种注册方式后面文档会详细介绍,当前将分别介绍如何通过三种不同的文件格式来定义上述流程。目前来讲,BPMN、JSON两种文件定义流程时,只需知道将流程配置文件放到resources目录并配置@EnableKstry(bpmnPath = "./bpmn/*.bpmn,./bpmn/*.json")使启动类可以解析到即可。

# 1.6.1 BPMN格式定义流程

    业务流程建模符号BPMN(Business Process Modeling Notation)是BPMI(The Business Process Management Initiative)开发的一套可视化流程建模标准(BPMN)。其主要目标是提供一些被所有业务用户容易理解的符号,拼装组合成流程图,进而使相关方可以简单直观的从流程的轮廓分析到这些流程的业务实现。Kstry框架支持了部分BPMN符号语法,可以极为方便的完成系统中复杂业务的可视化流程定义。并提供了流程配置控制台:配置台地址 (opens new window)

image-WX20230926

student-score-query-process (opens new window)

    在配置台中定义如上流程(流程文件地址 (opens new window)),如下所示可将定义好的流程复制到粘贴板。之后在项目的resources/bpmn/路径下创建扩展名为.bpmn的文件并将粘贴板的内容复制进文件内。比如定义文件名:student-score-query-process.bpmn

image-WX20230926

    上面的流程可以看出,BPMN文件虽然可以展示流程图但并非是图片文件,而是文本文件。而在文本文件中也是多由XML文件的格式来表示。流程文件被定义出来之后便可以通过引擎解析执行了。下面是介绍上述流程图中的一些配置细节。

image-WX20230926

    上图展示了定义 查询学生基本信息 服务节点的配置。task-component的值与服务组件名即代码中的类名相对应(name属性为空时默认取的是类名首字母小写),task-service的值与服务节点方法即代码中的方法名相对应。当task-service的值在当前应用中全局唯一时,task-component属性值允许为空。如此配置便可以将流程配置文件中的服务节点与代码中的方法进行关联。流程执行时每执行到某个节点时实际在代码中会调用与之相对应的方法。

image-WX20230926

    当需要同时执行多个耗时操作并且操作之间没有前后依赖关系时,可采用并发执行的手段来加快拿到响应结果的时间。框架支持在包含网关、并行网关两个网关上增加open-async=true配置的方式开启后面多流程执行的并发。可见从串行变为并行的升级成本是极低的。

image-WX20230926

    上述查询学生信息的例子中仅需学生成绩信息时才需要下半部分的查询动作。框架支持在流程线段上定义条件表达式来判断和控制后续的流程是否需要执行。表达式通过SpringSpEL引擎解析执行。不仅支持类似var.student.age>20这种普通表达式判断,还支持@notBlank(var.student.name)这种常用函数,甚至在给定函数不满足业务诉求是还支持自定义函数来丰富条件表达式的判断,需要了解自定义函数细节可跳转至下面流程编排部分。

image-WX20230926

    上述流程中有一个细节,获取学生学年列表 服务节点方法返回通知到var数据域的是一个List<StudyExperience> studyExperienceList变量值。而在后续 查询班级信息 服务节点方法中遍历的是var.classIds。那么var.classIds这个值是从哪里来的呢,这里就用到了框架提供的指令能力。
    内部实现中指令其实就是服务节点,之所以以指令的形式存在是为了方便框架用户抽象一些公用方法,在核心服务方法执行前后做一些所需的数据处理。比如上述例子中studyExperienceListclassIds的转化就用到了框架自带的c-jscript指令。该指令的功能是通过解析执行流程定义中的js脚本来操作各域中的数据。下面是具体的使用方式:

// task-property属性设置
{
    "result-converter": "object_to_long_list",
    "return-target": "var.classIds"
}
1
2
3
4
5
// c-jscript指令Content
var classIds = [];
for (var i = 0; i< kvar.studyExperienceList.length; i++)
{
    classIds.push(kvar.studyExperienceList[i].classId);
}
return JSON.stringify(classIds)

1
2
3
4
5
6
7
8

    在 获取学生学年列表 服务节点执行完成之后,因为配置了c-jscript后置指令所以会进入到指令中执行。指令脚本会从studyExperienceList中依次取出classId放到新定义的classIds中并序列化成String返回。又因为task-property中指定了object_to_long_list类型转换器,将String类型转换成为List<Long>类型,最终将转换后的结果通知到var域的classIds变量中。这里只是简单介绍下各个功能点的使用场景,对于task-property属性作用、指令、类型转换器等可在后续文档中看到详细介绍。

# 1.6.2 JSON格式定义流程

    JSON格式是一种极简的流程定义方式。与BPMN格式相比优势在于小文件时简单易懂、容易转换生成、占用空间小易于传输等。而略势也很明显,最重要的便是彻底失去了可视化的能力,流程节点多了之后阅读起来将会非常困难。那么框架支持JSON格式流程定义的意义是什么呢?

    支持JSON格式流程定义的最重要的原因就是其容易转换生成的特点。可视化流程配置工具层出不穷,各种开源也是争奇斗艳。BPMN格式仅仅是这众多工具中的一种,因为历史遗留、公司要求、接入成本等众多因素影响,不可能要求所有使用者都将BPMN作为流程配置的首选。为了满足更多用户的不同诉求,就需要一种中间协议。可以将各种流程配置文件转换成这种中间协议,以供框架来理解和执行,无疑JSON格式是一个很好的选择。有了这层协议无论使用者使用了何种可视化流程配置工具,都可以借此被Kstry框架所理解和执行。

    下面是对上述 学生信息查询流程 的JSON格式定义描述:

[{
  "process-id": "Process_1693841776073",
  "start-id": "student-score-query-json-process",
  "process-nodes": [{
    "id": "student-score-query-json-process",
    "name": "student-score-query-json-process",
    "next-nodes": ["Gateway_0moclik"],
    "type": "start_event"
  }, {
    "id": "Gateway_0moclik",
    "next-nodes": ["Activity_1tuuink", "Activity_1lzcrrr", "Flow_1jmfkgr"],
    "properties": {
      "open-async": true // 开启并发
    },
    "type": "inclusive_gateway"
  }, {
    "id": "Activity_1tuuink",
    "name": "查询学生\n基本信息",
    "next-nodes": ["Gateway_0avnapl"],
    "properties": {
      "task-component": "studentScoreService",
      "task-service": "getStudentBasic"
    },
    "type": "service_task"
  }, {
    "id": "Activity_1lzcrrr",
    "name": "查询学生\n敏感信息",
    "next-nodes": ["Gateway_0avnapl"],
    "properties": {
      "task-component": "studentScoreService",
      "task-service": "getStudentPrivacy"
    },
    "type": "service_task"
  }, {
    "id": "Gateway_0avnapl",
    "next-nodes": ["Activity_1x55mgq"],
    "properties": {},
    "type": "inclusive_gateway"
  }, {
    "id": "Activity_1x55mgq",
    "name": "装配学生信息",
    "next-nodes": ["Gateway_02qlo7f"],
    "properties": {
      "task-component": "studentScoreService",
      "task-service": "assembleStudentInfo"
    },
    "type": "service_task"
  }, {
    "flow-expression": "req.needScore", // 流程线段上定义条件表达式
    "id": "Flow_1jmfkgr",
    "name": "if(need-score-list)",
    "next-nodes": ["Activity_0q5ad88"],
    "type": "sequence_flow"
  }, {
    "id": "Activity_0q5ad88",
    "name": "获取学生\n学年列表",
    "next-nodes": ["Gateway_0qmo5am"],
    "properties": {
      "task-service": "getStudyExperienceList", // 使用指令
      "c-jscript": "var classIds = [];\nfor (var i = 0; i< kvar.studyExperienceList.length; i++)\n{\n    classIds.push(kvar.studyExperienceList[i].classId);\n}\nreturn JSON.stringify(classIds)",
      "task-property": "{\n    \"result-converter\": \"object_to_long_list\",\n    \"return-target\": \"var.classIds\"\n}"
    },
    "type": "service_task"
  }, {
    "id": "Gateway_0qmo5am",
    "next-nodes": ["Activity_1h1otxv", "Activity_1v1y60m"],
    "properties": {
      "open-async": true // 开启并发
    },
    "type": "inclusive_gateway"
  }, {
    "id": "Activity_1h1otxv",
    "name": "查询班级信息",
    "next-nodes": ["Gateway_0t2t6ox"],
    "properties": {
      "task-service": "getClasInfoById"
    },
    "type": "service_task"
  }, {
    "id": "Activity_1v1y60m",
    "name": "查询学生\n分数列表",
    "next-nodes": ["Gateway_0t2t6ox"],
    "properties": {
      "task-service": "getStudentScoreList"
    },
    "type": "service_task"
  }, {
    "id": "Gateway_0t2t6ox",
    "next-nodes": ["Activity_0itm6m7"],
    "properties": {
      "strict-mode": false // 指定并行网关为非严格模式
    },
    "type": "parallel_gateway"
  }, {
    "id": "Activity_0itm6m7",
    "name": "组装历年成绩列表",
    "next-nodes": ["Gateway_02qlo7f"],
    "properties": {
      "task-service": "assembleScoreClassInfo"
    },
    "type": "service_task"
  }, {
    "id": "Gateway_02qlo7f",
    "next-nodes": ["Activity_0n1ystm"],
    "properties": {},
    "type": "inclusive_gateway"
  }, {
    "id": "Activity_0n1ystm",
    "name": "各维度信息聚合",
    "next-nodes": ["2e5a5716e4404ca6843abffb807e23b3"],
    "properties": {
      "task-service": "getQueryScoreResponse"
    },
    "type": "service_task"
  }, {
    "id": "2e5a5716e4404ca6843abffb807e23b3",
    "next-nodes": [],
    "type": "end_event"
  }]
}]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

student-score-query-process (opens new window)

# 1.6.3 代码方式定义流程

    无论是BPMN格式还是JSON格式,在代码开发工具比如eclipse、IDEA中如果没有很好的插件支持,配置文件与代码便是割裂的。使用两种格式的配置文件时,在开发工具中各个服务节点方法之间将不存在任何关联和引用。这样会导致一些问题,比如仅仅只看代码是不知道业务如何实现的、开发过程中不能像传统方法一样通过快捷键在相互引用的方法之间快速跳转等,由此框架还支持了代码方式定义流程。

    可以自定义方法返回ProcessLink或者SubProcessLink来定义流程和子流程。方法上标注@Bean注解将流程对象放入Spring容器中即可在执行时生效。当流程复杂不易阅读时还可以通过抽离子流程的方式将整个大流程分层成不同的子流程来帮助规划和理解。

    服务节点方法可以作为lambda表达式的形式参与进整个流程的组装,解决了各个服务节点方法之间不存在任何关联和引用的问题,可以在开发工具中使用快捷键快速跳转。熟悉代码方式的流程定义后,阅读整个流程时虽然比不上BPMN可视化那样简洁明了,但理解起来也一定比没有任何模块划分一气呵成的代码方便不少。

    另外无论是BPMN格式文件还是JSON格式文件,最终都是用代码流程定义的方式来解析生效的。所以使用其他协议的流程配置工具时也可以跳过JSON格式的转化,直接用代码方式解析生成流程。

    下面是对上述 学生信息查询流程 的代码方式定义描述:

@Bean
public ProcessLink studentScoreQueryProcess() {
    String instructContent = "var classIds = [];"
            + "for (var i = 0; i< kvar.studyExperienceList.length; i++)"
            + "{"
            + "    classIds.push(kvar.studyExperienceList[i].classId);"
            + "}"
            + "return JSON.stringify(classIds)";
    StartProcessLink processLink = StartProcessLink.build(DefProcess::studentScoreQueryProcess);
    InclusiveJoinPoint asyncInclusive = processLink.nextInclusive(processLink.inclusive().openAsync().build());
    InclusiveJoinPoint asyncInclusive2 = asyncInclusive
            .nextService(Exp.b(e -> e.isTrue(ScopeTypeEnum.REQUEST, QueryScoreRequest.F.needScore)), StudentScoreService::getStudyExperienceList).build()
            .nextInstruct("jscript", instructContent).name("JS脚本").property("{\"result-converter\": \"object_to_long_list\",\"return-target\": \"var.classIds\"}").build()
            .nextInclusive(processLink.inclusive().openAsync().build());

    processLink.inclusive().build().joinLinks(
                    processLink.parallel().notStrictMode().build().joinLinks(
                            asyncInclusive2.nextService(StudentScoreService::getClasInfoById).build(),
                            asyncInclusive2.nextService(StudentScoreService::getStudentScoreList).build()
                    ).nextService(StudentScoreService::assembleScoreClassInfo).build(),
                    processLink.inclusive().build().joinLinks(
                            asyncInclusive.nextService(StudentScoreService::getStudentBasic).build(),
                            asyncInclusive.nextService(StudentScoreService::getStudentPrivacy).build()
                    ).nextService(StudentScoreService::assembleStudentInfo).build()
            )
            .nextService(StudentScoreService::getQueryScoreResponse).build()
            .end();
    return processLink;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

studentScoreQueryProcess (opens new window)

# 1.7 调用执行

    框架的执行依托于StoryEngine对象,该对象会随着框架初始化进程被托管至Spring容器中。所以使用起来会很方便,只需按照普通Spring Bean的方式注入即可。StoryEngine对象有三个方法,firefireAsyncserialize所实现功能分别是同步执行流程、异步执行流程、将容器中的流程序列化成JSON。

StudentController (opens new window)

# 1.7.1 同步方式调用执行

@RestController
@RequestMapping("/student")
public class StudentController {

    @Resource
    private StoryEngine storyEngine;

    @PostMapping("/query")
    public R<QueryScoreResponse> studentQuery() {
        QueryScoreRequest request = new QueryScoreRequest();
        request.setStudentId(77L);
        request.setNeedScore(true);
        StoryRequest<QueryScoreResponse> fireRequest = ReqBuilder
                .returnType(QueryScoreResponse.class) // 指定返回类型
                .recallStoryHook(WebUtil::recallStoryHook) // 流程结束的回溯
                .trackingType(TrackingTypeEnum.SERVICE_DETAIL) // 指定监控类型
                .request(request) // 指定req域参数
                .varScopeData(new QueryScoreVarScope()) // 指定var域数据载体,可不指定使用默认值
                .startId("student-score-query-process") // 指定开始事件ID
                .build();
        TaskResponse<QueryScoreResponse> result = storyEngine.fire(fireRequest);
        return result.isSuccess() ? R.success(result.getResult()) : R.error(NumberUtils.toInt(result.getResultCode(), -1), result.getResultDesc());
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class R<T> {

    private boolean success;

    private Integer code;

    private String msg;

    private T data;

    public static <D> R<D> success(D data) {
        R<D> res = new R<>();
        res.setSuccess(true);
        res.setCode(0);
        res.setMsg("success");
        res.setData(data);
        return res;
    }

    public static <D> R<D> error(int code, String desc) {
        R<D> res = new R<>();
        res.setCode(code);
        res.setMsg(desc);
        res.setSuccess(false);
        return res;
    }
}

@Slf4j
public class WebUtil {
    public static void recallStoryHook(RecallStory recallStory) {
        MonitorTracking monitorTracking = recallStory.getMonitorTracking();
        List<NodeTracking> storyTracking = monitorTracking.getStoryTracking();
        List<String> collect = storyTracking.stream().map(nt -> GlobalUtil.format("{}({}ms)", nt.getNodeName(), nt.getSpendTime())).collect(Collectors.toList());
        log.info("Story startId: {}, service node spend list: {}", recallStory.getStartId(), String.join(",", collect));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

    StoryRequest是执行流程的入参,包含了全部所需的参数。样例中recallStoryHook的作用是在流程执行完之后无论成功或是失败都会进行一次回调,回调函数使用RecallStory作为入参,可获取:各个作用域变量、角色、最终返回结果、链路追踪器等。在上述样例中指定了WebUtil::recallStoryHook作用是打印各个服务节点的执行耗时。比如:

Story startId: student-score-query-process, service node spend list: 查询学生敏感信息(0ms),查询学生基本信息(0ms),获取学生学年列表(0ms),JS脚本(12ms),装配学生信息(0ms),查询学生分数列表(0ms),查询班级信息(0ms),组装历年成绩列表(0ms),各维度信息聚合(0ms)
1

    另一个需要简单介绍下的是trackingType用来指定链路追踪级别。SERVICE_DETAIL的含义是打印各个服务节点执行时的细节详情。比如下面是 装配学生信息 服务节点的执行信息

{
  "endTime": "2023-09-30T17:42:53.752762000",
  "index": 16,
  "methodName": "assembleStudentInfo",
  "nodeId": "Activity_1x55mgq-12",
  "nodeName": "装配学生信息",
  "nodeType": "SERVICE_TASK",
  "noticeTracking": [{    // 通知到var域的数据信息
    "converter": "BASIC_CONVERTER", // 使用到的类型转换器名称
    "scopeType": "VARIABLE",
    "sourceName": "student",
    "targetName": "student",
    "targetType": "cn.kstry.flux.demo.facade.student.QueryScoreVarScope",
    "value": "{\"address\":\"XX省XX市XX区\",\"birthday\":\"1994-01-01\",\"id\":77,\"idCard\":\"133133199401012345\",\"name\":\"张一\"}"
  }],
  "paramTracking": [{   // 服务节点方法执行入参信息
    "scopeType": "VARIABLE",
    "sourceName": "studentBasic",
    "targetName": "studentBasic",
    "targetType": "cn.kstry.flux.demo.dto.student.StudentBasic",
    "value": "{\"address\":\"XX省XX市XX区\",\"id\":77,\"name\":\"张一\"}"
  }, {
    "scopeType": "VARIABLE",
    "sourceName": "studentPrivacy",
    "targetName": "studentPrivacy",
    "targetType": "cn.kstry.flux.demo.dto.student.StudentPrivacy",
    "value": "{\"birthday\":\"1994-01-01\",\"id\":77,\"idCard\":\"133133199401012345\"}"
  }],
  "spendTime": 0,
  "startTime": "2023-09-30T17:42:53.752442000",
  "targetName": "cn.kstry.flux.demo.service.student.StudentScoreService",
  "threadId": "kstry-task-thread-pool-1",
  "toNodeIds": ["Activity_0n1ystm-32"]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 1.7.2 异步方式调用执行

@RestController
@RequestMapping("/student")
public class StudentController {

    @PostMapping("/scoreQuery")
    public Mono<R<QueryScoreResponse>> scoreQuery() {
        QueryScoreRequest request = new QueryScoreRequest();
        request.setStudentId(66L);
        request.setNeedScore(true);
        StoryRequest<QueryScoreResponse> fireRequest = ReqBuilder
                .returnType(QueryScoreResponse.class) // 指定返回类型
                .recallStoryHook(WebUtil::recallStoryHook) // 流程结束的回溯
                .trackingType(TrackingTypeEnum.SERVICE_DETAIL) // 指定监控类型
                .request(request) // 指定req域参数
                .varScopeData(new QueryScoreVarScope()) // 指定var域数据载体,可不指定使用默认值
                .startId("student-score-query-process") // 指定开始事件ID
                .build();
        Mono<QueryScoreResponse> fireAsync = storyEngine.fireAsync(fireRequest);
        return WebUtil.dataDecorate(request, fireAsync);
    }
}

public class WebUtil {
    public static <T> Mono<R<T>> resultDecorate(Object req, Mono<R<T>> result) {
        int defErrorCode = -1;
        return result.doOnSuccess(r -> log.info("req: {}, result success: {}", req, r)).onErrorResume(err -> {
            if (err instanceof BusinessException) {
                log.error("req: {}, task-service: {}, result error: ", req, GlobalUtil.transferNotEmpty(err, BusinessException.class).getTaskIdentity(), err);
            } else {
                log.error("req: {}, result error: ", req, err);
            }
            return Mono.just(R.error(ExceptionUtil.tryGetCode(err).map(s -> NumberUtils.toInt(s, defErrorCode)).orElse(defErrorCode), err.getMessage()));
        });
    }
    public static <T> Mono<R<T>> dataDecorate(Object req, Mono<T> result) {
        return resultDecorate(req, result.flatMap(r -> {
            if (r instanceof R) {
                return (Mono<R<T>>) result;
            }
            return Mono.just(R.success(r));
        }));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

    异步流程执行中,fireAsync返回值是Mono类型。Flux和Mono都是Java Reactor响应式中的重要概念,目前Kstry仅采用了Mono作为异步流程调用后的返回结果。在详细了解Mono之前,可以简单的将其理解为功能更加强大的CompletableFuture。使用Mono可以注册流程结束之后被执行的正常或异常的回调操作。而更多的则是像上面例子中一样和类似SpringFlux这种响应式框架做无缝衔接,来对外提供WEB服务。