SpringCloud Feign的分析

时间:2023-03-09 09:12:24
SpringCloud Feign的分析

Feign是一个声明式的Web Service客户端,它使得编写Web Serivce客户端变得更加简单。我们只需要使用Feign来创建一个接口并用注解来配置它既可完成。

@FeignClient(value = "qrcodepay-dike-service")
public interface TestRoute {
@RequestMapping(value = "/dike/get", method = RequestMethod.GET)
HdResult get();
}

我们只需要在相应的接口上添加@FeignClient注解即可将他声明为一个web客户端。这其中的原理我们后续分析。我们首先先关注下feign暴露的几个配置。

  • value: 目标服务名,一般都是 application.name
  • fallback : 服务降级策略

@FeignClient(value = "qrcodepay-dike-service",fallback = TestRoute.TestRouteFaback.class)
public interface TestRoute {
@RequestMapping(value = "/dike/get", method = RequestMethod.GET)
HdResult get();
}
@Component
class TestRouteFaback implements TestRoute{
@Override
public HdResult get() {
return HdResult.makeFail("服务降级");
}
}
  • fallbackFactory :fallback的升级版,可以获取更加详细的异常信息
@FeignClient(value = "qrcodepay-dike-service",fallbackFactory = TestRoute.TestRouteFallbackFactory.class)
public interface TestRoute {
@RequestMapping(value = "/dike/get", method = RequestMethod.GET)
HdResult get(); @Component
class TestRouteFallbackFactory implements FallbackFactory<TestRoute>{
private static final Logger logger = LoggerFactory.getLogger(TestRouteFallbackFactory.class);
@Override
public TestRoute create(Throwable throwable) {
String msg = throwable == null ? "" : throwable.getMessage();
if (!StringUtils.isEmpty(msg)) {
logger.error("异常信息打印:{}",msg);
}
return new TestRoute() {
@Override
public HdResult get() {
return HdResult.makeFail(msg);
}
};
} }
}
  • configuration:重写feign的配置

具体哪些内容可以配置我们可以看  FeignClientsConfiguration和feign.Feign.Builder。

下面用两种方式重写feign的配置

覆盖原有的配置bean达到重写目的

@Configuration
public class FeignBreakerConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new UserErrorDecoder();
}
/**
* 自定义错误解码器 只有返回http status 非200才会进入
*/
public class UserErrorDecoder implements ErrorDecoder {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Exception decode(String methodKey, Response response) {
Exception exception = null;
try {
String json = Util.toString(response.body().asReader());
System.out.println("自定义解码:"+json);
exception = new RuntimeException(json);
HdResult result=HdResult.makeFail(json);
// 业务异常包装成 HystrixBadRequestException,不进入熔断逻辑
// if (!result.isSuccess()) {
// exception = new HystrixBadRequestException(result.getMessage());
// }
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
return exception;
}
}
}

自定义客户端达到重写的目的

@Import(FeignClientsConfiguration.class)
@RestController
public class DefaultController {
private FeignClientService feignClientService;
public DefaultController(Decoder decoder, Encoder encoder, Client client){
this.feignClientService = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.requestInterceptor(new BasicAuthRequestInterceptor("user","password"))
.target(FeignClientService.class,"http://eureka-client");
} @RequestMapping(name = "/default",method = RequestMethod.GET)
public String getInfo(){
return feignClientService.getValue("hello world!");
}
}

feignclient最常用的配置大致如上,接下来介绍下feign实现的原理。

先说结论,feign是通过动态代理的技术将一个interface变为Web Service客户端。那我们应该从哪里入手呢。在使用feign的时候,我们应该关注两个注解,一个就是我们上文所说的feignClient,但是仅仅只用这个注解feign是不会生效的,必须要在启动类上加上EnableFeignClients,feign才会自动扫描feignClient。所以我们的入口应该是 EnableFeignClients

SpringCloud Feign的分析

EnableFeignClients 导入了FeignClientsRegistrar,这个注解真正的逻辑就在FeignClientsRegistrar中

SpringCloud Feign的分析

这个类实现了三个接口,我们先关注 ImportBeanDefinitionRegistrar,这是spring动态注册bean的接口。所以spring在启动的时候会调用以下方法

public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}

将配置类纳入beandefinationMap管理 ,这一块更为详细的内容可以看  SpringIoc分析

private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}

扫描FeignClient注解,将interface纳入beanDefination

public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
} for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName()); String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration")); registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}

接下来,我们需要找到jdk代理的地方

我们在构建feign的地方发现如下方法

public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
}

最终我们在SynchronousMethodHandler类中发现了真正拦截的代码

public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}

真正执行的逻辑如下,这里也是feign最为关键的地方。这里我们主要关注下真正请求的那一行。如果想对feign做debug或者重写一些配置,参考这里会是一个很好的入口。

Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
} Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
}
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else if (decode404 && response.status() == 404) {
return decoder.decode(response, metadata.returnType());
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}

这里的client是请求客户端,feign统一封装为LoadBalancerFeignClient

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration { @Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
} }

默认的Client 是HttpURLConnection,同时 feign也支持httpclient和okhhtp

@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration { @Autowired(required = false)
private HttpClient httpClient; @Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
if (this.httpClient != null) {
return new ApacheHttpClient(this.httpClient);
}
return new ApacheHttpClient();
}
} @Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
protected static class OkHttpFeignConfiguration { @Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient; @Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
if (this.okHttpClient != null) {
return new OkHttpClient(this.okHttpClient);
}
return new OkHttpClient();
}
}

只要满足 配置条件,就可以将httpclient或okhhtp引入,这里举例说明怎么使用httpclient

在pom文件加上:

<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>RELEASE</version>
</dependency>

在配置文件上加上feign.httpclient.enabled为true(默认为true,可不写)

最后,我们再看看feign是怎么使用ribbon的,上文我们说过feign统一将client封装为LoadBalancerFeignClient,fein的请求最终都会到以下代码

public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost); IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}

具体我们可以看下 executeWithLoadBalancer 

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build(); try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
} }

在submit方法里,发现了如下代码

// Use the load balancer
Observable<T> o =
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
}

这里的selectServer 最终会调用 ILoadBalancer 选择一个server

ILoadBalancer lb = getLoadBalancer();
if (host == null) {
// Partial URI or no URI Case
// well we have to just get the right instances from lb - or we fall back
if (lb != null){
Server svc = lb.chooseServer(loadBalancerKey);

关于这方面的具体内容,请参考 SpringCloud Ribbon的分析

以上,就是对feign的具体分析