嵌套模板设计模式优雅解决通用调用第三方服务方案

Posted carl-zhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌套模板设计模式优雅解决通用调用第三方服务方案相关的知识,希望对你有一定的参考价值。

最近公司正在服务化,因为公司是建筑行业的产业互联网。所以对于工人进出场考勤或者核心链路上都是需要用户进行证明“我是我”这个问题。那么就涉及到调用 实名认证人脸比对人脸质量身份证OCR 等第三方服务。

1、概述

之前我们小组的成员把这个业务逻辑抽取了出来,但是服务方一直没有对接。服务调用自身调用实名相关的接口一直不尽如人意,所以现在准备对接我们的服务。可以对第三方服务支持多种渠道路由方式来提高实名服务的成功率。

由于现在的服务只是从之前的业务逻辑里面提取出来的,虽然满足接口的正常调用但是不满足业务的新需求。我就和小伙伴一起添加新的逻辑以及重构了一下代码来满足业务需求。

标题中提到的是嵌套模板设计模式优雅解决通用调用第三方服务方案,需要排除需要业务方保证幂等这种情况(虽然可以做,但是上面的接口更满足于不需要业务方保证幂等)。基于对于业务方的信任以及接口简单性(尽量少的提供参数),上面的那些第三方接口其实是满足的。但是对于短信这种第三方服务来说,就必须需要业务方保证幂等。同样请求号的短信只能发送一次,是否需要重试交给业务方这样比较合理。所以在短信服务设计的时间,就需要让业务方传递请求业务编码以及一个唯一值。

2、系统分析

下面我们来大体的聊一下业务需求:

2.1 响应上游请求号

如果多渠道实名认证也失败,根据渠道响应的异常码决定是否让用户进入人工审核的流程。并且在人工审核单详情页面会显示渠道失败的信息。请求号会在调用我们接口的时候都会生成,这个请求号会关联我们保存的上游的请求信息和响应信息以及我们请求渠道的请求和响应信息。所以我们需要把这个请求号响应给上游。

2.2 响应上游渠道请求概要信息

之前业务方在请求实名相关的接口调用渠道其实是有进行打点的。业务方在考虑接入实名服务接口的时候,提出这样一个诉求。就是所有的服务都会经过实名服务,那么是不是应该把业务打点这个逻辑下层到实名服务。我基于以下二点说服业务方不会接下这个需求:

第一点:首先实名服务抽取出来,就不应该关心上游的业务逻辑。实名服务的功能是帮助上游屏蔽掉调用第三方渠道。包括对接多个渠道、渠道路由、统一异常响应码以及统一响应等。如果业务方需要各个渠道的成功失败率,我觉得是合理的。但是如果是带有业务含义,我觉得不太合理。

第二点:要解决这个问题就是前端直接调用我的服务,然后由前端告诉业务方结果。这种其实是不可取的,因为前端其实是不可信的(请求可以篡改)。只有在一种情况下,实名这个领域可以对外提供接口,就是实名服务已经是平台级服务了,就是除了对公司提供服务以外还对其它公司提供服务(现阶段还没有达到这个能力)。

业务方后面又提出一个需求,就是返回各个渠道的基本信息。本来我是觉得实名服务就是一个渠道,上游本不关心渠道的信息的。但是考虑到业务方需要打点,这一点我妥协了,给上游提供了以下的渠道信息:

渠道响应概念信息

@Data
public class ChannelRequestInfo 

    /**
     * 调用渠道是否成功 0-失败;1-成功
     */
    private int success;
    /**
     * 渠道码
     */
    private String channelCode;
    /**
     * 调用渠道开始时间
     */
    private long startTimeMs;
    /**
     * 调用渠道结束时间
     */
    private long endTimeMs;


注:上游一次请求有可能调用多个渠道,所以返回的是渠道概要信息列表。

3、代码设计

3.1 请求号生成

用户每个请求都会返回请求号,在系统中我们是使用 Feign 进行远程服务调用的,所以通过 Spring MVC 的拦截器(HandlerInterceptor)机制,在请求的时候会预先生成唯一一个请求号,并且保存到 ThreadLocal 当中。

