基于Forest实践|如何更优雅的统一处理请求签名

652 阅读4分钟

Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。

本文基于Forest实践,来源于实际业务场景需求,诞生了本篇文章,通过本篇你可以学习到如何更优雅的统一处理请求签名,相信你会有所收获!

@[toc]

1、背景介绍

我们之所以选择Forest,而不选择Spring RestTemplate,是因为Forest支持你通过定义一个接口,通过其相关增强注解,你就可以像在本地调用方法一样,实现调用HTTP请求方法。同时,它提供了一系列缺省配置参数,使得你可以很简单的配置,就可以以最小的研发代码量实现你的需求。

Forest实现原理跟MyBatisMapperProxy类似,Spring容器初始化时,通过扫描包下使用了@BaseRequest的相关接口,通过动态代理生成Http访问类。

当我们程序调用接口方法时,就是从Forest上下文对象中获取预先初始化的代理类,然后委托OkHttp3或者HttpClient实现http调用,只不过Forest优雅的封装了这一系列背后工作,使得我们的代码更精简,更优雅,所以它也是一个非常轻量级的Http工具。

本文介绍,就是当我们业务场景中定义了大量的client接口,但是这些提供接口的三方API都需要我们在请求header增加签名,这时候我们具体应该怎么做呢?

2、实现思路

2.1、基于@Header增强

代码样例

/**
 * @description: 抵押客户公众号服务端
 * @Date : 2021/9/7 3:37 PM
 * @Author : 石冬冬-Seig Heil
 */
@BaseRequest(
        baseURL = "${domain.car_mortgage_customer}",
        interceptor = {CarMortgageCustomerInterceptor.class,SimpleForestInterceptor.class}
)
public interface CarMortgageCustomerApi {
    /**
     * 推送公众号模板消息
     * 当前消息模板在<car-mortgage-customer>应用中,基于xdiamond配置。
     * 对于抵押网络只需要双方拟定好模板中的变量即可。
     * @param dto
     * @return
     */
    @PostRequest("/mortgage/mortgageNotice")
    @TraceMarker(appCode = "${$1.mortgageCode}",traceType = TraceTypeEnum.CAR_MORTGAGE_CUSTOMER_INVOKE)
    RespDTO<String> notice(@Header("sign") String sign, @JSONBody MortgageNoticeDTO dto);
}

如上图,我们通过对方法参数通过@Header增强,然后设置注解的sign,这时候程序调用的时候,就会吧sign的值设置为请求header中的sign

方案总结

我们从上述代码样例中发现,倘若Client提供的方法非常多,我们需要每个成员方法都需要增加一个参数String sign,这意味着所有涉及调用该方法的时候,需要给sign参数赋值,如果有一天三方接口邀请请求header再增加其他参数时,我们就会把涉及到所有的调用地方都得做变动,这样以来难免波及范围大,不符合”开闭原则“。

2.2、基于Proxy委托代理

其实我们可以思考一下,是否可以改善上述方案面临的缺陷,其实我们可以增加一个代理类,这样对于调用Client的地方,统一委托给代理类,通过代理类进而做相关增强处理,后续需要变动的话,在代理类修改相应业务逻辑即可。

代码样例

/**
 * @description: 抵押客户公众号服务端
 * @Date : 2021/9/7 3:41 PM
 * @Author : 石冬冬-Seig Heil
 */
@Component
public class CarMortgageCustomerApiProxy {

    @Value("${forest.variables.domain.car_mortgage_customer.sign}")
    String fortuneCatSign;

    @Autowired
    CarMortgageCustomerApi carMortgageCustomerApi;
    /**
     * 推送通知
     * @param dto
     * @return
     */
    public RespDTO<String> notice(MortgageNoticeDTO dto){
        return carMortgageCustomerApi.notice(fortuneCatSign,dto);
    }
}

方案总结

从上述代码我们可以看出,由于CarMortgageCustomerApi的成员方法notice(@Header("sign") String sign, @JSONBody MortgageNoticeDTO dto);需要参数sign,那我们通过代理类CarMortgageCustomerApiProxy统一处理,通过@Value获取配置,进而设置给方法成员方法sign,如果有一天notice方法,在基于@Header注解增强,增加其他参数,我们只需要在代理类中涉及的方法统一添加即可,无需相应的业务方调用修改即可。

事实上,我们通过代理模式Proxy,本来响应业务逻辑调用CarMortgageCustomerApi的成员方法,这时候我们通过委托代理类CarMortgageCustomerApiProxy来发布方法,本质上target依然还是CarMortgageCustomerApi

不过我们发现这虽然解决了相应业务不需要变动这个问题,但是后续client增加请求header参数,需要Proxy对应的代理类的所有成员方法都需要变动,而且如果调用三个应用服务,就需要增加三个Proxy,难道就没有更好的方法了?有,继续往下看!!!

2.3、基于Interceptor

代码样例

首先我们可以定义一个增强注解,提供一个内部枚举类ApiEnum,该类用于声明具体哪些client需要进行header签名处理。该增强注解有一个ApiEnum signFor();成员方法,需要相应增强时,指明使用ApiEnum的枚举成员。

/**
 * @description: request header 签名注解
 * @Date : 2021/11/4 4:00 PM
 * @Author : 石冬冬-Seig Heil
 */
