需求背景
项目将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接口合并的方式, 我觉得都行看个人喜好;