ServiceContextHandlerInterceptor.java

public class ServiceContextHandlerInterceptor extends HandlerInterceptorAdapter 

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
        ServiceContext serviceContext = ServiceContext.createEmpty();

        IdManager idManager = ApplicationContextHelper.getBean(IdManager.class);
        serviceContext.setRequestId(idManager.getId());

        ServiceContextHolder.set(serviceContext);

        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
        ServiceContextHolder.cleanUp();
    

因为上游请求日志以及请求渠道日志表都需要通过这个请求号关联,所以把它保存到 ThreadLocal 变量当中,整个链路都需要这个参数。

3.2 保存渠道日志以及报警

对于上面提到需要调用的实名认证人脸比对人脸质量身份证OCR 等第三方服务。这些功能都有一些共通点:

  • 保存渠道信息
  • 调用渠道
  • 保存渠道日志
  • 失败报警

基于 Spring 事务模板(TransactionTemplate) 设计思想,我抽取出了 ChannelLogTemplate 这个模板类:

ChannelLogTemplate.java

@Component("channelLogTemplate")
public class ChannelLogTemplate 

    @Resource(name = "channelLogSupport")
    private ChannelLogSupport channelLogSupport;

    public <T> T execute(Object request, ChannelInfo channelInfo, ChannelLogCallback<T> callback) 
        // 添加渠道信息到上下文

        try 
        	// 渠道调用
            T result = callback.doInExecute();

			// 渠道调用成功之后
            callback.processAfterExecuteSuccess();

			// 保存渠道日志
            saveChannelLog(request, channelInfo, null);

			// 响应结果
            return result;
         catch (BizException e) 
        	// 保存渠道日志
            saveChannelLog(request, channelInfo, e);

			// 钉钉报警

			// 抛出异常前处理逻辑
            callback.processBeforeThrowException(e);

            throw e;
         catch (ThirdBizException e) 
        	// 保存渠道日志
            saveChannelLog(request, channelInfo, e);

			// 抛出异常前处理逻辑
            callback.processBeforeThrowException(e);

            throw e;
         catch (Exception e) 
        	// 保存渠道日志
            saveChannelLog(request, channelInfo, e);

			// 钉钉报警

			// 抛出异常前处理逻辑
            callback.processBeforeThrowException(e);

            throw new BizException(ReturnCodeEnum.SYSTEM_ERROR, e.getMessage());
        
    

    /**
     * 保存渠道日志
     *
     * @param request
     * @param channelInfo
     * @param e
     */
    private void saveChannelLog(Object request, ChannelInfo channelInfo, Exception e) 
        // todo
    




@FunctionalInterface
public interface ChannelLogCallback<T> 

    /**
     * 任务操作
     * @return
     */
    T doInExecute();

    default void processAfterExecuteSuccess() 
        // 默认不做事
    

    default void processBeforeThrowException(Exception e) 
        // 默认不做事
    


为了服务出现异常及时做出响应,当调用第三方异常都会触发钉钉报警。而如果是调用第三方是业务异常,比如:说实名认证的时候身份证号与人脸照片不符合的时候,我就会手动抛出 ThirdBizException 异常,这个异常并不会触发钉钉报警。

回调接口 ChannelLogCallback里面有三个方法:

  • doInExecute():调用渠道回调逻辑
  • processAfterExecuteSuccess():成功调用渠道响应时触发,场景是当用户进行银行卡三要素成功之后,我们就会把银行卡号、姓名、身份证号保存到数据库当中。当有同样的请求来的时候后不再走第三方渠道,直接返回成功。
  • processBeforeThrowException():抛出异常前触发,当用于银行卡三要验证失败时,结果会在缓存中缓存一段时间。

3.3 通用响应对象

在上面我们提到我们需要对上游返回二个参数:一个是请求号;另一个是渠道请求概要信息列表。其实需要另外一个参数的,本来我们服务定义的响应对象是:

Resut.java

public class Resut<T> 

	// 响应码
    private String code;

	// 响应信息
    private String msg;

	// 响应对象
    private T data;

