项目上开发了OpenFeign的自定义解码器,用来统一处理返回结果。
开发完后测试已经生效了,过两天后,这块代码没有变动的情况下,发现请求结果突然又不走自定义的解码器了。
代码如下
解码器 BaseResponseFeignDecoder
@Slf4j
public class BaseResponseFeignDecoder implements Decoder {
static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
if (response.body() == null) {
throw new DecodeException(response.status(), "没有返回有效的数据", response.request());
}
String bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
//对结果进行转换
TypeFactory typeFactory = objectMapper.getTypeFactory();
JavaType resultType = typeFactory.constructParametricType(BaseResponse.class, typeFactory.constructType(type));
BaseResponse<?> result = objectMapper.readValue(bodyStr, resultType);
//如果返回错误,且为内部错误,则直接抛出异常
if (!BaseConstants.HTTP_RESPONSE_CODE_SUCCESS.equals(result.getCode())) {
throw new DecodeException(response.status(), "接口返回错误:" + result.getMsg(), response.request());
}
return result.getData();
}
}
配置类 BaseResponseFeignConfig
public class BaseResponseFeignConfig {
@Bean
public Decoder feignDecoder() {
return new BaseResponseFeignDecoder();
}
}
Feign接口定义 FinValidationFeign
@FeignClient(name = "masterdata", path = "/api/validation", configuration = BaseResponseFeignConfig.class)
public interface FinValidationFeign {
// 各类feign接口
}
由于当前代码没有变动,怀疑是解码器被别人的新开发的代码给覆盖了。但排查之后项目里并没有其他解码器相关的代码。
只能跟踪解码器的加载进行排查。
OpenFeign
客户端会在应用启动时进行加载。
根据 FeignClient
注解跟踪到 org.springframework.cloud.openfeign.FeignClientsRegistrar
的 registerFeignClients
方法。
我们可以看到加载时,通过registerClientConfiguration
方法加载自定义配置
通过代码可以看到注册的 beanName
为 name + "." + FeignClientSpecification.class.getSimpleName()
, 也就是 masterdata.feignClientSpecification
由此可以看出当多个Client 的 name
一致时,会使用最后一个加载的client的配置。
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
// 通过扫包将 FeignClient 注解的代码都加载出来
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
// 循环初始化Feign客户端
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");
// 加载 FeignClient 注解的参数
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
// 处理自定义配置, 默认值 {}, 无自定义配置也会走这步
registerClientConfiguration(registry, name, attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// 根据name + FeignClientSpecification 进行Spring的Bean注册
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
这时候再扭头过来看这两天加的代码,发现新增了一个 同名name的client,并且没配置自定义解码器,加载顺序在 FinValidationFeign
之后,导致他的配置覆盖掉了 FinValidationFeign
。一起变成了走默认的解码器。
@FeignClient(name = "masterdata", path = "/api/query")
public interface FinQueryFeign {
// 各类feign接口
}
因为对应服务在重构,返回值存在两个包装类,没办法进行统一配置。
因为是beanName相同导致的配置覆盖,而我们能修改的name是通过 String name = getClientName(attributes);
获取的
可以看到 name 是优先获取 contextId
, 我们可以通过配置contextId进行区分,避免覆盖。
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());
}
解决后的代码
@FeignClient(name = "masterdata", contextId = "masterdata-validation", path = "/api/validation", configuration = BaseResponseFeignConfig.class)
public interface FinValidationFeign {
// 各类feign接口
}