How to cooperate with OpenFeign's elegant record requests and returned information

Combine the previous article How to cooperate with RestTemplate's elegant record requests and returned information We can easily use the Interceptor record information provided by restTemplate. For empirical reasons, can we also find its Interceptor through OpenFeign and do so? Actually not.

We can see from @Enable FeignClients that the @Import (FeignClients Registrar. class)@Import annotation injects the specified class into the Spring container as a bean. The FeignClients Registrar loads and scans the corresponding classes of FeignClient according to the defined path. After injecting beans, it is intercepted when requesting FeignClient's method through jdk's agent.

This paper mainly analyses why we can't use the Request Interceptor provided by openFeign to process, because this Interceptor is very different from RestTemplate#Interceptor, it can only do pre-request processing (eg: interface signature, unified labeling, authentication information, etc.). OpenFeign Principle Principles are not the subject of this paper.

Code ReflectiveFeign. class # new Instance (Target < T > target)

public <T> T newInstance(Target<T> target) {
   Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
   Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
   List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

   for (Method method : target.type().getMethods()) {
     if (method.getDeclaringClass() == Object.class) {
       continue;
     } else if (Util.isDefault(method)) {
       DefaultMethodHandler handler = new DefaultMethodHandler(method);
       defaultMethodHandlers.add(handler);
       methodToHandler.put(method, handler);
     } else {
       methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
     }
   }
   InvocationHandler handler = factory.create(target, methodToHandler);
   T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
       new Class<?>[] {target.type()}, handler);

   for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
     defaultMethodHandler.bindTo(proxy);
   }
   return proxy;
 }
 
 
 // ParseHandlersByName.class#apply(Target key) 
  public Map<String, MethodHandler> apply(Target key) {
     List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
     Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
     for (MethodMetadata md : metadata) {
       BuildTemplateByResolvingArgs buildTemplate;
       if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
         buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
       } else if (md.bodyIndex() != null) {
         buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
       } else {
         buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
       }
       result.put(md.configKey(),
           factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
     }
     return result;
   }

As you can see, a proxy class factory.create is generated for each method. Whenever the target method is called, it is processed by Synchronous MethodHandler to generate RequestTemplate objects based on parameters. SynchronousMethodHandler.class#invoke(Object[] argv)

