Forest is an open source Java HTTP client framework that binds all HTTP request information (including URLS, headers, and bodies) to your own custom Interface methods. The ability to send HTTP requests by calling local interface methods.

This article is based on Forest practice and derived from actual business scenario requirements. In this article, you can learn how to store request response packets based on Forest tripartite interface. If you read this article, you will learn something.

@[toc]

1. Background

We chose Forest over Spring RestTemplate because Forest allows you to invoke HTTP request methods as if they were local by defining an interface with associated enhanced annotations. At the same time, it provides a set of default configuration parameters, so that you can easily configure, with minimal amount of development code to achieve your requirements.

The implementation principle of Forest is similar to MapperProxy of MyBatis. During the initialization of Spring container, the relevant interface of @Baserequest is used under the scanning package and Http access classes are generated through dynamic proxy.

When we call the interface method, we get the pre-initialized proxy class from the Forest context object, and then delegate the HTTP call to OkHttp3 or HttpClient. Forest elegantly encapsulates this work, making our code simpler and more elegant. So it’s also a very lightweight Http tool.

By looking at Forest’s development manual, we know that we can define interceptors to uniformly print request response messages, but we hope to achieve more fine-grained customization, such as persisting some critical logs, rather than simply printing logs. We want to store logs by implementing a search query based on an associated field, such as a transaction number. At this point, if we store logs based on MySQL, how should we design this requirement scenario?

2. Implementation scheme

MethodAnnotationLifeCycle

In addition to achieve Forest Interceptor interface Interceptor, in fact, we can realize MethodAnnotationLifeCycle this interface, this interface exactly what’s that?

/** * The lifecycle of method annotations *@param<A> Annotation class *@param<I> Return type */
public interface MethodAnnotationLifeCycle<A extends Annotation.I> extends Interceptor<I> {

    void onMethodInitialized(ForestMethod method, A annotation);

    @Override
    default void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {}@Override
    default void onSuccess(I data, ForestRequest request, ForestResponse response) {}}Copy the code

MethodAnnotationLifeCycle this interface inherits the Interceptor, it provides A method Annotation statement A extends the Annotation, so that if A method through enhanced Annotation, the interceptors will be executed. This interface provides three methods

  • void onMethodInitializedBefore the: method is executed, the value can be queriedForestMethodGet the relevant information in the context, and then do something about it, or rewriteForestMethodValue of the associated attribute.
  • default void onErrorThe: method is called back when the request is incorrectly executed. This method can be embellished with the default keyword without implementation.
  • default void onSuccess: This method is called back when it is successfully executed. This method can be modified with the default keyword without implementation.

TraceMarker

/ * * *@description: Notes to record link logs *@Date : 2021/6/21 4:31 PM
 * @Author: Shi Dongdong -Seig Heil */
@Documented
@MethodLifeCycle(TraceMarkerLifeCycle.class)
@RequestAttributes
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TraceMarker {
    /** * Order number *@return* /
    String appCode(a);

    /** * Link log type *@return* /
    TraceTypeEnum traceType(a);
}
Copy the code

We go through the custom an enhanced annotation, the enhanced annotations need to increase the @ MethodLifeCycle (TraceMarkerLifeCycle. Class), there are two members

  • String appCode(): Specifies the associated field for storing logs. You can search and filter logs based on this field.
  • TraceTypeEnum traceType(): Specifies the service type of log records. We can classify logs according to services, such as calling service A. Call the B service, and divide the class.

TraceMarkerLifeCycle

And then customize a TraceMarkerLifeCycle class, implementing an interface MethodAnnotationLifeCycle, declare the specified annotation is let’s just custom TraceMarker, more importantly, We need to inject the spring-provided annotations into the Spring container so that we can call the related methods of the logging store by injecting our logging store Service class when dealing with the logging store.

/ * * *@description: Trace LifeCycle *@Date : 2021/6/21 4:32 PM
 * @Author: Shi Dongdong -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);
    }

    /** * Record traceLog *@param data
     * @param request
     * @param response
     */
    void saveTraceRecord(Object data, ForestRequest request, ForestResponse response){
        try {
          	// Get TraceMarker's member parameters by calling the parent's getAttributeAsString method
            String appCode = getAttributeAsString(request,"appCode");
          	// Get the business type that stores logs by calling getAttribute of the parent class
            TraceTypeEnum traceTypeEnum = (TraceTypeEnum)getAttribute(request,"traceType"); Class<? > methodClass = request.getMethod().getMethod().getDeclaringClass();// Declare target, that is, the package to which we access the interface + interface name + method
            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();
          	// Call the service class that stores logs
            traceLogService.insertRecord(traceLog);
        } catch (Exception e) {
            log.error("[saveTraceRecord]",e); }}}Copy the code

