前言
这章主要结合源码,列举一些使用Feign遇到的问题和解决方法。
一、No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?
在第二章的源码阅读里,出现过这个异常信息的源码,位置是在FeignClientFactoryBean的getObject获取动态代理的流程中,loadBalance方法。可以看到报错的原因是从子容器获取Client实例为空。
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
// 子容器获取feign.Client的实现类,这是之后feign调用流程的主入口,一般实现是LoadBalancerFeignClient
Client client = getOptional(context, Client.class);
if (client != null) {
// ...
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
解决方案一
引入spring-cloud-starter-netflix-ribbon
原因
ILoadBalancer是由ribbon-loadbalancer的jar包引入的,而ribbon-loadbalancer的jar包由spring-cloud-netflix-ribbon引入,当ILoadBalancer.class存在与ribbon集成就走FeignRibbonClientAutoConfiguration
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Import({DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
}
DefaultFeignLoadBalancedConfiguration导入了LoadBalancerFeignClient作为feign.Client实现类
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
clientFactory);
}
}
解决方案二
引入spring-cloud-starter-netflix-eureka-client或者spring-cloud-starter-consul-discovery
原因
与服务注册发现组件(Eureka、Consul)会引入spring-cloud-loadbalancer.jar。
设置spring.cloud.loadbalancer.ribbon.enabled=false会走FeignLoadBalancerAutoConfiguration配置。默认情况下spring.cloud.loadbalancer.ribbon.enabled=true,所以一般集成Eureka、Consul,还是会走解决方案一里的配置类。
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@Import({DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
}
DefaultFeignLoadBalancerConfiguration引入FeignBlockingLoadBalancerClient作为feign.Client的实现类
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null),
loadBalancerClient);
}
}
二、No fallbackFactory instance of type class XXXFallBackFactory found for feign client
容器中找不到FallbackFactory。
原因:容器中找不到FallbackFactory。
class HystrixTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
// ...
// 获取name contextId>name
String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
: factory.getContextId();
// 获取fallbackFactory的class对象
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
// 往Builder里注入fallbackFactory
// fallbackFactory是通过FeignContext.getInstance(name, fallbackFactory)获取的
return targetWithFallbackFactory(name, context, target, builder,
fallbackFactory);
}
return feign.target(target);
}
}
解决方案一:全局Spring容器注入FallbackFactory
坏处是平常做业务开发时,feign-client常常作为一个二方包提供给别的业务线调用,别的业务线需要扫描或者手动注入FallbackFactory,非常不方便,而且容易引导调用方直接扫描com.xxx下的所有组件,这是有风险的。
- 案例
提供二方包stock-service-client.jar
@Component
public class StockFallbackFactory implements FallbackFactory<StockClient> {
}
@FeignClient(name="stock-service", fallbackFactory = StockFallbackFactory.class)
引入stock-service-client.jar的feign-client的业务线,可能会将StockFallbackFactory注入到全局ioc容器。
@SpringBootApplication
@ComponentScan(basePackages = "com.xxx")
@EnableFeignClients(basePackages = "com.xxx.stock.client")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
好一些的做法是
@SpringBootApplication
@EnableFeignClients(basePackages = "com.xxx.stock.client")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Configuration
public class FallbackFactoryConfiguration {
@Bean
public StockFallbackFactory stockFallbackFactory() {
return new StockFallbackFactory();
}
}
解决方案二:子容器注入FallbackFactory
全局注入FallbackFactory的做法违背了Feign的初衷,正常做法应该是让子容器注入这个FallbackFactory,而不是全局spring容器注入。
@FeignClient(name="stock-service",
fallbackFactory = StockFallbackFactory.class,
configuration = StockFallbackFactory.class)
更优秀的二方包提供FeignClient,需要将FeignClient设置为primary=false提供出去
@FeignClient(name="stock-service",
primary = false,
fallbackFactory = StockFallbackFactory.class,
configuration = StockFallbackFactory.class)
这样其他业务线接入,就可以通过继承这个接口,用一个新的FeignClient注解定义fallbackFactory
@FeignClient(name="stock-service",
primary = true,
fallbackFactory = MyStockFallbackFactory.class,
configuration = MyStockFallbackFactory.class)
三、The bean 'xxx.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
The bean 'trade-service.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
Action: Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
原因
往往出现于同一个服务,提供多个client类导致beanName重复。
扫描FeignClient后,注册每个FeignClient注解的configurations到全局Spring容器时,封装为FeignClientSpecification,注册时BeanName重复。
- 获取FeignClientSpecification的BeanName前缀,contextId>value>name>serviceId
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
+ FeignClient.class.getSimpleName());
}
- 注册FeignClientSpecification
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// name + "." + FeignClientSpecification.class.getSimpleName() 这个BeanName出现多次
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
解决方案一
spring.main.allow-bean-definition-overriding=true允许BeanName重复,后注册BeanDefination的会覆盖前面注册的。
但是这样会造成隐患,导致后续某个client所在的子容器中bean找不到,比如通过configurations注入的Hystrix的FallbackFactory。而且spring.main.allow-bean-definition-overriding是对于全局Spring容器而言的,风险高。
解决方案二
设置@FeignClient的contextId属性,让不同的client有不同的FeignClientSpecification名字。并且这样设置不同的FeignClient会有不同的子容器,完全隔离。
解决方案三
直接把同一个服务提供的方法放入同一个client中。
四、为什么Feign.Builder是原型模式
Feign.Builder作为构建Feign动态代理的核心组件,为什么不设计为单例模式?
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
什么时候同一个子容器会产生两个Feign.Builder
因为开启spring.main.allow-bean-definition-overriding=true后,像这样两个FeignClient实际使用了同一个子容器
@FeignClient(name = "trade-service")
public interface StockClient3 {
}
@FeignClient(name = "trade-service")
public interface StockClient4 {
}
为什么要使用原型模式
参考FeignClientFactoryBean构造Feign.Builder的这段逻辑,发现Logger实例对象并非从子容器获取,每个Builder里的Logger是跟着被代理的FeignClient走的,StockClient3和StockClient4是两个不同的被代理者,如果用单例,给Builder单例对象的logger属性赋值,之前的就会被覆盖,这显然不太合理。
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);
Feign.Builder builder = get(context, Feign.Builder.class)
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
configureFeign(context, builder);
return builder;
}
而且一个Feign.Builder建造者对应一个最后生成的动态代理,从设计角度考虑也更加合理,后续扩展不会因为Builder是单例而有困扰。