先来看看这里说的多层继承是啥意思,假设现在有A、B、C三个接口,继承关系如下:
public interface A {}
public interface B extends A {}
public interface C extends B {}
C接口通过B间接继承A接口,这种方式上并没有什么毛病吧?而且也很常见。但是如果这个时候,在C接口上加上@FeignClient
注解,默认情况下是会报错的,报错信息如下:
Caused by: java.lang.IllegalStateException: Only single-level inheritance supported: GoodsClient
at feign.Util.checkState(Util.java:130)
at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:55)
at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:154)
at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:52)
at feign.Feign$Builder.target(Feign.java:251)
at org.springframework.cloud.openfeign.HystrixTargeter.target(HystrixTargeter.java:38)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.loadBalance(FeignClientFactoryBean.java:243)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:272)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:252)
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:171)
... 30 common frames omitted
报错信息已经提示的很明显了,GoodsClient
接口只支持有一个继承关系。那为什么会有这样的异常呢?我们又该如何解决?
1. 封装前OpenFeign接口
OpenFeign接口作为业务模块对外的接口,主要是提供一些查询方法供其它微服务进行数据查询。比如账务微服务的账单模块,通常情况下对于一些关联的对象数据,我们只会保存其唯一标识,所以在搜索界面需要展示对应的对象数据就要去查招商微服务的项目、商户、合同数据。
假设我们没有对OpenFeign接口做封装,来看看项目、商户、合同模块的Feign接口时怎样的。
- 项目Feign接口
public interface StoreApi {
/**
* 根据uuid获取实体对象
*
* @param uuid 唯一标识
* @param fetchPropertyInfo 是否获取实体相关联的其它实体完整信息,默认为true
* @return
*/
@GetMapping("/{uuid}")
ResponseResult<Store> findById(@PathVariable("uuid") String uuid, @RequestParam(value = "fetchPropertyInfo", defaultValue = "true") boolean fetchPropertyInfo);
/**
* 批量查询指定唯一标识集合的实体,并将结果转换成Map结构,其中key为uuid,value为实体对象。<p>
* 注意:如果某个uuid的实体不存在,那么结果中不会包含该键
*
* @param uuids 唯一标识集合
* @return
*/
@PostMapping("/ids")
ResponseResult<Map<String, Store>> findAllByIds(@RequestBody Set<String> uuids);
/**
* 根据查询定义查询
*
* @param definition 查询定义
* @return
*/
@PostMapping("/query")
ResponseResult<SummaryQueryResult<Store>> query(@RequestBody QueryDefinition definition);
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-StoreClient", path = "/store")
public interface StoreClient extends StoreApi {
}
- 商户Feign接口
public interface TenantApi {
/**
* 根据uuid获取实体对象
*
* @param uuid 唯一标识
* @param fetchPropertyInfo 是否获取实体相关联的其它实体完整信息,默认为true
* @return
*/
@GetMapping("/{uuid}")
ResponseResult<Tenant> findById(@PathVariable("uuid") String uuid, @RequestParam(value = "fetchPropertyInfo", defaultValue = "true") boolean fetchPropertyInfo);
/**
* 批量查询指定唯一标识集合的实体,并将结果转换成Map结构,其中key为uuid,value为实体对象。<p>
* 注意:如果某个uuid的实体不存在,那么结果中不会包含该键
*
* @param uuids 唯一标识集合
* @return
*/
@PostMapping("/ids")
ResponseResult<Map<String, Tenant>> findAllByIds(@RequestBody Set<String> uuids);
/**
* 根据查询定义查询
*
* @param definition 查询定义
* @return
*/
@PostMapping("/query")
ResponseResult<SummaryQueryResult<Tenant>> query(@RequestBody QueryDefinition definition);
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-TenantClient", path = "/tenant")
public interface TenantClient extends TenantApi {
}
- 合同Feign接口
public interface ContractApi {
/**
* 根据uuid获取实体对象
*
* @param uuid 唯一标识
* @param fetchPropertyInfo 是否获取实体相关联的其它实体完整信息,默认为true
* @return
*/
@GetMapping("/{uuid}")
ResponseResult<Contract> findById(@PathVariable("uuid") String uuid, @RequestParam(value = "fetchPropertyInfo", defaultValue = "true") boolean fetchPropertyInfo);
/**
* 批量查询指定唯一标识集合的实体,并将结果转换成Map结构,其中key为uuid,value为实体对象。<p>
* 注意:如果某个uuid的实体不存在,那么结果中不会包含该键
*
* @param uuids 唯一标识集合
* @return
*/
@PostMapping("/ids")
ResponseResult<Map<String, Contract>> findAllByIds(@RequestBody Set<String> uuids);
/**
* 根据查询定义查询
*
* @param definition 查询定义
* @return
*/
@PostMapping("/query")
ResponseResult<SummaryQueryResult<Contract>> query(@RequestBody QueryDefinition definition);
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-ContractClient", path = "/contract")
public interface ContractClient extends ContractApi {
}
有没有发现,存在大量的代码重复?这还只是三个模块,如果有100个模块,我们也是这么写吗?显然是不可能的。那不管OpenFeign接口允不允许多层继承,先来看看我们是如何封装的。
2. 封装后的OpenFeign接口
我们将三个公共方法抽离出来,作为最顶层的接口:
- ClientApi
public interface ClientApi<T extends IsEntity> {
/**
* 根据uuid获取实体对象
*
* @param uuid 唯一标识
* @param fetchPropertyInfo 是否获取实体相关联的其它实体完整信息,默认为true
* @return
*/
@GetMapping("/{uuid}")
ResponseResult<T> findById(@PathVariable("uuid") String uuid, @RequestParam(value = "fetchPropertyInfo", defaultValue = "true") boolean fetchPropertyInfo);
/**
* 批量查询指定唯一标识集合的实体,并将结果转换成Map结构,其中key为uuid,value为实体对象。<p>
* 注意:如果某个uuid的实体不存在,那么结果中不会包含该键
*
* @param uuids 唯一标识集合
* @return
*/
@PostMapping("/ids")
ResponseResult<Map<String, T>> findAllByIds(@RequestBody Set<String> uuids);
/**
* 根据查询定义查询
*
* @param definition 查询定义
* @return
*/
@PostMapping("/query")
ResponseResult<SummaryQueryResult<T>> query(@RequestBody QueryDefinition definition);
}
- 项目Feign接口
那么对于项目模块来说,Feign接口的定义就变成了这样:
public interface StoreApi extends ClientApi<Store> {
// 在这里,再定义项目模块个性化的接口
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-StoreClient", path = "/store")
public interface StoreClient extends StoreApi {
}
- 商户Feign接口
public interface TenantApi extends ClientApi<Tenant> {
// 在这里,再定义合同模块个性化的接口
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-TenantClient", path = "/tenant")
public interface TenantClient extends TenantApi {
}
- 合同Feign接口
public interface ContractApi extends ClientApi<Contract> {
// 在这里,再定义合同模块个性化的接口
}
@FeignClient(name = "mall-investment-provider", contextId = "mall-investment-ContractClient", path = "/contract")
public interface ContractClient extends ContractApi {
}
其中StoreApi
、TenantApi
、ContractApi
是用来对每个业务模块个性化的扩展。是不是感觉一下子清净了很多?三个模块这么写,100个模块也是如此干净的。
3. 解决多层继承
说完了我们最终重构和封装后的Feign接口继承结构,我们再来说说如何解决一开始碰到的异常:
Caused by: java.lang.IllegalStateException: Only single-level inheritance supported: xxxFeign
3.1 问题出现的原因
通过跟踪源码,我们发现feign.Contract
这个类中有一个静态内部类BaseContract
,类中的方法parseAndValidatateMetadata
就有如下的一段校验方法:
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
Util.checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s", new Object[]{targetType.getSimpleName()});
Util.checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s", new Object[]{targetType.getSimpleName()});
if (targetType.getInterfaces().length == 1) {
Util.checkState(targetType.getInterfaces()[0].getInterfaces().length == 0, "Only single-level inheritance supported: %s", new Object[]{targetType.getSimpleName()});
}
}
BaseContract
有两个实现类,一个是Default
,一个是SpringMvcContract
,通过调试代码,我们发现其真正在校验的走的是SpringMvcContract
这个实现类:
3.2 解决方案
出问题的地方找到了,那么如何解决这个问题,无非就是怎么改变SpringMvcContract
中的默认行为。百度了一圈全都是些只抛问题不给解决的文章,这就不得不说一下Google的强大了,不说能完全搜索到解决方案,但至少有用的信息比百度搜索出来的结果多得多。有兴趣的朋友可以看看这篇文章:https://github.com/OpenFeign/feign/issues/320,这里我就直接给出结果了。
- 首先写一个类继承
SpringMvcContract
public class HierarchicalContract extends SpringMvcContract {
private ResourceLoader resourceLoader;
@Override
public List<MethodMetadata> parseAndValidatateMetadata(final Class<?> targetType) {
Util.checkState(targetType.getTypeParameters().length == 0,
"Parameterized types unsupported: %s",
targetType.getSimpleName());
final Map<String, MethodMetadata> result = new LinkedHashMap<>();
for (final Method method : targetType.getMethods()) {
if (method.getDeclaringClass() == Object.class || (method.getModifiers() & Modifier.STATIC) != 0
|| Util.isDefault(method)) {
continue;
}
final MethodMetadata metadata = this.parseAndValidateMetadata(targetType, method);
Util.checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s", metadata.configKey());
result.put(metadata.configKey(), metadata);
}
return new ArrayList<>(result.values());
}
@Override
public MethodMetadata parseAndValidateMetadata(final Class<?> targetType, final Method method) {
final MethodMetadata methodMetadata = super.parseAndValidateMetadata(targetType, method);
final LinkedList<Class<?>> classHierarchy = new LinkedList<>();
classHierarchy.add(targetType);
this.findClass(targetType, method.getDeclaringClass(), classHierarchy);
classHierarchy.stream()
.map(this::processPathValue)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.ifPresent((path) -> methodMetadata.template().insert(0, path));
return methodMetadata;
}
private Optional<String> processPathValue(final Class<?> clz) {
Optional<String> result = Optional.empty();
final RequestMapping classAnnotation = clz.getAnnotation(RequestMapping.class);
if (classAnnotation != null) {
final RequestMapping synthesizeAnnotation = synthesizeAnnotation(classAnnotation, clz);
// Prepend path from class annotation if specified
if (synthesizeAnnotation.value().length > 0) {
String pathValue = Util.emptyToNull(synthesizeAnnotation.value()[0]);
pathValue = this.resolveValue(pathValue);
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
result = Optional.of(pathValue);
}
}
return result;
}
private String resolveValue(final String value) {
if (StringUtils.hasText(value) && this.resourceLoader instanceof ConfigurableApplicationContext) {
return ((ConfigurableApplicationContext) this.resourceLoader).getEnvironment().resolvePlaceholders(value);
}
return value;
}
@Override
protected void processAnnotationOnClass(final MethodMetadata data, final Class<?> clz) {
// skip this step
}
private boolean findClass(final Class<?> currentClass, final Class<?> searchClass,
final LinkedList<Class<?>> classHierarchy) {
if (currentClass == searchClass) {
return true;
}
final Class<?>[] interfaces = currentClass.getInterfaces();
for (final Class<?> currentInterface : interfaces) {
classHierarchy.add(currentInterface);
final boolean findClass = this.findClass(currentInterface, searchClass, classHierarchy);
if (findClass) {
return true;
}
classHierarchy.removeLast();
}
return false;
}
@Override
public void setResourceLoader(final ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
super.setResourceLoader(resourceLoader);
}
}
- 然后将该类注册中bean即可
@Configuration
public class AccountConfig {
@Bean
public Contract feignContract() {
return new HierarchicalContract();
}
}