MiniMall:啥?openFeign接口不能多层继承

先来看看这里说的多层继承是啥意思,假设现在有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 {

}

其中StoreApiTenantApiContractApi是用来对每个业务模块个性化的扩展。是不是感觉一下子清净了很多?三个模块这么写,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();
    }
}
——End——
更多精彩分享,可扫码关注微信公众号哦。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值