目录

Spring Cloud OpenFeign

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  </dependency>

<dependencyManagement>
     <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@SpringBootApplication
@EnableFeignClients
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

@FeignClient(value = "jplaceholder", url = "https://jsonplaceholder.typicode.com/")
public interface JSONPlaceHolderClient {

    @GetMapping("/posts")
    List<Post> getPosts();

    @GetMapping(value = "/posts/{postId}", produces = "application/json")
    Post getPostById(@PathVariable("postId") Long postId);
}

@FeignClient

放在客户端,保证与服务提供方 Controller 方法签名相同即可。

也可以提供方写接口,提供方 Controller 继承,消费方只需要继承即可。

其实 @FeignClient 也可以定义在服务提供方,使用方引入提供方的 api jar 即可注入 client。

1
2
3
@FeignClient(name = "provider")
public interface ProviderClient extends ProviderApi {
}

@PathVariable 注解中的属性值 value 不能为空,不然会报错。

1
@PathVariable(value=name) String name

这个问题不会在Controller类上出现,但会在 Feign 接口上出现。也就是通常加了@FeignClient 注解的接口上。

配置 Configuration

了解每个 Feign 客户端都由一组可定制的组件组成非常重要。

Spring Cloud 使用 FeignClientsConfiguration 类为每个命名客户端按需创建一个新的默认集,我们可以按照下一节中的说明自定义该类。

上面的类包含这些bean:

  • Decoder - ResponseEntityDecoder,它包装了 SpringDecoder,用于解码 Response
  • Encoder SpringEncoder 用于对 RequestBody 进行编码。
  • Logger Slf4jLogger 是 Feign 使用的默认记录器。
  • Contract SpringMvcContract,提供注解处理
  • Feign-Builder Feign.Builder 用于构建组件。
  • Client LoadBalancerFeignClient 或默认 Feign 客户端

自定义配置

# xxx 表示服务名 connectTimeout和readTimeout 必须同时配置才能生效
feign.client.config.xxx.connectTimeout=1000
feign.client.config.xxx.readTimeout=3000

单个 FeginClient 配置

1
2
3
4
5
6
7
8
@FeginClient(name = "user", configuration = UserFeignClientConfig.class)
public interface UserFeignClient{}

public class UserFeignClientConfig{
  public Request.Options options() {
    return new Request.Options(10L, TimeUnit.SECONDS, 12L, TimeUnit.SECONDS, false);
  }
}

全局配置(优先级低)

1
2
3
4
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
1
2
3
4
5
6
7
@Configuration
public class MyClientConfiguration {
    @Bean
    public OkHttpClient client() {
        return new OkHttpClient();
    }
}

日志打印功能

首先需要配置Feign的打印日志的级别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Configuration
public class FeignConfig {
    /**
     * NONE:默认的,不显示任何日志
     * BASIC:仅记录请求方法、URL、响应状态码及执行时间
     * HEADERS:出了BASIC中定义的信息之外,还有请求和响应的头信息
     * FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元素
     */
    @Bean
    public Logger.Level feginLoggerLevel() {
        return Logger.Level.FULL;
    }
}

第二步,需要设置打印的Feign接口。Feign为每个客户端创建一个logger。默认情况下,logger的名称是Feigh接口的完整类名。需要注意的是,Feign的日志打印只会对DEBUG级别做出响应。

1
2
3
4
#与server同级
logging:
  level:
    com.ynthm.demo.consumer.feign.ProviderClient: debug

Spring Cloud CircuitBreaker Fallbacks

If Spring Cloud CircuitBreaker is on the classpath and feign.circuitbreaker.enabled=true, Feign will wrap all methods with a circuit breaker.

负载均衡

拦截器 RequestInterceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public RequestInterceptor requestInterceptor() {
  return requestTemplate -> {
      requestTemplate.header("user", username);
      requestTemplate.header("password", password);
      requestTemplate.header("Accept", ContentType.APPLICATION_JSON.getMimeType());
  };
}

public class FeignConfig {
  @Bean  
  public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {  
    return new BasicAuthRequestInterceptor("ynthm", "password");  
  }
}
@FeignClient(name="demo-app", configuration = FeignConfig.class)
public interface DemoClient {
  @GetMapping("/hello")
  String hello(@RequestParam String name);
}
1
2
3
4
5
6
feign:
  client:
    config:
      default:
        requestInterceptors:
          com.baeldung.cloud.openfeign.JSONPlaceHolderInterceptor

错误处理

Feign 的默认错误处理程序 ErrorDecoder.Default 总是抛出 FeignException。

现在,这种行为并不总是最有用的。因此,要自定义抛出的异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {

        switch (response.status()){
            case 400:
                return new BadRequestException();
            case 404:
                return new NotFoundException();
            default:
                return new Exception("Generic error");
        }
    }
}