From the code snippet above, we can see that the member method void saveTraceRecord is called in void onSuccess.

TraceLog

This class is a store log ORM entity, specific fields can see as follows, it includes the request method of input and output message, and the method of the class and method names, more important is that it has a appCode, subsequent log storage can be based on appCode search query, and then analyze the problem come from line to follow up, relative to check server logs, Experience is better.

/ * * *@description: System link logs *@Date: 2020/4/10 12:02 PM *@Author: Shi Dongdong -Seig Heil */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TraceLog implements Serializable {
    /** * primary key */
    @ ApiModelProperty (value = "primary key")
    private Integer id;

    /** * Number */
    @ ApiModelProperty (value = "number")
    private String appCode;

    /** * Monitoring type */
    @apiModelProperty (value=" monitor type ")
    private Integer traceType;

    /** * url */
    @ApiModelProperty(value="url")
    private String url;

    /** * target */
    @ApiModelProperty(value="target")
    private String target;

    /** * request time */
    @apiModelProperty (value=" request time ")
    private Date requestTime;

    /** * response time */
    @apiModelProperty (value=" response time ")
    private Date responseTime;
    /** * Request message */
    @apiModelProperty (value=" request message ")
    private String requestBody;

    /** * Response message */
    @apiModelProperty (value=" response message ")
    private String responseBody;
}
Copy the code

Client method enhancements

/ * * *@description: Mortgage customer public account server *@Date : 2021/9/7 3:37 PM
 * @Author: Shi Dongdong -Seig Heil */
@BaseRequest( baseURL = "${domain.car_mortgage_customer}", interceptor = {RequestSignedInterceptor.class,SimpleForestInterceptor.class} )
public interface CarMortgageCustomerApi {
    The current message template is in the <car-mortgage-customer> application, based on the Xdiamond configuration. * For the mortgage network, only variables in the template need to be drawn up by both parties. *@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);
    /** * Public account push text message (that is, session message, * <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> * Current message template in <car-mortgage-customer> application, based on xdiamond configuration. *@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);
}
Copy the code

From the above code, we can see the following code

@TraceMarker(appCode = "${$0}",traceType = TraceTypeEnum.CAR_MORTGAGE_CUSTOMER_INVOKE)
Copy the code

For this appCode, which can use Forest’s template engine directly, $0 represents the first parameter of the enhanced annotation, which is String idNo.

Log link storage UI display

Main page of log link

Log link details page

The log message outputs the full URL of the requested method, along with all the class + method names.

conclusion

Us through a custom annotations TraceMarker, and then customize a class implements MethodAnnotationLifeCycle interface, can treat all the request method if add the annotation TraceMarker to intercept, then the method to get request input and output message, And parameters related to the interface request method.

Through MySQL database log storage, and then provides a query page for log link analysis, so that different teams of users, including r & D, test, product, can carry out daily troubleshooting, this is called enabling. Compared with Linux server, query logs by Linux command, the threshold is lower, better experience, after all, some students do not use Linux-related commands, if the server deployed many nodes, to query logs on the server is worse.

Our logs are stored in ElasticSearch via ELK, and we provide query kanban via Kibana. However, this is more tailored to our business scenarios. For important triad interfaces, we can use the above solution. You can quickly troubleshoot related problems. More importantly, we can also rewrite the OnError method to add a pin alarm or email alarm.