Based on Forest practice | how to handle request signature more gracefully and uniformly

Keywords: Java

Forest is an open source Java HTTP client framework. It can bind all HTTP request information (including URL, Header, Body and other information) to your customized Interface method, and send HTTP requests by calling local Interface methods.

This article is based on Forest practice and comes from the actual business scenario requirements. Through this article, you can learn how to more gracefully and uniformly handle the request response message storage based on the Forest three-party interface. If you finish reading this article, I believe you will gain something!

1. Background introduction

The reason why we choose Forest instead of Spring RestTemplate is that Forest allows you to call HTTP request methods like calling methods locally by defining an interface and using its related enhanced annotations. At the same time, it provides a series of default configuration parameters, so that you can configure it very simply and realize your requirements with the minimum amount of R & D code.

The implementation principle of Forest is similar to that of MyBatis MapperProxy. During the initialization of Spring container, Http access classes are generated through dynamic proxy by scanning the relevant interfaces of @ BaseRequest under the package.

When our program calls the interface method, we get the pre initialized proxy class from the Forest context object, and then entrust OkHttp3 or HttpClient to implement HTTP calls. However, Forest gracefully encapsulates the work behind this series, making our code more concise and elegant. Therefore, it is also a very lightweight HTTP tool.

By referring to the relevant development manuals of Forest, we know that we can uniformly print request response messages by defining interceptors, but we hope to achieve more fine-grained customization, such as data storage and persistence of some key logs, rather than simply printing and outputting logs. If we want to store logs, we can search and query according to an associated field (such as transaction number). At this time, if we store logs based on MySQL, how should we design this demand scenario?

2. Implementation scheme

MethodAnnotationLifeCycle

In addition to implementing the Interceptor interface of Forest, we can actually implement the MethodAnnotationLifeCycle interface. What does this interface do?

/**
 * Method annotation lifecycle
 * @param <A> Annotation 
 * @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) {

    }
}

MethodAnnotationLifeCycle this interface inherits the Interceptor and provides the declaration of method enhanced annotation. In this way, if a method is enhanced through annotation, the Interceptor will be executed. This interface provides three methods

  • void onMethodInitialized: before the method is executed, you can obtain relevant information from the ForestMethod context and then do some related things, or override the relevant attribute values of ForestMethod.
  • default void onError: the method will call back when it executes a request error. The method does not need to be implemented and can be modified by the default keyword.
  • default void onSuccess: the method will be called back when it is executed successfully. The method can be modified by the default keyword without implementation.

TraceMarker

/**
 * @description: Notes for recording 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();

    /**
     * Link log type
     * @return
     */
    TraceTypeEnum traceType();
}

We customize an enhanced annotation. The enhanced annotation needs to add @ methodlifecycle (tracemarkerlife. Class), which has two member methods

  • String appCode(): Specifies the associated field for storing logs, which can be used for subsequent log search, filtering and query.
  • TraceTypeEnum traceType(): declare the business type of log record storage. We can classify logs according to business, such as calling service A; Call B service and divide it into one class.

TraceMarkerLifeCycle

Then customize a tracemarkerlife cycle class, implement the interface MethodAnnotationLifeCycle, and declare that the specified annotation is the TraceMarker we just customized. More importantly, we need to inject the annotation provided by Spring into the Spring container, so that we can call its related methods by injecting our log storage Service class when processing log storage.

/**
 * @description: Trace Record 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 the member parameters of TraceMarker by calling the getAttributeAsString method of the parent class
            String appCode = getAttributeAsString(request,"appCode");
          	//By calling the getAttribute method of the parent class, get the business type that stores the log
            TraceTypeEnum traceTypeEnum = (TraceTypeEnum)getAttribute(request,"traceType");
            Class<?> methodClass = request.getMethod().getMethod().getDeclaringClass();
          	//Declare the target, that is, the package to which our access interface belongs + 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 the log
            traceLogService.insertRecord(traceLog);
        } catch (Exception e) {
            log.error("[saveTraceRecord]",e);
        }
    }
}

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

TraceLog

This class is an ORM entity that stores logs. The specific fields can be seen as follows. It includes request method input and output messages, as well as method class and method name. More importantly, it has an appCode. Subsequent log storage can query and search based on appCode, and then analyze and follow up online problems. It is more experiential than viewing server logs.

/**
 * @description: System link log
 * @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;

    /**
     * Odd Numbers
     */
    @ApiModelProperty(value="Odd Numbers")
    private String appCode;

    /**
     * Monitoring type
     */
    @ApiModelProperty(value="Monitoring 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;
}

Client method enhancement

/**
 * @description: The official account of the mortgage customer service
 * @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 {
    /**
     * Pushing official account template message
     * The current message template is configured based on xdiamond in the < car mortgage customer > application.
     * For the mortgage network, both parties only need to formulate the variables in the template.
     * @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);
    /**
     * The customer pushes the text message of the official account (that is, the session message, the session message is valid for 48 hours).
     * <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>
     * The current message template is configured based on xdiamond in the < car mortgage customer > application.
     * @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);
}

From the above code, we can see such a piece of code

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

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

Display of log link storage UI

Log link main page

Log link details page

The log message outputs the complete url of the request method and all class + method names.

summary

By customizing an annotation TraceMarker, and then customizing a class to implement the MethodAnnotationLifeCycle interface, we can intercept all request methods if the annotation TraceMarker is added, and then obtain the input and output messages of the request method and the relevant parameters of the interface request method.

The logs are stored in MySQL, and then a query page for log link analysis is provided, which can be provided to different team user roles, including R & D, testing and products, for daily problem troubleshooting. This is the so-called empowerment. Compared with the Linux server, querying logs through linux commands has lower threshold and better experience. After all, some students will not use Linux related commands. If the server deploys more nodes, it is even worse to query logs on the server.

Although our logs are also collected and stored in ElasticSearch through ELK, and query Kanban is provided through Kibana, this customized according to business scenarios can better meet our needs. The two complement each other. For important three-party interfaces, we can quickly troubleshoot relevant problems based on the above scheme. More importantly, we can also override the OnError method to add nail alarm or mail alarm. Why not.

Posted by BLaZuRE on Mon, 08 Nov 2021 02:15:23 -0800