Feign的日志配置

有时候我们遇到Bug,比如接口调用失败、参数没收到等问题,或者想看看调用性能,就需要配置Feign的日志了,以此让Feign把请求信息输出来。

全局配置

定义一个配置类,指定日志级别:

package com.morris.user.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 注意: 此处配置@Configuration注解就会全局生效,如果想指定对应微服务生效,就不能配置@Configuration
@Configuration
public class FeignConfig {

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

通过源码可以看到日志等级有4种,分别是:

  • NONE:不记录任何日志(默认值),性能最佳,适用于生产
  • BASIC:仅记录请求方法、URL、响应状态代码以及执行时间,适用于生产环境追踪问题
  • HEADERS:在BASIC级别的基础上,记录请求和响应的header
  • FULL:记录请求和响应的header、body和元数据,比较适用于开发及测试环境定位问题

上面只是指定日志的内容,还需要在配置文件中配置Client的日志级别才能正常输出日志,格式是"logging.level.feign接口包路径=debug":

logging:
  level:
    com.morris.user.client.OrderClient: debug

会看到如下的打印日志:

2023-08-14 13:53:42.948 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] ---> GET http://order-service/order/findOrderByUserId?userId=1 HTTP/1.1
2023-08-14 13:53:42.948 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] ---> END HTTP (0-byte body)
2023-08-14 13:53:43.169 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] <--- HTTP/1.1 200 (213ms)
2023-08-14 13:53:43.170 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] connection: keep-alive
2023-08-14 13:53:43.170 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] content-type: application/json
2023-08-14 13:53:43.170 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] date: Mon, 14 Aug 2023 05:53:43 GMT
2023-08-14 13:53:43.170 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] keep-alive: timeout=60
2023-08-14 13:53:43.170 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] transfer-encoding: chunked
2023-08-14 13:53:43.171 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] 
2023-08-14 13:53:43.171 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] [{"id":1,"userId":1,"goodName":"Iphone 13","price":9999}]
2023-08-14 13:53:43.171 DEBUG 22948 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#findOrderByUserId] <--- END HTTP (57-byte body)

局部配置

局部配置可以让指定调用的微服务生效,在@FeignClient注解中指定使用的配置类

package com.morris.user.client;

import com.morris.user.config.FeignConfig;
import com.morris.user.entity.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient(value = "order-service", path = "/order", configuration = FeignConfig.class)
public interface OrderClient {

    @GetMapping("findOrderByUserId")
    List<Order> findOrderByUserId(@RequestParam("userId") Long userId);

}

注意FeignConfig类不要加@Configuration注解,否则就会被Spring扫描到,变成了一个全局的配置。

同样需要在配置文件中指定日志的级别才会打印:

logging:
  level:
    com.morris.user.client.OrderClient: debug

局部配置可以在yml中配置,对应属性配置类: org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration

feign:
  client:
    config:
      order-service:  #对应微服务
        loggerLevel: FULL

拦截器

每次Feign发起http调用之前,会去执行拦截器中的逻辑,拦截器可以修改请求的参数或者头部信息。

例如我们可以在拦截器中对请求头增加认证信息,增加链路追踪信息。

下面通过拦截器在请求头中增加一个参数实现分布式系统中请求的链路追踪,自定义拦截器需要实现RequestInterceptor接口:

package com.morris.user.config;

import feign.RequestInterceptor;
import feign.RequestTemplate;

import java.util.UUID;

public class FeignTraceRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 业务逻辑
        String traceId = UUID.randomUUID().toString();
        template.header("TID", traceId);
    }
}

全局配置

与日志的全局配置一样,放入到一个配置文件中。

package com.morris.user.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 注意: 此处配置@Configuration注解就会全局生效,如果想指定对应微服务生效,就不能配置@Configuration
@Configuration
public class FeignConfig {

    @Bean
    public FeignTraceRequestInterceptor feignTraceRequestInterceptor() {
        return new FeignTraceRequestInterceptor();
    }
}

局部配置

在@FeignClient注解中指定配置文件,注意FeignConfig类不要加@Configuration注解,否则就会被Spring扫描到,变成了一个全局的配置。

@FeignClient(value = "order-service", path = "/order", configuration = FeignConfig.class)

也可以在yml中配置:

feign:
  client:
    config:
      order-service:  #对应微服务
        requestInterceptors[0]: #配置拦截器
          com.morris.user.config.FeignTraceRequestInterceptor

order-service端可以通过@RequestHeader获取请求参数,建议在Filter,Interceptor中统一处理。

超时时间配置

通过Options可以配置连接超时时间和读取超时时间,Options的第一个参数是连接的超时时间(ms),默认值是 2s;第二个是请求处理的超时时间(ms),默认值是5s。

全局配置

与日志、拦截器的全局配置一样,放入到一个配置文件中。

package com.morris.user.config;

import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 注意: 此处配置@Configuration注解就会全局生效,如果想指定对应微服务生效,就不能配置@Configuration
@Configuration
public class FeignConfig {

