解决@FeignClient的地址被映射成Mapping地址, 导致重复注册端点错误

需求背景

项目将FeignClient进行服务调用的接口类抽取到公共独立的API模块

 

项目结构实现效果

1. 公共API模块

2. A系统 Controller实现API模块的接口

3. B系统 使用继承API接口的FeignClient调用A系统

A项目的Controller与B项目的FeignClient方法就通过 API模块的接口达成了一致

如图

 

需求原因

当C服务也需要调用A服务的Controller

那就需要把B服务的FeignClient接口复制一份到C服务中使用

FeignClient接口复制到每个项目等同于再写一遍, 复用性和维护性差

 

出现问题

当FeignClient接口抽取到独立的API模块后,

由于API模块是公共的, A系统也会自己引入A系统自己的FeignClient的API接口

当A系统启动后出现错误

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'com.xxx.remote.market.MarketEntryClient' method 
public abstract com.xxx.XHResult<x> com.xxx.MarketEntryService.findAll()
to {[/market/entry/findAll],methods=[GET]}: There is already 'marketEntryController' bean method

Ambiguous mapping. Cannot map 'xxx' method public abstract xxx to {[xxx],methods=[GET]}: There is already 'xxx' bean method

大意就是Maping路径对应的Bean方法已经存在, 意思就是URL路径重复注册了.

 

源码分析

从API模块中删除FeignClient接口就不会报错, 问题点就在于FeignClient接口

寻找报错位置:

通过到springMVC源码中搜索报错的关键字 There is already

位置org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#assertUniqueMethodMapping

可以看到是register方法中调用的这个验证,

alt+f7找到向上找register的调用源头

 

调用来源org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods

中调用的registerHandlerMethod再调用的register方法

而register方法中的Mapping参数在上层叫做handler,由更上层提供

那就继续向上找

源头就在这,这里名字叫做beanName

if (beanType != null && isHandler(beanType)) {
   detectHandlerMethods(beanName);
}

在这里打下断点重新启动项目

这里可以看大FeignClient接口被当做Handler类注册端点了

Controler的方法在这之前已经注册过,这里FeignClient的方法再次注册就会出现重复注册的错误

看到在调用detectHandlerMethods方法前有一个isHandler(beanType)的判断,跟进

@Override
protected boolean isHandler(Class<?> beanType) {
   return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
         AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

从源码上可以看到只要带有@Controller或@RequestMapping注解的方法都会返回true

正好我们的FeignClient接口是继承带有@RequestMapping注解的接口所以也会返回true

 

解决办法

可以通过覆写isHandler方法来排除@FeignClient注解的方法

在springBoot 2.x中有两种方式(通过继承RequestMappingHandlerMapping覆写isHandler方法), 

完整代码

第一种方式

@Configuration
@ConditionalOnClass({Feign.class})
public class FeignConfig extends WebMvcConfigurationSupport {

    @Override
    @Nullable
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new FeignRequestMappingHandlerMapping();
    }

    private static class FeignRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
        @Override
        protected boolean isHandler(Class<?> beanType) {
            return super.isHandler(beanType) &&
                    beanType.getAnnotation(FeignClient.class) == null;
        }
    }
}

这种写法会让WebMvcAutoConfiguration失效(SpringBoot自动装配MVC配置的类)

当项目中有WebMvcConfigurationSupport的类就不会初始化

第二种方式

@Configuration
@ConditionalOnClass({Feign.class})
public class FeignConfig implements WebMvcRegistrations {

    private RequestMappingHandlerMapping requestMappingHandlerMapping = new FeignRequestMappingHandlerMapping();

    @Override
    @Nullable
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return requestMappingHandlerMapping;
    }

    private static class FeignRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
        @Override
        protected boolean isHandler(Class<?> beanType) {
            return super.isHandler(beanType) &&
                    beanType.getAnnotation(FeignClient.class) == null;
        }
    }
}

这种方法WebMvcAutoConfiguration会生效

会装载WebMvcAutoConfiguration里(组合模式)

@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {

在构造方法中就会将实现WebMvcRegistrations接口的类传入

在调用createRequestMappingHandlerMapping的时候就可以把我们自定义的RequestMappingHandlerMapping载入

这两种方法在我们项目中实测中都没什么问题, 可以完美的排除带有FeignClient的接口方法

 

实现原理

第一种方式是通过继承WebMvcConfigurationSupport覆写createRequestMappingHandlerMapping方法

实现的自定义RequestMappingHandlerMapping, 在源码上也有说明,允许用户自定义

第二种方法是依赖于SpringBoot默认自动自动配置的方式插入的

其实EnableWebMvcConfiguration继承的DelegatingWebMvcConfiguration上游也是继承的WebMvcConfigurationSupport.

如果你项目用了@EnableWebMvc注解

配置类也是DelegatingWebMvcConfiguration

WebMvcConfigurationSupport类上注释原话

This is the main class providing the configuration behind the MVC Java config.

表示这个类是SpringMVC配置的核心

 

第一种方式问题

使用第一种方式之后项目引入静态资源放在rescoure\static目录,会出现无法映射的情况(错误404)

原因是实现了WebMvcConfigurationSupport会让SpringBoot默认的静态资源配置不生效

解决

实现addResourceHandlers方法即可.

如何实现:

照抄SpringBoot默认实现代码

位置WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addResourceHandlers

registry.addResourceHandler(staticPathPattern)
        .addResourceLocations(getResourceLocations(
                this.resourceProperties.getStaticLocations()))
        .setCachePeriod(getSeconds(cachePeriod))
        .setCacheControl(cacheControl);

后期引入swagger的时候, swagger的UI页面无法访问也是这里导致的, 要把swagger的静态页面路径在这里一引入

所以更推荐使用第二种方式解决

 

其他问题

该文案例中

Controller类和Fegin接口共同实现/继承的Service接口, 达到接口统一;

其实也可以将Fegin接口和Service接口合并为一个接口.

即: A系统的Controller类继承Fegin接口, B系统直接使用Fegin接口就可以调用A系统了

到底使用 Fegin接口继承的Service接口的方式, 还是使用Fegin接口与Service接口合并的方式, 我觉得都行看个人喜好;

 

 

 

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值