因为我们想像 dubbo
调用远程服务一样,节省构建请求 body
并发送 http
请求,还要手动反序列化响应结果的步骤。使用 feign
能够让我们像同进程的接口方法调用一样调用远程进程的接口。
feign
是 spring cloud
组件中的一个轻量级 restful
的 http
服务客户端,内置了 ribbon
(因此使用 feign
也需要引入 ribbon
的依赖)。 openfeign
是 spring cloud
在 feign
的基础上支持了 spring mvc
的注解,如 @RequesMapping
、 @GetMapping
、 @PostMapping
等。
使用 openfeign
声明接口的例子:
@FeignClient(name = 'sck-demo-provider', path = "/v1", url = "http://sck-demo-provider", primary = false) public interface DemoService { @GetMapping("/hello") GenericResponse<String> sayHello(); } 复制代码
feign
用于服务消费端,即接口调用端,因此需要将服务提供端暴露的接口提取出来创建一个 Module
。当然,服务提供端也会依赖这个 Module
,因为数据传输对象 DTO
需要共用,也将 DTO
类跟接口放在一起,但不推荐服务提供者强制使用 implements
去实现接口。
使用 openfegin
我们可以不用在 yaml
文件添加任何关于 openfegin
的配置,而只需要在一个被 @Configuration
注释的配置类上或者 Application
启动类上添加 @EnableFeignClients
注解。例如:
@EnableFeignClients(basePackages = {"com.wujiuye.sck.consumer"}) public class SckDemoConsumerApplication { } 复制代码
basePackages
属性用于指定被 @FeignClient
注解注释的接口所在的包的包名,或者也可以直接指定 clients
属性, clients
属性可以直接指定一个或多个被 @FeignClient
注释的类。 basePackages
是一个数组,如果被 @FeignClient
注解注释的接口比较分散,可以指定多个包名,而不使用一个大的包名,这样可以减少包扫描耗费的时间,不拖慢应用的启动速度。
@Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { } 复制代码
@EnableFeignClients
注解使用 @Import
导入 FeignClientsRegistrar
类,这是一个 ImportBeanDefinitionRegistrar
,因此我们重点关注它的 registerBeanDefinitions
方法。(关于 Spring
的知识点,默认大家都懂了)。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); } } 复制代码
重点关注 registerFeignClients
方法,该方法负责读取 @EnableFeignClients
的属性,获取需要扫描的包名,然后扫描指定的所有包名下的被 @FeignClient
注解注释的接口,将扫描出来的接口调用 registerFeignClient
方法注册到 spring
容器。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); String contextId = getContextId(attributes); definition.addPropertyValue("contextId", contextId); // ...... definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); boolean primary = (Boolean) attributes.get("primary"); beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } } 复制代码
如 registerFeignClient
源码所示,该方法根据读取 @FeignClient
注解的属性配置,以及该接口的类名信息,向 Spring bean
工厂注册一个 FeignClientFactoryBean
,从名称可以看出这是一个 FactoryBean
,因此接下来我们主要看这个 FactoryBean
的 getObject
方法、 getObjectType
方法。 getObjectType
方法不用说,肯定是返回当前的被 @FeignClient
注解注释的那个接口的类名。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { @Override public Object getObject() throws Exception { return getTarget(); } } 复制代码
getObject
方法调用 getTarget
方法,但由于 getTarget
方法太长,只截取部分。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { <T> T getTarget() { FeignContext context = this.applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); // ....... String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { client = ((LoadBalancerFeignClient) client).getDelegate(); } if (client instanceof FeignBlockingLoadBalancerClient) { client = ((FeignBlockingLoadBalancerClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); } } 复制代码
Client
是 http
协议接口调用的实现,其定义如下:
public interface Client { Response execute(Request request, Options options) throws IOException; } 复制代码
正常情况下, getTarget
方法中调用 getOptional
方法获取到的 Client
是 NULL
。不正常情况就是添加了 ribbon
的 starter
包,这时拿到的 Client
是 LoadBalancerFeignClient
,我们后面分析。
不管怎样, FeignClientFactoryBean
的 getTarget
方法最后都是调用 Target
的 target
方法来获取实现该接口的实例。 Target
的实现类有两个: DefaultTargeter
和 HystrixTargeter
。在不使用 Hystrix
的情况下,我们只分析 DefaultTargeter
的实现。
class DefaultTargeter implements Targeter { @Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { return feign.target(target); } } 复制代码
如上源码所示, DefaultTargeter
调用 Feign.Builder
实例的 target
方法生成接口的实例,我们继续跟踪 target
方法的调用链,直到找到创建接口实例的方法。
如图所示, Feign.Builder
的 newInstance
方法正是创建接口实例的方法。有两种实现,一种是支持接口方法异步调用的,一种是普通的同步调用实现。不过这里用的还是 ReflectiveFeign
。
public class ReflectiveFeign extends Feign { @Override public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); // ...... Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); // ...... InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); // ....... return proxy; } } 复制代码
很熟悉的 JDK
动态代理。因此, Feign
并不会为接口生成实现类,而是生成一个动态代理对象。 factory.create
这句创建的 InvocationHandler
正是 FeignInvocationHandler
,此 InvocationHandler
是 JDK
实现动态代理的 InvocationHandler
,而 MethodHandler
是 feign
定义的 MethodHandler
。 methodToHandler
存储的是接口中定义的方法与 feign
生成的 MethodHandler
映射关系。
static class FeignInvocationHandler implements InvocationHandler { private final Target target; private final Map<Method, MethodHandler> dispatch; FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) { this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // .... return dispatch.get(method).invoke(args); } } 复制代码
当我们调用接口的方法时,都会走到 FeignInvocationHandler
的 invoke
方法, invoker
方法根据 method
获取对应的 MethodHandler
,并调用 MethodHandler
的 invoke
方法。往后 MethodHandler
要做的事情我们也基本能猜测得出来了。
想要找出这些 MethodHandler
在哪创建的,就需要回头从 FeignClientFactoryBean
的 getTarget
调用 DefaultTargeter
的 target
方法开始, DefaultTargeter
的 target
方法直接调用 Feign.Builder
的 target
方法, Feign.Builder
的 target
方法在调用 newInstance
方法之前调用了自身的 build
方法。
public class Feign{ public static class Builder{ public <T> T target(Target<T> target) { return build().newInstance(target); } public Feign build() { //...... ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } } } 复制代码
build
生成的 Feign
实例是 ReflectiveFeign
,因此, ReflectiveFeign
的 newInstance
方式的这句:
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); 复制代码
targetToHandlersByName
就是 ParseHandlersByName
的实例,我们来看下 ParseHandlersByName
的 apply
方法。
static final class ParseHandlersByName{ public Map<String, MethodHandler> apply(Target target) { // 解析接口的方法,生成MethodMetadata实例 List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); // 遍历接口方法 for (MethodMetadata md : metadata) { // ...... if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { // 调用Factory实例的create方法来创建MethodHandler result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result; } } 复制代码
apply
方法负责解析接口的方法,并为每个接口方法调用 Factory
实例的 create
方法创建 MethodHandler
。
static class Factory { public MethodHandler create(Target<?> target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding); } } 复制代码
创建的 MethodHandler
的类型是 SynchronousMethodHandler
。因此,当我们调用接口的方法时,最终调用的是 SynchronousMethodHandler
的 invoke
方法。
public class SynchronousMethodHandler implements MethodHandler{ @Override public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template, options); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } } } 复制代码
invoke
方法做的事情就是包装请求参数调用接口,如果配置了重试次数,失败会重试。还记得 FeignClientFactoryBean
的 getTarget
方法调用 Target
的 target
方法时传递的一个 HardCodedTarget
实例吗?这个就是用来生成请求参数 Request
的。
executeAndDecode
方法:
public class SynchronousMethodHandler implements MethodHandler{ Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { Request request = targetRequest(template); Response response; long start = System.nanoTime(); try { response = client.execute(request, options); response = response.toBuilder() .request(request) .requestTemplate(template) .build(); } catch (IOException e) { throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); if (decoder != null) return decoder.decode(response, metadata.returnType()); // ....... } } 复制代码
Client
我们前面提到过,就是实现发送 http
协议请求的,那 SynchronousMethodHandler
实例的这个 client
是哪个 Client
的实现类呢?答案在 Fegin
的内部类 Builder
:
public class Fegin{ public static class Build{ private Client client = new Client.Default(null, null); } } 复制代码
其实上面分析的源码过程,到 Fegin
的 Builder
开始,就是 Fegin
的代码,而不是 openfeign
。文章开头说 openfeign
支持 spring mvc
的注解,但是我们好像跳过了,这里提供一个调用链,大家可以根据这个调用链去找源码。
feign.ReflectiveFeign.ParseHandlersByName.apply > feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>) > feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>, java.lang.reflect.Method) > org.springframework.cloud.openfeign.support.SpringMvcContract.processAnnotationOnMethod 复制代码
SpringMvcContract
的 processAnnotationOnMethod
方法源码如下:
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware { @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation .annotationType().isAnnotationPresent(RequestMapping.class)) { return; } RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class); // HTTP Method RequestMethod[] methods = methodMapping.method(); if (methods.length == 0) { methods = new RequestMethod[] { RequestMethod.GET }; } checkOne(method, methods, "method"); data.template().method(Request.HttpMethod.valueOf(methods[0].name())); // path checkAtMostOne(method, methodMapping.value(), "value"); if (methodMapping.value().length > 0) { String pathValue = emptyToNull(methodMapping.value()[0]); if (pathValue != null) { pathValue = resolve(pathValue); if (!pathValue.equals("/")) { data.template().uri(pathValue, true); } } } // produces parseProduces(data, method, methodMapping); // consumes parseConsumes(data, method, methodMapping); // headers parseHeaders(data, method, methodMapping); data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>()); } } 复制代码
通过分析 openfeign
的源码,我们已经了解了 openfeign
是怎样与 Spring
整合的,以及 feign
到底做了什么,在源码分析的过程中,我们忽略了一些细节,而这些留到我们遇到问题时再去深挖。
openfeign
是怎么拿到url的? 你不好奇 openfeign
是怎么拿到注册中心的服务 url
的吗?
@FeignClient(name = YcpayConstant.SERVICE_NAME, path = "/v1", primary = false) 复制代码
当我们未配置 @FeignClient
的 url
属性时, name
就起作用了。 FeignClientFactoryBean
的 getTarget
方法被我们忽略的代码:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { <T> T getTarget() { // ..... if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { // 就是这句 this.url = "http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } // ....... } } 复制代码
假设我们配置的 name
为 sck-demo-provider
,那么生成的 url
就是:
http://sck-demo-provider 复制代码
你不好奇吗?为什么使用 openfeign
时,不配置 url
,且不导入 ribbon
的依赖会报错?
异常信息如下:
No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon? 复制代码
在分析 FeignClientFactoryBean
的 getTarget
方法源码时,我们漏掉了一些代码:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { <T> T getTarget() { // ...... if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); // loadBalance return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient) client).getDelegate(); } if (client instanceof FeignBlockingLoadBalancerClient) { // not load balancing because we have a url, // but Spring Cloud LoadBalancer is on the classpath, so unwrap client = ((FeignBlockingLoadBalancerClient) client).getDelegate(); } builder.client(client); } //...... } } 复制代码
两种情况:
URL
,那么 getOptional
方法不会返回 null
,且返回的 Client
是 LoadBalancerFeignClient
,但不会抛出异常。 URL
,则走负载均衡逻辑,走的是 loadBalance
方法,且抛出异常。 class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { // getOptional的最终调用: // public <T> T getInstance(String name, Class<T> type) { // AnnotationConfigApplicationContext context = getContext(name); // if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, // type).length > 0) { // return context.getBean(type); // } // return null; // } Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); } } 复制代码
根据前面的分析,正常情况下 getOptional
方法返回的 Client
绝对是 NULL
,所以就执行到了 loadBalance
方法的最后一行代码,抛出 IllegalStateException
异常。
现在我们可以猜测,难道添加 ribbon
之后, getOptional
就不返回 NULL
了吗?自动创建了一个 Client
实例并交由 Spring
管理?这个 Client
又是什么?
我们看下 spring-cloud-starter-openfeign
依赖导入的 spring-cloud-openfeign-core
,查看 自动配置文件 spring.factories
。
spring.factories
文件的内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=/ org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration // 其它省略 复制代码
其中 FeignRibbonClientAutoConfiguration
应该就是我们要找的。
@ConditionalOnClass({ ILoadBalancer.class, Feign.class }) @ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", matchIfMissing = true) @Configuration(proxyBeanMethods = false) @AutoConfigureBefore(FeignAutoConfiguration.class) @EnableConfigurationProperties({ FeignHttpClientProperties.class }) @Import({ HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class }) public class FeignRibbonClientAutoConfiguration { // ...... } 复制代码
当 spring.cloud.loadbalancer.ribbon.enabled
配置为 true
或者未配置时, @ConditionalOnProperty
自动配置条件都会成立。但是,当不导入 ribbon
的 starter
时, ILoadBalancer
是不存在的, @ConditionalOnClass
不满足条件,只有导入 ribbon
的 starter
包时,才会导入 HttpClientFeignLoadBalancedConfiguration
、 OkHttpFeignLoadBalancedConfiguration
、 DefaultFeignLoadBalancedConfiguration
这几个配置类。
HttpClientFeignLoadBalancedConfiguration
生效的条件是我们项目中添加 fegin-httpclient
的依赖; OkHttpFeignLoadBalancedConfiguration
生效的条件是我们项目中添加了 okhttp
的依赖,且配置了 feign.okhttp.enabled
为 true
; DefaultFeignLoadBalancedConfiguration
生效的条件是前两者都不生效。 @Configuration(proxyBeanMethods = false) class DefaultFeignLoadBalancedConfiguration { @Bean @ConditionalOnMissingBean public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { return new LoadBalancerFeignClient(new Client.Default(null, null),cachingFactory,clientFactory); } } 复制代码
所以,当不导入 ribbon
的 starter
时, ILoadBalancer
不存在, FeignRibbonClientAutoConfiguration
自动配置不会起作用,没有注入 Client
,但是因为没有配置 url
,所以走了 loadBalance
, loadBalance
方法中拿不到 Client
,最终抛出异常。
那么怎么解决这个问题? 两种方法:
ribbon
的 starter
依赖 如 sck-demo项目
种添加 ribbon
的 starter
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> </dependency> 复制代码
ribbon
的 starter
依赖,因为用不到,那么就需要显示配置 url
, 你可以将 url
配置 http://${spring.application.name}
。 笔者主要通过阅读源码解决自己的一些疑问,也希望通过本篇的分析能够帮助到大家。