如何设计组件化链路?
一、最常见的三层架构
以我们平常接触较多的三层架构开始:biz-core-common。
二、为什么要用组件化链路
可以想象这样一种场景,有一种产品,处理的逻辑很复杂,代码很长。随着业务的发展,这个产品衍生出很多其它具有类似性质,但处理链路各有差异的产品。此时,怎样去设计代码的结构,才能更好地提高代码的复用性呢? 比如我们常见的18位付款码,对于一次解码链路,就要涉及复杂的逻辑,要经历十余种业务处理阶段,还要考虑一些不影响主链路的弱依赖逻辑。而除了18位otp离线码之外,还有19位离线码、18位在线码、19位在线码、24位db在线码等等。同时又涉及发码、解码、支付token置换等逻辑,对于一个专门处理码的平台,需要定制的逻辑就有百余种。每一种码的每一个流程之间都有差异性,但也有共性,若不做特殊处理,每一种码都在biz层编排业务逻辑,复用core层的方法,其开发的复杂性也是很大的,并且随着产品的持续增多,代码中的分支会越来越多,影响理解、维护与新增。 那么如何最大化的把通用逻辑抽离出来,提高其复用性、可维护性以及可拓展性呢?组件化链路的设计是其中的一种方式。三、组件化链路设计
NodeWrapper便是每个子流程抽像出来的节点,对于18离线条码的业务处理流程barCodeOfflineOtp18ParseProcess,只需要去依次处理每个节点的业务逻辑就好了。通过这样的方式,便可以通过一个统一的流程处理方法,执行所有组件化的业务流程,如下所示。<!-- 18位离线条码-parse处理流程 --><bean id="barCodeOfflineOtp18ParseProcess" class="com.alipay.mobilecodec.component.service.model.ProcessModel"><property name="name" value="barCodeOfflineOtp18ParseProcess" /><property name="nodes"><list><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="siteCodeLocalCheckNode" /><property name="nodeParametersMaping" ref="siteCodeLocalCheckMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="offlineCodePrefixLengthRejectNode" /><property name="nodeParametersMaping" ref="offlineCodePrefixLengthRejectMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="offlineCodeParseInfoBuildNode" /><property name="nodeParametersMaping" ref="offlineCodeParseInfoBuildMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="acauthIndexQueryNode" /><property name="nodeParametersMaping" ref="acauthIndexQueryMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="offlineCodeTerminalUserInfoProcessNode" /><property name="nodeParametersMaping" ref="offlineCodeTerminalUserInfoProcessMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="acauthTerminalQueryNode" /><property name="nodeParametersMaping" ref="acauthTerminalQueryMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="parseTerminalInfoFillNode" /><property name="nodeParametersMaping" ref="parseTerminalInfoFillMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><!-- 等待跳转节点 --><property name="nodeName" value="sdkBizSourceConvertNode" /><property name="node" ref="sdkBizSourceConvertNode" /><property name="nodeParametersMaping" ref="sdkBizSourceConvertMapping" /></bean><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeWrapper"><property name="node" ref="offlineCodeDecodeBizIdConvertNode" /><property name="nodeParametersMaping" ref="offlineCodeDecodeBizIdConvertMapping" /></bean></list></property></bean>
通过业务上下文context保存每个节点执行的结果并向下传递,首先执行nodeProcess主链路强依赖的节点,节点的执行结果影响链路推进,而后extensionProcess执行弱依赖的节点,执行失败并不影响主流程。这样我们对于业务逻辑的处理就变为了根据传参识别业务身份->执行对应流程->返回业务执行流程上下文context。而对于业务流程的处理,都只用在xml配置一下node,便可复用现有的逻辑。 更进一步地,对于ProcessModel以及NodeWrapper,我们应该怎么设计才能承载上述的功能实现?public ProcessContext execute(ProcessContext context, ProcessModel process) {LoggerUtil.info(LOGGER, "开始执行流程, processName=", process.getName(), ".");/*** node处理*/context = nodeProcess(context, process);/*** extension处理*/extensionProcess(context, process);return context;}
3.2.1 节点设计
ProcessModel与NodeWrapper之间的关系如下自然,节点执行的能力要交予节点自己,一个节点的内部参数如下public class ProcessModel extends ToString {/*** 流程名称*/private String name;/*** 普通节点列表*/private List<NodeWrapper> nodes;/*** 扩展节点列表*/private List<ExtensionWrapper> extensions;
一个节点会有节点名称nodeName、真正的执行节点node、以及参数转换的nodeParametersMaping。每个参数的设计都有其特殊的考虑,首先是业务节点名称,这个参数有什么意义呢?其实是在业务执行的过程中,可能会有分支链路的出现,根据不同的结果,可能会跳过一些节点。此时便可以指定下一个执行节点的nodeName,实现节点跳转的功能。 对于ProcessNode来说,它才是严格意义上的执行节点。public class NodeWrapper {/*** 业务节点名称*/private String nodeName;/*** 业务节点*/private ProcessNode node;/*** 参数转化*/private NodeParametersMaping nodeParametersMaping = null;}
processNode是一个接口,每个节点给予其具体实现。此处的入参是Map<String, Object> params,上文已经说到,节点处理是通过context传递的,但对于每个节点而言,并不需要所有context的所有参数。所以对于下一节点执行的时候,只需通过context取出需要的部分执行即可。这引发了下一个问题,如何设计一种通用的方法,去转换参数呢,这就是NodeParametersMaping所要做的。public interface ProcessNode {/*** 入参. 每个节点发布的时候,必须定义出入参数表.** @param params* @return*/ProcessNodeResult process(Map<String, Object> params);
这里contextToNode便是用于从context中取出某些参数,转换成节点入参的配置。nodeToContextMapping便是节点执行结果放进context的配置。那这里为什么是一个Map<String, NodeToContextMaping>类型的数据呢,主要是因为节点执行可能成功可能失败,失败的结果也有很多,此处主要是根据不同的执行结果,取出相应的NodeToContextMaping,往context里填充数据。NodeToContextMaping如下public class NodeParametersMaping {/*** context to node 的转化配置*/private Map<String, String> contextToNode = null;/*** node to context 的转化配置* Map<结果码,Map<结果属性,上下文目标设置属性>>*/private Map<String, NodeToContextMaping> nodeToContextMapping = null;/*** 配置死的静态入参 key = params的key*/private Map<String, Object> staticParamsToNode = null;}
这里的processMethod便是用于指定下一个执行节点的,是执行下一个执行节点,还是跳转节点。nodeToContext便是将节点执行结果放进context的配置文件,不同的执行结果会有不同的配置。该配置也是事先在xml中配置实现的public class NodeToContextMaping {/*** Node中的result.resultCode转化成流程里的code*/private int processCode = -1;/*** 后续如何处理*/private NodeProcessMethod processMethod = NodeProcessMethod.nextNode();/*** Node中的result结果转化到contextdata中*/private Map<String, String> nodeToContext = null;/*** 操作类的*/private Map<String, String> contextToContext = null;/*** 静态参数设置到context中*/private Map<String, Object> staticParamsToContext = null;}
以上述为例,当key=1时,说明发生错误,该节点processMethod是ERROR_NODE不需要向下执行。当key=1时,将执行结果中的terminalResult放入context中的terminalResult,并且processMethod是默认值,执行下一节点。 至此,关于节点的设计思路便结束了。通过这样的设计,节点执行的顺序、不同的分支逻辑都可以覆盖到了,并且对于新增业务逻辑,在xml中配置即可,不需要重新编排复杂的业务逻辑。此时又有一个问题,context和node间具体传递参数的逻辑是怎样实现的呢?<property name="nodeToContextMapping"><map><entry key="1"><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeToContextMaping"><property name="processCode" value="101" /><property name="processMethod"><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeProcessMethod"><property name="nodeProcessWay" value="ERROR_NODE"/></bean></property></bean></entry><entry key="3"><bean class="com.alipay.mobilecodec.component.service.node.wrapper.NodeToContextMaping"><property name="nodeToContext"><map><entry key="bizObj.terminalResult" value="terminalResult"/></map></property></bean></entry></map></property>
3.2.2 参数转换的实现
context是一个业务执行上下文,上下文类内中包含了所有执行过的节点结果,那是如何设计一个通用的转换方法,使得只需要在xml中配置参数转换map,就可以实现节点参数与context之间的参数转换的呢?以一个context中取参数转换node入参的case为例,一个contexToNode的定义如下这里的配置的key:codeParseInfo.macauthIndex,表示的含义是从context中取值,相当于取出context.codeParseInfo.macauthIndex,而value:acauthIndex表示的含义是节点node的入参params的key。大概的含义是params.put(acauthIndex, context.codeParseInfo.macauthIndex)。具体的实现方式为<bean id="acauthIndexQueryMapping"class="com.alipay.mobilecodec.component.service.node.wrapper.NodeParametersMaping"><property name="contextToNode"><map><entry key="codeParseInfo.macauthIndex" value="acauthIndex" /></map></property>
通过PropertyUtils.getProperty()方法,取出context.codeParseInfo.macauthIndex的值,再通过PropertyUtils.setProperty()方法,将值放入params当中。从而实现params.put(acauthIndex, context.codeParseInfo.macauthIndex)的作用。public void toNode(ProcessContext context, Map<String, Object> params)throws IllegalAccessException,InvocationTargetException,NoSuchMethodException {if (MapUtils.isNotEmpty(contextToNode)) {Iterator<Entry<String, String>> it = contextToNode.entrySet().iterator();while (it.hasNext()) {Entry<String, String> en = it.next();String sourceProperty = en.getKey();String targetProperty = en.getValue();Object val = PropertyUtils.getProperty(context, sourceProperty);PropertyUtils.setProperty(params, targetProperty, val);}}}
代码中的parseMatchers装有所有的待解析码的匹配规则,依次取出parseMatcher,判断当前传入的码值是否符合当前规则,若符合,则组装结果ParseMatchResult。parseMatchers的配置如下public ParseMatchResult getParseMatchResultByCode(String srcCode) {if (parseMatchers != null && parseMatchers.size() > 0) {Iterator<ParseMatcher> pmIt = parseMatchers.iterator();while (pmIt.hasNext()) {ParseMatcher pm = pmIt.next();if (pm != null && pm.getRegexPatternList() != null&& pm.getRegexPatternList().size() > 0) {if (pm.isNumeric() != StringUtil.isNumeric(srcCode)) {continue;}if (!pm.getCodeLengthSet().contains(srcCode.length())) {continue;}String unconfusedCode;if (pm.isConfused()) {unconfusedCode = UnConfuseUtil.getUnConfusedCode(srcCode, pm.getFullPrefixLength());} else {unconfusedCode = srcCode;}Iterator<Pattern> partternsIt = pm.getRegexPatternList().iterator();while (partternsIt.hasNext()) {Pattern pattern = partternsIt.next();if (pattern.matcher(unconfusedCode).matches()) {return buildParseMatchResult(srcCode, unconfusedCode, pm);}}} else {throw new RuntimeException("(getParseMatchResultByCode)invalid matcher!");}}}return null}
通过对每种码的规则在xml中进行配置,解析码的时候依次去解析即可。这是parse解析码的业务类型,拓展到其余的业务类型,业务身份的模型关系如下<property name="parseMatchers"><list><ref bean="BAR_CODE_GRAY_ONLINE_16_MATCHER"/><!--<ref bean="BAR_CODE_ONLINE_17_MATCHER"/>--><ref bean="BAR_CODE_ONLINE_18_MATCHER"/><ref bean="BAR_CODE_ONLINE_18_DB_MATCHER"/><ref bean="BAR_CODE_OFFLINE_M2_18_MATCHER"/><ref bean="BAR_CODE_OFFLINE_OTP_18_MATCHER"/><!--<ref bean="BAR_CODE_ONLINE_19_MATCHER"/>--><!--<ref bean="BAR_CODE_ONLINE_19_DB_MATCHER"/>--><ref bean="BAR_CODE_OFFLINE_OTP_19_MATCHER"/><ref bean="BAR_CODE_CGCP_ONLINE_24_DB_MATCHER"/><ref bean="SECURITY_CODE_35_MATCHER"/><ref bean="AUTH_TOKEN_37_MATCHER"/><ref bean="HEALTH_INSURANCE_CODE_28_MATCHER"/><ref bean="DCEP_STANDARD_ONLINE_19_DB_MATCHER"/><ref bean="DCEP_MIX_ONLINE_19_DB_MATCHER"/><ref bean="BAR_CODE_DCEP_SITE_MATCHER"/><ref bean="BAR_CODE_DCEP_OFFLINE_OTP_19_MATCHER"/><ref bean="DCEP_MIX_ONLINE_19_CACHE_MATCHER"/><ref bean="DCEP_STANDARD_ONLINE_19_CACHE_MATCHER"/></list></property><bean id="BAR_CODE_OFFLINE_OTP_18_MATCHER"class="com.alipay.mobilecodec.component.service.matcher.ParseMatcher"><property name="regexList"><list><value>^(2[5-9]|30)([0-7]\d{2}|8[0-7]\d|98\d|99[0-8])\d{13}$</value><value>^(2[5-9]|30)6\d{16}$</value></list></property><property name="codeBizType" value="BAR_CODE_OFFLINE_OTP_18"/><property name="codeType" value="BAR_CODE"/><property name="codeLengthSet"><set><value>18</value><value>19</value></set></property><property name="confused" value="true"/><property name="useOtp" value="true"/><property name="barCodeUidFomate" value="FROM_ACAUTH"/></bean>
四、总结
组件化链路是一种解决节点间复用问题的设计思想,在老链路的基础上,提高了代码复用性、可维护性以及可拓展性。某种特定的方法并不是最重要的,最重要的还是提高代码复用性,解耦的思想。