Spring中实现微服务综合交易的验证和升级

了解金融科技中的综合交易如何帮助确保质量和信心,验证重大更新或新功能后的业务功能。

在金融科技应用程序、移动应用程序或网络中,在贷款申请等领域部署新功能需要仔细验证。使用真实用户数据(尤其是个人身份信息 (PII))的传统测试提出了重大挑战。综合交易提供了一种解决方案,可以在安全且受控的环境中彻底测试新功能,而不会损害敏感数据。

通过模拟应用程序内的真实用户交互,综合交易使开发人员和 QA 团队能够识别受控环境中的潜在问题。综合交易有助于确保金融应用程序的各个方面在任何重大更新或新功能推出后都能正常运行。在本文中,我们深入研究了使用synthetic合成交易的方法之一。

金融应用的综合交易
1、主要业务实体
每个金融应用程序的核心都是一个关键实体,无论是客户、用户还是贷款申请本身。该实体通常由唯一标识符定义,作为系统内交易和操作的基石。该实体首次创建时的起始点提供了将其分类为合成或真实的战略机会。这种分类至关重要,因为它决定了实体将经历的交互的性质。

从一开始就将实体标记为synthetic 合成或用于测试目的,可以在应用程序生态系统内的测试数据和真实数据之间进行清晰的划分。随后,与该实体进行的所有交易和操作都可以安全地识别为综合交易的一部分。这种方法确保应用程序的功能可以在现实环境中得到彻底的测试。

2、拦截和管理综合交易
实现综合交易的一个关键组成部分在于在HTTP请求级别拦截和管理这些交易。利用 Spring 的 HTTP 拦截器机制,我们可以通过检查特定的 HTTP 标头来识别和处理合成交易。

下面代码SyntheticTransactionInterceptor充当主要的看门人,确保只有被识别为合成的交易才允许通过测试路径。下面是实现:

@Component
public class SyntheticTransactionInterceptor implements HandlerInterceptor {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    SyntheticTransactionService syntheticTransactionService;

    @Autowired
    SyntheticTransactionStateManager syntheticTransactionStateManager;

    @Override
    public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object object) throws Exception {
        String syntheticTransactionId = request.getHeader("x-synthetic-transaction-uuid");
        if (syntheticTransactionId != null && !syntheticTransactionId.isEmpty()){
            if (this.syntheticTransactionService.validateTransactionId(syntheticTransactionId)){
                logger.info(String.format(
"Request initiated for synthetic transaction with transaction id:%s", syntheticTransactionId));
                this.syntheticTransactionStateManager.setSyntheticTransaction(true);
                this.syntheticTransactionStateManager.setTransactionId(syntheticTransactionId);
            }
        }
        return true;
    }
}

在这种实现方式中,拦截器会寻找一个携带 UUID 的特定 HTTP 标头(x-synthetic-transaction-uuid)。这个 UUID 不是普通的标识符,而是一个经过验证、有时间限制的密钥,指定用于synthetic 交易。验证过程包括检查 UUID 的有效性、寿命以及是否曾被使用过,从而确保synthetic 测试过程的安全性和完整性。

 synthetic  ID 经synthetic 交易拦截器(SyntheticTransactionInterceptor)验证后,synthetic 交易状态管理器(SyntheticTransactionStateManager)将在维护当前请求的synthetic 上下文方面发挥关键作用。SyntheticTransactionStateManager 在设计时考虑到了请求范围,这意味着它的生命周期与单个 HTTP 请求息息相关。这种范围界定对于在应用程序更广泛的操作上下文中保持合成交易的完整性和隔离性至关重要。通过将状态管理器与请求范围绑定,应用程序可以确保合成交易状态不会渗入无关的操作或请求中。以下是synthetic 状态管理器的实现:

@Component
@RequestScope
public class SyntheticTransactionStateManager {
    private String transactionId;
    private boolean syntheticTransaction;

    public String getTransactionId() {
        return transactionId;
    }

    public void setTransactionId(String transactionId) {
        this.transactionId = transactionId;
    }

    public boolean isSyntheticTransaction() {
        return syntheticTransaction;
    }

    public void setSyntheticTransaction(boolean syntheticTransaction) {
        this.syntheticTransaction = syntheticTransaction;
    }
}

