原创 吴就业 132 0 2020-06-27
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/7c7eb3472aef4d049f9bd7dc21dcfa37
作者:吴就业
链接:https://wujiuye.com/article/7c7eb3472aef4d049f9bd7dc21dcfa37
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
本篇文章写于2020年06月27日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
本篇内容:
feign
?openfeign
是怎么拿到url的?ribbon
应用会启动不起来?feign
?因为我们想像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);
}
//......
}
}
两种情况:
* 1、如果指定了URL
,那么getOptional
方法不会返回null
,且返回的Client
是LoadBalancerFeignClient
,但不会抛出异常。
* 2、如果不指定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
,最终抛出异常。
那么怎么解决这个问题?
两种方法:
* 1、 添加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}
。笔者主要通过阅读源码解决自己的一些疑问,也希望通过本篇的分析能够帮助到大家。
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
本篇我们再对Ribbon的重试机制地实现做详细分析,从源码分析找出我们想要地答案,即如何配置Ribbon实现调用每个服务的接口使用不一样的重试策略,如配置失败重试多少次,以及自定义重试策略RetryHandler。
本篇介绍OpenFeign与Feign的关系、Feign底层实现原理、Ribbon是什么、Ribbon底层实现原理、Ribbon是如何实现失败重试的?
本篇我们将从一个简单的demo上手Spring Cloud kubernetes,当然,我们只用到Spring Cloud kubernetes的服务注册与发现、配置中心模块。
选择Spring Cloud Kubernetes意味着我们想要将服务部署到Kubernetes集群,Spring Cloud Kubernetes为我们实现了Spring Cloud的一些接口,让我们可以快速搭建Spring Cloud微服务项目框架,并能使用Kubernetes云原生服务。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。