@Documented
@RequestAttributes
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequestSigned {
    /**
     * 签名枚举
     * @return
     */
    ApiEnum signFor();

    /**
     * Api 枚举类
     */
    enum ApiEnum{
        /**
         * 抵押客户公众号
         */
        CarMortgageCustomerApi,
        /**
         * 抵押专员公众号
         */
        CarFortuneCatApi
    }
}

然后定义一个拦截器,实现forest的Interceptor接口,然后重写beforeExecute(ForestRequest request)方法,代码样例如下:

/**
 * @description: 请求增加签名的拦截器
 * @Date : 2021/11/4 2:50 PM
 * @Author : 石冬冬-Seig Heil
 */
@Slf4j
@Component
public class RequestSignedInterceptor implements Interceptor<Object> {

    @Value("${forest.variables.domain.car_fortune_cat.sign}")
    String fortuneCatSign;

    @Value("${forest.variables.domain.car_mortgage_customer.sign}")
    String mortgageCustomerSign;
    /**
     * 存储需要增加签名的API枚举以及对应的签名
     */
    final Map<RequestSigned.ApiEnum,String> SIGN_MAP = new HashMap<>();

    @PostConstruct
    void init(){
        SIGN_MAP.put(RequestSigned.ApiEnum.CarFortuneCatApi,fortuneCatSign);
        SIGN_MAP.put(RequestSigned.ApiEnum.CarMortgageCustomerApi,mortgageCustomerSign);
        log.info("[RequestSignedInterceptor],SIGN_MAP={}", JSONObject.toJSONString(SIGN_MAP));
    }

    @Override
    public boolean beforeExecute(ForestRequest request) {
        RequestSigned requestSigned = request.getMethod().getMethod().getAnnotation(RequestSigned.class);
        if(null != requestSigned){
            request.addHeader("sign", SIGN_MAP.get(requestSigned.signFor()));
        }
        log.info("[RequestSignedInterceptor],header={}", JSONObject.toJSONString(request.getHeaders()));
        return true;
    }
}

上述代码,通过SIGN_MAP来初始化所有枚举对应的签名,该成员是一个Map<RequestSigned.ApiEnum,String>数据结构,key为RequestSigned.ApiEnum的枚举成员,value为对应client的配置签名。我们重写了InterceptorbeforeExecute(ForestRequest request)这个方法,通过从ForestRequest上下文获取当前请求Method,进而判断该方法是否通过RequestSigned增强,如果有增强注解,则获取对应增强注解指定的API枚举值,然后通过调用request.addHeader方法,设置sign值即可。

然后我们通过对Client比如CarMortgageCustomerApi通过@BaseRequest来指明interceptor即可。

/**
 * @description: 抵押客户公众号服务端
 * @Date : 2021/9/7 3:37 PM
 * @Author : 石冬冬-Seig Heil
 */
@BaseRequest(
        baseURL = "${domain.car_mortgage_customer}",
  			//这里指明我们自定义的拦截器
        interceptor = {RequestSignedInterceptor.class}
)
public interface CarMortgageCustomerApi {
    /**
     * 推送公众号模板消息
     * 当前消息模板在<car-mortgage-customer>应用中,基于xdiamond配置。
     * 对于抵押网络只需要双方拟定好模板中的变量即可。
     * @param dto
     * @return
     */
    @PostRequest("/mortgage/mortgageNotice")
    @TraceMarker(appCode = "${$0.mortgageCode}",traceType = TraceTypeEnum.CAR_MORTGAGE_CUSTOMER_INVOKE)
    @RequestSigned(signFor = RequestSigned.ApiEnum.CarMortgageCustomerApi)
    RespDTO<String> notice(@JSONBody MortgageNoticeDTO dto);
    /**
     * 抵押客户推送公众号文本消息(也就是会话消息,会话消息有效期48小时)
     * <p>
     * When you look at this, it's hard to believe that invoking this method is just a parameter(idNo).
     * In a fact, another application(CarMortgageCustomer) is entrusted to achieve this. It implements template configuration based on XDiamond,
     * obtains message template, and then invokes wechat API to realize text message sending.
     * </p>
     * 当前消息模板在<car-mortgage-customer>应用中,基于xdiamond配置。
     * @param idNo
     * @return
     */
    @GetRequest("/crzRelease/sendMessage/${idNo}")
    @TraceMarker(appCode = "${$0}",traceType = TraceTypeEnum.CAR_MORTGAGE_CUSTOMER_INVOKE)
    @RequestSigned(signFor = RequestSigned.ApiEnum.CarMortgageCustomerApi)
    RespDTO<JSONObject> sendWechatTextMessage(@Var("idNo") String idNo);
}

方案总结

这种通过自定义拦截器的思想,相对于上面两种方案看起来更优雅一些,不需要提供Proxy代理类,毕竟如果有多少个Client,倘若需要签名处理,就会增加相应的Proxy,后续header增加其他参数,Proxy代码也都需要修改。然而,通过结合自定义Forest的拦截器,并使用自定义注解增强,组合使用,通过这种设计思路,所有涉及签名的业务逻辑只需要在Interceptor统一处理即可,这样也符合”开闭原则“。