暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于Forest实践|如何统一对请求响应进行日志处理

秋夜无霜 2021-11-07
1438

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

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

1、背景介绍2、实现思路2.1、基于@Header增强代码样例方案总结2.2、基于Proxy委托代理代码样例方案总结2.3、基于Interceptor代码样例方案总结

1、背景介绍

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

Forest
实现原理跟MyBatis
MapperProxy
类似,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的配置签名。我们重写了Interceptor
beforeExecute(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统一处理即可,这样也符合”开闭原则“。


文章转载自秋夜无霜,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论