前端時間打算將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<x> com.xxx.MarketEntryService.findAll()
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);