前端时间打算将FeignClient进行服务调用的接口类抽取成独立的模块
发生报错后看了一遍SpringMVC的初始化源码后解决问题
过程比较清晰觉得有必要记录一下
项目情况:
项目API模块
A项目 Controller实现API模块的接口
B项目 FeignClient继承API模块的接口
这样子A项目的Controller与B项目的FeignClient方法就通过 API模块的接口达成了一致
如图
需求
我这时候有一个C服务也需要调用A服务的Controller
那就需要把B服务的FeignClient接口复制一份到C服务中使用
问题
当我需要用A服务的接口时, 我就要去其他服务找找有没有继承好的接口复制过来用.
感觉复用性不高,重复性动作无意义.还不如把FeignClient也放到API模块中大家一起用,省的到处复制粘贴
报错
当我把FeignClient丢到API模块后出现报错
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'com.xxx.remote.market.MarketEntryClient' method
public abstract com.xxx.XHResult
to {[/market/entry/findAll],methods=[GET]}: There is already 'marketEntryController' 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) &&
!AnnotatedElementUtils.hasAnnotation(beanType, FeignClient.class);
}
}
}
这种写法会让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) &&
!AnnotatedElementUtils.hasAnnotation(beanType, FeignClient.class);
}
}
}
这种方法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);