Forest
是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。本文基于Forest实践,来源于实际业务场景需求,诞生了这篇文章。通过这篇文章,你可以学习到如何更优雅的统一处理基于
Forest
三方接口请求响应报文存储,如果你读完这篇文章,相信你会有所收获!
文章目录
1、背景介绍
我们之所以选择Forest
,而不选择Spring RestTemplate
,是因为Forest
支持你通过定义一个接口,通过其相关增强注解,你就可以像在本地调用方法一样,实现调用HTTP请求方法。同时,它提供了一系列缺省配置参数,使得你可以很简单的配置,就可以以最小的研发代码量实现你的需求。
Forest
实现原理跟MyBatis
的MapperProxy
类似,Spring
容器初始化时,通过扫描包下使用了@BaseRequest
的相关接口,通过动态代理生成Http访问类。
当我们程序调用接口方法时,就是从Forest
上下文对象中获取预先初始化的代理类,然后委托OkHttp3
或者HttpClient
实现http
调用,只不过Forest
优雅的封装了这一系列背后工作,使得我们的代码更精简,更优雅,所以它也是一个非常轻量级的Http
工具。
通过查阅Forest
的相关开发手册,我们知道可以通过定义拦截器进而统一打印请求响应报文,但是我们希望可以实现更细粒度的定制,比如把一些关键的日志进行数据存储持久化,并非只是简单的打印输出日志。我们想要实现存储日志,可以按照一个关联字段(比如交易号),实现搜索查询。这时候,倘若我们基于MySQL
存储日志,这种需求场景我们应该怎么设计呢?
2、实现方案
MethodAnnotationLifeCycle
除了可以实现Forest
的拦截器接口Interceptor
,其实我们可以实现MethodAnnotationLifeCycle
这个接口,这个接口具体干啥的呢?
/**
* 方法注解的生命周期
* @param <A> 注解类
* @param <I> 返回类型
*/
public interface MethodAnnotationLifeCycle<A extends Annotation, I> extends Interceptor<I> {
void onMethodInitialized(ForestMethod method, A annotation);
@Override
default void one rror(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {
}
@Override
default void onSuccess(I data, ForestRequest request, ForestResponse response) {
}
}
MethodAnnotationLifeCycle
该接口继承了Interceptor
,它提供了方法增强注解的声明A extends Annotation
,这样以来,如果一个方法通过注解增强,这个拦截器就会被执行。这个接口提供了三个方法
-
void onMethodInitialized
:方法执行前,可以对从ForestMethod
上下文中获取相关信息,然后做一些相关的事情,或者重写ForestMethod
的相关属性值。 -
default void one rror
:方法执行请求错误时会回调该方法,该方法可以不需要实现,通过default关键字进行修饰。 -
default void onSuccess
:方法执行成功时会回调该方法,该方法可以不需要实现,通过default关键字进行修饰。
TraceMarker
/**
* @description: 记录链路日志的注解
* @Date : 2021/6/21 4:31 PM
* @Author : 石冬冬-Seig Heil
*/
@Documented
@MethodLifeCycle(TraceMarkerLifeCycle.class)
@RequestAttributes
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TraceMarker {
/**
* 订单号
* @return
*/
String appCode();
/**
* 链路日志类型
* @return
*/
TraceTypeEnum traceType();
}
我们通过自定义一个增强注解,该增强注解需要增加@MethodLifeCycle(TraceMarkerLifeCycle.class)
,有两个成员方法
-
String appCode()
:指定存储日志的关联字段,后续可以按照该字段进行日志搜索过滤查询。 -
TraceTypeEnum traceType()
:声明日志记录存储的业务类型,我们可以对日志按照业务分类,比如调用A服务,划分一类;调用B服务,再划分一类。
TraceMarkerLifeCycle
然后自定义一个TraceMarkerLifeCycle
类,实现接口MethodAnnotationLifeCycle
,声明指定的注解是是咱们刚才自定义的TraceMarker
,更重要的是,我们需要通过Spring
提供的注解注入到Spring
容器中,这样我们可以在处理日志存储时,通过注入我们的日志存储Service
类,调用其相关方法。
/**
* @description: Trace记录 LifeCycle
* @Date : 2021/6/21 4:32 PM
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
@Component
public class TraceMarkerLifeCycle implements MethodAnnotationLifeCycle<TraceMarker,Object>{
@Autowired
TraceLogService traceLogService;
@Override
public void onMethodInitialized(ForestMethod method, TraceMarker annotation) {
log.info("[TraceMarkerLifeCycle|onMethodInitialized],method={},annotation={}",method.getMethodName(),annotation.getClass().getSimpleName());
}
@Override
public void onSuccess(Object data, ForestRequest request, ForestResponse response) {
saveTraceRecord(data,request,response);
}
/**
* 记录traceLog
* @param data
* @param request
* @param response
*/
void saveTraceRecord(Object data, ForestRequest request, ForestResponse response){
try {
//通过调用父类的getAttributeAsString方法,进而获取TraceMarker的成员参数
String appCode = getAttributeAsString(request,"appCode");
//通过调用父类的getAttribute方法,获取存储日志的业务类型
TraceTypeEnum traceTypeEnum = (TraceTypeEnum)getAttribute(request,"traceType");
Class<?> methodClass = request.getMethod().getMethod().getDeclaringClass();
//声明target,即我们访问接口所属包+接口名称+方法
String target = new StringBuilder(methodClass.getName())
.append("#")
.append(request.getMethod().getMethodName())
.toString();
TraceLog traceLog = TraceLog.builder()
.appCode(appCode)
.traceType(traceTypeEnum.getIndex())
.url(request.getUrl())
.target(target)
.requestTime(response.getRequestTime())
.responseTime(response.getResponseTime())
.requestBody(JSONObject.toJSONString(request.getArguments()))
.responseBody(JSONObject.toJSONString(data))
.build();
//调用存储日志的service类
traceLogService.insertRecord(traceLog);
} catch (Exception e) {
log.error("[saveTraceRecord]",e);
}
}
}
从上述代码片段中,我们可以看到在void onSuccess
,中调用了void saveTraceRecord
这个成员方法。
TraceLog
这个类是一个存储日志的ORM
实体,具体字段可以看如下,它包括请求方法输入和输出报文,以及方法的类及方法名称,更重要的是它有一个appCode
,后续日志存储就可以基于appCode
查询搜索,进而对线上问题进行分析跟进,相对于查看服务器日志,体验性更好。
/**
* @description: 系统链路日志
* @Date : 2020/4/10 下午12:02
* @Author : 石冬冬-Seig Heil
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TraceLog implements Serializable {
/**
* 主键
*/
@ApiModelProperty(value="主键")
private Integer id;
/**
* 单号
*/
@ApiModelProperty(value="单号")
private String appCode;
/**
* 监控类型
*/
@ApiModelProperty(value="监控类型")
private Integer traceType;
/**
* url
*/
@ApiModelProperty(value="url")
private String url;
/**
* target
*/
@ApiModelProperty(value="target")
private String target;
/**
* 请求时间
*/
@ApiModelProperty(value="请求时间")
private Date requestTime;
/**
* 响应时间
*/
@ApiModelProperty(value="响应时间")
private Date responseTime;
/**
* 请求报文
*/
@ApiModelProperty(value="请求报文")
private String requestBody;
/**
* 响应报文
*/
@ApiModelProperty(value="响应报文")
private String responseBody;
}
Client方法增强
/**
* @description: 抵押客户公众号服务端
* @Date : 2021/9/7 3:37 PM
* @Author : 石冬冬-Seig Heil
*/
@BaseRequest(
baseURL = "${domain.car_mortgage_customer}",
interceptor = {RequestSignedInterceptor.class,SimpleForestInterceptor.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);
}
从上述代码中,我们可以看到这样一段代码
@TraceMarker(appCode = "${$0}",traceType = TraceTypeEnum.CAR_MORTGAGE_CUSTOMER_INVOKE)
该appCode
,可以直接使用Forest
的模板引擎,$0代表增强注解的第一个参数,即String idNo
。
日志链路存储UI展示
日志链路主页面
日志链路详情页面
日志报文输出了请求方法的完整url,以及所有的类+方法名字。
总结
我们通过自定义一个注解TraceMarker
,然后自定义一个类实现MethodAnnotationLifeCycle
接口,就可以对所有请求方法如果增加了该注解TraceMarker
进行拦截,进而获取请求方法的输入输出报文,以及接口请求方法的相关参数。
通过MySQL
落库日志存储,然后提供了一个日志链路分析的查询页面,这样就可以提供给不同的团队用户角色,包括研发、测试、产品,都可以进行日常问题排查,这就是所谓的赋能。相对于Linux服务器通过Linux命令查询日志,门槛低,体验性更好,毕竟有些同学人家不会使用Linux
相关命令,如果服务器部署的节点比较多,去服务器查询日志更糟糕。
虽然我们的日志也通过ELK
把应用服务输出的日志采集存储到ElasticSearch
,并通过Kibana
提供了查询看板,不过这种根据业务场景量身定制的更满足我们的需求,两者相辅相成,对于重要的三方接口,我们可以基于上述方案,就可以快捷排查相关问题。更重要的是,我们也可以重写OnError
方法,增加钉钉报警或者邮件报警,何乐而不为。