предисловие
В этой главе в основном объединен исходный код, чтобы перечислить некоторые проблемы и решения, возникающие при использовании Feign.
1. Клиент Feign для балансировки нагрузки не определен. Вы забыли включить spring-cloud-starter-netflix-ribbon?
В исходном коде, читаемом в Главе 2, появился исходный код этой информации об исключении, а местоположением является метод loadBalance в процессе получения динамического прокси из getObject FeignClientFactoryBean. Видно, что причина ошибки в том, что экземпляр 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
причина
ILoadBalancer представлен пакетом jar ленты-loadbalancer, а пакет jar ленты-loadbalancer представлен spring-cloud-netflix-ribbon.Когда ILoadBalancer.class существует и интегрируется с лентой, он идет 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 по-прежнему займет класс конфигурации в Решении 1.
@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);
}
}
2. Не найден экземпляр fallbackFactory класса XXXFallBackFactory для фиктивного клиента.
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);
}
}
Решение 1. Контейнер Global 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)
Бизнес-линия feign-client, которая представляет stock-service-client.jar, может внедрить 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();
}
}
Решение 2. Подконтейнер внедряет 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)
Таким образом, когда другие бизнес-линии подключены, вы можете определить fallbackFactory с новой аннотацией FeignClient, наследуя этот интерфейс.
@FeignClient(name="stock-service",
primary = true,
fallbackFactory = MyStockFallbackFactory.class,
configuration = MyStockFallbackFactory.class)
3. Компонент 'xxx.FeignClientSpecification' не может быть зарегистрирован. Компонент с таким именем уже определен, и переопределение отключено.
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
причина
Часто появляясь в одной и той же службе, предоставление нескольких клиентских классов приводит к дублированию beanNames.
После сканирования FeignClient при регистрации конфигураций каждой аннотации FeignClient в глобальном контейнере Spring она инкапсулируется как FeignClientSpecification, а BeanName повторяется во время регистрации.
- Получите префикс BeanName для FeignClientSpecification, 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, зарегистрированный позже, перезапишет предыдущую регистрацию.
Однако это вызовет скрытые опасности, из-за которых bean-компонент не будет найден в подконтейнере, где находится клиент, например FallbackFactory Hystrix, внедренный через конфигурации. а такжеspring.main.allow-bean-definition-overriding
Для глобального контейнера Spring риск высок.
Решение второе
Задайте свойство contextId @FeignClient, чтобы у разных клиентов были разные имена FeignClientSpecification. Таким образом, у разных FeignClient будут разные подконтейнеры, которые полностью изолированы.
Решение третье
Напрямую поместите методы, предоставляемые одной и той же службой, в один и тот же клиент.
В-четвертых, почему 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 {
}
Зачем использовать шаблон прототипа
Ссылаясь на логику создания Feign.Builder FeignClientFactoryBean, обнаружено, что объект экземпляра Logger не получен из подконтейнера. Logger в каждом Builder следует за проксируемым FeignClient. StockClient3 и StockClient4 — два разных прокси. Для синглтона , присвоить значение свойству logger объекта singleton Builder, и предыдущее будет перезаписано, что явно неразумно.
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 является синглтоном.