当我们持久化关键实体(无论是客户、用户还是贷款应用程序)时,应用程序的服务层或存储层会咨询合成交易状态管理器(SyntheticTransactionStateManager),以确认交易的合成性质。如果交易确实是合成的,应用程序不仅会继续保留合成标识符,还会保留实体本身是合成的指示符。这就为合成交易流程奠定了基础。这种方法可确保从实体被标记为合成的那一刻起,所有相关操作和未来的 API(无论是涉及数据处理还是业务逻辑执行)都将以受控的方式进行。

对于从金融应用启动的进一步 API 调用,在到达微服务时,我们会根据所提供的令牌或实体标识符为该特定请求加载应用上下文。在加载上下文期间,我们会确定关键业务实体(如贷款申请、用户/客户)是否是合成的。如果是,我们就会将状态管理器的 syntheticTransaction 标志设置为 true,并分配应用上下文中的合成交易ID。

这种方法无需为应用流程中的后续调用传递合成交易ID 标头。我们只需要在创建关键业务实体的初始 API 调用中发送合成交易 ID。由于这一步涉及使用金融应用程序(无论是移动平台还是网络平台)可能不支持的显式标头,因此我们可以使用 Postman 或类似工具手动进行首次 API 调用。之后,应用程序就可以继续执行金融应用程序本身的其余流程。除了管理应用程序内的合成交易外,考虑外部第三方 API 调用在合成交易上下文中的行为也至关重要。

外部第三方 API 交互
在处理具有个人身份信息(PII)的关键实体的金融应用程序中,我们通常利用外部第三方服务对用户提供的数据进行验证和欺诈检查。这些服务对于 PII 验证和信用局报告检索等任务至关重要。但是,在处理合成交易时,我们不能调用这些第三方服务。

解决方案包括在合成交易中为这些外部服务创建模拟响应或使用存根。这种方法可以确保合成交易的处理逻辑与真实交易相同,但无需向第三方服务提交实际数据。相反,我们会模拟这些服务在被调用真实数据时所提供的响应。这样,我们就能彻底测试应用程序的集成点和数据处理逻辑。

下面是提取局报告的代码片段。该调用是创建关键实体的 API 调用的一部分,随后我们将调用申请人的局报告:

