如何配合OpenFeign优雅的记录请求以及返回的信息

结合上一篇 如何配合RestTemplate优雅的记录请求以及返回的信息 我们可以很方便的利用restTemplate提供的Interceptor记录信息,出于经验的问题我们是不是也可以通过OpenFeign找到它的Interceptor然后这么实现的呢?其实不然。

我们可以通过@EnableFeignClients看到@Import(FeignClientsRegistrar.class) @Import注解将指定的类作为Bean注入到Spring容器中。FeignClientsRegistrar主要是根据定义路径加载扫描被FeignClient相应的类,注入bean之后,通过jdk的代理,当请求Feign Client的方法时会被拦截

本文主要分析为什么不能利用openFeign提供的RequestInterceptor 进行处理,因为这个InterceptorRestTemplate#Interceptor区别很大,他只能做请求前的处理(eg: 接口签名、统一标示、认证信息等等)。OpenFeign原理 原理不作为本文的主题。

代码 ReflectiveFeign.class#newInstance(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;
   }

可以看出为每个方法生成一个代理类 factory.create... 每当目标方法调用时都会被SynchronousMethodHandler 进行处理根据参数生成RequestTemplate对象。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);
 }
 

看到RequestInterceptor 被调用的地方,所以FeignRequestInterceptor是在请求前在创建RequestTemplate的时候。

那么怎么才能让OpenFeign记录请求和响应日志呢。

我们看到封装完请求信息。response = client.execute(request, options);才是执行request请求以及接收response响应。Client.java。重写这个client,spring 容器启动的时候创建我们重写的client 便可以实现。那么是没有大问题。但是为什么要自己重写的呢?毕竟不一定都会有这种需求。官方也没给出解释,只是建议重写client。根据上一篇restTemplate记录信息,我们是不是可以按照restTemplate写法(责任链模式)进行封装扩展造*呢?结果可能让你很失望。因为我们没有类似BufferingClientHttpRequestFactory东西进行流copy,因为feign提供的Response.class是final类型的,我们没有办法通过自己进行流copy,这个准备提个issues问问。自己重写Client 代码如下:

/**
* @author liweigao
* @date 2019/8/26 上午10:17
*/
@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(",");
       });

       //去除末尾逗号
       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();
       }
   }
}
记录一下遇到的问题,以及解决的办法。
上一篇:10月05号 有向图有环图搜问题思路,拓扑排序和dfs


下一篇:《机器学习与数据科学(基于R的统计学习方法)》——2.12 读取Twitter数据