Forest
是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。本文基于Forest实践,来源于实际业务场景需求,诞生了本篇文章,通过本篇你可以学习到如何更优雅的统一处理请求签名,相信你会有所收获!
@[toc]
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统一处理即可,这样也符合”开闭原则“。