@Override
@Retry(name = "BUREAU_PULL", fallbackMethod = "getBureauReport_Fallback")
public CreditBureauReport getBureauReport(SoftPullParams softPullParams, ErrorsI error) {
    CreditBureauReport result = null;
    try {
        Date dt = new Date();
        logger.info(
"UWServiceImpl::getBureauReport method call at :" + dt.toString());
        CreditBureauReportRequest request = this.getCreditBureauReportRequest(softPullParams);
        RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate(softPullParams.getUserLoanAccountId(),
"BUREAU_PULL",
                softPullParams.getAccessToken(),
"BUREAU_PULL", error);
        HttpHeaders headers = this.getHttpHeaders(softPullParams);
        HttpEntity<CreditBureauReportRequest> entity = new HttpEntity<>(request, headers);
        long startTime = System.currentTimeMillis();
        String uwServiceEndPoint =
"/transaction";
        String bureauCallUrl = String.format(
"%s%s", appConfig.getUnderwritingTransactionApiPrefix(), uwServiceEndPoint);
        if (syntheticTransactionStateManager.isSyntheticTransaction()) {
            result = this.syntheticTransactionService.getPayLoad(syntheticTransactionStateManager.getTransactionId(),
                    
"BUREAU_PULL", CreditBureauReportResponse.class);
            result.setCustomerId(softPullParams.getUserAccountId());
            result.setLoanAccountId(softPullParams.getUserLoanAccountId());
        } else {
            ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(bureauCallUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
            result = responseEntity.getBody();
        }
        long endTime = System.currentTimeMillis();
        long timeDifference = endTime - startTime;
        logger.info(
"Time taken for API call BUREAU_PULL/getBureauReport call 1: " + timeDifference);
    } catch (HttpClientErrorException exception) {
        logger.error(
"HttpClientErrorException occurred while calling BUREAU_PULL API, response string: " + exception.getResponseBodyAsString());
        throw exception;
    } catch (HttpStatusCodeException exception) {
        logger.error(
"HttpStatusCodeException occurred while calling BUREAU_PULL API, response string: " + exception.getResponseBodyAsString());
        throw exception;
    } catch (Exception ex) {
        logger.error(
"Error occurred in getBureauReport. Detail error:", ex);
        throw ex;
    }
    return result;
}

上面的代码片段非常复杂,但我们不必深究其中的细节。我们需要关注的是下面的代码片段:

if (syntheticTransactionStateManager.isSyntheticTransaction()) {
    result = this.syntheticTransactionService.getPayLoad(syntheticTransactionStateManager.getTransactionId(),
            "BUREAU_PULL", CreditBureauReportResponse.class);
    result.setCustomerId(softPullParams.getUserAccountId());
    result.setLoanAccountId(softPullParams.getUserLoanAccountId());
} else {
    ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(bureauCallUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
    result = responseEntity.getBody();
}


它通过 SyntheticTransactionStateManager 检查合成交易。如果为真,它就会调用内部服务 SyntheticTransactionService 来获取合成局报告数据,而不是求助于第三方。

Synthetic 合成数据服务
合成数据服务 SyntheticTransactionServiceImpl 是一种通用实用程序服务,其职责是从数据存储中提取合成数据、解析数据并将其转换为作为参数传递的对象类型。下面是该服务的实现:

@Service
@Qualifier("syntheticTransactionServiceImpl")
public class SyntheticTransactionServiceImpl implements SyntheticTransactionService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    SyntheticTransactionRepository syntheticTransactionRepository;

    @Override
    public <T> T getPayLoad(String transactionUuid, String extPartnerServiceType, Class<T> responseType) {
        T payload = null;
        try {
            SyntheticTransactionPayload syntheticTransactionPayload = this.syntheticTransactionRepository.getSyntheticTransactionPayload(transactionUuid, extPartnerServiceType);
            if (syntheticTransactionPayload != null && syntheticTransactionPayload.getPayload() != null){
                ObjectMapper objectMapper = new ObjectMapper()
                        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                payload = objectMapper.readValue(syntheticTransactionPayload.getPayload(), responseType);
            }
        }
        catch (Exception ex){
            logger.error(
"An error occurred while getting the synthetic transaction payload, detail error:", ex);
        }
        return payload;
    }

    @Override
    public boolean validateTransactionId(String transactionId) {
        boolean result = false;
        try{
            if (transactionId != null && !transactionId.isEmpty()) {
                if (UUID.fromString(transactionId).toString().equalsIgnoreCase(transactionId)) {
                   
//Removed other validation checks, this could be financial application specific check.
                }
            }
        }
        catch (Exception ex){
            logger.error(
"SyntheticTransactionServiceImpl::validateTransactionId - An error occurred while validating the synthetic transaction id, detail error:", ex);
        }
        return result;
    }


通过通用方法 getPayLoad(),我们提供了高度的重用性,能够返回各种类型的合成响应。这就减少了为不同的外部交互提供多个特定模拟服务的需要。

为了存储不同类型外部第三方服务的不同有效载荷,我们使用了如下通用表格结构:

CREATE TABLE synthetic_transaction (
  id int NOT NULL AUTO_INCREMENT,
  transaction_uuid varchar(36)
  ext_partner_service varchar(30)
  payload mediumtext
  create_date datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

ext_partner_service:这是外部服务标识符,我们从表中提取其有效载荷。在上面的局报告示例中,它将是 BUREAU_PULL。

结论
在我们对金融科技应用中合成交易的探索中,我们强调了合成交易在提高金融科技解决方案的可靠性和完整性方面的作用。通过利用合成交易,我们可以模拟真实的用户交互,同时规避与处理真实个人身份信息(PII)相关的风险。这种方法使我们的开发人员和质量保证团队能够在安全可控的环境中严格测试新功能和更新。

此外,我们通过 HTTP 拦截器和状态管理器等机制整合合成交易的策略,展示了一种适用于广泛应用的多功能方法。这种方法不仅简化了合成交易的整合,还大大提高了可重用性,从而无需为每个第三方服务交互设计独特的工作流程。

这种方法大大提高了金融应用解决方案的可靠性和安全性,确保可以放心地部署新功能。