TIP

前面演示了流程、节点、网关、事件、指令如何通过合适的编排来应对层出不穷的业务变化所带来的挑战。编排好的流程图中节点与节点之间并非是完全独立的。前几个服务节点的出参可能会是后一个节点所需要的几个入参。节点间数据关联无处不在。Kstry中承载节点间通信工作的角色就是 StoryBus

# 3.1 StoryBus介绍

a.drawio (1)

    🟢 每一个服务节点如果有需要,都可以从 StoryBus 中获取需要的参数,执行完之后,将需要的结果通知到 StoryBus

    🟢 StoryBus 中有四个数据域,分别是:

        🔷 req:保存请求传入的request对象,request对象本身不会发生变化。但对象里面的值允许通过后期的变量通知发生更新

        🔷 sta:保存节点执行完成后产生的变量,一经设置,将不再发生变化,如果出现重复设置会有警告日志(当只有对象的引用在sta域时,对象里面的字段还是可以发生更新的,这点与req相同)

        🔷 var:保存节点执行完成后产生的变量,可以被重复替换,对象里面的字段性质同上

        🔷 res:保存最终结果,作为 Story 执行完成后最终的结果返回给调用者(1.0.9版本开始,result更名为res

    🟢 节点出度中,可以定义条件表达式直接引用这四个域做条件判断,如流程编排中出现的:res.img != nullreq.source!='app'

    🟢 四个作用域被读写锁保护着,get获取读锁,notice获取写锁,防止出现并发问题

    🟢 开启异步模式后,同时创建的子任务都可以读写 StoryBus 中的变量,所以数据方面有前后依赖关系的节点,不能被创建到同一时间段执行的不同子任务中,可以通过聚合节点来保证节点执行时数据依赖的先后顺序

# 3.2 变量注解

# 3.2.1 获取变量

@XxxTaskParam 注解:

标注在服务节点入参前,从 StoryBus 中获取到变量后,直接赋值给参数变量,例如:

@NoticeSta
@TaskService(name = "get-evaluation-info")
public EvaluationInfo getEvaluationInfo(@ReqTaskParam("id") Long goodsId) {
    EvaluationInfo evaluationInfo = new EvaluationInfo();
    evaluationInfo.setEvaluateCount(20);
    log.info("goods id: {}, get EvaluationInfo: {}", goodsId, JSON.toJSONString(evaluationInfo));
    return evaluationInfo;
}
1
2
3
4
5
6
7
8

    🟢 @TaskParam:从 StoryBus 的 req、sta、var 域获取变量值,直接赋值给被标注参数

        🔷 value:来源字段名

        🔷 scopeEnum:指定是从哪个域获取字段,可取参数:ScopeTypeEnum.STABLEScopeTypeEnum.VARIABLEScopeTypeEnum.REQUEST

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @ReqTaskParam:从 StoryBus 的 req 域获取变量值,直接赋值给被标注参数

        🔷 value:来源字段名

        🔷 reqSelf:是否将客户端传入的 request 对象整体赋值给该参数,默认为:false

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @StaTaskParam:从 StoryBus 的 sta 域获取变量值,直接赋值给被标注参数

        🔷 value:来源字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @VarTaskParam:从 StoryBus 的 var 域获取变量值,直接赋值给被标注参数

        🔷 value:来源字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

TIP

value中支持使用$前缀来使用JSONPath (opens new window)语法获取变量。

比如$.var.studyExperienceList[*].classId就是从studyExperienceList对象集合中取出每一个对象的classId,然后将classId列表返回给目标变量

@XxxTaskField 注解:

标注在服务节点入参对象中的字段上,从 StoryBus 中获取到变量后,直接赋值给被标注字段,例如:

@TaskComponent(name = "logistic")
public class LogisticService {

    @NoticeSta
    @TaskService(name = "get-logistic-insurance")
    public LogisticInsurance getLogisticInsurance(GetLogisticInsuranceRequest request) {
        log.info("request source:{}", request.getSource());
        LogisticInsurance logisticInsurance = new LogisticInsurance();
        logisticInsurance.setDesc("运费险描述");
        logisticInsurance.setType(1);
        return logisticInsurance;
    }
}

@Data
public class GetLogisticInsuranceRequest {

    @ReqTaskField("source")
    private String source;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

    🟢 @TaskField:从 StoryBus 的 req、sta、var 域获取变量值,赋值给被标注字段

        🔷 value:来源字段名

        🔷 scopeEnum:指定是从哪个域获取字段,可取参数:ScopeTypeEnum.STABLEScopeTypeEnum.VARIABLEScopeTypeEnum.REQUEST

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @ReqTaskField:从 StoryBus 的 req 域获取变量值,赋值给被标注字段

        🔷 value:来源字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @StaTaskField:从 StoryBus 的 sta 域获取变量值,赋值给被标注字段

        🔷 value:来源字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @VarTaskField:从 StoryBus 的 var 域获取变量值,赋值给被标注字段

        🔷 value:来源字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

TIP

value中支持使用$前缀来使用JSONPath (opens new window)语法获取变量。

比如$.var.studyExperienceList[*].classId就是从studyExperienceList对象集合中取出每一个对象的classId,然后将classId列表返回给目标变量

# 3.2.2 通知变量

注解可以与@TaskService 一起使用,指定服务节点的结果返回值通知到 StoryBus 的哪些作用域中

@TaskComponent(name = "order")
public class OrderService {

    @NoticeScope(scope = {ScopeTypeEnum.STABLE, ScopeTypeEnum.VARIABLE})
    @TaskService(name = "get-order-info")
    public OrderInfo getOrderInfo(@ReqTaskParam("id") Long goodsId) {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderedCount(10);
        log.info("goods id: {}, get OrderInfo: {}", goodsId, JSON.toJSONString(orderInfo));
        return orderInfo;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

注解可以标注在服务节点方法的结果类上,也可以标注在结果类的字段上,例如:

@NoticeSta(targrt = "goodsDetail")
public class GoodsDetail {

    private Long id;

    private String name;
}

@Data
@Builder
public class InitSkuResponse {

    @NoticeSta(targrt = "goodsDetail.skuInfos")
    private List<SkuInfo> skuInfos;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

    🟢 @NoticeSta:可以与@TaskService 一起使用,还可以标注在方法返回结果的类上或者类字段上,字段结果被通知到 StoryBus 中的 sta 域中

        🔷 targrt:通知到指定作用域的字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @NoticeVar:可以与@TaskService 一起使用,还可以标注在方法返回结果的类上或者类字段上,字段结果被通知到 StoryBus 中的 var 域中

        🔷 targrt:通知到指定作用域的字段名

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @NoticeResult:可以与@TaskService 一起使用,还可以标注在方法返回结果的类上或者类字段上,字段结果被通知到 StoryBus 中的 result 域中

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

    🟢 @NoticeScope:可以与@TaskService 一起使用,还可以标注在方法返回结果的类上或者类字段上,字段结果默认被通知到 StoryBus 中的 sta 和 var两个域中

        🔷 targrt:通知到指定作用域的字段名

        🔷 scope:未指定时,默认将结果通知到 StoryBus 中的 sta 和 var两个域中。指定后以实际指定域为准

        🔷 converter:指定使用的类型转换器名称,默认不指定,类型转换时自动匹配转换器

TIP

如果需要在一个服务节点方法结束时通知多个变量至StoryBus,除了上面定义对象的方式,还可以使用ScopeDataNotice对象。比如:return ScopeDataNotice.sta().notice("age", 1).notice("name", "name");就是将agename两个变量通知到了sta域

# 3.3 ScopeDataOperator对象

ScopeDataOperator 提供了操作StoryBus中各个数据域变量的入口。可以直接作为服务节点的入参被定义

@TaskService(name = "increase-one")
public void increaseOne(ScopeDataOperator operator) {
    ReentrantReadWriteLock.WriteLock writeLock = operator.writeLock();
    writeLock.lock();
    try {
        MethodInvokeBo reqScope = operator.getReqScope();
        reqScope.setA(reqScope.getA() + 1);
    } finally {
        writeLock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11

具体功能如下:


/**
 * 获取 Req 对象
 *
 * @return 数据结果
 */
<T> T getReqScope();

/**
 * 获取 Sta 对象
 *
 * @return 数据结果
 */
<T extends ScopeData> T getStaScope();

/**
 * 获取 Var 对象
 *
 * @return 数据结果
 */
<T extends ScopeData> T getVarScope();

/**
 * 获取 Result 对象
 *
 * @return 数据结果
 */
<T> Optional<T> getResult();

/**
 * 请求 ID
 *
 * @return 请求 ID
 */
String getRequestId();

/**
 * 获取开始事件 ID
 *
 * @return 开始事件 ID
 */
String getStartId();

/**
 * 获取业务 ID
 *
 * @return 业务 ID
 */
Optional<String> getBusinessId();

/**
 * 从 Req 域获取数据
 *
 * @param name 数据名称
 * @return 数据结果
 */
<T> Optional<T> getReqData(String name);

/**
 * 从 Sta 域获取数据
 *
 * @param name 数据名称
 * @return 数据结果
 */
<T> Optional<T> getStaData(String name);

/**
 * 从 Var 域获取数据
 *
 * @param name 数据名称
 * @return 数据结果
 */
<T> Optional<T> getVarData(String name);

/**
 * 使用取值表达式获取数据
 *
 * @param expression 取值表达式
 * @return 数据结果
 */
<T> Optional<T> getData(String expression);

/**
 * 获取服务节点属性,服务节点属性在节点定义时指定
 *
 * @return 服务节点属性
 */
Optional<String> getTaskProperty();

/**
 * 遍历执行迭代器的每一项数据时,获取当前项数据
 *
 * @return 数据结果
 */
<T> Optional<T> iterDataItem();

/**
 * 拿到 StoryBus 中的读锁
 *
 * @return 读锁
 */
ReentrantReadWriteLock.ReadLock readLock();

/**
 * 使用取值表达式获取数据, 如果不存在会创建并赋值到表达式指定位置,创建失败或指定位置失败返回空值
 *
 * @param expression 取值表达式
 * @param supplier 没有获取到目标值时调用获取默认值
 *
 * @return 数据结果
 */
<T> Optional<T> computeIfAbsent(String expression, Supplier<T> supplier);

/**
 * 设置数据
 *
 * @param expression 取值表达式
 * @param target 变量值
 *
 * @return 是否设置成功,如果已经存在将会设置失败
 */
boolean setData(String expression, Object target);

/**
 * 设置 Sta 域变量
 *
 * @param name 变量名
 * @param target 变量值
 *
 * @return 是否设置成功,如果已经存在将会设置失败
 */
boolean setStaData(String name, Object target);

/**
 * 设置 Var 域变量
 *
 * @param name 变量名
 * @param target 变量值
 *
 * @return 是否设置成功
 */
boolean setVarData(String name, Object target);

/**
 * 设置 Result
 *
 * @param target Result对象
 * @return 是否设置成功,如果已经存在将会设置失败
 */
boolean setResult(Object target);

/**
 * 拿到 StoryBus 中的写锁
 *
 * @return 写锁
 */
ReentrantReadWriteLock.WriteLock writeLock();

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158

# 3.4 参数校验

    🟢 服务节点方法参数如果是自定义对象时,对象中的字段支持JSR303格式校验

    🟢 项目中需要引入hibernate-validator依赖,然后使用注解标注参数就会生效,如下@NotBlank

@TaskComponent(name = "logistic")
public class LogisticService {

    @NoticeSta
    @TaskService(name = "get-logistic-insurance")
    public LogisticInsurance getLogisticInsurance(GetLogisticInsuranceRequest request) {
        log.info("request source:{}", request.getSource());
        LogisticInsurance logisticInsurance = new LogisticInsurance();
        logisticInsurance.setDesc("运费险描述");
        logisticInsurance.setType(1);
        return logisticInsurance;
    }
}

@Data
public class GetLogisticInsuranceRequest {

    // 判断 source 不为空
    @NotBlank
    @ReqTaskField("source")
    private String source;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

    🟢 校验参数发现异常后,会中止 Story 流程,以异常的方式返回校验结果

# 3.5 参数生命周期

TIP

Kstry 提供了服务节点入参的生成方式自定义和生命周期方法。默认情况下入参对象是通过反射方式创建的,但如果有必要时可指定将参数交由 Spring 容器维护创建,这样就可以用到 Bean 对象的注入、生命周期方法等功能。

服务节点入参类:

@Data
@SpringInitialization
public class DetailPostProcessRequest implements ParamLifecycle {

    @Resource
    private ShopService shopService;

    @StaTaskField("shopInfo")
    private ShopInfo shopInfo;
    
    ...
        
    @Override
    public void before(ScopeDataOperator scopeDataOperator) {
        ParamLifecycle.super.before(scopeDataOperator);
    }

    @Override
    public void after(ScopeDataOperator scopeDataOperator) {
        if (shopInfo != null) {
            shopService.makeLabel(shopInfo.getLabels());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

    🟢 定义 ShopService 变量,默认反射创建的情况下,该变量是不会被赋值的。参数类上标注@SpringInitialization注解,告知 Kstry 这个节点参数需要托管给 Spring 容器,此时 ShopService 变量就会被 Spring 容器注入值

    🟢 参数类实现 ParamLifecycle 接口进行生命周期方法定义,接口有两个方法可以被重写

        🔷 before(ScopeDataOperator scopeDataOperator):参数对象创建后就会被调用,发生在Spring容器注入值之后,Kstry字段赋值之前

        🔷 after(ScopeDataOperator scopeDataOperator):发生在字段赋值之后,用于对赋值后的参数进行再一步的处理

    🟢 在 after()方法中调用 ShopService 的打标能力进行打标操作

    🟢 ParamLifecycle存在一个子类SpringParamLifecycle,实现SpringParamLifecycle时可以获取Spring容器的ApplicationContext

注意

节点方法参数被 @XxxTaskParam 注解修饰时,参数Bean字段装配、字段校验、Spring容器初始化、生命周期方法都会失效。原因是被 @XxxTaskParam 修饰的参数无需进行初始化和任何操作,会被 StoryBus 中获取到的值直接赋值

# 3.6 参数类型转换器

TIP

流程中参数无论是从StoryBus中读取到服务节点方法,还是方法执行完成之后通知到StoryBus中。都有可能出现实际参数与接收类型不匹配的情况。框架支持显式和隐式两种方式来使用类型转换器

# 3.6.1 类型转换器定义

项目中需要新增类型转换器时,可以定义转换器类实现TypeConverter接口,然后将自定义转换器托管给Spring容器即可

public interface TypeConverter<S, T> extends Ordered {

    T doConvert(S source, @Nullable Class<?> needType);

    /**
     * 获取源数据类型
     */
    Class<S> getSourceType();

    /**
     * 获取目标数据类型
     */
    Class<T> getTargetType();

    /**
     * 获取转换器名称
     */
    String getConvertName();

    /**
     * 默认的类型转换器匹配规则
     * 使用类型转换器时,如果指定了转换器名字,直接使用指定的转换器不会进行转换器匹配操作,只有未指定转换器名字时才会使用match匹配合适的类型转换器
     *
     * @param collGeneric needType为List、Set时,collGeneric是集合上的泛型
     */
    default boolean match(Object source, Class<?> needType, @Nullable Class<?> collGeneric) {
        if (source == null || needType == null) {
            return false;
        }
        return ElementParserUtil.isAssignable(getSourceType(), source.getClass()) && ElementParserUtil.isAssignable(getTargetType(), needType);
    }

    /**
     * 对外暴露使用的转换方法。
     * 默认不支持同类型转换(同类型转换会返回原值),需要同类型转换时可以覆写该方法实现
     *
     * @param collGeneric needType为List、Set时,collGeneric是集合上的泛型
     */
    @SuppressWarnings("unchecked")
    default T convert(S source, Class<?> needType, Class<?> collGeneric) {
        AssertUtil.notNull(source);
        if (ElementParserUtil.isAssignable(getTargetType(), source.getClass()) && (needType == null || ElementParserUtil.isAssignable(needType, source.getClass()))) {
            return (T) source;
        }
        return doConvert(source, needType);
    }

    /**
     * 同时匹配多个转换器时,使用优先级最高的
     */
    default int getOrder() {
        return 0;
    }
}
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

类型转换器定义样例:

@Component
public class Map2BusTestRequestConverter implements TypeConverter<Map, BusTestRequest> {

    @Override
    public BusTestRequest doConvert(Map source, @Nullable Class<?> needType) {
        return JSON.parseObject(JSON.toJSONString(source), getTargetType());
    }

    @Override
    public Class<Map> getSourceType() {
        return Map.class;
    }

    @Override
    public Class<BusTestRequest> getTargetType() {
        return BusTestRequest.class;
    }

    @Override
    public String getConvertName() {
        return "MAP-TO-BUS-TEST";
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

    🟢 一般情况下实现TypeConverter接口后,重写以下四个方法即可:

        🔷 T doConvert(S source, Class<?> needType):定义实际转换的逻辑。source是源数据,needType是需要转换成为的类型,这个类型是允许出现空的

        🔷 Class<S> getSourceType():返回转换器适配的源数据类型

        🔷 Class<T> getTargetType():返回转换器适配的目标数据类型

        🔷 String getConvertName():返回转换器名字

    🟢 TypeConverter接口继承了Spring中的Ordered接口,当同时匹配到多个转换器时,getOrder()返回值越小优先级越高。不指定默认返回0

    🟢 转换器实际使用时,可以分别通过指定转换结果的类型、转换器名称两种方式来使用类型转换器。指定名称的方式优先级会高于指定类型。当指定名称时转换器会跳过match(Object source, Class<?> needType, Class<?> collGeneric)方法的匹配,直接调用匹配成功转换器的convert(S source, Class<?> needType, Class<?> collGeneric)方法。

    🟢 当以指定返回值类型方式使用转换器时,match(Object source, Class<?> needType, Class<?> collGeneric)方法会被执行,用来进行转换器的匹配。默认情况下该方法会根据实际源类型、目标类型与转换器中指定的源类型、目标类型是否匹配来判断是否使用当前类型转换器。如果定义一个类型转换器并且不希望它通过类型被匹配到时,可以重写该方法直接返回false。这样新定义的类型转换器就只能用指定名称的方式使用了

    🟢 类型转换时容器会先调用convert(S source, Class<?> needType, Class<?> collGeneric)方法,默认情况下,该方法会判断实际需要的类型与源数据类型是否相同,如果相同将不做任何转换直接返回源数据。若想要跳过这个限制可以在转换器中重写此方法

    🟢 无论是指定类型还是指定名称的方式使用类型转换器,如果源数据类型和需要的目标类型(如果有)中存在与类型转换器不匹配的,类型转换器将不会生效

# 3.6.2 类型转换器使用

# 隐式使用方式

    隐式使用方式无需任何操作,框架默认开启。服务节点方法从StoryBus中获取数据、执行完成后通知数据到StoryBus中时都会经过类型转换器的隐式处理。比如实际类型是个Map,接收变量的类型是Bean。此时就会匹配合适的转换器进行类型转换。

类型转换器会在以下场景中隐式生效:

    🟢 使用@XxxTaskParam、@XxxTaskField注解从StoryBus中获取变量

    🟢 使用@NoticeXxx注解或者ScopeDataNotice对象将方法返回值通知到StoryBus中

    🟢 调用ScopeDataOperator对象的各种set方法,往StoryBus中写数据

# 显示使用方式

显示使用方式需要显示指定要使用哪个类型转换器。显示使用类型转换器场景如下:

    🟢 使用@XxxTaskParam、@XxxTaskField注解从StoryBus中获取变量

    🟢 使用@NoticeXxx注解或者ScopeDataNotice对象将方法返回值通知到StoryBus中

    🟢 EnhancedDataOperator(ScopeDataOperator子类)对象从StoryBus中获取变量

    🟢 task-params属性配置节点方法入参

# 使用类型转换器对象

    类型转换器是托管在Spring容器中的TypeConverterProcessor对象。需要使用时直接从Spring上下文中注入即可。该对象提供以下几个方法:

    🟢 convert(S source, Class<T> targetType):指定返回值类型的方式使用类型转换器

    🟢 convert(String convertName, S source):指定名称的方式使用类型转换器

    🟢 convert(String convertName, S source, Class<T> targetType):同时指定名称和返回值类型的方式使用类型转换器

    🟢 convert(String convertName, S source, Class<T> targetType, Class<?> collGeneric):同时指定名称和返回值类型的方式使用类型转换器,targetType是List、Set类型时可以指定集合上的泛型

    以上这些方法,均返回Pair<String, T>对象。key:实际使用到的类型转换器名称,value:转换后的对象。如果未匹配到转换器,key返回null,value返回原来的source。下面是在js指令的片段代码,演示类型转换器的实际使用:

// JS 脚本获取结果
Object result = ((Invocable) jsEngine).invokeFunction(invokeMethodName);

// 对结果进行转换
if (result != null && property != null && !StringUtils.isAllBlank(property.getReturnType(), property.getResultConverter())) {

    // 使用类型转换器,可以指定名称也可以指定类型
    result = typeConverterProcessor.convert(property.getResultConverter(), result, Optional.ofNullable(property.getReturnType()).filter(StringUtils::isNotBlank).map(rt -> {
        try {
            return Class.forName(rt);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }).orElse(null)).getValue();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.6.3 默认转换器

TIP

针对一些比较常见的格式转换,框架提供了一些可以直接使用的默认转换器

转换器类名 转换器名称 默认开启 入参类型 出参类型 优先级
Object2BooleanTypeConverter OBJECT_TO_BOOLEAN Object Boolean 0
Date2StringTypeConverter DATE_TO_STRING Date String 0
String2DateTypeConverter STRING_TO_DATE String Date 0
LocalDateTime2StringTypeConverter LOCAL_DATETIME_TO_STRING LocalDateTime String 0
String2LocalDateTimeTypeConverter STRING_TO_LOCAL_DATETIME String LocalDateTime 0
CollectionGenericTypeConverter COLLECTION_GENERIC_TYPE_CONVERTER Collection(A) Collection(B) 1000
BasicTypeConverter BASIC_CONVERTER Object Object 2147483647
OneItem2ListTypeConverter ONE_ITEM_TO_LIST Object List 0
OneItem2SetTypeConverter ONE_ITEM_TO_SET Object Set 0
FirstItemFromListTypeConverter FIRST_ITEM_FROM_LIST List Object 0

# 3.7 配置节点方法入参

TIP

框架最初通过配置侧指定task-property属性,然后在服务节点方法中获取,以此来满足不同配置节点引用同一服务节点方法时在个性化差异上的诉求。但是这种方式不够灵活,不能满足参数中存在变量的情况。目前框架支持了task-params属性,做到了更加精细化的支持配置侧指定服务节点方法入参

配置演示

63860bd414901524254021fc792dd321

task-params内容:

{
    "params": {
        "busTest[t:map-to-bus-test]": {
            "id": 12,
            "desc": "描述信息12",
            "ar": {
                "name": "@var.arName"
            },
            "localNow[t:string_to_local_datetime]": "@var.dateStr"
        }, // 定义有常量也有从StoryBus中获取变量的map结构。然后指定转换器,将map转成BusTestRequest对象。
        "requests": [{
            "id": "@$.var.requests[0].id", // task-params同样支持在表达式加$使用JSONPath获取参数
            "desc": "@var.requests.[0].desc"
        }, {
            "id": "@var.requests.[1].id",
            "desc": "@var.requests.[1].desc",
            "now[t:string_to_date]": "@var.dateStr"
        }, {
            "id": 19,
            "desc": "描述信息19"
        }], // 对集合的支持,以及集合中定义对象的支持
        "reqDescList2": [
            ["@var.requests.[0].desc", 1, 2, 3],
            ["@var.requests.[0].desc", "@var.requests.[1].desc"]
        ], // 对集合中直接获取StoryBus中变量的支持
        "requests2": [
            [
                [{
                    "descList": ["@var.requests.[0].desc", "@var.requests.[1].desc"]
                }]
            ]
        ] // 多重集合的支持
    },
    "reqDescList[t:basic_converter]": ["@var.requests.[0].desc", "@var.requests.[1].desc"]
}
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

服务节点方法:

@TaskService
public void testConverterMapping(Map<String, Object> params, String reqDescList) {
    Assert.assertNotNull(params.get("busTest"));
    Assert.assertTrue(params.get("busTest") instanceof BusTestRequest);
    Assert.assertNotNull(((BusTestRequest) params.get("busTest")).getLocalNow());
    Assert.assertEquals(((BusTestRequest) params.get("busTest")).getAr().getName(), "AR名称");
    Assert.assertEquals(3, ((List<?>) params.get("requests")).size());
    Assert.assertNotNull(((Map<?, ?>) ((List<?>) params.get("requests")).get(1)).get("now"));
    Assert.assertEquals(54, ((List<?>) params.get("requests")).stream().mapToInt(m -> (Integer) ((Map<?, ?>) m).get("id")).sum());
    Assert.assertNotNull(reqDescList);
    Assert.assertEquals(2, ((List<?>) params.get("reqDescList2")).size());
    Assert.assertEquals(4, ((List<?>) ((List<?>) params.get("reqDescList2")).get(0)).size());
    Assert.assertEquals(2, ((List<?>) ((List<?>) params.get("reqDescList2")).get(1)).size());
    Assert.assertEquals(1, ((List<?>) ((List<?>) params.get("reqDescList2")).get(0)).get(1));
    Assert.assertTrue(((List<?>) ((List<?>) ((List<?>) params.get("requests2")).get(0)).get(0)).get(0) instanceof Map);
    System.out.println(JSON.toJSONString(params));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

测试方法:

@Test
public void test04() {
    InScopeData varScopeData = new InScopeData(ScopeTypeEnum.VARIABLE);
    varScopeData.put("dateStr", "2022-02-03 11:00:00");
    varScopeData.put("arName", "AR名称");
    varScopeData.put("requests", Lists.newArrayList(
            BusTestRequest.builder().id(17).desc("描述信息17").build(),
            BusTestRequest.builder().id(18).desc("描述信息18").build()
    ));

    StoryRequest<Void> fireRequest = ReqBuilder.returnType(Void.class)
            .trackingType(TrackingTypeEnum.SERVICE_DETAIL).varScopeData(varScopeData).startId("testConverterMappingProcess").build();
    TaskResponse<Void> result = storyEngine.fire(fireRequest);
    Assert.assertTrue(result.isSuccess());
    System.out.println(JSON.toJSONString(result));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

    🟢 task-params属性要求必须是JSON对象格式。上面例子中JSON对象中的params、reqDescList对应服务节点方法的两个入参

    🟢 默认情况框架会拿到方法入参名从JSON对象中获取指定参数项。也可以使用@TaskParam("reqId")注解指定方法入参名

    🟢 task-params中的变量获取表达式格式:@{数据位置},比如:@var.arName

    🟢 task-params中的变量获取,同样支持表达式加$使用JSONPath获取参数。比如:@$.var.requests[0].id

    🟢 使用类型转换器格式:{字段名}[t:{转换器名字}],比如:busTest[t:map-to-bus-test],其中t不区分大小写

    🟢 如果代码、task-params中同时指定某个参数的赋值,最终会以task-params中的为准

    🟢 task-params默认开启,也可以在application.yml中配置kstry.story.define-node-params=false来禁用