    @Bean
    public Request.Options options() {
        return new Request.Options(5000, 10000);
    }
}

超时了会抛出SocketTimeoutException异常:

2023-08-15 09:45:53.595 ERROR 11104 --- [nio-8030-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.RetryableException: Read timed out executing GET http://order-service/order/findOrderByUserId] with root cause

java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_281]
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_281]

局部配置

在@FeignClient注解中指定配置文件,注意FeignConfig类不要加@Configuration注解,否则就会被Spring扫描到,变成了一个全局的配置。

@FeignClient(value = "order-service", path = "/order", configuration = FeignConfig.class)

也可以在yml中配置:

feign:
  client:
    config:
      order-service:  #对应微服务
        # 连接超时时间,默认2s
        connectTimeout: 5000
        # 请求处理超时时间,默认5s
        readTimeout: 10000

Feign的底层用的是Ribbon,但超时时间以Feign配置为准。

客户端组件配置

Feign中默认使用JDK原生的URLConnection发送HTTP请求,我们可以集成别的组件来替换掉URLConnection,比如Apache HttpClient,OkHttp。

Feign发起调用真正执行逻辑:feign.Client#execute(扩展点)

配置Apache HttpClient

引入HttpClient依赖:

<!-- Apache HttpClient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>10.1.0</version>
</dependency>

然后修改yml配置,将Feign的Apache HttpClient启用:

feign:
  #feign 使用 Apache HttpClient  可以忽略,默认开启
  httpclient:
    enabled: true

关于配置可参考源码:org.springframework.cloud.openfeign.FeignAutoConfiguration

测试:调用会进入feign.httpclient.ApacheHttpClient#execute

配置OkHttp

引入OkHttp的依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

然后修改yml配置,将Feign的HttpClient禁用,启用OkHttp,配置如下:

feign:
  #feign 使用 okhttp
  httpclient:
    enabled: false
  okhttp:
    enabled: true

关于配置可参考源码:org.springframework.cloud.openfeign.FeignAutoConfiguration

测试:调用会进入feign.okhttp.OkHttpClient#execute

GZIP压缩配置

开启压缩可以有效节约网络资源,提升接口性能,我们可以配置GZIP来压缩数据:

feign:
  # 配置 GZIP 来压缩数据
  compression:
    request:
      enabled: true
      # 配置压缩的类型
      mime-types: text/xml,application/xml,application/json
      # 最小压缩值
      min-request-size: 2048
    response:
      enabled: true

开启后在日志中会看到请求头中有gzip:

2023-08-15 14:38:38.482 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] ---> POST http://order-service/order/bigJson HTTP/1.1
2023-08-15 14:38:38.482 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Accept-Encoding: gzip
2023-08-15 14:38:38.483 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Accept-Encoding: deflate
2023-08-15 14:38:38.483 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Content-Encoding: gzip
2023-08-15 14:38:38.483 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Content-Encoding: deflate
2023-08-15 14:38:38.483 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Content-Length: 289891
2023-08-15 14:38:38.483 DEBUG 23664 --- [nio-8030-exec-1] com.morris.user.client.OrderClient       : [OrderClient#bigJson] Content-Type: application/json

注意:只有当Feign的Http组件不是okhttp3的时候,压缩才会生效,配置源码在FeignAcceptGzipEncodingAutoConfiguration

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties({FeignClientEncodingProperties.class})
@ConditionalOnClass({Feign.class})
@ConditionalOnBean({Client.class})
@ConditionalOnProperty(
    value = {"feign.compression.response.enabled"},
    matchIfMissing = false
)
@ConditionalOnMissingBean(
    type = {"okhttp3.OkHttpClient"}
)
@AutoConfigureAfter({FeignAutoConfiguration.class})
public class FeignAcceptGzipEncodingAutoConfiguration {
    public FeignAcceptGzipEncodingAutoConfiguration() {
    }

    @Bean
    public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
        return new FeignAcceptGzipEncodingInterceptor(properties);
    }
}

核心代码就是@ConditionalOnMissingBean(type=“okhttp3.OkHttpClient”),表示Spring BeanFactory中不包含指定的bean时条件匹配,也就是没有启用okhttp3时才会进行压缩配置。

编码器解码器配置

Feign中提供了自定义的编码解码器设置,同时也提供了多种编码器的实现,比如Gson、Jaxb、Jackson。我们可以用不同的编码解码器来处理数据的传输。如果你想传输XML格式的数据,可以自定义XML编码解码器来实现获取使用官方提供的Jaxb。

扩展点:Encoder & Decoder

public interface Encoder {
    void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}
public interface Decoder {
    Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
}

Java配置方式

配置编码解码器只需要在Feign的配置类中注册Decoder和Encoder这两个类即可:

@Bean
public Decoder decoder() {
    return new JacksonDecoder();
}
@Bean
public Encoder encoder() {
    return new JacksonEncoder();
}

yml配置方式

feign:
  client:
    config:
      order-service:  #对应微服务
        # 配置编解码器
        encoder: feign.jackson.JacksonEncoder
        decoder: feign.jackson.JacksonDecoder