版本信息:
- springBoot 2.2.6.RELEASE
- springCloud Hoxton.RELEASE
源码分析
1.1 Feign支持替换OkHttp的做法
1.1.1 代码变动
- 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign的Starter的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
</dependencies>
- 修改配置
feign:
client:
defaultToProperties: true
defaultConfig: myconf
config:
myconf:
connectTimeout: 8000
readTimeout: 120000
okhttp:
enabled: true
httpclient:
enabled: false
1.1.2 原理分析
详见此类,当引入feign-okhttp
配置后,且存在feign.okhttp.enabled
配置时,将初始化该Config类,那么便会加载feignClient
的bean。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
如上述代码所示:首先创建了feign.okhttp.OkHttpClient
来装饰okhttp3.OkHttpClient
对象(装饰器模式:接口不变,功能增强)然后作为实际的代理client传入了LoadBalancerFeignClient
对象中。
故:若装饰底层的client对象,即可自定义实现feignClient
类
1.2 方案讨论
能否使用Feign提供的拦截器完成监控?
Feign接口提供了RequestInterceptor可拦截请求对象,文章详见——feign调用时使用RequestInterceptor设置request对象
那么若实现监控的逻辑能否写在RequestInterceptor中?
不可以!
理由:监控feign接口的调用情况,需要获取到的参数eureka的serverName
、实际ip:port
。但如下图所示,无法获取到上述参数。
结论:只能去装饰Client对象完成监控逻辑。
1.3 装饰逻辑实现
1.3.1 自定义feignClient bean
@Configuration
public class FeignSupportConfig {
@Bean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory,okhttp3.OkHttpClient client) {
MyOpenFeignHttpClient delegate = new MyOpenFeignHttpClient(new feign.okhttp.OkHttpClient(client));
//先执行下面这个Client(MyLoadBalancerFeignClient)的execute -> 后再执行上面一个Client(MyOpenFeignHttpClient)的execute
return new MyLoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
查看结果:
源码位置:feign.SynchronousMethodHandler#executeAndDecode
可以看到执行的client已经为自定义的MyLoadBalancerFeignClient
。
1.3.2 获取到serverName并放置到header
需要装饰LoadBalancerFeignClient
类,将serverName放置到header中。代码如下面所示:
public class MyLoadBalancerFeignClient extends LoadBalancerFeignClient {
public MyLoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
super(delegate, lbClientFactory, clientFactory);
}
/**
* 装饰器模式
*
* @param request 不可改变的request对象,故填充header只能重写
* @param options 配置信息
* @return
* @throws IOException
*/
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
Collection<String> headerValues = new ArrayList<>();
headerValues.add(clientName);
Map<String, Collection<String>> safeCopy = new ConcurrentHashMap<>(request.headers());
safeCopy.put(WPSConstants.FEIGN_METRIC_HEADER_SERVICENAME, headerValues);
Request newRequest = Request.create(request.httpMethod(),
request.url(),
safeCopy,
request.requestBody());
return super.execute(newRequest, options);
}
}
1.3.3 获取ip:port并完成监控逻辑
实际使用的Client对象为OkHttp,而在feignClient
中,我们真正传入的为MyOpenFeignHttpClient
。
public class MyOpenFeignHttpClient implements Client {
private final OkHttpClient client;
public MyOpenFeignHttpClient(OkHttpClient client) {
this.client = client;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URIBuilder uriBuilder;
String serviceName = CollectionUtils.isEmpty(request.headers().get(WPSConstants.FEIGN_METRIC_HEADER_SERVICENAME)) ?
"" : request.headers().get(WPSConstants.FEIGN_METRIC_HEADER_SERVICENAME).iterator().next();
try {
uriBuilder = new URIBuilder(request.url());
} catch (URISyntaxException e) {
throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
}
String remoteAddr =
uriBuilder.getPort() > 0 ? uriBuilder.getHost() + ":" + uriBuilder.getPort() : uriBuilder.getHost();
int status = 506; //IOException的特定特殊码
String path = uriBuilder.getPath();
Response response;
OpenFeignMetricsInstance openFeignMetricsInstance = (OpenFeignMetricsInstance) SpringUtils.getBean("openFeignMetricsInstance");
try {
//记录api的metric
final Stopwatch stopwatch = Stopwatch.createStarted();
//真正执行的逻辑
response = client.execute(request, options);
stopwatch.stop();
status = response.status();
long time = stopwatch.elapsed(TimeUnit.MILLISECONDS);
//监控逻辑——记录status以及响应时间
return response;
} catch (Exception e) {
//监控逻辑——记录异常
throw e;
} finally {
//监控逻辑——记录总数
}
}
}