【feign】重写OpenFeign的Client记录日志

概述

项目里使用了Feign进行远程调用,有时为了便于排查问题,需要记录请求和响应日志,下面简介一下如何保存Feign日志到数据库(Redis/MongoDB):

  • 重写FeignClient记录日志
  • 使用Aspect切面记录日志

本文依赖:

  • spring-boot-starter-parent:2.4.2
  • spring-cloud-starter-openfeign:3.0.0

重写FeignClient记录日志

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

默认情况下,OpenFeign使用feign.Client.Default发起http请求。我们可以重写Client,并注入Bean来替换掉feign.Client.Default,从而实现日志记录,当然也可以做其它事情,比如添加Header。

通过对源码feign.SynchronousMethodHandler#executeAndDecode response = client.execute(request, options);分析不难发现:执行request请求以及接收response响应的是feign.Client(默认feign.Client.Default1)。重写这个Client,spring 容器启动的时候创建我们重写的Client便可以实现。由于feign提供的Response.class是final类型,导致我们没有办法进行流copy,所以我们需要创建一个类似BufferingClientHttpRequestFactory东西进行流copy。

在FeignClient中配置

@FeignClient(url = "${weather.api.url}", name = "logFeignClient", configuration = FeignConfiguration.class)

编写FeignConfiguration

public class FeignConfiguration {
    @Bean
    public Client feignClient() {
        return new LogClient(null, null);
    }
}

重写Client

@Slf4j
public class LogClient extends Client.Default {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public LogClient(SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) {
        super(socketFactory, hostnameVerifier);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Exception exception = null;
        BufferingFeignClientResponse bufferingFeignClientResponse = null;
        try {
            bufferingFeignClientResponse = new BufferingFeignClientResponse(super.execute(request, options));
        } catch (Exception exp) {
            log.error(exp.getMessage(), exp);
            exception = exp;
            throw exp;
        } finally {
            stopWatch.stop();
            this.logAndSave(request, bufferingFeignClientResponse, stopWatch, exception);
        }

        Response response = bufferingFeignClientResponse.getResponse().toBuilder()
                .body(bufferingFeignClientResponse.getBody(), bufferingFeignClientResponse.getResponse().body().length())
                .build();
        bufferingFeignClientResponse.close();

        return response;
    }

    private void logAndSave(Request request, BufferingFeignClientResponse bufferingFeignClientResponse, StopWatch stopWatch, Exception exception) {
        // 组装request及response信息
        StringBuilder sb = new StringBuilder("[log started]\r\n");
        sb.append(request.httpMethod()).append(" ").append(request.url()).append("\r\n");
        // 请求Header
        combineHeaders(sb, request.headers());
        combineRequestBody(sb, request.body(), request.requestTemplate().queries());
        sb.append("\r\nResponse cost time(ms): ").append(stopWatch.getLastTaskTimeMillis());
        if (bufferingFeignClientResponse != null) {
            sb.append("  status: ").append(bufferingFeignClientResponse.status());
        }
        sb.append("\r\n");
        if (bufferingFeignClientResponse != null) {
            // 响应Header
            combineHeaders(sb, bufferingFeignClientResponse.headers());
            combineResponseBody(sb, bufferingFeignClientResponse.toBodyString(), bufferingFeignClientResponse.headers().get(HttpHeaders.CONTENT_TYPE));
        }
        if (exception != null) {
            sb.append("Exception:\r\n  ").append(exception.getMessage()).append("\r\n");
        }
        sb.append("\r\n[log ended]");
        log.debug(sb.toString());
        // 保存日志信息至缓存,可替换成MySQL或者MongoDB存储
        redisTemplate.opsForValue().set("sbLog" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")), sb.toString());
    }

    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 combineRequestBody(StringBuilder sb, byte[] body, Map<String, Collection<String>> params) {
        if (params != null) {
            sb.append("Request Params:\r\n").append("  ").append(params).append("\r\n");
        }
        if (body != null && body.length > 0) {
            sb.append("Request Body:\r\n").append("  ").append(new String(body)).append("\r\n");
        }
    }

    private static void combineResponseBody(StringBuilder sb, String respStr, Collection<String> collection) {
        if (respStr == null) {
            return;
        }
        if (collection.contains(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                respStr = JSON.parseObject(respStr).toString();
                //no care this exception
            } catch (JSONException ignored) {
            }
        }
        sb.append("Body:\r\n").append(respStr).append("\r\n");
    }

    static final class BufferingFeignClientResponse implements Closeable {
        private final 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 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);
        }
    }
}

使用Aspect切面记录日志

这个不推荐,因为它无法打印出具体的url、header等数据,有兴趣的可以看看全局记录Feign的请求和响应日志手动AOP这两篇文章

参考


  1. 官方文档提到: Client feignClient: If Spring Cloud LoadBalancer is on the classpath, FeignBlockingLoadBalancerClient is used. If none of them is on the classpath, the default feign client is used. ↩︎

### OpenFeign 4.0 新特性 OpenFeign作为声明式的Web服务客户端,简化了HTTP API调用过程。对于版本4.0而言,虽然具体提及此版本的新特性的直接资料较少见,但从相关技术演进和发展趋势来看,可以推测一些改进方向[^2]。 #### 日志级别增强 在日志管理方面,提供了更细致的日志控制选项。开发者能够通过设置不同的日志级别来获取所需的信息量: - `NONE`:关闭所有日志输出。 - `BASIC`:记录基本的请求方法、URL及响应状态码和执行时间。 - `HEADERS`:除了上述基本信息外,还会打印出请求与响应头部信息。 - `FULL`:最详细的模式,会记录完整的请求和响应细节,包括但不限于头信息、主体内容等。 这些级别的设定有助于调试期间更好地理解API交互情况,并可根据实际需求调整日志粒度以优化性能表现。 ```yaml logging: level: com.example.feignclient: DEBUG ``` 以上配置可使指定路径下的Feign Client启用DEBUG级别的日志输出,从而方便观察其工作流程。 --- ### 使用指南 为了有效利用OpenFeign的功能,在项目集成过程中需要注意以下几个要点: 1. **引入依赖** 需要在项目的构建文件(如Maven的pom.xml或Gradle的build.gradle)中加入必要的库依赖项,确保应用程序能识别并加载OpenFeign组件。 2. **定义接口** 创建一个Java接口用于描述目标RESTful API的服务契约,其中每个方法对应一次远程调用操作。使用注解方式标注参数映射关系和服务地址等信息。 3. **启动类配置** 在Spring Boot应用的主要入口处添加@EnableFeignClients注解,激活对Feign Clients的支持机制。 4. **自定义配置(可选)** 如果希望进一步定制化行为,则可以通过编写特定于某个Feign Client实例的Bean来进行属性覆盖或是重写默认的行为逻辑。 ```java @FeignClient(name="exampleService", url="${service.url}") public interface ExampleClient { @GetMapping("/api/resource") ResponseEntity<Resource> getResource(); } ``` 这段代码展示了如何创建一个简单的Feign Client接口,用来访问名为`exampleService`的服务资源。 --- ### 更新日志 关于具体的更新日志条目,通常这类信息会被维护在一个官方文档页面或者是GitHub仓库中的CHANGELOG.md文件里。由于当前提供的参考资料并未涉及确切的变更列表,建议查阅开源社区发布的正式公告或者查看源码历史提交记录来获得最权威的第一手消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值