场景假设

成熟的项目中一定存在着繁杂且相互关联的业务逻辑关系,这些业务逻辑形成了服务的基础构架,以此应对着各种来自业务方、渠道方、甚至系统内部层出不穷的需求挑战。随着时间的推移,慢慢会发现,不同的渠道、不同的业务、甚至不同的生产环境对一些细节点的要求是不一样的。如何在保持原有架构清晰完整的前提下满足各种业务对自定义参数的诉求将是一个非常严峻的挑战。Kstry提供了灵活的业务变量定义方式,可以在业务域、服务域、和运行环境等维度进行分类定义获取

# 5.1 变量初体验

@EnableKstry注解增加propertiesPath属性指定配置文件位置

@EnableKstry(bpmnPath = "./bpmn/*.bpmn", propertiesPath = "./config/*.yml")
@SpringBootApplication
public class KstryDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(KstryDemoApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

创建配置文件并定义变量

# global-default.yml

default:
  banner: https://aass.png # 变量1
  shop-blacklist-ids: # 变量2
    - 2
    - 3
    - 4
    - 5
1
2
3
4
5
6
7
8
9

    🟢 配置文件后缀名必须是yml、yaml否则无法解析

    🟢 配置文件名字本身无要求,只要符合propertiesPath的定义,都会进行解析

    🟢 default:是默认域,如果其他维度都没有解析到key对应的变量,会返回在 default 域中定义的变量

使用变量

@TaskComponent(name = GoodsCompKey.goods)
public class GoodsService {

    @Resource
    private KvAbility kvAbility;

    @TaskService(name = "detail-post-process")
    public void detailPostProcess(DetailPostProcessRequest request) {

        List<String> list = kvAbility.getList("shop-blacklist-ids", String.class);
        String banner = kvAbility.getObject("banner", String.class).orElse(null);
        log.info("shop-blacklist-ids: {}", JSON.toJSONString(list));
        log.info("banner: {}", banner);
    }
}

// 日志:
// - shop-blacklist-ids: ["2","3","4","5"]
// - banner: https://aass.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

    🟢 KvAbility 是 Kstry 提供的获取变量的组件,所有变量都是通过这个组件获取的

# 5.2 服务维度变量

场景假设

在加载店铺信息时需要设置店铺的标签信息,而标签信息又是经常变化的,所以应该以变量的形式存在。如果放在default域,后面业务变量多了之后会太过混乱,显然是不合适的。这时就应该按照服务维度来划分变量

定义变量

# goods-config.yml

init-shop-info:
  labels:
    - url: https://xxx.png
      name: L1
      index: 1
    - url: https://xxx2.png
      name: L2
      index: 2
1
2
3
4
5
6
7
8
9
10

    🟢 如果将配置信息还放在global-default.yml中也是可以执行的,但为了区分开看着更清楚,可以再新建一个专门放商品服务配置的配置文件

    🟢 init-shop-info也是一个域,和default属于同一类型。不同的是default是全局默认域用来兜底,前者则需要显示指定使用才能起作用

服务维度使用变量

@TaskComponent(name = ShopCompKey.shop)
public class ShopService {

    @Resource
    private KvAbility kvAbility;

        @TaskService(name = "get-shopInfo-goodsId", kvScope = "init-shop-info")
        public ShopInfo getShopInfoByGoodsId(@ReqTaskParam("id") Long goodsId) throws InterruptedException {
          List<ShopLabel> labels = kvAbility.getList("labels", ShopLabel.class);
        ...
        }
}
1
2
3
4
5
6
7
8
9
10
11
12

    🟢 @TaskService注解有kvScope属性可以指定变量的服务域,指定该变量后,执行当前服务节点时容器会先从指定服务域查找变量,找不到时才从default域取

# 5.3 业务维度变量

场景假设

还是风控审查图片的例子,假设现在有很多业务线都在使用这个图片审查的能力,但是不同业务线对图片尺寸的要求不一样。这时如果只是在@TaskService注解中定义kvScope属性显然是不行的。因为无法区分是哪个业务线,图片需要满足什么尺寸的要求。如何才可以在业务维度使用变量呢?

定义变量

# risk-control-config.yml 同样是为了维护和阅读方便才新建的文件

# 风控默认配置
risk-control: # 服务维度
  img-max-size: 100

# 业务维度变量配置
special-channel: # 对应请求入参传入的businessId(业务域)
  risk-control: # 风控域变量(服务域)
    img-max-size: 200 # 指定变量值
1
2
3
4
5
6
7
8
9
10

    🟢 risk-control是服务域变量名,定义了与风控服务相关的变量

    🟢 special-channel是业务域变量名,对应于请求入参传入的businessId。businessId匹配成功后会匹配当前执行的服务节点是否是在risk-control服务域下,如果也匹配成功会获取img-max-size=200。否则使用risk-control域变量,如果还是没有则使用default域变量

业务维度使用变量

@TaskComponent(name = "risk-control")
public class RiskControlService {

    @Resource
    private KvAbility kvAbility;

    @TaskService(name = "check-img", kvScope = "risk-control")
    public void checkImg(CheckInfo checkInfo) {

        AssertUtil.notNull(checkInfo);
        AssertUtil.notBlank(checkInfo.getImg());
        log.info("check img: {}, size: {}", checkInfo.getImg(), kvAbility.getString("img-max-size").orElse(null));
    }

    @TaskService(name = "check-img", kvScope = "risk-control", ability = "triple")
    public void tripleCheckImg(CheckInfo checkInfo) {

        AssertUtil.notNull(checkInfo);
        AssertUtil.notBlank(checkInfo.getImg());
        log.info("triple check img: {}, size: {}", checkInfo.getImg(), kvAbility.getString("img-max-size").orElse(null));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

    🟢 @TaskService注解指定kvScope属性,说明当前服务节点在risk-control服务域下。如果业务维度没匹配成功则会使用服务域的变量,如果还是匹配会尝试使用default域变量

@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Resource
    private StoryEngine storyEngine;

    @PostMapping("/show")
    public GoodsDetail showGoods(@RequestBody GoodsDetailRequest request) {

        StoryRequest<GoodsDetail> req = ReqBuilder.returnType(GoodsDetail.class).startId("kstry-demo-goods-show").request(request).build();
        if (StringUtils.isNotBlank(request.getBusinessId())) {
            req.setBusinessId(request.getBusinessId());
        }
        TaskResponse<GoodsDetail> fire = storyEngine.fire(req);
        if (fire.isSuccess()) {
            return fire.getResult();
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

    🟢 业务维度变量起作用的重点是businessId,此例中businessId=special-channel时,匹配special-channel域的变量

# 5.4 环境维度变量

场景假设

针对店铺黑名单,生产环境与开发环境不一样,如何满足呢?

定义变量

# global-default.yml

default:
  banner: https://aass.png
  shop-blacklist-ids:
    - 2
    - 3
    - 4
    - 5

default@dev:
  shop-blacklist-ids:
    - 8
    - 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14

    🟢 如default@dev,指定域所属环境的格式是:变量域名 + @ + 环境

    🟢 此处的环境与 Spring 中的 ActiveProfiles 是对应的

    🟢 如果dev环境未匹配到,会匹配没有指定环境的同域名变量

    🟢 业务维度变量,环境参数同样定义在业务域名字后面

使用变量

@TaskComponent(name = GoodsCompKey.goods)
public class GoodsService {

    @Resource
    private KvAbility kvAbility;

    @TaskService(name = "detail-post-process")
    public void detailPostProcess(DetailPostProcessRequest request) {

        // 从 default 域,获取店铺黑名单
        List<String> list = kvAbility.getList("shop-blacklist-ids", String.class);
        String banner = kvAbility.getObject("banner", String.class).orElse(null);
        log.info("shop-blacklist-ids: {}", JSON.toJSONString(list));
        log.info("banner: {}", banner);
    }
}

// dev 环境日志
// - shop-blacklist-ids: ["8","9"]
// - banner: https://aass.png

// 非 dev 环境日志
// - shop-blacklist-ids: ["2","3","4","5"]
// - banner: https://aass.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 5.5 获取变量顺序

带businessId获取变量与不带businessId获取变量步骤如图所示:

get-var-flow

带businessId获取变量:

    🟢 找业务维度中的,环境匹配且服务域也匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找业务维度中的,服务域匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找业务维度中的,环境匹配且是default域的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找业务维度中的,是default域的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找环境匹配且服务域也匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找服务域匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找环境匹配且是default域的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找是default域的变量,如果有返回对应值,结束获取流程,若没有返回空

不带businessId获取变量:

    🟢 找环境匹配且服务域也匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找服务域匹配的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找环境匹配且是default域的变量,如果有返回对应值,结束获取流程,若没有继续下一步

    🟢 找是default域的变量,如果有返回对应值,结束获取流程,若没有返回空

# 5.6 动态获取变量

TIP

上面介绍的是通过配置文件来使用变量,这种方式是静态的,变量修改生效需要重启应用。框架也提供了动态获取变量的方式,在不重启应用的前提下可以获取到新的变量值

@Component
public class DynamicValue implements DynamicKValue {

    @Override
    public long version(String key) {
        return DynamicKValue.super.version(key);
    }

    @Override
    public Optional<Object> getValue(String key, KvScope kvScope) {
        Assert.assertEquals("new-per-scope", kvScope.getScope());
        Assert.assertEquals("business-channel", kvScope.getBusinessId().orElse(null));
        if (key.equals("dynamic")) {
            return Optional.of("dynamic");
        }
        return Optional.empty();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

    🟢 创建实现DynamicKValue接口的类实例,放入Spring容器中,动态获取变量组件即会生效,DynamicKValue接口提供一下两个方法:

        🔷 getValue(String key, KvScope kvScope):传入key,根据key动态获取变量。kvScope可以拿到配置在@TaskService注解上的kvScope属性和当前流程的businessId

        🔷 version(String key):控制变量版本。获取变量时,如果版本未更新会先从缓存中取,缓存有效期1天。该方法有默认实现,不重写时默认返回-1,代表不使用变量缓存

    🟢 先获取配置文件中的变量,如果未获取到时才会动态获取

注意

配置文件中变量key存在而value为空和key不存在是两种情况。前者会被认为是成功获取到了变量,后者才会进入到动态获取流程