背景
周末在家,正忙着在黑神话中(被)暴揍大头娃娃,突然接到一顿夺命连环call,声称我的登录服务挂了,无法正常登录。
我紧忙思考着近期上线的内容,但是怎么也想不到近期有什么登录改动上线。但是出于负责任的态度,我还是分析登录日志,找一些蛛丝马迹。
不找不知道,一找吓一跳,所有的登录UA在日志中都变成了这个:
Apache-HttpClient/4.5.6 (Java/1.8.0_311)
经过缜密的分析,我发现是最近一次上线中,新增了一个FeignClient出现的问题。
原因
相关版本
Spring Boot 2.0.2-RELEASE
Spring Cloud OpenFeign Core 2.0.1.RELEASE
相关操作
新增了一个AuthTokenApi类,注解为@FeignClient(value = "passport-service", path = "/authentication/authToken") 导致整个"passport-service"的client的"configuration"失效,导致feign无法自动透传请求头。
原理
- org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
// 重点,会按照hashCode自动进行打乱排序
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
// 此时进行了注册FeignClientSpecification
//如果有多个FeignClientSpecification,会取最后注册的一个
//万恶之源!
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
修复动作
修改该注解 @FeignClient(value = "passport-service", path = "/authentication/authToken",configuration = FeignAuthConfig.class)
反思
根本原因在于旧版本Feign中,Client的beanName就是serviceName,导致配置会互相覆盖,容易产生许多意外的错误。
诚然,引入的依赖有着出人意料的坑,很难防范。但是也在于Fegin了解不够透彻,没有对源码有着深入的理解,应该在使用依赖库时,深入的了解其实现原理,减少技术债务,才能更好的避免使用中遇到的问题。
预防措施
总共有3种方案可以解决这个问题
-
升级spring cloud feign版本至2.1.x及后续,支持为FeignClient增加contextId字段,可以一劳永逸避免此问题。
-
优点
- 一劳永逸,也同时避免各个Client的配置互相污染
-
缺点
- 代价较高,需要改造原有项目,原本的坑可能已经成为了业务,排查难度大,升级有风险。
-
-
将
FeignAuthConfig设置为通用配置项,无需每个项目中配置,即可避免类似问题。-
优点
- 无需手动引入,避免手动出现的错误
-
缺点
- 部分服务本来无需相关透传信息,因此增加了网络消耗,降低性能;也有可能由于顶替请求头等信息,产生其他意外的错误。
-
-
复盘此问题,告知注解中configuration的重要性,口头约定每个人在自己Feign Client中加入。
-
优点
- 无需改动代码
-
缺点
- 后续仍有可能出现类似错误,口头约束不保险。
-
扩展阅读
feign相关
-
流程
-
初始化逻辑
- FeignClientsRegistrar扫描相关注解,根据service name注册
FeignClientSpecificationBean FeignContext通过继承NamedContextFactory<FeignClientSpecification>,获取到所有的相关实例作为configurations
- FeignClientsRegistrar扫描相关注解,根据service name注册
-
构建实例逻辑
FeignClientFactoryBean通过FeignContext.getInstance(name,Feign.Builder.class)为所有FeignClient接口提供实现。FeignClientFactoryBean通过configureFeign方法,对于每个实例进行针对性配置。
-
spring Bean注册相关
- DefaultListableBeanFactory#registerBeanDefinition
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
// 略
BeanDefinition oldBeanDefinition;
oldBeanDefinition = this.beanDefinitionMap.get(beanName);
if (oldBeanDefinition != null) {
// 校验与日志逻辑,略
// 替换旧的BeanDefinition
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
//首次创建,略
}
if (oldBeanDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}