public class ClientConfiguration {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}

同一服务多 @FeignClient

2.1.0.RELEASE版本之后 @FeignClient 新增了一个contextId属性,专门用于解决这个场景。

contextId默认不填等同于name

 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
/**
* 商品品牌feign独立配置
**/
public class GoodsBrandFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(4000, 4000);  
  }
}
/
/**
* 商品服务品牌模块
**/
@FeignClient(name = "goods", contextId = "brand", path = "brand", configuration = GoodsBrandFeignConfiguration.class)
public interface GoodsBrandFeign{}

/**
* 商品文章feign独立配置
**/
public class GoodsArticleFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(5000, 5000);  
  }
}
/
/**
* 商品服务文章模块
**/
@FeignClient(name = "goods", contextId = "article", path = "article", configuration = GoodsArticleFeignConfiguration.class)
public interface GoodsArticleFeign {}

https 配置

如果服务端没有 keystore 只有 truststore 可以将配置放到非 server.ssl

1
2
3
4
server:
  ssl:
    trust-store: classpath:ynthm-truststore.jks
    trust-store-password: changeit
 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
@Slf4j
public class FeignHttpsConfig {

  @Value("${server.ssl.trust-store}")
  private String trustStorePath;

  @Value("${server.ssl.trust-store-password}")
  private String trustStorePassword;

  @Bean
  @Scope("prototype")
  public Feign.Builder feignBuilder(Client.Default client) {
    return Feign.builder().client(client);
  }

  @Bean
  public Client.Default client() {
    try {
      SSLContext context = createSslContext();
      return new Client.Default(
          context.getSocketFactory(), new OkHttpClientFactory.TrustAllHostnames());
    } catch (Exception e) {
      log.error("Create feign client with SSL config failed", e);
      return new Client.Default(null, null);
    }
  }

  /** 创建并初始化 SSLContext */
  private SSLContext createSslContext() {
    try {
      TrustManager[] tm = null;
      if (!Strings.isNullOrEmpty(trustStorePath)) {
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(
            FeignHttpsConfig.class.getResourceAsStream(trustStorePath),
            trustStorePassword.toCharArray());

        // 创建信任库
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
        trustManagerFactory.init(trustStore);
        tm = trustManagerFactory.getTrustManagers();
      }

      // 初始化 SSLContext
      SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
      sslContext.init(null, tm, new SecureRandom());

      return sslContext;
    } catch (Exception ex) {
      throw new BaseException(ex);
    }
  }
}

@FeignClient(
    name = "svc-user",
    url = "https://localhost:8803",
    configuration = {FeignHttpsConfig.class})
public interface FeignClientDemo {

  @GetMapping("/users")
  Result<List<User>> test();
}

绕过证书验证

如果客户端使用 okhttp

1
2
3
4
5
6
        <!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-okhttp -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>11.10</version>
        </dependency>
1
2
3
4
5
6
@Bean
        public Feign okHttpClient(){
            okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder();
            OkHttpClient build = new DefaultOkHttpClientFactory(builder).createBuilder(true).build();
            return Feign.builder().client(new feign.okhttp.OkHttpClient(build)).build();
        }

因为 DefaultOkHttpClientFactory 中已经实现了绕过证书验证的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  public OkHttpClient.Builder createBuilder(boolean disableSslValidation) {
    if (disableSslValidation) {
      try {
        X509TrustManager disabledTrustManager = new OkHttpClientFactory.DisableValidationTrustManager();
        TrustManager[] trustManagers = new TrustManager[]{disabledTrustManager};
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init((KeyManager[])null, trustManagers, new SecureRandom());
        SSLSocketFactory disabledSSLSocketFactory = sslContext.getSocketFactory();
        this.builder.sslSocketFactory(disabledSSLSocketFactory, disabledTrustManager);
        this.builder.hostnameVerifier(new OkHttpClientFactory.TrustAllHostnames());
      } catch (NoSuchAlgorithmException var6) {
        LOG.warn("Error setting SSLSocketFactory in OKHttpClient", var6);
      } catch (KeyManagementException var7) {
        LOG.warn("Error setting SSLSocketFactory in OKHttpClient", var7);
      }
    }

    return this.builder;
  }

如果客户端使用 httpclient

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// httpclient
  @Bean
  public Client skipSSLClient() {
    try {
      return Optional.of(
             new SSLContextBuilder()
                 .loadTrustMaterial(null, (chain, authType) -> true)
                 .build()
               ).map(s -> new Client.Default(
                               s.getSocketFactory(),
                               new NoopHostnameVerifier())
               ).get();
    } catch (Exception e) {
      return new Client.Default(null, null);
    }
  }

附录