Имитация чтения исходного кода (5) Анализ типичных проблем

Java

предисловие

  В этой главе в основном объединен исходный код, чтобы перечислить некоторые проблемы и решения, возникающие при использовании 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=trueBeanName разрешено повторять, и 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 является синглтоном.