@Override
 public Object invoke(Object[] argv) throws Throwable {
   RequestTemplate template = buildTemplateFromArgs.create(argv);
   Retryer retryer = this.retryer.clone();
   while (true) {
     try {
       return executeAndDecode(template);
     } catch (RetryableException e) {
       try {
         retryer.continueOrPropagate(e);
       } catch (RetryableException th) {
         Throwable cause = th.getCause();
         if (propagationPolicy == UNWRAP && cause != null) {
           throw cause;
         } else {
           throw th;
         }
       }
       if (logLevel != Logger.Level.NONE) {
         logger.logRetry(metadata.configKey(), logLevel);
       }
       continue;
     }
   }
 }
 
 Object executeAndDecode(RequestTemplate template) throws Throwable {
   Request request = targetRequest(template);
   if (logLevel != Logger.Level.NONE) {
     logger.logRequest(metadata.configKey(), logLevel, request);
   }

   Response response;
   long start = System.nanoTime();
   try {
     response = client.execute(request, options);
   ...
}
 Request targetRequest(RequestTemplate template) {
   for (RequestInterceptor interceptor : requestInterceptors) {
     interceptor.apply(template);
   }
   return target.apply(template);
 }
 

Seeing where the Request Interceptor is called, Feign's Request Interceptor is created before the request.

So how can OpenFeign log requests and responses?

We see the request information encapsulated. response = client.execute(request, options); it is to execute the request request and receive the response response. Client.java. Rewrite the client, and create the client we rewrite when the spring container starts. So there's no big problem. But why rewrite it yourself? After all, this need is not always there. Officials gave no explanation, but suggested rewriting the client. According to the previous article restTemplate Recording information, can we encapsulate and expand wheels according to restTemplate? The result may disappoint you. Because we don't have something like Buffering Client HttpRequestFactory for streaming copy, because the Response.class provided by feign is final, we can't do streaming copy by ourselves, so we're ready to ask an issues question. Rewrite the Client code yourself as follows:

/**
* @author liweigao
* @date 2019/8/26 10:17 a.m.
*/
@Slf4j
public class SuperClient extends Client.Default {

   private static final String CONTENT_TYPE = "Content-Type";

   private Response response;

   @Nullable
   private byte[] body;

   /**
    * Null parameters imply platform defaults.
    *
    * @param sslContextFactory
    * @param hostnameVerifier
    */
   public SuperClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
       super(sslContextFactory, hostnameVerifier);
   }

   @Override
   public Response execute(Request request, Request.Options options) throws IOException {

       StopWatch stopWatch = new StopWatch();
       stopWatch.start();

       this.response = super.execute(request, options);
       stopWatch.stop();

       // request
       Map reqMap = null;
       byte[] body = request.body();
       Charset charset = Objects.isNull(request.charset()) ? Charset.defaultCharset() : request.charset();
       HttpHeaders httpHeaders = convert(request.headers());
       String reqStr = null;
       MediaType reqMediaType;
       if (Objects.nonNull(reqMediaType = httpHeaders.getContentType())) {
           if (reqMediaType.includes(MediaType.MULTIPART_FORM_DATA)) {
               body = new byte[]{0};
           }
           reqStr = new String(body, charset);
           if ((reqMediaType.includes(MediaType.APPLICATION_JSON_UTF8) || reqMediaType.includes(MediaType.APPLICATION_JSON))) {
               //json format paramters
               try {
                   reqMap = JSON.parseObject(reqStr);
                   reqStr = null;
                   //no care this exception
               } catch (JSONException e) {
               }
           }
       }

       //response
       Map repMap = null;
       String resStr = null;
       Collection<String> collection;
       if (Objects.nonNull(this.response.headers()) && !CollectionUtils.isEmpty(collection =
               this.response.headers().get(CONTENT_TYPE))) {

           StringBuilder resBody = new StringBuilder();
           try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getBody(), charset))) {
               String line = bufferedReader.readLine();
               while (line != null) {
                   resBody.append(line);
                   line = bufferedReader.readLine();
               }
           }
           if (!collection.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) {
               resStr = resBody.toString();
           }
           if (collection.contains(MediaType.APPLICATION_JSON_VALUE) || collection.contains(MediaType.APPLICATION_JSON)) {
               try {
                   repMap = JSON.parseObject(reqStr);
                   resStr = null;
                   //no care this exception
               } catch (JSONException e) {
               }
           }
       }
       RestLog.builder().costTime(stopWatch.getLastTaskTimeMillis()).headers(httpHeaders)
               .method(request.method()).reqBody(reqStr).resJson(repMap).reqJson(reqMap).reqUrl(request.url())
               .resBody(resStr).resStatus(this.response.status()).build().print();

       Response response = this.response.toBuilder().body(getBody(), this.response.body().length()).build();

       close();

       return response;
   }


   private HttpHeaders convert(Map<String, Collection<String>> headers) {

       HttpHeaders httpHeaders = new HttpHeaders();
       if (Objects.nonNull(headers)) {
           headers.forEach((k, v) -> {
               httpHeaders.set(k, convert(v));
           });
       }
       return httpHeaders;
   }

   private String convert(Collection<String> strings) {

       StringBuilder builder = new StringBuilder();

       strings.forEach(s -> {
           builder.append(s).append(",");
       });

       //Remove the end comma
       if (builder.length() > 0) {
           builder.delete(builder.length() - 2, builder.length() - 1);
       }
       return builder.toString();
   }

   private InputStream getBody() throws IOException {
       if (this.body == null) {
           this.body = StreamUtils.copyToByteArray(this.response.body().asInputStream());
       }
       return new ByteArrayInputStream(this.body);
   }

   private void close() {

       if (this.response != null) {
           this.response.close();
       }
   }
}
Record the problems encountered and the solutions.

Keywords: JSON Spring JDK Java

Added by Dark[NSF] on Mon, 26 Aug 2019 10:59:12 +0300