通过重写 OpenFeign 客户端实现自定义日志存储

举报
程序员-上善若水 发表于 2022/07/19 22:17:51 2022/07/19
【摘要】 一、重写 OpenFeign 客户端实现自定义日志存储 OpenFeign 是一个常用的客户端负载工具,以下简称 feign, 使用过 feign 客户端的小伙伴应该都知道,默认的情况下使用 feign...

一、重写 OpenFeign 客户端实现自定义日志存储

OpenFeign 是一个常用的客户端负载工具,以下简称 feign, 使用过 feign 客户端的小伙伴应该都知道,默认的情况下使用 feign 客户端是不会打印请求日志的,如果需要开启 feign 的日志也是非常的简单,只需要声明一个 Logger.Level 即可,关于 feign 的相关讲解,也可以参考下面我的博客:

https://blog.csdn.net/qq_43692950/article/details/121993565

但是上面的日志还是打印在了控制台或日志文件中,假如我们需要将关键的信息写入 es 中或者关系型数据库中,可能就没办法满足我们了,那我们需要实现这一功能该怎么办呢?

我们应该了解 feign 无非就是个客户端网络请求工具,只是封装成为了我们熟知 SpringMVC 的风格,如果不使用 feign 客户端,使用 java 自带的 HttpURLConnectionfeign 底层也是用的这个) 或者 第三方的 OKHttp 等,也可以实现微服务之间的调用,但相对开发的工作量也增加了许多。

再回到 feign 客户端这,既然是网络请求工具,我们肯定可以找到请求的地方,上面讲到 feign 底层是用的 HttpURLConnection ,这个在什么位置呢?其实可以看FeignClient 接口 的实现:

在这里插入图片描述
其中如果不指定自定义的 Client ,最后会到 Client.Default 中进行执行,可以看下 execute 方法:

在这里插入图片描述

这里返回的就是 HttpURLConnection ,再具体源码,这里就不过多解析了,写的也比较清楚,感兴趣的小伙伴可以继续探索下,那既然找到了突破口,我们就可以通过重写 Client 的方式,自定义打印逻辑了。

再开始前,先在服务提供者写两个接口便于测试:

@RestController
@RequestMapping("/test")
public class ProviderController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/getData")
    public JSONObject getData(String data) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", 200);
        jsonObject.put("message", "come from : " + port);
        jsonObject.put("data", data);
        return jsonObject;
    }

    @PostMapping("/postData")
    public JSONObject postData(@RequestBody Map<String, Object> data) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", 200);
        jsonObject.put("message", "come from : " + port);
        jsonObject.put("data", data);
        return jsonObject;
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

下面声明出对应的 feign 客户端:

@Component
@FeignClient(value = "PROVIDER",contextId = "ProviderFeignService",path = "/test")
public interface ProviderFeignService {
    @GetMapping("/getData")
    JSONObject getData(@RequestParam(name = "data") String data);

    @PostMapping("/postData")
    JSONObject postData(@RequestBody Map<String, Object> data);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

测试一下是否正常:

@RestController
@RequestMapping("/feign/consumer")
public class FeignController {
    @Autowired
    ProviderFeignService providerFeignService;

    @GetMapping("/getData")
    public JSONObject getData() {
        String data = "小毕超";
        return providerFeignService.getData(data);
    }

