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);
}
}
|
附录