通过重写 OpenFeign 客户端实现自定义日志存储
一、重写 OpenFeign 客户端实现自定义日志存储
OpenFeign
是一个常用的客户端负载工具,以下简称 feign
, 使用过 feign
客户端的小伙伴应该都知道,默认的情况下使用 feign
客户端是不会打印请求日志的,如果需要开启 feign
的日志也是非常的简单,只需要声明一个 Logger.Level
即可,关于 feign 的相关讲解,也可以参考下面我的博客:
但是上面的日志还是打印在了控制台或日志文件中,假如我们需要将关键的信息写入 es
中或者关系型数据库中,可能就没办法满足我们了,那我们需要实现这一功能该怎么办呢?
我们应该了解 feign
无非就是个客户端网络请求工具,只是封装成为了我们熟知 SpringMVC
的风格,如果不使用 feign
客户端,使用 java
自带的 HttpURLConnection
(feign
底层也是用的这个) 或者 第三方的 OKHttp
等,也可以实现微服务之间的调用,但相对开发的工作量也增加了许多。
再回到 feign
客户端这,既然是网络请求工具,我们肯定可以找到请求的地方,上面讲到 feign
底层是用的 HttpURLConnection
,这个在什么位置呢?其实可以看Feign
的 Client
接口 的实现:
其中如果不指定自定义的 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
,但是 Response
被 final
标注,因此代表着不可复制,解决办法可以创建一个 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
- 点赞
- 收藏
- 关注作者
评论(0)