本来业务方通过 code 就可以返回这次请求是否成功,现在业务方不管调用渠道是否成功都需要把请求渠道概要信息列表返回。通过 code 只能表示调用实名认证服务成功,并不能代码渠道是否成功。而且响应对象(data)里面可能需要包含其它业务信息。比如,身份证OCR 的时候需要返回身份证信息,但是如果请求渠道失败必须要返回请求渠道的信息身份证信息这个时候就是空的。业务方 通过code并不能知道身份证信息是否有值,如果把判断空值这个逻辑交给业务方那么逻辑就太混乱了而且不利于接口对接的简易性。

我觉得应该由服务提供方提供一种判断机制来保证业务参数是否有值。这个时候需要在通用返回对象里面标记是否调用渠道成功,如果成功了实名认证就保证业务参数必然是有值的。而如果调用渠道成功,就应该是拿不到业务参数的。这个时候上面的 Resut 里面data 这个响应对象必须继承下面通用返回对象:

BaseResponseDto.java

@Data
public class BaseResponseDto 

    /**
     * 是否调用渠道成功
     */
    private boolean success;
    /**
     * 请求ID
     */
    private String requestId;

    /**
     * 请求渠道信息
     */
    private List<RequestInfoDto> requestInfos = new ArrayList<>();

    public BaseResponseDto()
        this.success = true;
    
    
    /**
     * 添加请求信息
     *
     * @param requestInfoDto
     */
    public void addRequestInfo(RequestInfoDto requestInfoDto) 
        requestInfos.add(requestInfoDto);
    


所有的调用第三方接口都需要返回上面的信息,所以基于之前的渠道日志的模板类我在上面一层又套了一层模板,就是文章标题上面的提到的嵌套模板。

ChannelInfoTemplate.java

@Component("channelInfoTemplate")
public class ChannelInfoTemplate 

    @Resource(name = "channelLogTemplate")
    private ChannelLogTemplate channelLogTemplate;

    public <T> T execute(Object request, BaseResponseDto response, ChannelInfo channelInfo, ChannelLogCallback<T> callback) 
		response.setRequestId(ServiceContextHolder.get().getRequestId());

        RequestInfoDto requestInfo = beforeExecute(channelInfo);

        try 
            T result = channelLogTemplate.execute(request, channelInfo, callback);

            afterExecuteSuccess(requestInfo, response);

            return result;
         catch (Exception e) 
            response.setSuccess(false);
            
            afterExecuteException(requestInfo, response);

            throw e;
        
    

    /**
     * 执行操作前
     * @param channelInfo
     * @return
     */
    private RequestInfoDto beforeExecute(ChannelInfo channelInfo) 
        RequestInfoDto requestInfoDto = new RequestInfoDto();
        requestInfoDto.setSuccess(SuccessIntegerEnum.YES.getCode());
        requestInfoDto.setChannelCode(channelInfo.getChannelCode());
        requestInfoDto.setStartTimeMs(System.currentTimeMillis());
        return requestInfoDto;
    

    /**
     * 执行成功操作后
     * @param requestInfo
     */
    private void afterExecuteSuccess(RequestInfoDto requestInfo, BaseResponseDto response) 
        requestInfo.setEndTimeMs(System.currentTimeMillis());
        response.addRequestInfo(requestInfo);
    

    /**
     * 执行异常操作后
     * @param requestInfo
     */
    private void afterExecuteException(RequestInfoDto requestInfo, BaseResponseDto response) 
        requestInfo.setSuccess(SuccessIntegerEnum.NO.getCode());
        requestInfo.setEndTimeMs(System.currentTimeMillis());
        response.addRequestInfo(requestInfo);
    


这个时候并没有处理异常(把异常吃掉),因为调用这个接口的上面有可能会对异常进行特殊处理。

以上是关于嵌套模板设计模式优雅解决通用调用第三方服务方案的主要内容,如果未能解决你的问题,请参考以下文章

嵌套模板设计模式优雅解决通用调用第三方服务方案

还有的时候,会遇到DataGrid里面嵌套DataGrid(重叠嵌套),然后里面的鼠标滚轮无法响应外面的滚动,为此记录下解决方案

模板模式

设计模式-模板方法

如何在 NestJS 中热重载联邦网关

mustache模板引擎