目录
  • 1、项目里定义FeignClient接口
  • 2、单个FeignClient接口开启日志
  • 3、所有FeignClient接口 开启日志
    • 3.1、修改FeignConfiguration
    • 3.2、还是修改 application.yml 配置
    • 3.3、OK了,此时项目里
  • 4、重写FeignClient输出日志
    • 5、使用Aspect切面输出日志

      项目里使用了Feign进行远程调用,有时为了问题排查,需要开启请求和响应日志

      下面简介一下如何开启Feign日志:

      注:本文基于

      • spring-boot-starter-parent 2.3.4.RELEASE
      • spring-cloud-starter-openfeign 2.2.3.RELEASE

      1、项目里定义FeignClient接口

      package com.example.demo.feign;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.GetMapping;
      @FeignClient(name = "deom", url = "https://www.baidu.com")
      public interface FeignDemo {
          @GetMapping("/")
          String test();
      }

      2、单个FeignClient接口开启日志

      在 application.yml 里指定Feign接口日志级别为DEBUG,类型为FULL:

      注:com.example.demo.feign.FeignDemo就是上面定义的FeignClient接口

      logging:
        level:
          com.example.demo.feign.FeignDemo: debug
      # 下面的配置,也可以写代码代替
      # @Bean
      # public Logger.Level level() { return Logger.Level.FULL; }
      feign:
        client:
          config:
            default:
              loggerLevel: full

      OK了,重启项目,调用 FeignDemo.test() 方法后,会输出如下日志:

      2020-10-13 11:46:24.161 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] —> GET https://www.baidu.com HTTP/1.1
      2020-10-13 11:46:24.162 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] —> END HTTP (0-byte body)
      2020-10-13 11:46:24.255 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] <— HTTP/1.1 200 OK (93ms)
      2020-10-13 11:46:24.255 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] content-length: 2443
      2020-10-13 11:46:24.255 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] content-type: text/html
      2020-10-13 11:46:24.256 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] date: Tue, 13 Oct 2020 03:46:24 GMT
      2020-10-13 11:46:24.256 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] server: bfe
      2020-10-13 11:46:24.256 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] 
      2020-10-13 11:46:24.257 DEBUG 20824 — [nio-8080-exec-4] com.example.demo.feign.FeignDemo         : [FeignDemo#test] <!DOCTYPE html>
      <!–STATUS OK–><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
                      </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

      3、所有FeignClient接口 开启日志

      上面的方法,只能开启单个FeignClient接口,如果项目里有10个接口,那么要在yml里配置10项,而且以后添加新的FeignClient,还要记得去修改yml配置,太麻烦。

      所以,下面是开启所有FeignClient接口日志的配置:

      3.1、修改FeignConfiguration

      自定义feign.Logger,如下:

      package com.example.demo.cacheDemo;
      import feign.slf4j.Slf4jLogger;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      @Configuration
      public class FeignConfiguration {
          @Bean
          public feign.Logger logger() {
              return new Slf4jLogger();
          }
      }

      3.2、还是修改 application.yml 配置

      logging:
        level:
          # 删除具体的FeignClient接口配置,只保留这一个就好了
          feign.Logger: debug
      # 也可以写代码代替
      # @Bean
      # public Logger.Level level() { return Logger.Level.FULL; }
      feign:
        client:
          config:
            default:
              loggerLevel: full

      3.3、OK了,此时项目里

      不管新增多少个 FeignClient,都会输出日志。

      4、重写FeignClient输出日志

      根据上面输出的日志,可以看到是多条INFO日志,在并发时,很有可能会互相干扰,而且格式也无法调整。

      我们知道,Feign默认情况下,是使用 feign.Client.Default 发起http请求;

      我们可以重写Client,并注入Bean来替换掉 feign.Client.Default,从而实现日志记录,当然也可以做其它任意事情了,比如添加Header。下面是注入Bean的代码:

      // 默认不注入,如果yml配置里有 logging.level.beinet.cn.demostudy.MyClient 才注入
      @Bean
      @ConditionalOnProperty("logging.level.beinet.cn.demostudy.MyClient")
      MyClient getClient() throws NoSuchAlgorithmException, KeyManagementException {
          // 忽略SSL校验
          SSLContext ctx = SSLContext.getInstance("SSL");
          X509TrustManager tm = new X509TrustManager() {
              @Override
              public void checkClientTrusted(X509Certificate[] chain, String authType) {
              }
              @Override
              public void checkServerTrusted(X509Certificate[] chain, String authType) {
              }
              @Override
              public X509Certificate[] getAcceptedIssuers() {
                  return null;
              }
          };
          ctx.init(null, new TrustManager[]{tm}, null);
          return new MyClient(ctx.getSocketFactory(), (hostname, sslSession) -> true);
      }

      下面是重写的Client完整代码:

      package beinet.cn.demostudy;
      import feign.Client;
      import feign.Request;
      import feign.Response;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.util.StreamUtils;
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.SSLSocketFactory;
      import java.io.*;
      import java.util.Collection;
      import java.util.Map;
      @Slf4j
      public class MyClient extends Client.Default {
          public MyClient(SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) {
              super(socketFactory, hostnameVerifier);
          }
          @Override
          public Response execute(Request request, Request.Options options) throws IOException {
              StringBuilder sb = new StringBuilder("[log started]\r\n");
              sb.append(request.httpMethod()).append(" ").append(request.url()).append("\r\n");
              CombineHeaders(sb, request.headers()); // 请求Header
              CombineBody(sb, request.body());
              long costTime = -1;
              Exception exception = null;
              BufferingFeignClientResponse response = null;
              long begin = System.currentTimeMillis();
              try {
                  response = new BufferingFeignClientResponse(super.execute(request, options));
                  costTime = (System.currentTimeMillis() - begin);
              } catch (Exception exp) {
                  costTime = (System.currentTimeMillis() - begin);
                  exception = exp;
                  throw exp;
              } finally {
                  sb.append("\r\nResponse cost time(ms): ").append(String.valueOf(costTime));
                  if (response != null)
                      sb.append("  status: ").append(response.status());
                  sb.append("\r\n");
                  if (response != null) {
                      CombineHeaders(sb, response.headers()); // 响应Header
                      sb.append("Body:\r\n").append(response.body()).append("\r\n");
                  }
                  if (exception != null) {
                      sb.append("Exception:\r\n  ").append(exception.getMessage()).append("\r\n");
                  }
                  sb.append("\r\n[log ended]");
                  log.debug(sb.toString());
              }
              Response ret = response.getResponse().toBuilder()
                      .body(response.getBody(),
                              response.getResponse().body().length()).build();
              response.close();
              return ret;
          }
          private static void CombineHeaders(StringBuilder sb, Map<String, Collection<String>> headers) {
              if (headers != null && !headers.isEmpty()) {
                  sb.append("Headers:\r\n");
                  for (Map.Entry<String, Collection<String>> ob : headers.entrySet()) {
                      for (String val : ob.getValue()) {
                          sb.append("  ").append(ob.getKey()).append(": ").append(val).append("\r\n");
                      }
                  }
              }
          }
          private static void CombineBody(StringBuilder sb, byte[] body) {
              if (body == null || body.length <= 0)
                  return;
              sb.append("Body:\r\n").append(new String(body)).append("\r\n");
          }
          static final class BufferingFeignClientResponse implements Closeable {
              private Response response;
              private byte[] body;
              private BufferingFeignClientResponse(Response response) {
                  this.response = response;
              }
              private Response getResponse() {
                  return this.response;
              }
              private int status() {
                  return this.response.status();
              }
              private Map<String, Collection<String>> headers() {
                  return this.response.headers();
              }
              private String body() throws IOException {
                  StringBuilder sb = new StringBuilder();
                  try (InputStreamReader reader = new InputStreamReader(getBody())) {
                      char[] tmp = new char[1024];
                      int len;
                      while ((len = reader.read(tmp, 0, tmp.length)) != -1) {
                          sb.append(new String(tmp, 0, len));
                      }
                  }
                  return sb.toString();
              }
              private InputStream getBody() throws IOException {
                  if (this.body == null) {
                      this.body = StreamUtils.copyToByteArray(this.response.body().asInputStream());
                  }
                  return new ByteArrayInputStream(this.body);
              }
              @Override
              public void close() {
                  this.response.close();
              }
          }
      }
      

      输出日志示例:

      2020-10-15 16:48:26.081 DEBUG 15664 — [           main] beinet.cn.demostudy.MyClient             : [log started]
      POST https://www.baidu.com?flg=3
      Headers:
        Content-Length: 14
        Content-Type: text/plain;charset=UTF-8
      Body:
      abcde我是ddd

      Response cost time(ms): 207  status: 200
      Headers:
        content-length: 2443
        content-type: text/html
        date: Thu, 15 Oct 2020 08:48:27 GMT
        server: bfe
      Body:
      <!DOCTYPE html>百度的html</html>

      5、使用Aspect切面输出日志

      这个不推荐,因为它无法打印出具体的url、header等数据,大家可以参考:

      不需要yml配置,直接在项目里添加如下代码即可:

      package com.example.demo.feign;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import lombok.Data;
      import lombok.extern.slf4j.Slf4j;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.annotation.Around;
      import org.aspectj.lang.annotation.Aspect;
      import org.aspectj.lang.annotation.Pointcut;
      import org.aspectj.lang.reflect.MethodSignature;
      import org.springframework.stereotype.Component;
      @Aspect
      @Component
      @Slf4j
      public class FeignAspect {
          // 这个也行 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
          // 参考 https://github.com/spring-cloud/spring-cloud-openfeign/issues/322
          @Pointcut("@within(org.springframework.cloud.openfeign.FeignClient)")
          public void feignClientPointcut() {
          }
          @Around("feignClientPointcut()")
          public Object feignAround(ProceedingJoinPoint joinPoint) throws Throwable {
              return logAround(joinPoint);
          }
          private static ObjectMapper mapper = new ObjectMapper();
          private Object logAround(ProceedingJoinPoint point) throws Throwable {
              long beginTime = System.currentTimeMillis();
              Object result = null;
              Exception exception = null;
              try {
                  result = point.proceed();
              } catch (Exception exp) {
                  exception = exp;
              }
              long time = System.currentTimeMillis() - beginTime;
              saveLog(point, result, exception, time);
              if (exception != null) {
                  throw exception;
              }
              return result;
          }
          private static void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception exception, long time) {
              Dto dto = new Dto();
              dto.setCostTime(time);
              try {
                  if (exception != null) {
                      dto.setExp(exception.toString());
                  }
                  if (result != null) {
                      dto.setResult(serial(result));
                  }
                  MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                  //请求的 类名、方法名
                  String className = joinPoint.getTarget().getClass().getName();
                  String signName = signature.getDeclaringTypeName();
                  if (!signName.equalsIgnoreCase(className))
                      signName += "|" + className;
                  dto.setClas(signName);
                  String methodName = signature.getName();
                  dto.setMethod(methodName);
                  //请求的参数
                  Object[] args = joinPoint.getArgs();
                  if (args != null && args.length > 0) {
                      dto.setPara(serial(args));
                  }
              } catch (Exception e) {
                  dto.setExp(e.toString());
              }
              if (exception != null) {
                  log.warn(dto.toString());
              } else {
                  log.info(dto.toString());
              }
          }
          private static String serial(Object obj) {
              try {
                  return mapper.writeValueAsString(obj);
              } catch (Exception ex) {
                  return obj.toString();
              }
          }
          @Data
          private static class Dto {
              /**
               * 调用类名
               */
              private String clas;
              /**
               * 调用方法名
               */
              private String method;
              /**
               * 调用的参数
               */
              private String para;
              /**
               * 方法返回结果
               */
              private String result;
              /**
               * 执行时长,毫秒
               */
              private long costTime;
              /**
               * 备注
               */
              private String remark;
              /**
               * 出现的异常
               */
              private String exp;
          }
      }
      

      OK,输出的日志如下:

      2020-10-13 14:24:48.321  INFO 21304 — [nio-8080-exec-3] com.example.demo.feign.FeignAspect       : FeignAspect.Dto(clas=com.example.demo.feign.FeignDemo|com.sun.proxy.$Proxy72, method=test, para=null, result="<!DOCTYPE html>\r\n<!–STATUS OK–><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class=\&;bg s_ipt_wr\&;><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class=\&;bg s_btn_wr\&;><input type=submit id=su value=百度一下 class=\&;bg s_btn\&; autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href=\&;http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === \&;\&; ? \&;?\&; : \&;&\&;)+ \&;bdorz_come=1\&;)+ '\&; name=\&;tj_login\&; class=\&;lb\&;>登录</a>');\r\n                </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style=\&;display: block;\&;>更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

      注:Feign官方文档:

      https://cloud.spring.io/spring-cloud-openfeign/reference/html/

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。