Feign源码阅读(五)常见问题解析

7,440 阅读5分钟

前言

  这章主要结合源码,列举一些使用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是单例而有困扰。