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

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

本文基于Forest实践,来源于实际业务场景需求,诞生了这篇文章。通过这篇文章,你可以学习到如何更优雅的统一处理基于Forest三方接口请求响应报文存储,如果你读完这篇文章,相信你会有所收获!

文章目录

1、背景介绍

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

Forest实现原理跟MyBatisMapperProxy类似,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展示

日志链路主页面

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

日志链路详情页面

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

日志报文输出了请求方法的完整url,以及所有的类+方法名字。

总结

我们通过自定义一个注解TraceMarker,然后自定义一个类实现MethodAnnotationLifeCycle接口,就可以对所有请求方法如果增加了该注解TraceMarker进行拦截,进而获取请求方法的输入输出报文,以及接口请求方法的相关参数。

通过MySQL落库日志存储,然后提供了一个日志链路分析的查询页面,这样就可以提供给不同的团队用户角色,包括研发、测试、产品,都可以进行日常问题排查,这就是所谓的赋能。相对于Linux服务器通过Linux命令查询日志,门槛低,体验性更好,毕竟有些同学人家不会使用Linux相关命令,如果服务器部署的节点比较多,去服务器查询日志更糟糕。

虽然我们的日志也通过ELK把应用服务输出的日志采集存储到ElasticSearch,并通过Kibana提供了查询看板,不过这种根据业务场景量身定制的更满足我们的需求,两者相辅相成,对于重要的三方接口,我们可以基于上述方案,就可以快捷排查相关问题。更重要的是,我们也可以重写OnError方法,增加钉钉报警或者邮件报警,何乐而不为。

上一篇:SAS forest


下一篇:SSH与TCP Wrapper 学习笔记