动态别名提升服务编排中值流转灵活性

98 阅读5分钟

我正在参加「掘金·启航计划」

首先是服务编排的概念,请记住服务编排的核心:灵活,复用!

服务编排的概念

在每个公司的系统中,总有一些拥有复杂业务逻辑的系统,这些系统承载着核心业务逻辑,几乎每个需求都和这些核心业务有关,这些核心业务业务逻辑冗长,涉及内部逻辑运算,缓存操作,持久化操作,外部资源调取,内部其他系统RPC调用等等。时间一长,项目几经易手,维护的成本得就会越来越高。各种硬代码判断,分支条件越来越多。代码的抽象,复用率也越来越低,各个模块之间的耦合度很高。一小段逻辑的变动,会影响到其他模块,需要进行完整回归测试来验证。如要灵活改变业务流程的顺序,则要进行代码大改动进行抽象,重新写方法。实时热变更业务流程,几乎很难实现。

服务编排的好处

如果你要对复杂业务逻辑进行新写或者重构,用服务编排框架可以自由组件编排,帮助解耦业务代码,让每一个业务片段都是一个组件,把复杂的业务逻辑按代码片段拆分成一个个小组件,并定义一个规则流程配置。这样,所有的组件,都能按照你的规则配置去进行复杂的流转。

什么是上下文值

如图中,在C节点处理时,如果存在一定依赖关系的话,它需要拿到A,B节点的计算内容,还有内部的通用参数,如请求公参,用户信息,AB数据等,整个流程编排的节点基本都需要依赖上下文参数,来请求对应的RPC接口,还有做些业务逻辑的判断。

public class FeedsHandler extends FeedsCommonHandler implements Handler<String, List<Floor>> {
    
    @Override
    public List<ChannelFloor> action(ParamContext.Param param, String o) {
        //通过Param来拉取上下文参数
        IndexRequest request = param.getParamByName(FeedsProcessEnum.INDEX_REQUEST.getName());
        HotSaleRequest bodyParam = param.getParamByName(FeedsProcessEnum.BODY_PARAM.getName());
        IndexFeedsShowControl showControl = param.getParamByName(FeedsProcessEnum.SHOW_CONTROL.getName());
        List<StoreBaseInfoDTO> storeBaseInfoList = param.getParamByName(FeedsProcessEnum.INDEX_FEEDS_LBS_ADAPTOR.getName());
        if (CollectionUtils.isEmpty(storeBaseInfoList)){
            return Collections.emptyList();
        }
        
}

上下文参数的传递方式

通过在ParamContext中使用ThreadLocal来传递参数,框架内部在计算完节点之后会将出参放到params里,params内部是Map<String, Object>构造,key就是节点的名称,values是节点运行结果。这样看上去是非常灵活了,一方面节点的值不需要手动传递,也支持自己去动态添加参数。

package com.jd.o2o.threadtools.context;

import com.google.gson.Gson;
import com.jd.o2o.threadtools.util.BeanMapUtil;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ParamContext {
    private static final ThreadLocal<Param> paramThreadLocal = new ThreadLocal();

    public ParamContext() {
    }

    public static Param getParam() {
        return (Param)paramThreadLocal.get();
    }

    public static void setParam(Param param) {
        if (param != null) {
            paramThreadLocal.set(param);
        }

    }

    public static void remove() {
        paramThreadLocal.remove();
    }

    public static class Param {
        private static Gson gson = new Gson();
        private Map<String, Object> beanMap;

        private Param(Map<String, Object> beanMap) {
            this.beanMap = beanMap;
        }

        public static final Param create() {
            return new Param(new ConcurrentHashMap());
        }

        public Map<String, Object> getBeanMap() {
            return this.beanMap;
        }

        public <T> T getParamByName(String name) {
            Map<String, Object> beanMap = this.getBeanMap();
            return beanMap == null ? null : beanMap.get(name);
        }

        public <T> T getCopyParamByName(String name) {
            Map<String, Object> beanMap = this.getBeanMap();
            return beanMap != null && beanMap.get(name) != null ? gson.fromJson(gson.toJson(beanMap.get(name)), beanMap.get(name).getClass()) : null;
        }

        public boolean putData(String name, Object obj) {
            if (name != null && obj != null) {
                if (this.getBeanMap() == null) {
                    create();
                }

                Map<String, Object> beanMap = this.getBeanMap();
                if (beanMap.containsKey(name)) {
                    return false;
                } else {
                    beanMap.put(name, obj);
                    return true;
                }
            } else {
                return false;
            }
        }
    }
}

\

相斥的灵活和复用

在第一次重构业务流程,使用服务编排框架时真的很舒服,所有复杂的业务都拆解成不同的单元,完全实现了业务的解耦,编排之后的业务清晰明了,非常方便后续的维护和功能迭代,只是有一点点服务爆炸的迹象。

但当我重构第二个接口,想要复用之前写好的单元时,每个单元内部的上下文值却将它牢牢的嵌入在原有流程中,为了复用这个单元,新的接口就要去兼容原有名称字段,虽然只是一些参数而已,但名不副实的调用关系已经给以后埋雷了,随着这个单元的不断复用,以后所有的流程都要围着它转,而不能真的实现灵活。

就是这个单元里简单的名称却能牢牢锁死它前后流程的关系,极其不可扩展,这个问题是在开发之初没有遇到的,只有复用的多了才会发现。

就比如在商品的子单元中,通过indexFeedsStoreAdaptor这个名称获取门店信息。

List filterAndSortedStores = param.getParamByName("indexFeedsStoreAdaptor");
而在商品的前置流程中,门店信息的获取也只是一个子单元,获取到门店信息后,还可能进行补全,查询标,过滤黑名单门店,过滤医药等合规问题,这些都是单独的流程去处理,因为不同接口对所需的门店数据也不一样,但商品单元中已经写死了只能用indexFeedsStoreAdaptor,可能其他接口在用商品单元时,前面并不需要过滤门店,但还是要将前置门店的单元的名称改为indexFeedsStoreAdaptor,方便后面商品单元来获取,而如果还有广告单元,广告可能获取门店的名称是StoreList,这样就导致同一套流程中,俩个复用的商品单元和广告单元不能一起使用,这样子单元越多,问题越严重。

动态别名的解决方法

首先在流程执行的入口,构造dynamicMap放入Params所在的TreadLocal中,这里就可以指定单元中要使用的名称,与内部固定的名称形成联动

Map<String, String> dynamicMap = new HashMap<>();
dynamicMap.put("sku","mySku");
  params.put("dynamicBeanNameMap", dynamicMap);

在ParamContext中加入动态别名获取方法

public <T> T getParamDynamicName(String name) {
    Map<String, Object> beanMap = this.getBeanMap();
    if (beanMap == null) {
        return null;
    }
    Map<String,String> dynamicNameMap = (Map<String, String>)beanMap.get("dynamicBeanNameMap");
    return (T) beanMap.get(dynamicNameMap.getOrDefault(name,""));
}

在执行单元Handler中可以该方法来拉取参数

public class PreHandler implements Handler<String, Adv> {
    @Override
    public Adv action(ParamContext.Param param, String object) {
        //拿到skuWrapper的执行结果
        Sku sku = param.getParamDynamicName("mySku");
        Store store = param.getParamByName("store");
        return adv;
    }
}

\

动态别名的引入,会对原有流程中参数进行一定的混淆,参数维护上有点麻烦,可以使用枚举类型解决,防止后期不好修改,这样把工作量交给流程入口,依旧保持子单元内部的简洁和通用性,也算是一种取舍。