掘金 后端 ( ) • 2024-04-25 11:33

一、背景介绍

还是基于《消失的字段》《消失的消息》两篇文章中提到的供应链金融项目,项目业务涉及向银行申请授信、发起融资等,开发过程中需要跟银行进行接口对接联调。与银行对接过的朋友应该都知道,跟银行进行接口对接联调是一项艰巨而缓慢的工作,而且配合度无法把控,项目进度无法得到保证。而我们这个项目不止对接一家银行,需要对接多家银行,客户可以根据自己的资产向平台对接的多家银行发起融资申请。

基于上述情况,为了保证项目内部开发可控,需要针对银行对接服务实现一套挡板机制,只需要根据银行接口响应数据结构,实现内部开发流程闭环。该挡板机制由如下要求:

(1)可以灵活对指定银行实施挡板机制,比如:只对华夏银行实施挡板机制;

(2)可以灵活对指定接口方法实施挡板机制,比如:只对融资申请方法实施挡板机制;

(3)可以不停服变更并生效挡板机制,比如:撤销对华夏银行的挡板,增加对工商银行的挡板;

二、实现方案

1、方案概述

目前项目已存在银行接口调用服务接口FinancingService及其实现类FinancingServiceImpl,为了保证开闭的设计原则,实现方案采用自定义注解和AspectJ技术,通过Nacos配置实现挡板机制的灵活切换。

首先定义一个银行服务接口的挡板实现类,实现挡板逻辑;然后定义一个挡板注解BankBaffle,应用于需要进行挡板的方法上面;最后编写注解拦截器,根据注解配置信息,进行相应的挡板服务调用。

2、具体实现

2.1 已有服务和接口描述

项目已有银行服务对接接口FinancingService及其实现类FinancingServiceImpl。

@Service
public interface FinancingService {
    Resp apply(ApplyReq applyReq);    // 融资申请
    Resp applyResult(ApplyResultQueryReq applyResultQueryReq);    // 融资申请结果查询
}

@Service
public class FinancingServiceImpl implements FinancingService {
    @Override
    public Resp apply(ApplyReq applyReq) {
        ...
    }
    
    @Override
    public Resp applyResult(ApplyResultQueryReq applyResultQueryReq) {
        ...
    }
}

2.2 定义银行接口挡板实现类

@Service
public class BaffleFinancingServiceImpl implements FinancingService {
    @Override
    public Resp apply(ApplyReq applyReq) {
        // 对应方法的挡板逻辑
        ...
    }
    
    @Override
    public Resp applyResult(ApplyResultQueryReq applyResultQueryReq) {
        // 对应方法的挡板逻辑
        ...
    }
}

2.3 定义挡板注解

package com.xxx.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BankBaffle {
    //  默认启动挡板逻辑
    String enable() default "true";

    // 挡板实现类,必须与业务类实现相同的接口
    String className();

    // 拦截指定的银行请求
    String bankIds();
}

2.4 实现注解切面

@Aspect
@Component
public class BankBaffleAspect implements EnvironmentAware {
    private static final Logger log = LoggerFactory.getLogger(BankBaffleAspect.class);

    // 挡板默认开启
    private static final String BAFFLE_ENABLE_DEFAULT = "true";

    private static final String BAFFLE_BANKID_ALL = "ALL";

    private Environment environment;

    // 挡板对象Map,起缓存作用
    @Resource
    private Map<String, FinancingService> map;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * 定义切入点
    */
    @Pointcut("@annotation(com.xxx.annotation.BankBaffle) ")
    public void entryPoint() {
        // 无需内容
    }

    @Around("entryPoint()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature sig = joinPoint.getSignature();
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        MethodSignature msig = (MethodSignature) sig;

        Object target = joinPoint.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        // 获取注解
        BankBaffle bankBaffle = currentMethod.getAnnotation(BankBaffle.class);
        // 解析注解参数
        String enable = this.environment.resolvePlaceholders(bankBaffle.enable());
        String classname = this.environment.resolvePlaceholders(bankBaffle.className());
        String bankIds = this.environment.resolvePlaceholders(bankBaffle.bankIds());

        if(BAFFLE_ENABLE_DEFAULT.equalsIgnoreCase(enable)) {
            // 容器中是否加载注入指定的挡板实例
            FinancingService baffleObj = map.get(classname);
            if(null == baffleObj) {
                return joinPoint.proceed();
            }

            // 是否配置指定执行挡板程序的银行
            String[] bankIdArray = StringUtils.split(bankIds, ",");
            if(null == bankIdArray || bankIdArray.length == 0) {
                return joinPoint.proceed();
            }

            List<String> bankIdList = Arrays.asList(bankIdArray);
            // 如果bankid值配置了‘ALL’,则所有银行请求全部进入挡板; 对指定的银行请求执行挡板程序,否则执行原程序;
            JSONObject methodParamValue = (JSONObject) JSON.toJSON(joinPoint.getArgs()[0]);
            String bankId = methodParamValue.getJSONObject("head").getString("bankid");
            if(bankIdList.contains(BAFFLE_BANKID_ALL) || bankIdList.contains(bankId)) {
                String methodName = msig.getMethod().getName();
                log.info("执行挡板程序:{}.{}", classname, msig.getMethod().getName());
                Method m = baffleObj.getClass().getMethod(methodName, msig.getParameterTypes());
                return m.invoke(baffleObj, joinPoint.getArgs());
            } else {
                return joinPoint.proceed();
            }
        } else {
            return joinPoint.proceed();
        }
    }
}

2.5 挡板应用

(1)在原银行对接接口实现类上增加注解@Primary,由于增加了BaffleFinancingServiceImpl,FinancingService接口存在两个实现类,自动注入将出现问题。加上@Primary注解表示优先注入FinancingServiceImpl。

@Primary
@Service
public class FinancingServiceImpl implements FinancingService {
    @Override
    public Resp apply(ApplyReq applyReq) {
        ...
    }
    
    @Override
    public Resp applyResult(ApplyResultQueryReq applyResultQueryReq) {
        ...
    }
}

(2)对需要进行挡板的方法上增加@BankBaffle注解,具体如下:

@Primary
@Service
public class FinancingServiceImpl implements FinancingService {
    
    @Override
    @BankBaffle(enable = "${baffle.bank.enable}",
        className = "${baffle.bank.classname}",
        bankIds = "${baffle.bank.method.apply-ids}"
    )
    public Resp apply(ApplyReq applyReq) {
        ...
    }
    
    @Override
    @BankBaffle(enable = "${baffle.bank.enable}",
        className = "${baffle.bank.classname}",
        bankIds = "${baffle.bank.method.apply-ids}"
    )
    public Resp applyResult(ApplyResultQueryReq applyResultQueryReq) {
        ...
    }
}

(3)在Nacos增加如下配置:

baffle:
    bank:
        enable: true
        classname: com.xxx.service.impl.BaffleFinancingServiceImpl
        apply-ids: ALL    //ALL表示所有银行都拦截,也可以指定bankId,与请求头匹配一致则进入挡板程序

三、总结

针对银行服务接口编写了对应的挡板服务实现类,通过自定义注解BankBaffle和注解拦截器BankBaffleAspect实现挡板服务与正常服务的切换,切换判断需要根据在Nacos中配置的挡板参数。

银行服务接口中哪个方法需要增加挡板机制,只需加上注解BankBaffle即可;需要拦截哪些银行,只需要在Nacos中配置对应的银行id即可;关闭和开启挡板服务只需要在Nacos中修改enable参数即可。

最后,上述方案还有比较大的优化空间,感兴趣的同学可以自由发挥。火花因碰撞而闪现!