    @GetMapping("/postData")
    public JSONObject postData() {
        Map<String, Object> data = new HashMap<>();
        data.put("param1", "小毕超1");
        data.put("param2", "小毕超2");
        return providerFeignService.postData(data);
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述
在这里插入图片描述
测试正常,下面开始本篇的主要内容,通过重写 OpenFeign 客户端实现自定义日志,先声明一个 bean 存储日志信息,分别存储请求的 url、请求头、请求参数、请球体、请求状态、返回体、返回头、接口耗时、及错误信息:

@Data
public class RequestLogDTO {
    private String url;
    private String requestHeader;
    private String requestBody;
    private String requestParams;
    private Integer status;
    private String resposeHeader;
    private String resposeBody;
    private long time;
    private String errMsg;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

下面开始重写 Feign 客户端,这里需要注意的是,如果是通过注册中心的方式,这里需要一步去注册中心拿去服务的步骤,并做个本地负载均衡,在最后 saveLog 方法中进行了汇总日志的打印,已经到了这一步,下面日志的存储应该就不需要过多说明了,其中需要注意的是 Response 的内容是放在了流中,而一旦读取出来,后面再读取便没有了,因此返回给上层需要重新构造一个 Response ,但是 Responsefinal 标注,因此代表着不可复制,解决办法可以创建一个 bean 使其 实现 Closeable ,将内容存放在该 bean 中:

@Slf4j
public class CustomFeignClient extends Client.Default {

    private AtomicInteger count = new AtomicInteger(0);

    private DiscoveryClient discoveryClient;

    CustomFeignClient(DiscoveryClient discoveryClient, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) {
        super(socketFactory, hostnameVerifier);
        this.discoveryClient = discoveryClient;
    }

    private ServiceInstance lbClient(String clientName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(clientName);
        if (Objects.isNull(instances) || instances.isEmpty()) {
            return null;
        }
        int count = getAndIncrement();
        int index = count % instances.size();
        return instances.get(index);
    }

    private int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.count.get();
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        } while (!this.count.compareAndSet(current, next));
        return next;
    }

    private String getHost(String clientName) {
        ServiceInstance server = lbClient(clientName);
        if (Objects.isNull(server)) {
            throw new RuntimeException("服务负载异常,无可用服务!");
        }
        return server.getHost() + ":" + server.getPort();
    }


    private String realityUrl(String originalUrl, String host, String realityHost) {
        String newUrl = originalUrl;
        if (originalUrl.startsWith("https://")) {
            newUrl = originalUrl.substring(0, 8) + realityHost + originalUrl.substring(8 + host.length());
        } else if (originalUrl.startsWith("http")) {
            newUrl = originalUrl.substring(0, 7) + realityHost + originalUrl.substring(7 + host.length());
        }
        StringBuffer buffer = new StringBuffer(newUrl);
        if (newUrl.startsWith("https://") && newUrl.length() == 8 || newUrl.startsWith("http://") && newUrl.length() == 7) {
            buffer.append("/");
        }
        return buffer.toString();
    }

    @SneakyThrows
    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        URI asUri = URI.create(request.url());
        String clientName = asUri.getHost();
        String url = realityUrl(request.url(), clientName, getHost(clientName));
        Request newRequest = Request.create(request.httpMethod(), url, request.headers(), request.body(), request.charset(), request.requestTemplate());
        long t = System.currentTimeMillis();
        Response response = null;
        BufferingResponse bufferingResponse = null;
        IOException err = null;
        try {
            bufferingResponse = new BufferingResponse(super.execute(newRequest, options));
            response = bufferingResponse.getResponse().toBuilder()
                    .body(bufferingResponse.getBody(), bufferingResponse.getResponse().body().length())
                    .build();
            bufferingResponse.close();
        } catch (IOException e) {
            log.error("Feign 执行失败:", e);
            err = e;
        }
        saveLog(newRequest, bufferingResponse, Objects.nonNull(err) ? err.getMessage() : null, (System.currentTimeMillis() - t));
        if (Objects.nonNull(err)) {
            throw err;
        }
        return response;
    }

    private void saveLog(Request request, BufferingResponse response, String errMsg, long time) {
        try {
            RequestLogDTO dto = new RequestLogDTO();
            dto.setUrl(URLDecoder.decode(request.url(), "UTF-8"));
            dto.setRequestHeader(JSONObject.toJSONString(request.headers()));
            if (Objects.nonNull(request.body())) {
                dto.setRequestBody(new String(request.body()));
            }
            dto.setRequestParams(JSONObject.toJSONString(request.requestTemplate().queries()));
            dto.setResposeBody(response.toBodyString());
            dto.setStatus(response.status());
            dto.setResposeHeader(JSONObject.toJSONString(response.headers()));
            dto.setErrMsg(errMsg);
            dto.setTime(time);

            /**
             * 自定义 写入逻辑,可以写入 es 或 关系型数据库,这里不做过多延伸。
             */
            log.info("请求日志:\n {}", dto.toString());
        } catch (UnsupportedEncodingException e) {
            log.error("日志打印异常:", e);
        }
    }


    static final class BufferingResponse implements Closeable {
        private final Response response;
        private byte[] body;

        private BufferingResponse(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 toBodyString() {
            try {
                return new String(toByteArray(getBody()), UTF_8);
            } catch (Exception e) {
                return super.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() {
            ensureClosed(response);
        }
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149

下面创建一个配置类,声明出上面的 client

public class FeignConfiguration {

    @Bean
    public Client feignClient(DiscoveryClient discoveryClient) {
        return new CustomFeignClient(discoveryClient,null, null);
    }
    
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

下面就可以在 feign 客户端接口中指明配置了:

@Component
@FeignClient(value = "PROVIDER",contextId = "ProviderFeignService",path = "/test",configuration = FeignConfiguration.class)
public interface ProviderFeignService {
    @GetMapping("/getData")
    JSONObject getData(@RequestParam(name = "data") String data);

    @PostMapping("/postData")
    JSONObject postData(@RequestBody Map<String, Object> data);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

最后开始测试 getData 方法:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
postData方法:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

文章来源: blog.csdn.net,作者:小毕超,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/qq_43692950/article/details/125833830

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。